diff options
Diffstat (limited to 'src/routes/accounts.rs')
| -rw-r--r-- | src/routes/accounts.rs | 236 |
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 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Form, Router, | ||
| 4 | extract::State, | ||
| 5 | response::{Html, Redirect}, | ||
| 6 | routing::{get, post}, | ||
| 7 | }; | ||
| 8 | use rusqlite::params; | ||
| 9 | use serde::Deserialize; | ||
| 10 | |||
| 11 | use crate::auth::{CurrentUser, hash_password}; | ||
| 12 | use crate::routes::AppState; | ||
| 13 | |||
| 14 | #[derive(Template)] | ||
| 15 | #[template(path = "accounts/manage.html")] | ||
| 16 | struct AccountManagementTemplate { | ||
| 17 | username: String, | ||
| 18 | is_superuser: bool, | ||
| 19 | password_change_required: bool, | ||
| 20 | accounts: Vec<AccountRow>, | ||
| 21 | message: Option<String>, | ||
| 22 | } | ||
| 23 | |||
| 24 | struct AccountRow { | ||
| 25 | id: i64, | ||
| 26 | username: String, | ||
| 27 | is_superuser: bool, | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Deserialize)] | ||
| 31 | pub struct CreateAccountForm { | ||
| 32 | pub username: String, | ||
| 33 | pub password: String, | ||
| 34 | pub is_superuser: Option<String>, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[derive(Debug, Deserialize)] | ||
| 38 | pub struct DeleteAccountForm { | ||
| 39 | pub id: i64, | ||
| 40 | } | ||
| 41 | |||
| 42 | #[derive(Debug, Deserialize)] | ||
| 43 | pub struct ChangePasswordForm { | ||
| 44 | pub current_password: String, | ||
| 45 | pub new_password: String, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Debug, Deserialize)] | ||
| 49 | pub struct SetPasswordForm { | ||
| 50 | pub id: i64, | ||
| 51 | pub new_password: String, | ||
| 52 | } | ||
| 53 | |||
| 54 | pub 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 | |||
| 66 | pub 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 | |||
| 104 | async 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 | |||
| 142 | async 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 | |||
| 164 | async 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 | |||
| 201 | async 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 | |||
| 230 | async 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 | } | ||
