-40
.sqlx/query-2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447.json
-40
.sqlx/query-2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "\n SELECT code, available_uses, created_at, disabled\n FROM invite_codes\n WHERE created_by_user = $1\n ORDER BY created_at DESC\n ",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "code",
9
-
"type_info": "Text"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "available_uses",
14
-
"type_info": "Int4"
15
-
},
16
-
{
17
-
"ordinal": 2,
18
-
"name": "created_at",
19
-
"type_info": "Timestamptz"
20
-
},
21
-
{
22
-
"ordinal": 3,
23
-
"name": "disabled",
24
-
"type_info": "Bool"
25
-
}
26
-
],
27
-
"parameters": {
28
-
"Left": [
29
-
"Uuid"
30
-
]
31
-
},
32
-
"nullable": [
33
-
false,
34
-
false,
35
-
false,
36
-
true
37
-
]
38
-
},
39
-
"hash": "2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447"
40
-
}
···
+17
.sqlx/query-59678fbb756d46bb5f51c9a52800a8d203ed52129b1fae65145df92d145d18de.json
+17
.sqlx/query-59678fbb756d46bb5f51c9a52800a8d203ed52129b1fae65145df92d145d18de.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) VALUES ($1, $2, $3, $4)",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Int4",
10
+
"Uuid",
11
+
"Text"
12
+
]
13
+
},
14
+
"nullable": []
15
+
},
16
+
"hash": "59678fbb756d46bb5f51c9a52800a8d203ed52129b1fae65145df92d145d18de"
17
+
}
+52
.sqlx/query-704b32d9ae2234ae12dad87f5f86230e16acaa1c0c229c66b39024bf9662f1e5.json
+52
.sqlx/query-704b32d9ae2234ae12dad87f5f86230e16acaa1c0c229c66b39024bf9662f1e5.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT\n ic.code,\n ic.available_uses,\n ic.created_at,\n ic.disabled,\n ic.for_account,\n (SELECT COUNT(*) FROM invite_code_uses icu WHERE icu.code = ic.code)::int as \"use_count!\"\n FROM invite_codes ic\n WHERE ic.for_account = $1\n ORDER BY ic.created_at DESC\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "code",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "available_uses",
14
+
"type_info": "Int4"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "created_at",
19
+
"type_info": "Timestamptz"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "disabled",
24
+
"type_info": "Bool"
25
+
},
26
+
{
27
+
"ordinal": 4,
28
+
"name": "for_account",
29
+
"type_info": "Text"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "use_count!",
34
+
"type_info": "Int4"
35
+
}
36
+
],
37
+
"parameters": {
38
+
"Left": [
39
+
"Text"
40
+
]
41
+
},
42
+
"nullable": [
43
+
false,
44
+
false,
45
+
false,
46
+
true,
47
+
false,
48
+
null
49
+
]
50
+
},
51
+
"hash": "704b32d9ae2234ae12dad87f5f86230e16acaa1c0c229c66b39024bf9662f1e5"
52
+
}
+16
.sqlx/query-b3d44806b6351d788048e6afe7a6623882fac70b466bf09596cad8eae1fc9dac.json
+16
.sqlx/query-b3d44806b6351d788048e6afe7a6623882fac70b466bf09596cad8eae1fc9dac.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "INSERT INTO invite_codes (code, available_uses, created_by_user, for_account)\n SELECT $1, $2, id, $3 FROM users WHERE is_admin = true LIMIT 1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Int4",
10
+
"Text"
11
+
]
12
+
},
13
+
"nullable": []
14
+
},
15
+
"hash": "b3d44806b6351d788048e6afe7a6623882fac70b466bf09596cad8eae1fc9dac"
16
+
}
-16
.sqlx/query-bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52.json
-16
.sqlx/query-bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
4
-
"describe": {
5
-
"columns": [],
6
-
"parameters": {
7
-
"Left": [
8
-
"Text",
9
-
"Int4",
10
-
"Uuid"
11
-
]
12
-
},
13
-
"nullable": []
14
-
},
15
-
"hash": "bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52"
16
-
}
···
+20
.sqlx/query-ce50221e621d89f7f7d315b0ccc7893b2c344e3612b56116a785248dda296424.json
+20
.sqlx/query-ce50221e621d89f7f7d315b0ccc7893b2c344e3612b56116a785248dda296424.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT id FROM users WHERE is_admin = true LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": []
14
+
},
15
+
"nullable": [
16
+
false
17
+
]
18
+
},
19
+
"hash": "ce50221e621d89f7f7d315b0ccc7893b2c344e3612b56116a785248dda296424"
20
+
}
-22
.sqlx/query-da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c.json
-22
.sqlx/query-da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT invites_disabled FROM users WHERE did = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "invites_disabled",
9
-
"type_info": "Bool"
10
-
}
11
-
],
12
-
"parameters": {
13
-
"Left": [
14
-
"Text"
15
-
]
16
-
},
17
-
"nullable": [
18
-
true
19
-
]
20
-
},
21
-
"hash": "da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c"
22
-
}
···
+1
-1
frontend/src/routes/Dashboard.svelte
+1
-1
frontend/src/routes/Dashboard.svelte
+7
-5
frontend/src/routes/InviteCodes.svelte
+7
-5
frontend/src/routes/InviteCodes.svelte
···
91
<button onclick={dismissCreated}>{$_('common.done')}</button>
92
</div>
93
{/if}
94
-
<section class="create-section">
95
-
<button onclick={handleCreate} disabled={creating}>
96
-
{creating ? $_('inviteCodes.creating') : $_('inviteCodes.createNew')}
97
-
</button>
98
-
</section>
99
<section class="list-section">
100
<h2>{$_('inviteCodes.yourCodes')}</h2>
101
{#if loading}
···
91
<button onclick={dismissCreated}>{$_('common.done')}</button>
92
</div>
93
{/if}
94
+
{#if auth.session?.isAdmin}
95
+
<section class="create-section">
96
+
<button onclick={handleCreate} disabled={creating}>
97
+
{creating ? $_('inviteCodes.creating') : $_('inviteCodes.createNew')}
98
+
</button>
99
+
</section>
100
+
{/if}
101
<section class="list-section">
102
<h2>{$_('inviteCodes.yourCodes')}</h2>
103
{#if loading}
+2
migrations/20251242_invite_code_for_account.sql
+2
migrations/20251242_invite_code_for_account.sql
+99
-112
src/api/server/invite.rs
+99
-112
src/api/server/invite.rs
···
1
use crate::api::ApiError;
2
use crate::auth::BearerAuth;
3
use crate::state::AppState;
4
-
use crate::util::get_user_id_by_did;
5
use axum::{
6
Json,
7
extract::State,
8
response::{IntoResponse, Response},
9
};
10
use serde::{Deserialize, Serialize};
11
use tracing::error;
12
-
use uuid::Uuid;
13
14
#[derive(Deserialize)]
15
#[serde(rename_all = "camelCase")]
···
25
26
pub async fn create_invite_code(
27
State(state): State<AppState>,
28
-
BearerAuth(auth_user): BearerAuth,
29
Json(input): Json<CreateInviteCodeInput>,
30
) -> Response {
31
if input.use_count < 1 {
32
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
33
}
34
-
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
35
-
Ok(id) => id,
36
-
Err(e) => return ApiError::from(e).into_response(),
37
-
};
38
-
let creator_user_id = if let Some(for_account) = &input.for_account {
39
-
match sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
40
-
.fetch_optional(&state.db)
41
-
.await
42
-
{
43
-
Ok(Some(row)) => row.id,
44
-
Ok(None) => return ApiError::AccountNotFound.into_response(),
45
-
Err(e) => {
46
-
error!("DB error looking up target account: {:?}", e);
47
-
return ApiError::InternalError.into_response();
48
-
}
49
-
}
50
-
} else {
51
-
user_id
52
-
};
53
-
let user_invites_disabled = sqlx::query_scalar!(
54
-
"SELECT invites_disabled FROM users WHERE did = $1",
55
-
auth_user.did
56
-
)
57
-
.fetch_optional(&state.db)
58
-
.await
59
-
.map_err(|e| {
60
-
error!("DB error checking invites_disabled: {:?}", e);
61
-
ApiError::InternalError
62
-
})
63
-
.ok()
64
-
.flatten()
65
-
.flatten()
66
-
.unwrap_or(false);
67
-
if user_invites_disabled {
68
-
return ApiError::InvitesDisabled.into_response();
69
-
}
70
-
let code = Uuid::new_v4().to_string();
71
match sqlx::query!(
72
-
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
73
code,
74
input.use_count,
75
-
creator_user_id
76
)
77
.execute(&state.db)
78
.await
79
{
80
-
Ok(_) => Json(CreateInviteCodeOutput { code }).into_response(),
81
Err(e) => {
82
error!("DB error creating invite code: {:?}", e);
83
ApiError::InternalError.into_response()
···
106
107
pub async fn create_invite_codes(
108
State(state): State<AppState>,
109
-
BearerAuth(auth_user): BearerAuth,
110
Json(input): Json<CreateInviteCodesInput>,
111
) -> Response {
112
if input.use_count < 1 {
113
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
114
}
115
-
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
116
-
Ok(id) => id,
117
-
Err(e) => return ApiError::from(e).into_response(),
118
};
119
-
let code_count = input.code_count.unwrap_or(1).max(1);
120
-
let for_accounts = input.for_accounts.unwrap_or_default();
121
let mut result_codes = Vec::new();
122
-
if for_accounts.is_empty() {
123
let mut codes = Vec::new();
124
for _ in 0..code_count {
125
-
let code = Uuid::new_v4().to_string();
126
if let Err(e) = sqlx::query!(
127
-
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
128
code,
129
input.use_count,
130
-
user_id
131
)
132
.execute(&state.db)
133
.await
···
137
}
138
codes.push(code);
139
}
140
-
result_codes.push(AccountCodes {
141
-
account: "admin".to_string(),
142
-
codes,
143
-
});
144
-
} else {
145
-
for account_did in for_accounts {
146
-
let target_user_id =
147
-
match sqlx::query!("SELECT id FROM users WHERE did = $1", account_did)
148
-
.fetch_optional(&state.db)
149
-
.await
150
-
{
151
-
Ok(Some(row)) => row.id,
152
-
Ok(None) => continue,
153
-
Err(e) => {
154
-
error!("DB error looking up target account: {:?}", e);
155
-
return ApiError::InternalError.into_response();
156
-
}
157
-
};
158
-
let mut codes = Vec::new();
159
-
for _ in 0..code_count {
160
-
let code = Uuid::new_v4().to_string();
161
-
if let Err(e) = sqlx::query!(
162
-
"INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
163
-
code,
164
-
input.use_count,
165
-
target_user_id
166
-
)
167
-
.execute(&state.db)
168
-
.await
169
-
{
170
-
error!("DB error creating invite code: {:?}", e);
171
-
return ApiError::InternalError.into_response();
172
-
}
173
-
codes.push(code);
174
-
}
175
-
result_codes.push(AccountCodes {
176
-
account: account_did,
177
-
codes,
178
-
});
179
-
}
180
}
181
Json(CreateInviteCodesOutput {
182
codes: result_codes,
183
})
···
220
BearerAuth(auth_user): BearerAuth,
221
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
222
) -> Response {
223
-
let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
224
-
Ok(id) => id,
225
-
Err(e) => return ApiError::from(e).into_response(),
226
-
};
227
let include_used = params.include_used.unwrap_or(true);
228
let codes_rows = match sqlx::query!(
229
r#"
230
-
SELECT code, available_uses, created_at, disabled
231
-
FROM invite_codes
232
-
WHERE created_by_user = $1
233
-
ORDER BY created_at DESC
234
"#,
235
-
user_id
236
)
237
.fetch_all(&state.db)
238
.await
239
{
240
-
Ok(rows) => {
241
-
if include_used {
242
-
rows
243
-
} else {
244
-
rows.into_iter().filter(|r| r.available_uses > 0).collect()
245
-
}
246
-
}
247
Err(e) => {
248
error!("DB error fetching invite codes: {:?}", e);
249
return ApiError::InternalError.into_response();
250
}
251
};
252
let mut codes = Vec::new();
253
for row in codes_rows {
254
let uses = sqlx::query!(
255
r#"
256
SELECT u.did, icu.used_at
···
273
.collect()
274
})
275
.unwrap_or_default();
276
codes.push(InviteCode {
277
code: row.code,
278
available: row.available_uses,
279
-
disabled: row.disabled.unwrap_or(false),
280
-
for_account: auth_user.did.clone(),
281
-
created_by: auth_user.did.clone(),
282
created_at: row.created_at.to_rfc3339(),
283
uses,
284
});
285
}
286
Json(GetAccountInviteCodesOutput { codes }).into_response()
287
}
···
1
use crate::api::ApiError;
2
+
use crate::auth::extractor::BearerAuthAdmin;
3
use crate::auth::BearerAuth;
4
use crate::state::AppState;
5
use axum::{
6
Json,
7
extract::State,
8
response::{IntoResponse, Response},
9
};
10
+
use rand::Rng;
11
use serde::{Deserialize, Serialize};
12
use tracing::error;
13
+
14
+
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
15
+
16
+
fn gen_random_token() -> String {
17
+
let mut rng = rand::thread_rng();
18
+
let mut token = String::with_capacity(11);
19
+
for i in 0..10 {
20
+
if i == 5 {
21
+
token.push('-');
22
+
}
23
+
let idx = rng.gen_range(0..32);
24
+
token.push(BASE32_ALPHABET[idx] as char);
25
+
}
26
+
token
27
+
}
28
+
29
+
fn gen_invite_code() -> String {
30
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
31
+
let hostname_prefix = hostname.replace('.', "-");
32
+
format!("{}-{}", hostname_prefix, gen_random_token())
33
+
}
34
35
#[derive(Deserialize)]
36
#[serde(rename_all = "camelCase")]
···
46
47
pub async fn create_invite_code(
48
State(state): State<AppState>,
49
+
BearerAuthAdmin(_auth_user): BearerAuthAdmin,
50
Json(input): Json<CreateInviteCodeInput>,
51
) -> Response {
52
if input.use_count < 1 {
53
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
54
}
55
+
56
+
let for_account = input.for_account.unwrap_or_else(|| "admin".to_string());
57
+
let code = gen_invite_code();
58
+
59
match sqlx::query!(
60
+
"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account)
61
+
SELECT $1, $2, id, $3 FROM users WHERE is_admin = true LIMIT 1",
62
code,
63
input.use_count,
64
+
for_account
65
)
66
.execute(&state.db)
67
.await
68
{
69
+
Ok(result) => {
70
+
if result.rows_affected() == 0 {
71
+
error!("No admin user found to create invite code");
72
+
return ApiError::InternalError.into_response();
73
+
}
74
+
Json(CreateInviteCodeOutput { code }).into_response()
75
+
}
76
Err(e) => {
77
error!("DB error creating invite code: {:?}", e);
78
ApiError::InternalError.into_response()
···
101
102
pub async fn create_invite_codes(
103
State(state): State<AppState>,
104
+
BearerAuthAdmin(_auth_user): BearerAuthAdmin,
105
Json(input): Json<CreateInviteCodesInput>,
106
) -> Response {
107
if input.use_count < 1 {
108
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
109
}
110
+
111
+
let code_count = input.code_count.unwrap_or(1).max(1);
112
+
let for_accounts = input
113
+
.for_accounts
114
+
.filter(|v| !v.is_empty())
115
+
.unwrap_or_else(|| vec!["admin".to_string()]);
116
+
117
+
let admin_user_id = match sqlx::query_scalar!(
118
+
"SELECT id FROM users WHERE is_admin = true LIMIT 1"
119
+
)
120
+
.fetch_optional(&state.db)
121
+
.await
122
+
{
123
+
Ok(Some(id)) => id,
124
+
Ok(None) => {
125
+
error!("No admin user found to create invite codes");
126
+
return ApiError::InternalError.into_response();
127
+
}
128
+
Err(e) => {
129
+
error!("DB error looking up admin user: {:?}", e);
130
+
return ApiError::InternalError.into_response();
131
+
}
132
};
133
+
134
let mut result_codes = Vec::new();
135
+
136
+
for account in for_accounts {
137
let mut codes = Vec::new();
138
for _ in 0..code_count {
139
+
let code = gen_invite_code();
140
if let Err(e) = sqlx::query!(
141
+
"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) VALUES ($1, $2, $3, $4)",
142
code,
143
input.use_count,
144
+
admin_user_id,
145
+
account
146
)
147
.execute(&state.db)
148
.await
···
152
}
153
codes.push(code);
154
}
155
+
result_codes.push(AccountCodes { account, codes });
156
}
157
+
158
Json(CreateInviteCodesOutput {
159
codes: result_codes,
160
})
···
197
BearerAuth(auth_user): BearerAuth,
198
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
199
) -> Response {
200
let include_used = params.include_used.unwrap_or(true);
201
+
202
let codes_rows = match sqlx::query!(
203
r#"
204
+
SELECT
205
+
ic.code,
206
+
ic.available_uses,
207
+
ic.created_at,
208
+
ic.disabled,
209
+
ic.for_account,
210
+
(SELECT COUNT(*) FROM invite_code_uses icu WHERE icu.code = ic.code)::int as "use_count!"
211
+
FROM invite_codes ic
212
+
WHERE ic.for_account = $1
213
+
ORDER BY ic.created_at DESC
214
"#,
215
+
auth_user.did
216
)
217
.fetch_all(&state.db)
218
.await
219
{
220
+
Ok(rows) => rows,
221
Err(e) => {
222
error!("DB error fetching invite codes: {:?}", e);
223
return ApiError::InternalError.into_response();
224
}
225
};
226
+
227
let mut codes = Vec::new();
228
for row in codes_rows {
229
+
let disabled = row.disabled.unwrap_or(false);
230
+
if disabled {
231
+
continue;
232
+
}
233
+
234
+
let use_count = row.use_count;
235
+
if !include_used && use_count >= row.available_uses {
236
+
continue;
237
+
}
238
+
239
let uses = sqlx::query!(
240
r#"
241
SELECT u.did, icu.used_at
···
258
.collect()
259
})
260
.unwrap_or_default();
261
+
262
codes.push(InviteCode {
263
code: row.code,
264
available: row.available_uses,
265
+
disabled,
266
+
for_account: row.for_account,
267
+
created_by: "admin".to_string(),
268
created_at: row.created_at.to_rfc3339(),
269
uses,
270
});
271
}
272
+
273
Json(GetAccountInviteCodesOutput { codes }).into_response()
274
}
+11
-88
tests/admin_invite.rs
+11
-88
tests/admin_invite.rs
···
84
}
85
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
-
});
93
-
let res = client
94
-
.post(format!(
95
-
"{}/xrpc/com.atproto.admin.disableAccountInvites",
96
-
base_url().await
97
-
))
98
-
.bearer_auth(&access_jwt)
99
-
.json(&payload)
100
-
.send()
101
-
.await
102
-
.expect("Failed to send request");
103
-
assert_eq!(res.status(), StatusCode::OK);
104
-
let create_payload = json!({
105
-
"useCount": 1
106
-
});
107
-
let res = client
108
-
.post(format!(
109
-
"{}/xrpc/com.atproto.server.createInviteCode",
110
-
base_url().await
111
-
))
112
-
.bearer_auth(&access_jwt)
113
-
.json(&create_payload)
114
-
.send()
115
-
.await
116
-
.expect("Failed to send request");
117
-
assert_eq!(res.status(), StatusCode::FORBIDDEN);
118
-
let body: Value = res.json().await.expect("Response was not valid JSON");
119
-
assert_eq!(body["error"], "InvitesDisabled");
120
-
}
121
-
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
-
});
129
-
let _ = client
130
-
.post(format!(
131
-
"{}/xrpc/com.atproto.admin.disableAccountInvites",
132
-
base_url().await
133
-
))
134
-
.bearer_auth(&access_jwt)
135
-
.json(&disable_payload)
136
-
.send()
137
-
.await;
138
-
let enable_payload = json!({
139
-
"account": did
140
-
});
141
-
let res = client
142
-
.post(format!(
143
-
"{}/xrpc/com.atproto.admin.enableAccountInvites",
144
-
base_url().await
145
-
))
146
-
.bearer_auth(&access_jwt)
147
-
.json(&enable_payload)
148
-
.send()
149
-
.await
150
-
.expect("Failed to send request");
151
-
assert_eq!(res.status(), StatusCode::OK);
152
-
let create_payload = json!({
153
-
"useCount": 1
154
-
});
155
-
let res = client
156
-
.post(format!(
157
-
"{}/xrpc/com.atproto.server.createInviteCode",
158
-
base_url().await
159
-
))
160
-
.bearer_auth(&access_jwt)
161
-
.json(&create_payload)
162
-
.send()
163
-
.await
164
-
.expect("Failed to send request");
165
-
assert_eq!(res.status(), StatusCode::OK);
166
-
}
167
-
168
-
#[tokio::test]
169
async fn test_disable_account_invites_no_auth() {
170
let client = client();
171
let payload = json!({
···
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
});
213
let create_res = client
214
.post(format!(
···
236
.await
237
.expect("Failed to send request");
238
assert_eq!(res.status(), StatusCode::OK);
239
let list_res = client
240
.get(format!(
241
-
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
242
base_url().await
243
))
244
.bearer_auth(&access_jwt)
···
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
262
});
263
let _ = client
264
.post(format!(
···
284
.await
285
.expect("Failed to send request");
286
assert_eq!(res.status(), StatusCode::OK);
287
let list_res = client
288
.get(format!(
289
-
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
290
base_url().await
291
))
292
.bearer_auth(&access_jwt)
···
295
.expect("Failed to get invite codes");
296
let list_body: Value = list_res.json().await.unwrap();
297
let codes = list_body["codes"].as_array().unwrap();
298
-
for code in codes {
299
assert_eq!(code["disabled"], true);
300
}
301
}
···
84
}
85
86
#[tokio::test]
87
async fn test_disable_account_invites_no_auth() {
88
let client = client();
89
let payload = json!({
···
124
#[tokio::test]
125
async fn test_disable_invite_codes_by_code() {
126
let client = client();
127
+
let (access_jwt, admin_did) = create_admin_account_and_login(&client).await;
128
let create_payload = json!({
129
+
"useCount": 5,
130
+
"forAccount": admin_did
131
});
132
let create_res = client
133
.post(format!(
···
155
.await
156
.expect("Failed to send request");
157
assert_eq!(res.status(), StatusCode::OK);
158
+
159
let list_res = client
160
.get(format!(
161
+
"{}/xrpc/com.atproto.admin.getInviteCodes",
162
base_url().await
163
))
164
.bearer_auth(&access_jwt)
···
178
let (access_jwt, did) = create_admin_account_and_login(&client).await;
179
for _ in 0..3 {
180
let create_payload = json!({
181
+
"useCount": 1,
182
+
"forAccount": did
183
});
184
let _ = client
185
.post(format!(
···
205
.await
206
.expect("Failed to send request");
207
assert_eq!(res.status(), StatusCode::OK);
208
+
209
let list_res = client
210
.get(format!(
211
+
"{}/xrpc/com.atproto.admin.getInviteCodes",
212
base_url().await
213
))
214
.bearer_auth(&access_jwt)
···
217
.expect("Failed to get invite codes");
218
let list_body: Value = list_res.json().await.unwrap();
219
let codes = list_body["codes"].as_array().unwrap();
220
+
let admin_codes: Vec<_> = codes.iter().filter(|c| c["forAccount"].as_str() == Some(&did)).collect();
221
+
for code in admin_codes {
222
assert_eq!(code["disabled"], true);
223
}
224
}
+124
-14
tests/invite.rs
+124
-14
tests/invite.rs
···
6
#[tokio::test]
7
async fn test_create_invite_code_success() {
8
let client = client();
9
-
let (access_jwt, _did) = create_account_and_login(&client).await;
10
let payload = json!({
11
"useCount": 5
12
});
···
25
assert!(body["code"].is_string());
26
let code = body["code"].as_str().unwrap();
27
assert!(!code.is_empty());
28
-
assert!(code.contains('-'), "Code should be a UUID format");
29
}
30
31
#[tokio::test]
···
49
}
50
51
#[tokio::test]
52
async fn test_create_invite_code_invalid_use_count() {
53
let client = client();
54
-
let (access_jwt, _did) = create_account_and_login(&client).await;
55
let payload = json!({
56
"useCount": 0
57
});
···
73
#[tokio::test]
74
async fn test_create_invite_code_for_another_account() {
75
let client = client();
76
-
let (access_jwt1, _did1) = create_account_and_login(&client).await;
77
let (_access_jwt2, did2) = create_account_and_login(&client).await;
78
let payload = json!({
79
"useCount": 3,
···
97
#[tokio::test]
98
async fn test_create_invite_codes_success() {
99
let client = client();
100
-
let (access_jwt, _did) = create_account_and_login(&client).await;
101
let payload = json!({
102
"useCount": 2,
103
"codeCount": 3
···
117
assert!(body["codes"].is_array());
118
let codes = body["codes"].as_array().unwrap();
119
assert_eq!(codes.len(), 1);
120
assert_eq!(codes[0]["codes"].as_array().unwrap().len(), 3);
121
}
122
123
#[tokio::test]
124
async fn test_create_invite_codes_for_multiple_accounts() {
125
let client = client();
126
-
let (access_jwt1, did1) = create_account_and_login(&client).await;
127
let (_access_jwt2, did2) = create_account_and_login(&client).await;
128
let payload = json!({
129
"useCount": 1,
···
169
}
170
171
#[tokio::test]
172
-
async fn test_get_account_invite_codes_success() {
173
let client = client();
174
let (access_jwt, _did) = create_account_and_login(&client).await;
175
let create_payload = json!({
176
-
"useCount": 5
177
});
178
let _ = client
179
.post(format!(
180
"{}/xrpc/com.atproto.server.createInviteCode",
181
base_url().await
182
))
183
-
.bearer_auth(&access_jwt)
184
.json(&create_payload)
185
.send()
186
.await
187
.expect("Failed to create invite code");
188
let res = client
189
.get(format!(
190
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
191
base_url().await
192
))
193
-
.bearer_auth(&access_jwt)
194
.send()
195
.await
196
.expect("Failed to send request");
···
205
assert!(code["disabled"].is_boolean());
206
assert!(code["createdAt"].is_string());
207
assert!(code["uses"].is_array());
208
}
209
210
#[tokio::test]
···
224
#[tokio::test]
225
async fn test_get_account_invite_codes_include_used_filter() {
226
let client = client();
227
-
let (access_jwt, _did) = create_account_and_login(&client).await;
228
let create_payload = json!({
229
-
"useCount": 5
230
});
231
let _ = client
232
.post(format!(
233
"{}/xrpc/com.atproto.server.createInviteCode",
234
base_url().await
235
))
236
-
.bearer_auth(&access_jwt)
237
.json(&create_payload)
238
.send()
239
.await
240
.expect("Failed to create invite code");
241
let res = client
242
.get(format!(
243
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
244
base_url().await
245
))
246
-
.bearer_auth(&access_jwt)
247
.query(&[("includeUsed", "false")])
248
.send()
249
.await
···
255
assert!(code["available"].as_i64().unwrap() > 0);
256
}
257
}
···
6
#[tokio::test]
7
async fn test_create_invite_code_success() {
8
let client = client();
9
+
let (access_jwt, _did) = create_admin_account_and_login(&client).await;
10
let payload = json!({
11
"useCount": 5
12
});
···
25
assert!(body["code"].is_string());
26
let code = body["code"].as_str().unwrap();
27
assert!(!code.is_empty());
28
+
assert!(code.contains('-'), "Code should be in hostname-xxxxx-xxxxx format");
29
+
let parts: Vec<&str> = code.split('-').collect();
30
+
assert!(parts.len() >= 3, "Code should have at least 3 parts (hostname + 2 random parts)");
31
}
32
33
#[tokio::test]
···
51
}
52
53
#[tokio::test]
54
+
async fn test_create_invite_code_non_admin() {
55
+
let client = client();
56
+
let (access_jwt, _did) = create_account_and_login(&client).await;
57
+
let payload = json!({
58
+
"useCount": 5
59
+
});
60
+
let res = client
61
+
.post(format!(
62
+
"{}/xrpc/com.atproto.server.createInviteCode",
63
+
base_url().await
64
+
))
65
+
.bearer_auth(&access_jwt)
66
+
.json(&payload)
67
+
.send()
68
+
.await
69
+
.expect("Failed to send request");
70
+
assert_eq!(res.status(), StatusCode::FORBIDDEN);
71
+
let body: Value = res.json().await.expect("Response was not valid JSON");
72
+
assert_eq!(body["error"], "AdminRequired");
73
+
}
74
+
75
+
#[tokio::test]
76
async fn test_create_invite_code_invalid_use_count() {
77
let client = client();
78
+
let (access_jwt, _did) = create_admin_account_and_login(&client).await;
79
let payload = json!({
80
"useCount": 0
81
});
···
97
#[tokio::test]
98
async fn test_create_invite_code_for_another_account() {
99
let client = client();
100
+
let (access_jwt1, _did1) = create_admin_account_and_login(&client).await;
101
let (_access_jwt2, did2) = create_account_and_login(&client).await;
102
let payload = json!({
103
"useCount": 3,
···
121
#[tokio::test]
122
async fn test_create_invite_codes_success() {
123
let client = client();
124
+
let (access_jwt, _did) = create_admin_account_and_login(&client).await;
125
let payload = json!({
126
"useCount": 2,
127
"codeCount": 3
···
141
assert!(body["codes"].is_array());
142
let codes = body["codes"].as_array().unwrap();
143
assert_eq!(codes.len(), 1);
144
+
assert_eq!(codes[0]["account"], "admin");
145
assert_eq!(codes[0]["codes"].as_array().unwrap().len(), 3);
146
}
147
148
#[tokio::test]
149
async fn test_create_invite_codes_for_multiple_accounts() {
150
let client = client();
151
+
let (access_jwt1, did1) = create_admin_account_and_login(&client).await;
152
let (_access_jwt2, did2) = create_account_and_login(&client).await;
153
let payload = json!({
154
"useCount": 1,
···
194
}
195
196
#[tokio::test]
197
+
async fn test_create_invite_codes_non_admin() {
198
let client = client();
199
let (access_jwt, _did) = create_account_and_login(&client).await;
200
+
let payload = json!({
201
+
"useCount": 2
202
+
});
203
+
let res = client
204
+
.post(format!(
205
+
"{}/xrpc/com.atproto.server.createInviteCodes",
206
+
base_url().await
207
+
))
208
+
.bearer_auth(&access_jwt)
209
+
.json(&payload)
210
+
.send()
211
+
.await
212
+
.expect("Failed to send request");
213
+
assert_eq!(res.status(), StatusCode::FORBIDDEN);
214
+
let body: Value = res.json().await.expect("Response was not valid JSON");
215
+
assert_eq!(body["error"], "AdminRequired");
216
+
}
217
+
218
+
#[tokio::test]
219
+
async fn test_get_account_invite_codes_success() {
220
+
let client = client();
221
+
let (admin_jwt, _admin_did) = create_admin_account_and_login(&client).await;
222
+
let (user_jwt, user_did) = create_account_and_login(&client).await;
223
+
224
let create_payload = json!({
225
+
"useCount": 5,
226
+
"forAccount": user_did
227
});
228
let _ = client
229
.post(format!(
230
"{}/xrpc/com.atproto.server.createInviteCode",
231
base_url().await
232
))
233
+
.bearer_auth(&admin_jwt)
234
.json(&create_payload)
235
.send()
236
.await
237
.expect("Failed to create invite code");
238
+
239
let res = client
240
.get(format!(
241
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
242
base_url().await
243
))
244
+
.bearer_auth(&user_jwt)
245
.send()
246
.await
247
.expect("Failed to send request");
···
256
assert!(code["disabled"].is_boolean());
257
assert!(code["createdAt"].is_string());
258
assert!(code["uses"].is_array());
259
+
assert_eq!(code["forAccount"], user_did);
260
+
assert_eq!(code["createdBy"], "admin");
261
}
262
263
#[tokio::test]
···
277
#[tokio::test]
278
async fn test_get_account_invite_codes_include_used_filter() {
279
let client = client();
280
+
let (admin_jwt, _admin_did) = create_admin_account_and_login(&client).await;
281
+
let (user_jwt, user_did) = create_account_and_login(&client).await;
282
+
283
let create_payload = json!({
284
+
"useCount": 5,
285
+
"forAccount": user_did
286
});
287
let _ = client
288
.post(format!(
289
"{}/xrpc/com.atproto.server.createInviteCode",
290
base_url().await
291
))
292
+
.bearer_auth(&admin_jwt)
293
.json(&create_payload)
294
.send()
295
.await
296
.expect("Failed to create invite code");
297
+
298
let res = client
299
.get(format!(
300
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
301
base_url().await
302
))
303
+
.bearer_auth(&user_jwt)
304
.query(&[("includeUsed", "false")])
305
.send()
306
.await
···
312
assert!(code["available"].as_i64().unwrap() > 0);
313
}
314
}
315
+
316
+
#[tokio::test]
317
+
async fn test_get_account_invite_codes_filters_disabled() {
318
+
let client = client();
319
+
let (admin_jwt, admin_did) = create_admin_account_and_login(&client).await;
320
+
321
+
let create_payload = json!({
322
+
"useCount": 5,
323
+
"forAccount": admin_did
324
+
});
325
+
let create_res = client
326
+
.post(format!(
327
+
"{}/xrpc/com.atproto.server.createInviteCode",
328
+
base_url().await
329
+
))
330
+
.bearer_auth(&admin_jwt)
331
+
.json(&create_payload)
332
+
.send()
333
+
.await
334
+
.expect("Failed to create invite code");
335
+
let create_body: Value = create_res.json().await.unwrap();
336
+
let code = create_body["code"].as_str().unwrap();
337
+
338
+
let disable_payload = json!({
339
+
"codes": [code]
340
+
});
341
+
let _ = client
342
+
.post(format!(
343
+
"{}/xrpc/com.atproto.admin.disableInviteCodes",
344
+
base_url().await
345
+
))
346
+
.bearer_auth(&admin_jwt)
347
+
.json(&disable_payload)
348
+
.send()
349
+
.await
350
+
.expect("Failed to disable invite code");
351
+
352
+
let res = client
353
+
.get(format!(
354
+
"{}/xrpc/com.atproto.server.getAccountInviteCodes",
355
+
base_url().await
356
+
))
357
+
.bearer_auth(&admin_jwt)
358
+
.send()
359
+
.await
360
+
.expect("Failed to send request");
361
+
assert_eq!(res.status(), StatusCode::OK);
362
+
let body: Value = res.json().await.expect("Response was not valid JSON");
363
+
let codes = body["codes"].as_array().unwrap();
364
+
for c in codes {
365
+
assert_ne!(c["code"].as_str().unwrap(), code, "Disabled code should be filtered out");
366
+
}
367
+
}