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