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
This commit is contained in:
Parman Mohammadalizadeh 2026-06-05 22:49:07 +03:30
parent 2318b6bdd0
commit 2b70267e01
10 changed files with 295 additions and 70 deletions

View file

@ -4,8 +4,8 @@
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen';
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getMirrorSyncStatus, getNotificationDestination, getOrgMembers, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, syncMirror, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetMirrorSyncStatusData, GetMirrorSyncStatusResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshRepositoryStatsData, RefreshRepositoryStatsResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, SyncMirrorData, SyncMirrorResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getMirrorSyncStatus, getNotificationDestination, getOrgMembers, getPasswordLoginStatus, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setPasswordLoginStatus, setRegistrationStatus, startDoctor, stopBackup, syncMirror, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetMirrorSyncStatusData, GetMirrorSyncStatusResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetPasswordLoginStatusData, GetPasswordLoginStatusResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshRepositoryStatsData, RefreshRepositoryStatsResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetPasswordLoginStatusData, SetPasswordLoginStatusResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, SyncMirrorData, SyncMirrorResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
export type QueryKey<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
@ -1361,6 +1361,41 @@ export const setRegistrationStatusMutation = (options?: Partial<Options<SetRegis
return mutationOptions;
};
export const getPasswordLoginStatusQueryKey = (options?: Options<GetPasswordLoginStatusData>) => createQueryKey('getPasswordLoginStatus', options);
/**
* Get whether password-based login is enabled
*/
export const getPasswordLoginStatusOptions = (options?: Options<GetPasswordLoginStatusData>) => queryOptions<GetPasswordLoginStatusResponse, DefaultError, GetPasswordLoginStatusResponse, ReturnType<typeof getPasswordLoginStatusQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getPasswordLoginStatus({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getPasswordLoginStatusQueryKey(options)
});
/**
* Enable or disable password-based login. Requires global admin role.
*/
export const setPasswordLoginStatusMutation = (options?: Partial<Options<SetPasswordLoginStatusData>>): UseMutationOptions<SetPasswordLoginStatusResponse, DefaultError, Options<SetPasswordLoginStatusData>> => {
const mutationOptions: UseMutationOptions<SetPasswordLoginStatusResponse, DefaultError, Options<SetPasswordLoginStatusData>> = {
mutationFn: async (fnOptions) => {
const { data } = await setPasswordLoginStatus({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.
*/

View file

@ -3,7 +3,7 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetMirrorSyncStatusData, GetMirrorSyncStatusResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, SyncMirrorData, SyncMirrorErrors, SyncMirrorResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetMirrorSyncStatusData, GetMirrorSyncStatusResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetPasswordLoginStatusData, GetPasswordLoginStatusResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetPasswordLoginStatusData, SetPasswordLoginStatusResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, SyncMirrorData, SyncMirrorErrors, SyncMirrorResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/**
@ -519,6 +519,23 @@ export const setRegistrationStatus = <ThrowOnError extends boolean = false>(opti
}
});
/**
* Get whether password-based login is enabled
*/
export const getPasswordLoginStatus = <ThrowOnError extends boolean = false>(options?: Options<GetPasswordLoginStatusData, ThrowOnError>) => (options?.client ?? client).get<GetPasswordLoginStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/password-login-status', ...options });
/**
* Enable or disable password-based login. Requires global admin role.
*/
export const setPasswordLoginStatus = <ThrowOnError extends boolean = false>(options: Options<SetPasswordLoginStatusData, ThrowOnError>) => (options.client ?? client).put<SetPasswordLoginStatusResponses, unknown, ThrowOnError>({
url: '/api/v1/system/password-login-status',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.
*/

View file

@ -4990,6 +4990,44 @@ export type SetRegistrationStatusResponses = {
export type SetRegistrationStatusResponse = SetRegistrationStatusResponses[keyof SetRegistrationStatusResponses];
export type GetPasswordLoginStatusData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/system/password-login-status';
};
export type GetPasswordLoginStatusResponses = {
/**
* Password login status
*/
200: {
enabled: boolean;
};
};
export type GetPasswordLoginStatusResponse = GetPasswordLoginStatusResponses[keyof GetPasswordLoginStatusResponses];
export type SetPasswordLoginStatusData = {
body: {
enabled: boolean;
};
path?: never;
query?: never;
url: '/api/v1/system/password-login-status';
};
export type SetPasswordLoginStatusResponses = {
/**
* Password login status updated
*/
200: {
enabled: boolean;
};
};
export type SetPasswordLoginStatusResponse = SetPasswordLoginStatusResponses[keyof SetPasswordLoginStatusResponses];
export type DownloadResticPasswordData = {
body: {
password: string;

View file

@ -3,7 +3,9 @@ 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";
@ -38,6 +40,22 @@ export function AdminPage({ appContext }: Props) {
},
});
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 }) });
};
@ -90,6 +108,24 @@ export function AdminPage({ appContext }: Props) {
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>

View file

@ -1,4 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useServerFn } from "@tanstack/react-start";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@ -10,13 +13,13 @@ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/clie
import { Label } from "~/client/components/ui/label";
import { authClient } from "~/client/lib/auth-client";
import { logger } from "~/client/lib/logger";
import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip";
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
import { PASSKEY_LOGIN_FAILED_ERROR } from "~/lib/sso-errors";
import { ResetPasswordDialog } from "../components/reset-password-dialog";
import { useNavigate } from "@tanstack/react-router";
import { normalizeUsername } from "~/lib/username";
import { cn } from "~/client/lib/utils";
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
import { getLoginOptions } from "~/server/lib/functions/login-options";
import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip";
import { PASSKEY_LOGIN_FAILED_ERROR } from "~/lib/sso-errors";
import { normalizeUsername } from "~/lib/username";
import { ResetPasswordDialog } from "../components/reset-password-dialog";
import { AlternativeSignInSection } from "../components/alternative-sign-in-section";
import { z } from "zod";
@ -53,6 +56,11 @@ function hasSkippedRecoveryKeyDownload(userId: string) {
export function LoginPage({ error }: LoginPageProps = {}) {
const navigate = useNavigate();
const getOptions = useServerFn(getLoginOptions);
const { data: loginOptions } = useSuspenseQuery({
queryKey: ["login-options"],
queryFn: getOptions,
});
const [showResetDialog, setShowResetDialog] = useState(false);
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [requires2FA, setRequires2FA] = useState(false);
@ -265,66 +273,68 @@ export function LoginPage({ error }: LoginPageProps = {}) {
return (
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div
className={cn("rounded-md border border-destructive/50 p-3 text-sm", {
hidden: !errorDescription,
})}
>
{errorDescription}
</div>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
{...field}
type="text"
placeholder="admin"
disabled={isLoggingIn}
autoComplete="username webauthn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<button
type="button"
className="text-xs text-muted-foreground hover:underline"
onClick={() => setShowResetDialog(true)}
>
Forgot your password?
</button>
</div>
<FormControl>
<Input
{...field}
type="password"
disabled={isLoggingIn}
autoComplete="current-password webauthn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" loading={isLoggingIn}>
Login
</Button>
</form>
</Form>
{loginOptions.passwordLoginEnabled && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div
className={cn("rounded-md border border-destructive/50 p-3 text-sm", {
hidden: !errorDescription,
})}
>
{errorDescription}
</div>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
{...field}
type="text"
placeholder="admin"
disabled={isLoggingIn}
autoComplete="username webauthn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<button
type="button"
className="text-xs text-muted-foreground hover:underline"
onClick={() => setShowResetDialog(true)}
>
Forgot your password?
</button>
</div>
<FormControl>
<Input
{...field}
type="password"
disabled={isLoggingIn}
autoComplete="current-password webauthn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" loading={isLoggingIn}>
Login
</Button>
</form>
</Form>
)}
<AlternativeSignInSection onPasskeySignIn={navigateAfterLogin} />

View file

@ -17,3 +17,4 @@ export const RESTORE_BLOCKED_ROOTS = [REPOSITORY_BASE, RESTIC_CACHE_DIR, RCLONE_
export const DEFAULT_EXCLUDES = [RESTIC_PASS_FILE, REPOSITORY_BASE];
export const REGISTRATION_ENABLED_KEY = "registrations_enabled";
export const PASSWORD_LOGIN_ENABLED_KEY = "password_login_enabled";

View file

@ -1,6 +1,8 @@
import { createServerFn } from "@tanstack/react-start";
import { hasActivePasskeyUser } from "~/server/modules/auth/helpers";
import { systemService } from "~/server/modules/system/system.service";
export const getLoginOptions = createServerFn({ method: "GET" }).handler(async () => ({
hasPasskeySignIn: await hasActivePasskeyUser(),
passwordLoginEnabled: await systemService.isPasswordLoginEnabled(),
}));

View file

@ -11,6 +11,10 @@ import {
getRegistrationStatusDto,
registrationStatusBody,
type RegistrationStatusDto,
getPasswordLoginStatusDto,
setPasswordLoginStatusDto,
passwordLoginStatusBody,
type PasswordLoginStatusDto,
getDevPanelDto,
type DevPanelDto,
} from "./system.dto";
@ -89,6 +93,24 @@ export const systemController = new Hono()
}
},
)
.get("/password-login-status", getPasswordLoginStatusDto, async (c) => {
const enabled = await systemService.isPasswordLoginEnabled();
return c.json<PasswordLoginStatusDto>({ enabled }, 200);
})
.put(
"/password-login-status",
requireAdmin,
setPasswordLoginStatusDto,
validator("json", passwordLoginStatusBody),
async (c) => {
const body = c.req.valid("json");
await systemService.setPasswordLoginEnabled(body.enabled);
return c.json<PasswordLoginStatusDto>({ enabled: body.enabled }, 200);
},
)
.get("/dev-panel", getDevPanelDto, async (c) => {
const enabled = systemService.isDevPanelEnabled();

View file

@ -123,6 +123,48 @@ export const setRegistrationStatusDto = describeRoute({
},
});
const passwordLoginStatusResponse = z.object({
enabled: z.boolean(),
});
export type PasswordLoginStatusDto = z.infer<typeof passwordLoginStatusResponse>;
export const passwordLoginStatusBody = z.object({
enabled: z.boolean(),
});
export const getPasswordLoginStatusDto = describeRoute({
description: "Get whether password-based login is enabled",
tags: ["System"],
operationId: "getPasswordLoginStatus",
responses: {
200: {
description: "Password login status",
content: {
"application/json": {
schema: resolver(passwordLoginStatusResponse),
},
},
},
},
});
export const setPasswordLoginStatusDto = describeRoute({
description: "Enable or disable password-based login. Requires global admin role.",
tags: ["System"],
operationId: "setPasswordLoginStatus",
responses: {
200: {
description: "Password login status updated",
content: {
"application/json": {
schema: resolver(passwordLoginStatusResponse),
},
},
},
},
});
const devPanelResponse = z.object({
enabled: z.boolean(),
});

View file

@ -6,7 +6,7 @@ import { cache, cacheKeys } from "../../utils/cache";
import { logger } from "@zerobyte/core/node";
import { db } from "../../db/db";
import { appMetadataTable } from "../../db/schema";
import { REGISTRATION_ENABLED_KEY } from "~/server/core/constants";
import { PASSWORD_LOGIN_ENABLED_KEY, REGISTRATION_ENABLED_KEY } from "~/server/core/constants";
const CACHE_TTL = 60 * 60;
@ -107,6 +107,26 @@ const setRegistrationEnabled = async (enabled: boolean) => {
logger.info(`Registration enabled set to: ${enabled}`);
};
const isPasswordLoginEnabled = async () => {
const result = await db.query.appMetadataTable.findFirst({
where: { key: PASSWORD_LOGIN_ENABLED_KEY },
});
// Default to true so existing deployments are unaffected
return result === undefined || result.value === "true";
};
const setPasswordLoginEnabled = async (enabled: boolean) => {
const now = Date.now();
await db
.insert(appMetadataTable)
.values({ key: PASSWORD_LOGIN_ENABLED_KEY, value: JSON.stringify(enabled), createdAt: now, updatedAt: now })
.onConflictDoUpdate({ target: appMetadataTable.key, set: { value: JSON.stringify(enabled), updatedAt: now } });
logger.info(`Password login enabled set to: ${enabled}`);
};
const isDevPanelEnabled = () => config.flags.enableDevPanel;
export const systemService = {
@ -114,5 +134,7 @@ export const systemService = {
getUpdates,
isRegistrationEnabled,
setRegistrationEnabled,
isPasswordLoginEnabled,
setPasswordLoginEnabled,
isDevPanelEnabled,
};