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:
parent
2318b6bdd0
commit
2b70267e01
10 changed files with 295 additions and 70 deletions
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue