feat: complete user management workflow

This commit is contained in:
2026-07-02 01:18:52 -07:00
parent e3e7b0d8e0
commit 99bcfb1038
12 changed files with 663 additions and 46 deletions

View File

@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import type { ApiResult } from "@/modules/core/server/api";
import type { Organization } from "@/modules/core/types";
type AuthMode = "login" | "register" | "setup";
@@ -30,6 +31,7 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
const [hasAccounts, setHasAccounts] = useState<boolean | null>(null);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const copy = modeCopy[mode];
const isSetup = mode === "setup";
@@ -39,17 +41,23 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
let isMounted = true;
async function loadState(): Promise<void> {
const [bootstrapResponse, sessionResponse] = await Promise.all([
const [bootstrapResponse, sessionResponse, organizationsResponse] = await Promise.all([
fetch("/api/auth/bootstrap"),
fetch("/api/auth/session"),
fetch("/api/organizations"),
]);
const bootstrap = (await bootstrapResponse.json()) as ApiResult<{ setupRequired: boolean }>;
const session = (await sessionResponse.json()) as ApiResult<{ account: unknown | null }>;
const organizationsResult = (await organizationsResponse.json()) as ApiResult<{ organizations: Organization[] }>;
if (!isMounted || !bootstrap.success || !session.success) {
return;
}
if (organizationsResult.success) {
setOrganizations(organizationsResult.organizations);
}
const initialized = !bootstrap.setupRequired;
setHasAccounts(initialized);
@@ -97,6 +105,7 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
const email = String(formData.get("email") ?? "");
const password = String(formData.get("password") ?? "");
const organization = String(formData.get("organization") ?? "");
const organizationId = String(formData.get("organizationId") ?? "");
const endpoint = isLogin ? "/api/auth/login" : isSetup ? "/api/auth/setup" : "/api/auth/register";
const response = await fetch(endpoint, {
@@ -107,9 +116,10 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
email,
password,
organization,
organizationId,
}),
});
const result = (await response.json()) as ApiResult<{ account: unknown }>;
const result = (await response.json()) as ApiResult<{ account: unknown; pendingApproval?: boolean }>;
setIsPending(false);
if (!result.success) {
@@ -117,6 +127,11 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
return;
}
if (result.pendingApproval) {
setMessage(result.reason);
return;
}
router.refresh();
router.replace(isLogin ? redirectTo : "/app/dashboard");
}
@@ -180,6 +195,23 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement {
</label>
) : null}
{mode === "register" && organizations.length > 0 ? (
<label className="block space-y-2">
<span className="text-sm font-medium"></span>
<select
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
name="organizationId"
>
<option value=""></option>
{organizations.map((organization) => (
<option key={organization.id} value={organization.id}>
{organization.name}
</option>
))}
</select>
</label>
) : null}
{!isLogin ? (
<label className="block space-y-2">
<span className="text-sm font-medium"></span>