diff --git a/src/auth/routes.rs b/src/auth/routes.rs --- a/src/auth/routes.rs +++ b/src/auth/routes.rs @@ -210,6 +210,29 @@ .did() .await .ok_or_else(|| AppError::Internal("no DID in OAuth session".into()))?; + // Check if the user is authorized to access the dashboard. + // Allow login when no users exist yet (first user will be bootstrapped as admin). + // Otherwise, only allow users already in the users table. + let user_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") + .fetch_one(&state.db) + .await + .map_err(|e| AppError::Internal(format!("user count query failed: {e}")))?; + + if user_count.0 > 0 { + let user_exists: Option<(i32,)> = sqlx::query_as(&adapt_sql( + "SELECT 1 FROM users WHERE did = ?", + state.db_backend, + )) + .bind(did.as_ref()) + .fetch_optional(&state.db) + .await + .map_err(|e| AppError::Internal(format!("user lookup failed: {e}")))?; + + if user_exists.is_none() { + return Ok((jar, Redirect::to("/login?error=not_authorized"))); + } + } + // Look up the client_key for the API client so we can store it in the session cookie // for per-client rate limiting. let client_key = if let Some(ref cid) = client_id { diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -1,13 +1,20 @@ "use client" import { useEffect } from "react" -import { useRouter } from "next/navigation" +import { useRouter, useSearchParams } from "next/navigation" import { LoginForm } from "@/components/login-form" import { useAuth } from "@/lib/auth-context" + +const ERROR_MESSAGES: Record = { + not_authorized: "Your account is not authorized to access this dashboard.", +} export default function LoginPage() { const { did } = useAuth() const router = useRouter() + const searchParams = useSearchParams() + const errorParam = searchParams.get("error") + const errorMessage = errorParam ? ERROR_MESSAGES[errorParam] ?? errorParam : null useEffect(() => { if (did) router.replace("/dashboard") @@ -18,7 +25,7 @@ return (
- +
) diff --git a/web/src/components/login-form.tsx b/web/src/components/login-form.tsx --- a/web/src/components/login-form.tsx +++ b/web/src/components/login-form.tsx @@ -15,8 +15,9 @@ import { Input } from "@/components/ui/input" export function LoginForm({ className, + externalError, ...props -}: React.ComponentProps<"div">) { +}: React.ComponentProps<"div"> & { externalError?: string | null }) { const [handle, setHandle] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -45,8 +46,8 @@ Sign in with your ATProto account to manage your AppView. - {error && ( -

{error}

+ {(externalError || error) && ( +

{externalError || error}

)} Handle