feat(web): login, first-run setup, org settings; org-aware AuthProvider
- Route group (app) holds AuthProvider + Sidebar, so /login and /setup
render without app chrome and never mount the provider.
- AuthProvider drops the removed auth_enabled flag and exposes
{user, org, isAdmin}.
- New login page (password + SSO), first-run setup page, and org settings
page with a members table and the OIDC provider form.
- Settings page and the Organization nav entry are gated on role, since
/api/settings now 403s for members.
- GET /api/org/oidc gains client_secret_set so the UI can show whether a
secret is stored; the secret itself is still never serialized, and an
empty submitted value still means "keep the stored one".
- Fix logout: the sidebar linked to /auth/logout with a GET, but the route
is POST-only, so logout was 404ing.
This commit is contained in:
@@ -1,49 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
|
||||
import { auth, type Org, type Role, type SessionUser } from "@/lib/api";
|
||||
|
||||
export interface User {
|
||||
user_id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
export type { Org, Role, SessionUser };
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
authEnabled: boolean;
|
||||
user: SessionUser | null;
|
||||
org: Org | null;
|
||||
/** True for owner and admin — the roles the /api/settings and /api/org routes require. */
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({ user: null, authEnabled: false });
|
||||
const AuthContext = createContext<AuthContextType>({ user: null, org: null, isAdmin: false });
|
||||
|
||||
export function useAuth() {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the authenticated app shell only (see app/(app)/layout.tsx). /login and
|
||||
* /setup live outside the group, so no pathname guard is needed here.
|
||||
*/
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [authEnabled, setAuthEnabled] = useState(false);
|
||||
const [user, setUser] = useState<SessionUser | null>(null);
|
||||
const [org, setOrg] = useState<Org | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/auth/me", { credentials: "include" })
|
||||
.then(async (res) => {
|
||||
if (res.status === 401) {
|
||||
window.location.href = "/auth/login";
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const status = await auth.bootstrapStatus();
|
||||
if (status.needs_setup) {
|
||||
window.location.href = "/setup";
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.auth_enabled === false) {
|
||||
setAuthEnabled(false);
|
||||
} else {
|
||||
setAuthEnabled(true);
|
||||
setUser(data as User);
|
||||
|
||||
const me = await auth.me();
|
||||
if (cancelled) return;
|
||||
setUser(me.user);
|
||||
setOrg(me.org);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
const status = (err as { status?: number }).status;
|
||||
if (status === 401) {
|
||||
window.location.href = "/login";
|
||||
return;
|
||||
}
|
||||
// Backend unreachable or unexpected failure — don't trap the user on a spinner.
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
// Backend unreachable — don't block the UI
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
@@ -54,9 +68,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
const isAdmin = user?.role === "owner" || user?.role === "admin";
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, authEnabled }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
<AuthContext.Provider value={{ user, org, isAdmin }}>{children}</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user