This commit is contained in:
Andras Schmelczer 2026-05-14 22:07:14 +01:00
parent 084117cea8
commit a8de0a614d
36 changed files with 1329 additions and 522 deletions

View file

@ -193,13 +193,31 @@ async fn verify_is_admin(
Ok(body["is_admin"].as_bool().unwrap_or(false))
}
async fn lookup_unused_invite(
fn redeemable_invite_filter(code: &str, user_id: &str) -> Result<String, &'static str> {
validate_invite_code(code)?;
if user_id.is_empty()
|| user_id.len() > 32
|| !user_id.bytes().all(|b| b.is_ascii_alphanumeric())
{
return Err("Invalid user id");
}
Ok(format!(
"code=\"{}\" && (used_by_id=\"\" || used_by_id=\"{}\")",
code, user_id
))
}
async fn lookup_redeemable_invite(
state: &AppState,
pb_url: &str,
token: &str,
code: &str,
user_id: &str,
) -> Result<Option<serde_json::Value>, Response> {
let filter = format!("code=\"{}\" && used_by_id=\"\"", code);
let filter = match redeemable_invite_filter(code, user_id) {
Ok(filter) => filter,
Err(msg) => return Err((StatusCode::BAD_REQUEST, msg).into_response()),
};
let lookup_url = format!(
"{pb_url}/api/collections/invites/records?filter={}&perPage=1",
urlencoding::encode(&filter)
@ -590,7 +608,7 @@ pub async fn post_redeem_invite(
}
};
let invite = match lookup_unused_invite(&state, pb_url, &token, &req.code).await {
let invite = match lookup_redeemable_invite(&state, pb_url, &token, &req.code, &user.id).await {
Ok(Some(invite)) => invite,
Ok(None) => {
return (StatusCode::NOT_FOUND, "Invalid or already used invite code").into_response()
@ -617,13 +635,17 @@ pub async fn post_redeem_invite(
return StatusCode::BAD_GATEWAY.into_response();
}
};
let used_by_id = invite["used_by_id"].as_str().unwrap_or_default();
if !used_by_id.is_empty() && used_by_id != user.id {
return (StatusCode::NOT_FOUND, "Invalid or already used invite code").into_response();
}
if invite_type == "admin" {
if let Err(response) = grant_license_for_invite(&state, pb_url, &token, &user.id).await {
if let Err(response) = mark_invite_used(&state, pb_url, &token, invite_id, &user.id).await {
return response;
}
if let Err(response) = mark_invite_used(&state, pb_url, &token, invite_id, &user.id).await {
if let Err(response) = grant_license_for_invite(&state, pb_url, &token, &user.id).await {
return response;
}
@ -635,6 +657,10 @@ pub async fn post_redeem_invite(
.into_response();
}
if !used_by_id.is_empty() {
return (StatusCode::NOT_FOUND, "Invalid or already used invite code").into_response();
}
match active_referral_checkout_user(&state, invite_id).await {
Ok(Some(active_user_id)) if active_user_id != user.id => {
return (
@ -663,6 +689,26 @@ pub async fn post_redeem_invite(
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redeemable_invite_filter_allows_unused_or_same_user_invite() {
let filter = redeemable_invite_filter("abc123", "user123").unwrap();
assert_eq!(
filter,
"code=\"abc123\" && (used_by_id=\"\" || used_by_id=\"user123\")"
);
}
#[test]
fn redeemable_invite_filter_rejects_unsafe_values() {
assert!(redeemable_invite_filter("bad-code", "user123").is_err());
assert!(redeemable_invite_filter("abc123", "bad-user").is_err());
}
}
/// List invites. Users only see invites they created, including admins.
pub async fn get_invites(
State(shared): State<Arc<SharedState>>,