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 { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { client } from '../client.gen';
|
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 { 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, 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 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> = [
|
export type QueryKey<TOptions extends Options> = [
|
||||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||||
|
|
@ -1361,6 +1361,41 @@ export const setRegistrationStatusMutation = (options?: Partial<Options<SetRegis
|
||||||
return mutationOptions;
|
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.
|
* 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 type { Client, Options as Options2, TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
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> & {
|
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.
|
* 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 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 = {
|
export type DownloadResticPasswordData = {
|
||||||
body: {
|
body: {
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { Users, Settings as SettingsIcon } from "lucide-react";
|
import { Users, Settings as SettingsIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
getPasswordLoginStatusOptions,
|
||||||
getRegistrationStatusOptions,
|
getRegistrationStatusOptions,
|
||||||
|
setPasswordLoginStatusMutation,
|
||||||
setRegistrationStatusMutation,
|
setRegistrationStatusMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card";
|
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) => {
|
const onTabChange = (value: string) => {
|
||||||
void navigate({ to: ".", search: () => ({ tab: value }) });
|
void navigate({ to: ".", search: () => ({ tab: value }) });
|
||||||
};
|
};
|
||||||
|
|
@ -90,6 +108,24 @@ export function AdminPage({ appContext }: Props) {
|
||||||
disabled={updateRegistrationStatusMutation.isPending}
|
disabled={updateRegistrationStatusMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { useCallback, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -10,13 +13,13 @@ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/clie
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { logger } from "~/client/lib/logger";
|
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 { 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 { AlternativeSignInSection } from "../components/alternative-sign-in-section";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
@ -53,6 +56,11 @@ function hasSkippedRecoveryKeyDownload(userId: string) {
|
||||||
|
|
||||||
export function LoginPage({ error }: LoginPageProps = {}) {
|
export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const getOptions = useServerFn(getLoginOptions);
|
||||||
|
const { data: loginOptions } = useSuspenseQuery({
|
||||||
|
queryKey: ["login-options"],
|
||||||
|
queryFn: getOptions,
|
||||||
|
});
|
||||||
const [showResetDialog, setShowResetDialog] = useState(false);
|
const [showResetDialog, setShowResetDialog] = useState(false);
|
||||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
const [requires2FA, setRequires2FA] = useState(false);
|
const [requires2FA, setRequires2FA] = useState(false);
|
||||||
|
|
@ -265,66 +273,68 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
|
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
|
||||||
<Form {...form}>
|
{loginOptions.passwordLoginEnabled && (
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<Form {...form}>
|
||||||
<div
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
className={cn("rounded-md border border-destructive/50 p-3 text-sm", {
|
<div
|
||||||
hidden: !errorDescription,
|
className={cn("rounded-md border border-destructive/50 p-3 text-sm", {
|
||||||
})}
|
hidden: !errorDescription,
|
||||||
>
|
})}
|
||||||
{errorDescription}
|
>
|
||||||
</div>
|
{errorDescription}
|
||||||
<FormField
|
</div>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="username"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="username"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Username</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Username</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
{...field}
|
<Input
|
||||||
type="text"
|
{...field}
|
||||||
placeholder="admin"
|
type="text"
|
||||||
disabled={isLoggingIn}
|
placeholder="admin"
|
||||||
autoComplete="username webauthn"
|
disabled={isLoggingIn}
|
||||||
/>
|
autoComplete="username webauthn"
|
||||||
</FormControl>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="password"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="password"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<div className="flex items-center justify-between">
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<FormLabel>Password</FormLabel>
|
||||||
type="button"
|
<button
|
||||||
className="text-xs text-muted-foreground hover:underline"
|
type="button"
|
||||||
onClick={() => setShowResetDialog(true)}
|
className="text-xs text-muted-foreground hover:underline"
|
||||||
>
|
onClick={() => setShowResetDialog(true)}
|
||||||
Forgot your password?
|
>
|
||||||
</button>
|
Forgot your password?
|
||||||
</div>
|
</button>
|
||||||
<FormControl>
|
</div>
|
||||||
<Input
|
<FormControl>
|
||||||
{...field}
|
<Input
|
||||||
type="password"
|
{...field}
|
||||||
disabled={isLoggingIn}
|
type="password"
|
||||||
autoComplete="current-password webauthn"
|
disabled={isLoggingIn}
|
||||||
/>
|
autoComplete="current-password webauthn"
|
||||||
</FormControl>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<Button type="submit" className="w-full" loading={isLoggingIn}>
|
/>
|
||||||
Login
|
<Button type="submit" className="w-full" loading={isLoggingIn}>
|
||||||
</Button>
|
Login
|
||||||
</form>
|
</Button>
|
||||||
</Form>
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
<AlternativeSignInSection onPasskeySignIn={navigateAfterLogin} />
|
<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 DEFAULT_EXCLUDES = [RESTIC_PASS_FILE, REPOSITORY_BASE];
|
||||||
|
|
||||||
export const REGISTRATION_ENABLED_KEY = "registrations_enabled";
|
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 { createServerFn } from "@tanstack/react-start";
|
||||||
import { hasActivePasskeyUser } from "~/server/modules/auth/helpers";
|
import { hasActivePasskeyUser } from "~/server/modules/auth/helpers";
|
||||||
|
import { systemService } from "~/server/modules/system/system.service";
|
||||||
|
|
||||||
export const getLoginOptions = createServerFn({ method: "GET" }).handler(async () => ({
|
export const getLoginOptions = createServerFn({ method: "GET" }).handler(async () => ({
|
||||||
hasPasskeySignIn: await hasActivePasskeyUser(),
|
hasPasskeySignIn: await hasActivePasskeyUser(),
|
||||||
|
passwordLoginEnabled: await systemService.isPasswordLoginEnabled(),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ import {
|
||||||
getRegistrationStatusDto,
|
getRegistrationStatusDto,
|
||||||
registrationStatusBody,
|
registrationStatusBody,
|
||||||
type RegistrationStatusDto,
|
type RegistrationStatusDto,
|
||||||
|
getPasswordLoginStatusDto,
|
||||||
|
setPasswordLoginStatusDto,
|
||||||
|
passwordLoginStatusBody,
|
||||||
|
type PasswordLoginStatusDto,
|
||||||
getDevPanelDto,
|
getDevPanelDto,
|
||||||
type DevPanelDto,
|
type DevPanelDto,
|
||||||
} from "./system.dto";
|
} 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) => {
|
.get("/dev-panel", getDevPanelDto, async (c) => {
|
||||||
const enabled = systemService.isDevPanelEnabled();
|
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({
|
const devPanelResponse = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { cache, cacheKeys } from "../../utils/cache";
|
||||||
import { logger } from "@zerobyte/core/node";
|
import { logger } from "@zerobyte/core/node";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { appMetadataTable } from "../../db/schema";
|
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;
|
const CACHE_TTL = 60 * 60;
|
||||||
|
|
||||||
|
|
@ -107,6 +107,26 @@ const setRegistrationEnabled = async (enabled: boolean) => {
|
||||||
logger.info(`Registration enabled set to: ${enabled}`);
|
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;
|
const isDevPanelEnabled = () => config.flags.enableDevPanel;
|
||||||
|
|
||||||
export const systemService = {
|
export const systemService = {
|
||||||
|
|
@ -114,5 +134,7 @@ export const systemService = {
|
||||||
getUpdates,
|
getUpdates,
|
||||||
isRegistrationEnabled,
|
isRegistrationEnabled,
|
||||||
setRegistrationEnabled,
|
setRegistrationEnabled,
|
||||||
|
isPasswordLoginEnabled,
|
||||||
|
setPasswordLoginEnabled,
|
||||||
isDevPanelEnabled,
|
isDevPanelEnabled,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue