Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,80 @@ const NOUNS: &[&str] = &[
/// create. Checked against the whole handle, so `administrator` is reserved
/// but `administrator-fan` is not — see `VENDOR_RESERVED` for the narrower set
/// that's also checked component-by-component.
///
/// Reviewed when claiming became free. Until then a claim cost a Pro
/// subscription, so the list only had to deter someone already paying; every
/// account can now attempt one. The additions are the names that read as "this
/// message comes from Voltius" — mail-system roles, trust words, and the terms
/// a billing or verification prompt would legitimately use.
const RESERVED: &[&str] = &[
"admin",
"administrator",
"support",
"help",
"helpdesk",
"voltius",
"voltiusapp",
"security",
"billing",
"payments",
"root",
"system",
"staff",
"moderator",
"mod",
"official",
"team",
"abuse",
"postmaster",
"webmaster",
"hostmaster",
"noreply",
"donotreply",
"notifications",
"account",
"accounts",
"verify",
"verified",
"trust",
"legal",
"privacy",
"info",
"contact",
"sales",
"api",
"owner",
];

/// Subset of `RESERVED` also rejected as a standalone `-`/`_` component
/// (`voltius-support`, `admin-2`). Narrower than `RESERVED` on purpose: the
/// list exists to stop vendor impersonation, not to ban ordinary English
/// words like "team" or "help" from appearing anywhere in a handle.
/// `noreply` also covers `no-reply` and `donotreply` covers `do-not-reply`:
/// `impersonation_key` strips the separators before the comparison.
const VENDOR_RESERVED: &[&str] = &[
"voltius",
"voltiusapp",
"support",
"security",
"billing",
"payments",
"admin",
"root",
"system",
"help",
"staff",
"official",
"moderator",
"abuse",
"postmaster",
"webmaster",
"hostmaster",
"noreply",
"donotreply",
"notifications",
"verify",
"verified",
];

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -206,6 +246,14 @@ mod tests {
"admin-2",
"administrator",
"team",
"no-reply",
"voltius-noreply",
"do-not-reply",
"verified-support",
"v3rified",
"postmaster",
"abuse",
"voltiusapp",
] {
assert_eq!(
validate_custom_handle(h),
Expand Down
32 changes: 21 additions & 11 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,24 +258,34 @@ pub async fn register(
};

// Auto-accept any pending invitations for this email
let pending = sqlx::query_as::<_, (Uuid, String)>(
"SELECT team_id, role FROM pending_invitations
let pending = sqlx::query_as::<_, (Uuid, String, Option<Uuid>)>(
"SELECT team_id, role, invited_by FROM pending_invitations
WHERE email = $1 AND accepted_at IS NULL AND expires_at > now()",
)
.bind(&email)
.fetch_all(&pool)
.await
.unwrap_or_default();

for (team_id, role) in &pending {
let _ = sqlx::query(
"INSERT INTO team_members (team_id, user_id, role) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
)
.bind(team_id)
.bind(user_id)
.bind(role)
.execute(&pool)
.await;
// Shared with the two explicit accept paths. This one used to write its own
// INSERT naming a `role` column that `team_members` has not had since the
// roles migration — the error was swallowed, so the invitations below were
// marked accepted while nobody was ever added to the team.
for (team_id, role, invited_by) in &pending {
match pool.acquire().await {
Ok(mut conn) => {
if let Err(status) = crate::routes::invitations::admit_member(
&mut conn, *team_id, user_id, *invited_by, role,
)
.await
{
error!(user_id = %user_id, team_id = %team_id, ?status, "Failed to auto-accept invitation on registration");
}
}
Err(e) => {
error!(error = %e, "Failed to acquire connection to auto-accept invitations")
}
}
}
if !pending.is_empty() {
let _ = sqlx::query(
Expand Down
9 changes: 1 addition & 8 deletions src/routes/billing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use uuid::Uuid;

use crate::auth::AuthUser;
use crate::lemonsqueezy::{parse_ls_datetime, tier_from_variant_id};
use crate::routes::email_not_verified_response;
use crate::self_host;

#[derive(Serialize)]
Expand Down Expand Up @@ -54,14 +55,6 @@ fn status_response(status: StatusCode) -> Response {
status.into_response()
}

fn email_not_verified_response() -> Response {
(
StatusCode::FORBIDDEN,
Json(serde_json::json!({ "error": "EMAIL_NOT_VERIFIED" })),
)
.into_response()
}

#[derive(Debug, Clone)]
struct LemonSubscriptionState {
subscription_id: String,
Expand Down
Loading