Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
237
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add admin functionality

+282 -313
-28
.sqlx/query-04c220298334c369872f0b0ad162b992c2353e28257b53f3f10cbff8abb26f5a.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT deactivated_at, takedown_ref FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "deactivated_at", 9 - "type_info": "Timestamptz" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "takedown_ref", 14 - "type_info": "Text" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - true, 24 - true 25 - ] 26 - }, 27 - "hash": "04c220298334c369872f0b0ad162b992c2353e28257b53f3f10cbff8abb26f5a" 28 - }
-22
.sqlx/query-1add22e111d5eff8beadbd832b4b8146d95da0a0ce8ce31dc9a2f930a26cc9ce.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT takedown_ref FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "takedown_ref", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - true 19 - ] 20 - }, 21 - "hash": "1add22e111d5eff8beadbd832b4b8146d95da0a0ce8ce31dc9a2f930a26cc9ce" 22 - }
+34
.sqlx/query-225c3844ce6962121e5cc0aa544c79d0f93bb3458487d79b64bd40ae9accd522.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT deactivated_at, takedown_ref, is_admin FROM users WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "deactivated_at", 9 + "type_info": "Timestamptz" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "takedown_ref", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "is_admin", 19 + "type_info": "Bool" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + true, 29 + true, 30 + false 31 + ] 32 + }, 33 + "hash": "225c3844ce6962121e5cc0aa544c79d0f93bb3458487d79b64bd40ae9accd522" 34 + }
+9 -3
.sqlx/query-6b67b2b6759f01be11d5997a3ad68d381f59a02235a6940877f62193af8d9761.json .sqlx/query-cc68023c320bc4376925c2cd921cd48045a47ca5841eef8c8889894f2c2452f6.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1", 3 + "query": "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref, u.is_admin\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 22 22 "ordinal": 3, 23 23 "name": "takedown_ref", 24 24 "type_info": "Text" 25 + }, 26 + { 27 + "ordinal": 4, 28 + "name": "is_admin", 29 + "type_info": "Bool" 25 30 } 26 31 ], 27 32 "parameters": { ··· 33 38 false, 34 39 true, 35 40 true, 36 - true 41 + true, 42 + false 37 43 ] 38 44 }, 39 - "hash": "6b67b2b6759f01be11d5997a3ad68d381f59a02235a6940877f62193af8d9761" 45 + "hash": "cc68023c320bc4376925c2cd921cd48045a47ca5841eef8c8889894f2c2452f6" 40 46 }
-28
.sqlx/query-90bcc8fb97f73a0b5f427971aca891936b3f906c2d4cdb4bf203dd6a4c9aa060.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "key_bytes", 9 - "type_info": "Bytea" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "encryption_version", 14 - "type_info": "Int4" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - true 25 - ] 26 - }, 27 - "hash": "90bcc8fb97f73a0b5f427971aca891936b3f906c2d4cdb4bf203dd6a4c9aa060" 28 - }
+9 -3
.sqlx/query-bee4276cbb537512cced16f7017d8f7c068d30f319ef965fa9ec9fb1a3490151.json .sqlx/query-49cd5f335121f5eb4f578f6ca3af40e95264ded8021cfc7490b578a96fb8db3c.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref,\n k.key_bytes as \"key_bytes?\", k.encryption_version as \"encryption_version?\"\n FROM oauth_token t\n JOIN users u ON t.did = u.did\n LEFT JOIN user_keys k ON u.id = k.user_id\n WHERE t.token_id = $1", 3 + "query": "SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, u.is_admin,\n k.key_bytes as \"key_bytes?\", k.encryption_version as \"encryption_version?\"\n FROM oauth_token t\n JOIN users u ON t.did = u.did\n LEFT JOIN user_keys k ON u.id = k.user_id\n WHERE t.token_id = $1", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 25 25 }, 26 26 { 27 27 "ordinal": 4, 28 + "name": "is_admin", 29 + "type_info": "Bool" 30 + }, 31 + { 32 + "ordinal": 5, 28 33 "name": "key_bytes?", 29 34 "type_info": "Bytea" 30 35 }, 31 36 { 32 - "ordinal": 5, 37 + "ordinal": 6, 33 38 "name": "encryption_version?", 34 39 "type_info": "Int4" 35 40 } ··· 45 50 true, 46 51 true, 47 52 false, 53 + false, 48 54 true 49 55 ] 50 56 }, 51 - "hash": "bee4276cbb537512cced16f7017d8f7c068d30f319ef965fa9ec9fb1a3490151" 57 + "hash": "49cd5f335121f5eb4f578f6ca3af40e95264ded8021cfc7490b578a96fb8db3c" 52 58 }
+46
.sqlx/query-e6077393f797f94d6048f01edd45b27a89ea481427753a860215d6ee85f8dcf8.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT u.deactivated_at, u.takedown_ref, u.is_admin,\n k.key_bytes as \"key_bytes?\", k.encryption_version as \"encryption_version?\"\n FROM users u\n LEFT JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "deactivated_at", 9 + "type_info": "Timestamptz" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "takedown_ref", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "is_admin", 19 + "type_info": "Bool" 20 + }, 21 + { 22 + "ordinal": 3, 23 + "name": "key_bytes?", 24 + "type_info": "Bytea" 25 + }, 26 + { 27 + "ordinal": 4, 28 + "name": "encryption_version?", 29 + "type_info": "Int4" 30 + } 31 + ], 32 + "parameters": { 33 + "Left": [ 34 + "Text" 35 + ] 36 + }, 37 + "nullable": [ 38 + true, 39 + true, 40 + false, 41 + false, 42 + true 43 + ] 44 + }, 45 + "hash": "e6077393f797f94d6048f01edd45b27a89ea481427753a860215d6ee85f8dcf8" 46 + }
+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT COUNT(*) as count FROM users", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "count", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [] 14 + }, 15 + "nullable": [ 16 + null 17 + ] 18 + }, 19 + "hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538" 20 + }
+1
migrations/20251218_add_is_admin.sql
··· 1 + ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;
+2 -9
src/api/admin/account/delete.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 16 17 17 18 pub async fn delete_account( 18 19 State(state): State<AppState>, 19 - headers: axum::http::HeaderMap, 20 + _auth: BearerAuthAdmin, 20 21 Json(input): Json<DeleteAccountInput>, 21 22 ) -> Response { 22 - let auth_header = headers.get("Authorization"); 23 - if auth_header.is_none() { 24 - return ( 25 - StatusCode::UNAUTHORIZED, 26 - Json(json!({"error": "AuthenticationRequired"})), 27 - ) 28 - .into_response(); 29 - } 30 23 let did = input.did.trim(); 31 24 if did.is_empty() { 32 25 return (
+2 -9
src/api/admin/account/email.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 26 27 27 28 pub async fn send_email( 28 29 State(state): State<AppState>, 29 - headers: axum::http::HeaderMap, 30 + _auth: BearerAuthAdmin, 30 31 Json(input): Json<SendEmailInput>, 31 32 ) -> Response { 32 - let auth_header = headers.get("Authorization"); 33 - if auth_header.is_none() { 34 - return ( 35 - StatusCode::UNAUTHORIZED, 36 - Json(json!({"error": "AuthenticationRequired"})), 37 - ) 38 - .into_response(); 39 - } 40 33 let recipient_did = input.recipient_did.trim(); 41 34 let content = input.content.trim(); 42 35 if recipient_did.is_empty() {
+3 -18
src/api/admin/account/info.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 35 36 36 37 pub async fn get_account_info( 37 38 State(state): State<AppState>, 38 - headers: axum::http::HeaderMap, 39 + _auth: BearerAuthAdmin, 39 40 Query(params): Query<GetAccountInfoParams>, 40 41 ) -> Response { 41 - let auth_header = headers.get("Authorization"); 42 - if auth_header.is_none() { 43 - return ( 44 - StatusCode::UNAUTHORIZED, 45 - Json(json!({"error": "AuthenticationRequired"})), 46 - ) 47 - .into_response(); 48 - } 49 42 let did = params.did.trim(); 50 43 if did.is_empty() { 51 44 return ( ··· 102 95 103 96 pub async fn get_account_infos( 104 97 State(state): State<AppState>, 105 - headers: axum::http::HeaderMap, 98 + _auth: BearerAuthAdmin, 106 99 Query(params): Query<GetAccountInfosParams>, 107 100 ) -> Response { 108 - let auth_header = headers.get("Authorization"); 109 - if auth_header.is_none() { 110 - return ( 111 - StatusCode::UNAUTHORIZED, 112 - Json(json!({"error": "AuthenticationRequired"})), 113 - ) 114 - .into_response(); 115 - } 116 101 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect(); 117 102 if dids.is_empty() { 118 103 return (
+3 -20
src/api/admin/account/profile.rs
··· 1 1 use crate::api::repo::record::create_record_internal; 2 + use crate::auth::BearerAuthAdmin; 2 3 use crate::state::AppState; 3 4 use axum::{ 4 5 Json, ··· 36 37 37 38 pub async fn create_profile( 38 39 State(state): State<AppState>, 39 - headers: axum::http::HeaderMap, 40 + _auth: BearerAuthAdmin, 40 41 Json(input): Json<CreateProfileInput>, 41 42 ) -> Response { 42 - let auth_header = headers.get("Authorization"); 43 - if auth_header.is_none() { 44 - return ( 45 - StatusCode::UNAUTHORIZED, 46 - Json(json!({"error": "AuthenticationRequired"})), 47 - ) 48 - .into_response(); 49 - } 50 - 51 43 let did = input.did.trim(); 52 44 if did.is_empty() { 53 45 return ( ··· 101 93 102 94 pub async fn create_record_admin( 103 95 State(state): State<AppState>, 104 - headers: axum::http::HeaderMap, 96 + _auth: BearerAuthAdmin, 105 97 Json(input): Json<CreateRecordAdminInput>, 106 98 ) -> Response { 107 - let auth_header = headers.get("Authorization"); 108 - if auth_header.is_none() { 109 - return ( 110 - StatusCode::UNAUTHORIZED, 111 - Json(json!({"error": "AuthenticationRequired"})), 112 - ) 113 - .into_response(); 114 - } 115 - 116 99 let did = input.did.trim(); 117 100 if did.is_empty() { 118 101 return (
+4 -27
src/api/admin/account/update.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 17 18 18 19 pub async fn update_account_email( 19 20 State(state): State<AppState>, 20 - headers: axum::http::HeaderMap, 21 + _auth: BearerAuthAdmin, 21 22 Json(input): Json<UpdateAccountEmailInput>, 22 23 ) -> Response { 23 - let auth_header = headers.get("Authorization"); 24 - if auth_header.is_none() { 25 - return ( 26 - StatusCode::UNAUTHORIZED, 27 - Json(json!({"error": "AuthenticationRequired"})), 28 - ) 29 - .into_response(); 30 - } 31 24 let account = input.account.trim(); 32 25 let email = input.email.trim(); 33 26 if account.is_empty() || email.is_empty() { ··· 70 63 71 64 pub async fn update_account_handle( 72 65 State(state): State<AppState>, 73 - headers: axum::http::HeaderMap, 66 + _auth: BearerAuthAdmin, 74 67 Json(input): Json<UpdateAccountHandleInput>, 75 68 ) -> Response { 76 - let auth_header = headers.get("Authorization"); 77 - if auth_header.is_none() { 78 - return ( 79 - StatusCode::UNAUTHORIZED, 80 - Json(json!({"error": "AuthenticationRequired"})), 81 - ) 82 - .into_response(); 83 - } 84 69 let did = input.did.trim(); 85 70 let handle = input.handle.trim(); 86 71 if did.is_empty() || handle.is_empty() { ··· 158 143 159 144 pub async fn update_account_password( 160 145 State(state): State<AppState>, 161 - headers: axum::http::HeaderMap, 146 + _auth: BearerAuthAdmin, 162 147 Json(input): Json<UpdateAccountPasswordInput>, 163 148 ) -> Response { 164 - let auth_header = headers.get("Authorization"); 165 - if auth_header.is_none() { 166 - return ( 167 - StatusCode::UNAUTHORIZED, 168 - Json(json!({"error": "AuthenticationRequired"})), 169 - ) 170 - .into_response(); 171 - } 172 149 let did = input.did.trim(); 173 150 let password = input.password.trim(); 174 151 if did.is_empty() || password.is_empty() {
+5 -36
src/api/admin/invite.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 18 19 19 20 pub async fn disable_invite_codes( 20 21 State(state): State<AppState>, 21 - headers: axum::http::HeaderMap, 22 + _auth: BearerAuthAdmin, 22 23 Json(input): Json<DisableInviteCodesInput>, 23 24 ) -> Response { 24 - let auth_header = headers.get("Authorization"); 25 - if auth_header.is_none() { 26 - return ( 27 - StatusCode::UNAUTHORIZED, 28 - Json(json!({"error": "AuthenticationRequired"})), 29 - ) 30 - .into_response(); 31 - } 32 25 if let Some(codes) = &input.codes { 33 26 for code in codes { 34 27 let _ = sqlx::query!( ··· 91 84 92 85 pub async fn get_invite_codes( 93 86 State(state): State<AppState>, 94 - headers: axum::http::HeaderMap, 87 + _auth: BearerAuthAdmin, 95 88 Query(params): Query<GetInviteCodesParams>, 96 89 ) -> Response { 97 - let auth_header = headers.get("Authorization"); 98 - if auth_header.is_none() { 99 - return ( 100 - StatusCode::UNAUTHORIZED, 101 - Json(json!({"error": "AuthenticationRequired"})), 102 - ) 103 - .into_response(); 104 - } 105 90 let limit = params.limit.unwrap_or(100).clamp(1, 500); 106 91 let sort = params.sort.as_deref().unwrap_or("recent"); 107 92 let order_clause = match sort { ··· 229 214 230 215 pub async fn disable_account_invites( 231 216 State(state): State<AppState>, 232 - headers: axum::http::HeaderMap, 217 + _auth: BearerAuthAdmin, 233 218 Json(input): Json<DisableAccountInvitesInput>, 234 219 ) -> Response { 235 - let auth_header = headers.get("Authorization"); 236 - if auth_header.is_none() { 237 - return ( 238 - StatusCode::UNAUTHORIZED, 239 - Json(json!({"error": "AuthenticationRequired"})), 240 - ) 241 - .into_response(); 242 - } 243 220 let account = input.account.trim(); 244 221 if account.is_empty() { 245 222 return ( ··· 283 260 284 261 pub async fn enable_account_invites( 285 262 State(state): State<AppState>, 286 - headers: axum::http::HeaderMap, 263 + _auth: BearerAuthAdmin, 287 264 Json(input): Json<EnableAccountInvitesInput>, 288 265 ) -> Response { 289 - let auth_header = headers.get("Authorization"); 290 - if auth_header.is_none() { 291 - return ( 292 - StatusCode::UNAUTHORIZED, 293 - Json(json!({"error": "AuthenticationRequired"})), 294 - ) 295 - .into_response(); 296 - } 297 266 let account = input.account.trim(); 298 267 if account.is_empty() { 299 268 return (
+2 -12
src/api/admin/server_stats.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, 4 5 extract::State, 5 - http::{HeaderMap, StatusCode}, 6 6 response::{IntoResponse, Response}, 7 7 }; 8 8 use serde::Serialize; 9 - use serde_json::json; 10 9 11 10 #[derive(Serialize)] 12 11 #[serde(rename_all = "camelCase")] ··· 19 18 20 19 pub async fn get_server_stats( 21 20 State(state): State<AppState>, 22 - headers: HeaderMap, 21 + _auth: BearerAuthAdmin, 23 22 ) -> Response { 24 - let auth_header = headers.get("Authorization"); 25 - if auth_header.is_none() { 26 - return ( 27 - StatusCode::UNAUTHORIZED, 28 - Json(json!({"error": "AuthenticationRequired"})), 29 - ) 30 - .into_response(); 31 - } 32 - 33 23 let user_count: i64 = match sqlx::query_scalar!("SELECT COUNT(*) FROM users") 34 24 .fetch_one(&state.db) 35 25 .await
+3 -18
src/api/admin/status.rs
··· 1 + use crate::auth::BearerAuthAdmin; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 32 33 33 34 pub async fn get_subject_status( 34 35 State(state): State<AppState>, 35 - headers: axum::http::HeaderMap, 36 + _auth: BearerAuthAdmin, 36 37 Query(params): Query<GetSubjectStatusParams>, 37 38 ) -> Response { 38 - let auth_header = headers.get("Authorization"); 39 - if auth_header.is_none() { 40 - return ( 41 - StatusCode::UNAUTHORIZED, 42 - Json(json!({"error": "AuthenticationRequired"})), 43 - ) 44 - .into_response(); 45 - } 46 39 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 47 40 return ( 48 41 StatusCode::BAD_REQUEST, ··· 208 201 209 202 pub async fn update_subject_status( 210 203 State(state): State<AppState>, 211 - headers: axum::http::HeaderMap, 204 + _auth: BearerAuthAdmin, 212 205 Json(input): Json<UpdateSubjectStatusInput>, 213 206 ) -> Response { 214 - let auth_header = headers.get("Authorization"); 215 - if auth_header.is_none() { 216 - return ( 217 - StatusCode::UNAUTHORIZED, 218 - Json(json!({"error": "AuthenticationRequired"})), 219 - ) 220 - .into_response(); 221 - } 222 207 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 223 208 match subject_type { 224 209 Some("com.atproto.admin.defs#repoRef") => {
+9 -2
src/api/identity/account.rs
··· 379 379 }; 380 380 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000); 381 381 let code_expires_at = chrono::Utc::now() + chrono::Duration::minutes(30); 382 + let is_first_user = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") 383 + .fetch_one(&mut *tx) 384 + .await 385 + .map(|c| c.unwrap_or(0) == 0) 386 + .unwrap_or(false); 382 387 let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 383 388 r#"INSERT INTO users ( 384 389 handle, email, did, password_hash, 385 390 preferred_notification_channel, 386 - discord_id, telegram_username, signal_number 387 - ) VALUES ($1, $2, $3, $4, $5::notification_channel, $6, $7, $8) RETURNING id"#, 391 + discord_id, telegram_username, signal_number, 392 + is_admin 393 + ) VALUES ($1, $2, $3, $4, $5::notification_channel, $6, $7, $8, $9) RETURNING id"#, 388 394 ) 389 395 .bind(short_handle) 390 396 .bind(&email) ··· 412 418 .map(|s| s.trim()) 413 419 .filter(|s| !s.is_empty()), 414 420 ) 421 + .bind(is_first_user) 415 422 .fetch_one(&mut *tx) 416 423 .await; 417 424 let user_id = match user_insert {
+38
src/auth/extractor.rs
··· 21 21 AuthenticationFailed, 22 22 AccountDeactivated, 23 23 AccountTakedown, 24 + AdminRequired, 24 25 } 25 26 26 27 impl IntoResponse for AuthError { ··· 50 51 StatusCode::UNAUTHORIZED, 51 52 "AccountTakedown", 52 53 "Account has been taken down", 54 + ), 55 + AuthError::AdminRequired => ( 56 + StatusCode::FORBIDDEN, 57 + "AdminRequired", 58 + "This action requires admin privileges", 53 59 ), 54 60 }; 55 61 ··· 176 182 177 183 match validate_bearer_token_cached_allow_deactivated(&state.db, &state.cache, token).await { 178 184 Ok(user) => Ok(BearerAuthAllowDeactivated(user)), 185 + Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 186 + Err(_) => Err(AuthError::AuthenticationFailed), 187 + } 188 + } 189 + } 190 + 191 + pub struct BearerAuthAdmin(pub AuthenticatedUser); 192 + 193 + impl FromRequestParts<AppState> for BearerAuthAdmin { 194 + type Rejection = AuthError; 195 + 196 + async fn from_request_parts( 197 + parts: &mut Parts, 198 + state: &AppState, 199 + ) -> Result<Self, Self::Rejection> { 200 + let auth_header = parts 201 + .headers 202 + .get(AUTHORIZATION) 203 + .ok_or(AuthError::MissingToken)? 204 + .to_str() 205 + .map_err(|_| AuthError::InvalidFormat)?; 206 + 207 + let token = extract_bearer_token(auth_header)?; 208 + 209 + match validate_bearer_token_cached(&state.db, &state.cache, token).await { 210 + Ok(user) => { 211 + if !user.is_admin { 212 + return Err(AuthError::AdminRequired); 213 + } 214 + Ok(BearerAuthAdmin(user)) 215 + } 216 + Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 179 217 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 180 218 Err(_) => Err(AuthError::AuthenticationFailed), 181 219 }
+34 -37
src/auth/mod.rs
··· 11 11 pub mod verify; 12 12 13 13 pub use extractor::{ 14 - AuthError, BearerAuth, BearerAuthAllowDeactivated, ExtractedToken, 14 + AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, ExtractedToken, 15 15 extract_auth_token_from_header, extract_bearer_token_from_header, 16 16 }; 17 17 pub use token::{ ··· 50 50 pub did: String, 51 51 pub key_bytes: Option<Vec<u8>>, 52 52 pub is_oauth: bool, 53 + pub is_admin: bool, 53 54 } 54 55 55 56 pub async fn validate_bearer_token( ··· 103 104 } 104 105 } 105 106 106 - let (decrypted_key, deactivated_at, takedown_ref) = if let Some(key) = cached_key { 107 + let (decrypted_key, deactivated_at, takedown_ref, is_admin) = if let Some(key) = cached_key { 107 108 let user_status = sqlx::query!( 108 - "SELECT deactivated_at, takedown_ref FROM users WHERE did = $1", 109 + "SELECT deactivated_at, takedown_ref, is_admin FROM users WHERE did = $1", 109 110 did 110 111 ) 111 112 .fetch_optional(db) ··· 114 115 .flatten(); 115 116 116 117 match user_status { 117 - Some(status) => (Some(key), status.deactivated_at, status.takedown_ref), 118 - None => (None, None, None), 118 + Some(status) => (Some(key), status.deactivated_at, status.takedown_ref, status.is_admin), 119 + None => (None, None, None, false), 119 120 } 120 121 } else if let Some(user) = sqlx::query!( 121 - "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref 122 + "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref, u.is_admin 122 123 FROM users u 123 124 JOIN user_keys k ON u.id = k.user_id 124 125 WHERE u.did = $1", ··· 142 143 .await; 143 144 } 144 145 145 - (Some(key), user.deactivated_at, user.takedown_ref) 146 + (Some(key), user.deactivated_at, user.takedown_ref, user.is_admin) 146 147 } else { 147 - (None, None, None) 148 + (None, None, None, false) 148 149 }; 149 150 150 151 if let Some(decrypted_key) = decrypted_key { ··· 200 201 did: did.clone(), 201 202 key_bytes: Some(decrypted_key), 202 203 is_oauth: false, 204 + is_admin, 203 205 }); 204 206 } 205 207 } ··· 208 210 209 211 if let Ok(oauth_info) = crate::oauth::verify::extract_oauth_token_info(token) 210 212 && let Some(oauth_token) = sqlx::query!( 211 - r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, 213 + r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, u.is_admin, 212 214 k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 213 215 FROM oauth_token t 214 216 JOIN users u ON t.did = u.did ··· 242 244 did: oauth_token.did, 243 245 key_bytes, 244 246 is_oauth: true, 247 + is_admin: oauth_token.is_admin, 245 248 }); 246 249 } 247 250 } ··· 280 283 .await 281 284 { 282 285 Ok(result) => { 283 - if !allow_deactivated { 284 - let deactivated = sqlx::query_scalar!( 285 - "SELECT deactivated_at FROM users WHERE did = $1", 286 - result.did 287 - ) 288 - .fetch_optional(db) 289 - .await 290 - .ok() 291 - .flatten() 292 - .flatten(); 293 - if deactivated.is_some() { 294 - return Err(TokenValidationError::AccountDeactivated); 295 - } 296 - } 297 - let takedown = 298 - sqlx::query_scalar!("SELECT takedown_ref FROM users WHERE did = $1", result.did) 299 - .fetch_optional(db) 300 - .await 301 - .ok() 302 - .flatten() 303 - .flatten(); 304 - if takedown.is_some() { 305 - return Err(TokenValidationError::AccountTakedown); 306 - } 307 - let key_bytes = sqlx::query!( 308 - "SELECT k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 286 + let user_info = sqlx::query!( 287 + r#"SELECT u.deactivated_at, u.takedown_ref, u.is_admin, 288 + k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 289 + FROM users u 290 + LEFT JOIN user_keys k ON u.id = k.user_id 291 + WHERE u.did = $1"#, 309 292 result.did 310 293 ) 311 294 .fetch_optional(db) 312 295 .await 313 296 .ok() 314 - .flatten() 315 - .and_then(|row| crate::config::decrypt_key(&row.key_bytes, row.encryption_version).ok()); 297 + .flatten(); 298 + let Some(user_info) = user_info else { 299 + return Err(TokenValidationError::AuthenticationFailed); 300 + }; 301 + if !allow_deactivated && user_info.deactivated_at.is_some() { 302 + return Err(TokenValidationError::AccountDeactivated); 303 + } 304 + if user_info.takedown_ref.is_some() { 305 + return Err(TokenValidationError::AccountTakedown); 306 + } 307 + let key_bytes = if let (Some(kb), Some(ev)) = (&user_info.key_bytes, user_info.encryption_version) { 308 + crate::config::decrypt_key(kb, Some(ev)).ok() 309 + } else { 310 + None 311 + }; 316 312 Ok(AuthenticatedUser { 317 313 did: result.did, 318 314 key_bytes, 319 315 is_oauth: true, 316 + is_admin: user_info.is_admin, 320 317 }) 321 318 } 322 319 Err(_) => Err(TokenValidationError::AuthenticationFailed),
+5 -5
tests/admin_email.rs
··· 18 18 let client = common::client(); 19 19 let base_url = common::base_url().await; 20 20 let pool = get_pool().await; 21 - let (access_jwt, did) = common::create_account_and_login(&client).await; 21 + let (access_jwt, did) = common::create_admin_account_and_login(&client).await; 22 22 let res = client 23 23 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) 24 24 .bearer_auth(&access_jwt) ··· 58 58 let client = common::client(); 59 59 let base_url = common::base_url().await; 60 60 let pool = get_pool().await; 61 - let (access_jwt, did) = common::create_account_and_login(&client).await; 61 + let (access_jwt, did) = common::create_admin_account_and_login(&client).await; 62 62 let res = client 63 63 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) 64 64 .bearer_auth(&access_jwt) ··· 92 92 async fn test_send_email_recipient_not_found() { 93 93 let client = common::client(); 94 94 let base_url = common::base_url().await; 95 - let (access_jwt, _) = common::create_account_and_login(&client).await; 95 + let (access_jwt, _) = common::create_admin_account_and_login(&client).await; 96 96 let res = client 97 97 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) 98 98 .bearer_auth(&access_jwt) ··· 113 113 async fn test_send_email_missing_content() { 114 114 let client = common::client(); 115 115 let base_url = common::base_url().await; 116 - let (access_jwt, did) = common::create_account_and_login(&client).await; 116 + let (access_jwt, did) = common::create_admin_account_and_login(&client).await; 117 117 let res = client 118 118 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) 119 119 .bearer_auth(&access_jwt) ··· 134 134 async fn test_send_email_missing_recipient() { 135 135 let client = common::client(); 136 136 let base_url = common::base_url().await; 137 - let (access_jwt, _) = common::create_account_and_login(&client).await; 137 + let (access_jwt, _) = common::create_admin_account_and_login(&client).await; 138 138 let res = client 139 139 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) 140 140 .bearer_auth(&access_jwt)
+8 -8
tests/admin_invite.rs
··· 7 7 #[tokio::test] 8 8 async fn test_admin_get_invite_codes_success() { 9 9 let client = client(); 10 - let (access_jwt, _did) = create_account_and_login(&client).await; 10 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 11 11 let create_payload = json!({ 12 12 "useCount": 3 13 13 }); ··· 38 38 #[tokio::test] 39 39 async fn test_admin_get_invite_codes_with_limit() { 40 40 let client = client(); 41 - let (access_jwt, _did) = create_account_and_login(&client).await; 41 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 42 42 for _ in 0..5 { 43 43 let create_payload = json!({ 44 44 "useCount": 1 ··· 86 86 #[tokio::test] 87 87 async fn test_disable_account_invites_success() { 88 88 let client = client(); 89 - let (access_jwt, did) = create_account_and_login(&client).await; 89 + let (access_jwt, did) = create_admin_account_and_login(&client).await; 90 90 let payload = json!({ 91 91 "account": did 92 92 }); ··· 122 122 #[tokio::test] 123 123 async fn test_enable_account_invites_success() { 124 124 let client = client(); 125 - let (access_jwt, did) = create_account_and_login(&client).await; 125 + let (access_jwt, did) = create_admin_account_and_login(&client).await; 126 126 let disable_payload = json!({ 127 127 "account": did 128 128 }); ··· 186 186 #[tokio::test] 187 187 async fn test_disable_account_invites_not_found() { 188 188 let client = client(); 189 - let (access_jwt, _did) = create_account_and_login(&client).await; 189 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 190 190 let payload = json!({ 191 191 "account": "did:plc:nonexistent" 192 192 }); ··· 206 206 #[tokio::test] 207 207 async fn test_disable_invite_codes_by_code() { 208 208 let client = client(); 209 - let (access_jwt, _did) = create_account_and_login(&client).await; 209 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 210 210 let create_payload = json!({ 211 211 "useCount": 5 212 212 }); ··· 255 255 #[tokio::test] 256 256 async fn test_disable_invite_codes_by_account() { 257 257 let client = client(); 258 - let (access_jwt, did) = create_account_and_login(&client).await; 258 + let (access_jwt, did) = create_admin_account_and_login(&client).await; 259 259 for _ in 0..3 { 260 260 let create_payload = json!({ 261 261 "useCount": 1 ··· 321 321 #[tokio::test] 322 322 async fn test_admin_enable_account_invites_not_found() { 323 323 let client = client(); 324 - let (access_jwt, _did) = create_account_and_login(&client).await; 324 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 325 325 let payload = json!({ 326 326 "account": "did:plc:nonexistent" 327 327 });
+24 -21
tests/admin_moderation.rs
··· 7 7 #[tokio::test] 8 8 async fn test_get_subject_status_user_success() { 9 9 let client = client(); 10 - let (access_jwt, did) = create_account_and_login(&client).await; 10 + let (access_jwt, did) = create_admin_account_and_login(&client).await; 11 11 let res = client 12 12 .get(format!( 13 13 "{}/xrpc/com.atproto.admin.getSubjectStatus", ··· 28 28 #[tokio::test] 29 29 async fn test_get_subject_status_not_found() { 30 30 let client = client(); 31 - let (access_jwt, _did) = create_account_and_login(&client).await; 31 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 32 32 let res = client 33 33 .get(format!( 34 34 "{}/xrpc/com.atproto.admin.getSubjectStatus", ··· 47 47 #[tokio::test] 48 48 async fn test_get_subject_status_no_param() { 49 49 let client = client(); 50 - let (access_jwt, _did) = create_account_and_login(&client).await; 50 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 51 51 let res = client 52 52 .get(format!( 53 53 "{}/xrpc/com.atproto.admin.getSubjectStatus", ··· 80 80 #[tokio::test] 81 81 async fn test_update_subject_status_takedown_user() { 82 82 let client = client(); 83 - let (access_jwt, did) = create_account_and_login(&client).await; 83 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 84 + let (_, target_did) = create_account_and_login(&client).await; 84 85 let payload = json!({ 85 86 "subject": { 86 87 "$type": "com.atproto.admin.defs#repoRef", 87 - "did": did 88 + "did": target_did 88 89 }, 89 90 "takedown": { 90 91 "apply": true, ··· 96 97 "{}/xrpc/com.atproto.admin.updateSubjectStatus", 97 98 base_url().await 98 99 )) 99 - .bearer_auth(&access_jwt) 100 + .bearer_auth(&admin_jwt) 100 101 .json(&payload) 101 102 .send() 102 103 .await ··· 111 112 "{}/xrpc/com.atproto.admin.getSubjectStatus", 112 113 base_url().await 113 114 )) 114 - .bearer_auth(&access_jwt) 115 - .query(&[("did", did.as_str())]) 115 + .bearer_auth(&admin_jwt) 116 + .query(&[("did", target_did.as_str())]) 116 117 .send() 117 118 .await 118 119 .expect("Failed to send request"); ··· 125 126 #[tokio::test] 126 127 async fn test_update_subject_status_remove_takedown() { 127 128 let client = client(); 128 - let (access_jwt, did) = create_account_and_login(&client).await; 129 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 130 + let (_, target_did) = create_account_and_login(&client).await; 129 131 let takedown_payload = json!({ 130 132 "subject": { 131 133 "$type": "com.atproto.admin.defs#repoRef", 132 - "did": did 134 + "did": target_did 133 135 }, 134 136 "takedown": { 135 137 "apply": true, ··· 141 143 "{}/xrpc/com.atproto.admin.updateSubjectStatus", 142 144 base_url().await 143 145 )) 144 - .bearer_auth(&access_jwt) 146 + .bearer_auth(&admin_jwt) 145 147 .json(&takedown_payload) 146 148 .send() 147 149 .await; 148 150 let remove_payload = json!({ 149 151 "subject": { 150 152 "$type": "com.atproto.admin.defs#repoRef", 151 - "did": did 153 + "did": target_did 152 154 }, 153 155 "takedown": { 154 156 "apply": false ··· 159 161 "{}/xrpc/com.atproto.admin.updateSubjectStatus", 160 162 base_url().await 161 163 )) 162 - .bearer_auth(&access_jwt) 164 + .bearer_auth(&admin_jwt) 163 165 .json(&remove_payload) 164 166 .send() 165 167 .await ··· 170 172 "{}/xrpc/com.atproto.admin.getSubjectStatus", 171 173 base_url().await 172 174 )) 173 - .bearer_auth(&access_jwt) 174 - .query(&[("did", did.as_str())]) 175 + .bearer_auth(&admin_jwt) 176 + .query(&[("did", target_did.as_str())]) 175 177 .send() 176 178 .await 177 179 .expect("Failed to send request"); ··· 187 189 #[tokio::test] 188 190 async fn test_update_subject_status_deactivate_user() { 189 191 let client = client(); 190 - let (access_jwt, did) = create_account_and_login(&client).await; 192 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 193 + let (_, target_did) = create_account_and_login(&client).await; 191 194 let payload = json!({ 192 195 "subject": { 193 196 "$type": "com.atproto.admin.defs#repoRef", 194 - "did": did 197 + "did": target_did 195 198 }, 196 199 "deactivated": { 197 200 "apply": true ··· 202 205 "{}/xrpc/com.atproto.admin.updateSubjectStatus", 203 206 base_url().await 204 207 )) 205 - .bearer_auth(&access_jwt) 208 + .bearer_auth(&admin_jwt) 206 209 .json(&payload) 207 210 .send() 208 211 .await ··· 213 216 "{}/xrpc/com.atproto.admin.getSubjectStatus", 214 217 base_url().await 215 218 )) 216 - .bearer_auth(&access_jwt) 217 - .query(&[("did", did.as_str())]) 219 + .bearer_auth(&admin_jwt) 220 + .query(&[("did", target_did.as_str())]) 218 221 .send() 219 222 .await 220 223 .expect("Failed to send request"); ··· 226 229 #[tokio::test] 227 230 async fn test_update_subject_status_invalid_type() { 228 231 let client = client(); 229 - let (access_jwt, _did) = create_account_and_login(&client).await; 232 + let (access_jwt, _did) = create_admin_account_and_login(&client).await; 230 233 let payload = json!({ 231 234 "subject": { 232 235 "$type": "invalid.type",
+3 -3
tests/admin_stats.rs
··· 1 1 mod common; 2 - use common::{base_url, client, create_account_and_login}; 2 + use common::{base_url, client, create_admin_account_and_login}; 3 3 use serde_json::Value; 4 4 5 5 #[tokio::test] 6 6 async fn test_get_server_stats() { 7 7 let client = client(); 8 8 let base = base_url().await; 9 - let (token1, _) = create_account_and_login(&client).await; 9 + let (token1, _) = create_admin_account_and_login(&client).await; 10 10 11 - let (_, _) = create_account_and_login(&client).await; 11 + let (_, _) = create_admin_account_and_login(&client).await; 12 12 13 13 let resp = client 14 14 .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base))
+18 -4
tests/common/mod.rs
··· 511 511 512 512 #[allow(dead_code)] 513 513 pub async fn create_account_and_login(client: &Client) -> (String, String) { 514 + create_account_and_login_internal(client, false).await 515 + } 516 + 517 + #[allow(dead_code)] 518 + pub async fn create_admin_account_and_login(client: &Client) -> (String, String) { 519 + create_account_and_login_internal(client, true).await 520 + } 521 + 522 + async fn create_account_and_login_internal(client: &Client, make_admin: bool) -> (String, String) { 514 523 let mut last_error = String::new(); 515 524 for attempt in 0..3 { 516 525 if attempt > 0 { ··· 539 548 }; 540 549 if res.status() == StatusCode::OK { 541 550 let body: Value = res.json().await.expect("Invalid JSON"); 542 - if let Some(access_jwt) = body["accessJwt"].as_str() { 543 - let did = body["did"].as_str().expect("No did").to_string(); 544 - return (access_jwt.to_string(), did); 545 - } 546 551 let did = body["did"].as_str().expect("No did").to_string(); 547 552 let conn_str = get_db_connection_string().await; 548 553 let pool = sqlx::postgres::PgPoolOptions::new() ··· 550 555 .connect(&conn_str) 551 556 .await 552 557 .expect("Failed to connect to test database"); 558 + if make_admin { 559 + sqlx::query!("UPDATE users SET is_admin = TRUE WHERE did = $1", &did) 560 + .execute(&pool) 561 + .await 562 + .expect("Failed to mark user as admin"); 563 + } 564 + if let Some(access_jwt) = body["accessJwt"].as_str() { 565 + return (access_jwt.to_string(), did); 566 + } 553 567 let verification_code: String = sqlx::query_scalar!( 554 568 "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'", 555 569 &did