summaryrefslogtreecommitdiffstats
path: root/src/routes/accounts.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/routes/accounts.rs236
1 files changed, 236 insertions, 0 deletions
diff --git a/src/routes/accounts.rs b/src/routes/accounts.rs
new file mode 100644
index 0000000..89774f8
--- /dev/null
+++ b/src/routes/accounts.rs
@@ -0,0 +1,236 @@
1use askama::Template;
2use axum::{
3 Form, Router,
4 extract::State,
5 response::{Html, Redirect},
6 routing::{get, post},
7};
8use rusqlite::params;
9use serde::Deserialize;
10
11use crate::auth::{CurrentUser, hash_password};
12use crate::routes::AppState;
13
14#[derive(Template)]
15#[template(path = "accounts/manage.html")]
16struct AccountManagementTemplate {
17 username: String,
18 is_superuser: bool,
19 password_change_required: bool,
20 accounts: Vec<AccountRow>,
21 message: Option<String>,
22}
23
24struct AccountRow {
25 id: i64,
26 username: String,
27 is_superuser: bool,
28}
29
30#[derive(Debug, Deserialize)]
31pub struct CreateAccountForm {
32 pub username: String,
33 pub password: String,
34 pub is_superuser: Option<String>,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct DeleteAccountForm {
39 pub id: i64,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct ChangePasswordForm {
44 pub current_password: String,
45 pub new_password: String,
46}
47
48#[derive(Debug, Deserialize)]
49pub struct SetPasswordForm {
50 pub id: i64,
51 pub new_password: String,
52}
53
54pub fn router() -> Router<AppState> {
55 Router::new()
56 .route("/accounts", get(manage_accounts))
57 .route("/accounts/create", post(create_account))
58 .route("/accounts/delete", post(delete_account))
59 .route("/accounts/set-password", post(set_password))
60 .route(
61 "/accounts/change-own-password",
62 post(change_own_password_from_accounts),
63 )
64}
65
66pub async fn change_password(
67 State(state): State<AppState>,
68 CurrentUser(account): CurrentUser,
69 Form(form): Form<ChangePasswordForm>,
70) -> Result<Redirect, Redirect> {
71 let conn = state.db.conn();
72
73 let hash: String = conn
74 .query_row(
75 "SELECT password FROM accounts WHERE id = ?1",
76 params![account.id],
77 |row| row.get(0),
78 )
79 .map_err(|_| Redirect::to("/login"))?;
80
81 if !crate::auth::verify_password(&form.current_password, &hash).unwrap_or(false) {
82 drop(conn);
83 return Ok(Redirect::to("/change-password"));
84 }
85
86 let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/login"))?;
87
88 conn.execute(
89 "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2",
90 params![new_hash, account.id],
91 )
92 .map_err(|_| Redirect::to("/login"))?;
93
94 conn.execute(
95 "DELETE FROM sessions WHERE account_id = ?1",
96 params![account.id],
97 )
98 .ok();
99 drop(conn);
100
101 Ok(Redirect::to("/login"))
102}
103
104async fn manage_accounts(
105 State(state): State<AppState>,
106 CurrentUser(account): CurrentUser,
107) -> Result<Html<String>, Redirect> {
108 if !account.is_superuser {
109 return Err(Redirect::to("/"));
110 }
111
112 let conn = state.db.conn();
113 let mut stmt = conn
114 .prepare("SELECT id, username, is_superuser FROM accounts ORDER BY username")
115 .map_err(|_| Redirect::to("/login"))?;
116
117 let accounts: Vec<AccountRow> = stmt
118 .query_map([], |row| {
119 Ok(AccountRow {
120 id: row.get(0)?,
121 username: row.get(1)?,
122 is_superuser: row.get::<_, i32>(2)? != 0,
123 })
124 })
125 .map_err(|_| Redirect::to("/login"))?
126 .filter_map(|r| r.ok())
127 .collect();
128 drop(stmt);
129 drop(conn);
130
131 let template = AccountManagementTemplate {
132 username: account.username,
133 is_superuser: account.is_superuser,
134 password_change_required: account.password_change_required,
135 accounts,
136 message: None,
137 };
138
139 Ok(Html(template.render().unwrap_or_default()))
140}
141
142async fn create_account(
143 State(state): State<AppState>,
144 CurrentUser(account): CurrentUser,
145 Form(form): Form<CreateAccountForm>,
146) -> Result<Redirect, Redirect> {
147 if !account.is_superuser {
148 return Ok(Redirect::to("/"));
149 }
150
151 let hash = hash_password(&form.password).map_err(|_| Redirect::to("/accounts"))?;
152 let is_superuser = form.is_superuser.as_deref() == Some("on");
153
154 let conn = state.db.conn();
155 let _ = conn.execute(
156 "INSERT INTO accounts (username, password, is_superuser) VALUES (?1, ?2, ?3)",
157 params![form.username, hash, is_superuser],
158 );
159 drop(conn);
160
161 Ok(Redirect::to("/accounts"))
162}
163
164async fn delete_account(
165 State(state): State<AppState>,
166 CurrentUser(account): CurrentUser,
167 Form(form): Form<DeleteAccountForm>,
168) -> Result<Redirect, Redirect> {
169 if !account.is_superuser || account.id == form.id {
170 return Ok(Redirect::to("/accounts"));
171 }
172
173 let conn = state.db.conn();
174 let superuser_count: i32 = conn
175 .query_row(
176 "SELECT COUNT(*) FROM accounts WHERE is_superuser = 1 AND id != ?1",
177 params![form.id],
178 |row| row.get(0),
179 )
180 .unwrap_or(0);
181
182 let is_superuser: bool = conn
183 .query_row(
184 "SELECT is_superuser FROM accounts WHERE id = ?1",
185 params![form.id],
186 |row| row.get::<_, i32>(0).map(|v| v != 0),
187 )
188 .unwrap_or(false);
189
190 if is_superuser && superuser_count == 0 {
191 drop(conn);
192 return Ok(Redirect::to("/accounts"));
193 }
194
195 let _ = conn.execute("DELETE FROM accounts WHERE id = ?1", params![form.id]);
196 drop(conn);
197
198 Ok(Redirect::to("/accounts"))
199}
200
201async fn set_password(
202 State(state): State<AppState>,
203 CurrentUser(account): CurrentUser,
204 Form(form): Form<SetPasswordForm>,
205) -> Result<Redirect, Redirect> {
206 if !account.is_superuser {
207 return Ok(Redirect::to("/"));
208 }
209
210 let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/accounts"))?;
211
212 let conn = state.db.conn();
213 conn.execute(
214 "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2",
215 params![new_hash, form.id],
216 )
217 .ok();
218
219 // Invalidate sessions for the target account
220 conn.execute(
221 "DELETE FROM sessions WHERE account_id = ?1",
222 params![form.id],
223 )
224 .ok();
225 drop(conn);
226
227 Ok(Redirect::to("/accounts"))
228}
229
230async fn change_own_password_from_accounts(
231 State(state): State<AppState>,
232 CurrentUser(account): CurrentUser,
233 Form(form): Form<ChangePasswordForm>,
234) -> Result<Redirect, Redirect> {
235 change_password(State(state), CurrentUser(account), Form(form)).await
236}