zerobyte/app/client/modules/admin/routes/admin-page.tsx
Parman Mohammadalizadeh 2b70267e01 feat: add option to disable password login
Adds a global admin setting to hide the username/password form on the
login page, useful for deployments that rely solely on SSO/OIDC or
passkeys. The form defaults to enabled so existing deployments are
unaffected.

Changes:
- Add PASSWORD_LOGIN_ENABLED_KEY constant
- Add isPasswordLoginEnabled / setPasswordLoginEnabled to system service
  (defaults to true when no DB record exists)
- Add GET/PUT /api/v1/system/password-login-status endpoints (GET
  auth-gated for admin use; setting is exposed to the login page via
  the existing getLoginOptions server function)
- Extend getLoginOptions to include passwordLoginEnabled
- Conditionally render the password login form in LoginPage
- Add "Enable password login" toggle in the admin System Settings tab,
  next to "Enable new user registrations"
- Add corresponding entries to generated API client files
  (types.gen.ts, sdk.gen.ts, @tanstack/react-query.gen.ts)

Closes #941
2026-06-05 22:49:07 +03:30

136 lines
4.7 KiB
TypeScript

import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Users, Settings as SettingsIcon } from "lucide-react";
import { toast } from "sonner";
import {
getPasswordLoginStatusOptions,
getRegistrationStatusOptions,
setPasswordLoginStatusMutation,
setRegistrationStatusMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card";
import { Label } from "~/client/components/ui/label";
import { Switch } from "~/client/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
import type { AppContext } from "~/context";
import { UserManagement } from "~/client/modules/settings/components/user-management";
type Props = {
appContext: AppContext;
};
export function AdminPage({ appContext }: Props) {
const { tab } = useSearch({ from: "/(dashboard)/admin/" });
const activeTab = tab || "users";
const navigate = useNavigate();
const registrationStatus = useSuspenseQuery({
...getRegistrationStatusOptions(),
});
const updateRegistrationStatusMutation = useMutation({
...setRegistrationStatusMutation(),
onSuccess: () => {
toast.success("Registration settings updated");
},
onError: (error) => {
toast.error("Failed to update registration settings", {
description: error.message,
});
},
});
const passwordLoginStatus = useSuspenseQuery({
...getPasswordLoginStatusOptions(),
});
const updatePasswordLoginStatusMutation = useMutation({
...setPasswordLoginStatusMutation(),
onSuccess: () => {
toast.success("Login settings updated");
},
onError: (error) => {
toast.error("Failed to update login settings", {
description: error.message,
});
},
});
const onTabChange = (value: string) => {
void navigate({ to: ".", search: () => ({ tab: value }) });
};
return (
<div className="space-y-6">
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
<TabsList>
<TabsTrigger value="users">Users</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
</TabsList>
<div className="mt-2">
<TabsContent value="users" className="mt-0">
<Card className="p-0 gap-0">
<div className="border-b border-border/50 bg-card-header p-6">
<CardTitle className="flex items-center gap-2">
<Users className="size-5" />
User Management
</CardTitle>
<CardDescription className="mt-1.5">Manage users, roles and permissions</CardDescription>
</div>
<UserManagement currentUser={appContext.user} />
</Card>
</TabsContent>
<TabsContent value="system" className="mt-0">
<Card className="p-0 gap-0">
<div className="border-b border-border/50 bg-card-header p-6">
<CardTitle className="flex items-center gap-2">
<SettingsIcon className="size-5" />
System Settings
</CardTitle>
<CardDescription className="mt-1.5">Manage system-wide settings</CardDescription>
</div>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="enable-registrations" className="text-base">
Enable new user registrations
</Label>
<p className="text-sm text-muted-foreground max-w-2xl">When enabled, new users can sign up</p>
</div>
<Switch
id="enable-registrations"
checked={registrationStatus.data.enabled}
onCheckedChange={(checked) =>
updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })
}
disabled={updateRegistrationStatusMutation.isPending}
/>
</div>
<div className="flex items-center justify-between pt-4 border-t border-border/50">
<div className="space-y-0.5">
<Label htmlFor="enable-password-login" className="text-base">
Enable password login
</Label>
<p className="text-sm text-muted-foreground max-w-2xl">
When disabled, the username and password form is hidden on the login page. Users can still sign in via SSO or passkeys.
</p>
</div>
<Switch
id="enable-password-login"
checked={passwordLoginStatus.data.enabled}
onCheckedChange={(checked) =>
updatePasswordLoginStatusMutation.mutate({ body: { enabled: checked } })
}
disabled={updatePasswordLoginStatusMutation.isPending}
/>
</div>
</CardContent>
</Card>
</TabsContent>
</div>
</Tabs>
</div>
);
}