mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
refactor!: merge settings into logto configs table
This commit is contained in:
parent
eec39f7d9a
commit
d0399eb8a4
34 changed files with 440 additions and 354 deletions
|
@ -8,7 +8,7 @@ import { createPoolFromConfig } from '../../../database.js';
|
|||
import {
|
||||
getCurrentDatabaseAlterationTimestamp,
|
||||
updateDatabaseTimestamp,
|
||||
} from '../../../queries/logto-config.js';
|
||||
} from '../../../queries/system.js';
|
||||
import { log } from '../../../utilities.js';
|
||||
import type { AlterationFile } from './type.js';
|
||||
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
LogtoOidcConfigKey,
|
||||
managementResource,
|
||||
defaultSignInExperience,
|
||||
createDefaultSetting,
|
||||
createDefaultAdminConsoleConfig,
|
||||
createDemoAppApplication,
|
||||
defaultRole,
|
||||
managementResourceScope,
|
||||
|
@ -26,9 +26,9 @@ import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database.js'
|
|||
import {
|
||||
getRowsByKeys,
|
||||
doesConfigsTableExist,
|
||||
updateDatabaseTimestamp,
|
||||
updateValueByKey,
|
||||
} from '../../../queries/logto-config.js';
|
||||
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
||||
import { getPathInModule, log, oraPromise } from '../../../utilities.js';
|
||||
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
||||
import { getAlterationDirectory } from '../alteration/utils.js';
|
||||
|
@ -89,7 +89,7 @@ const seedTables = async (connection: DatabaseTransactionConnection, latestTimes
|
|||
await Promise.all([
|
||||
connection.query(insertInto(managementResource, 'resources')),
|
||||
connection.query(insertInto(managementResourceScope, 'scopes')),
|
||||
connection.query(insertInto(createDefaultSetting(), 'settings')),
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
|
||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
||||
connection.query(insertInto(defaultRole, 'roles')),
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { AlterationState, LogtoConfig, LogtoConfigKey } from '@logto/schemas';
|
||||
import { logtoConfigGuards, LogtoConfigs, AlterationStateKey } from '@logto/schemas';
|
||||
import type { LogtoConfig, LogtoConfigKey, logtoConfigGuards } from '@logto/schemas';
|
||||
import { LogtoConfigs } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
import type { z } from 'zod';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
|
||||
|
@ -34,36 +34,3 @@ export const updateValueByKey = async <T extends LogtoConfigKey>(
|
|||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`
|
||||
);
|
||||
|
||||
export const getCurrentDatabaseAlterationTimestamp = async (pool: CommonQueryMethods) => {
|
||||
try {
|
||||
const result = await pool.maybeOne<LogtoConfig>(
|
||||
sql`select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}`
|
||||
);
|
||||
const parsed = logtoConfigGuards[AlterationStateKey.AlterationState].safeParse(result?.value);
|
||||
|
||||
return (parsed.success && parsed.data.timestamp) || 0;
|
||||
} catch (error: unknown) {
|
||||
const result = z.object({ code: z.string() }).safeParse(error);
|
||||
|
||||
// Relation does not exist, treat as 0
|
||||
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
||||
if (result.success && result.data.code === '42P01') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDatabaseTimestamp = async (
|
||||
connection: DatabaseTransactionConnection,
|
||||
timestamp: number
|
||||
) => {
|
||||
const value: AlterationState = {
|
||||
timestamp,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return updateValueByKey(connection, AlterationStateKey.AlterationState, value);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { AlterationStateKey, LogtoConfigs } from '@logto/schemas';
|
||||
import { AlterationStateKey, Systems } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import type { QueryType } from '../test-utilities.js';
|
||||
import { expectSqlAssert } from '../test-utilities.js';
|
||||
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config.js';
|
||||
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './system.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
|
@ -15,7 +15,7 @@ const pool = createMockPool({
|
|||
return mockQuery(sql, values);
|
||||
},
|
||||
});
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
const { table, fields } = convertToIdentifiers(Systems);
|
||||
const timestamp = 1_663_923_776;
|
||||
|
||||
describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
67
packages/cli/src/queries/system.ts
Normal file
67
packages/cli/src/queries/system.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import type { AlterationState, System } from '@logto/schemas';
|
||||
import { Systems, logtoConfigGuards, AlterationStateKey } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
const { fields } = convertToIdentifiers(Systems);
|
||||
|
||||
const doesTableExist = async (pool: CommonQueryMethods, table: string) => {
|
||||
const { rows } = await pool.query<{ regclass: Nullable<string> }>(
|
||||
sql`select to_regclass(${table}) as regclass`
|
||||
);
|
||||
|
||||
return Boolean(rows[0]?.regclass);
|
||||
};
|
||||
|
||||
export const doesSystemsTableExist = async (pool: CommonQueryMethods) =>
|
||||
doesTableExist(pool, Systems.table);
|
||||
|
||||
const getAlterationStateTable = async (pool: CommonQueryMethods) =>
|
||||
(await doesSystemsTableExist(pool))
|
||||
? sql.identifier([Systems.table])
|
||||
: sql.identifier(['_logto_configs']); // Fall back to the old config table
|
||||
|
||||
export const getCurrentDatabaseAlterationTimestamp = async (pool: CommonQueryMethods) => {
|
||||
const table = await getAlterationStateTable(pool);
|
||||
|
||||
try {
|
||||
const result = await pool.maybeOne<System>(
|
||||
sql`select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}`
|
||||
);
|
||||
const parsed = logtoConfigGuards[AlterationStateKey.AlterationState].safeParse(result?.value);
|
||||
|
||||
return (parsed.success && parsed.data.timestamp) || 0;
|
||||
} catch (error: unknown) {
|
||||
const result = z.object({ code: z.string() }).safeParse(error);
|
||||
|
||||
// Relation does not exist, treat as 0
|
||||
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
||||
if (result.success && result.data.code === '42P01') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDatabaseTimestamp = async (
|
||||
connection: DatabaseTransactionConnection,
|
||||
timestamp: number
|
||||
) => {
|
||||
const table = await getAlterationStateTable(connection);
|
||||
const value: AlterationState = {
|
||||
timestamp,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await connection.query(
|
||||
sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values (${AlterationStateKey.AlterationState}, ${sql.jsonb(value)})
|
||||
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
||||
`
|
||||
);
|
||||
};
|
|
@ -7,8 +7,8 @@ import { Outlet, useHref, useLocation, useNavigate } from 'react-router-dom';
|
|||
import AppError from '@/components/AppError';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import SessionExpired from '@/components/SessionExpired';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useScroll from '@/hooks/use-scroll';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
|
||||
import Sidebar, { getPath } from './components/Sidebar';
|
||||
|
@ -20,8 +20,8 @@ const AppContent = () => {
|
|||
const { isAuthenticated, isLoading: isLogtoLoading, error, signIn } = useLogto();
|
||||
const href = useHref('/callback');
|
||||
const { isLoading: isPreferencesLoading } = useUserPreferences();
|
||||
const { isLoading: isSettingsLoading } = useSettings();
|
||||
const isLoading = isPreferencesLoading || isSettingsLoading;
|
||||
const { isLoading: isConfigsLoading } = useConfigs();
|
||||
const isLoading = isPreferencesLoading || isConfigsLoading;
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
|
36
packages/console/src/hooks/use-configs.ts
Normal file
36
packages/console/src/hooks/use-configs.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import type { AdminConsoleData } from '@logto/schemas';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import type { RequestError } from './use-api';
|
||||
import useApi from './use-api';
|
||||
|
||||
const useConfigs = () => {
|
||||
const { isAuthenticated, error: authError } = useLogto();
|
||||
const shouldFetch = isAuthenticated && !authError;
|
||||
const {
|
||||
data: configs,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<AdminConsoleData, RequestError>(shouldFetch && '/api/configs/admin-console');
|
||||
const api = useApi();
|
||||
|
||||
const updateConfigs = async (json: Partial<AdminConsoleData>) => {
|
||||
const updatedConfigs = await api
|
||||
.patch('/api/configs/admin-console', {
|
||||
json,
|
||||
})
|
||||
.json<AdminConsoleData>();
|
||||
void mutate(updatedConfigs);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading: !configs && !error,
|
||||
configs,
|
||||
error,
|
||||
mutate,
|
||||
updateConfigs,
|
||||
};
|
||||
};
|
||||
|
||||
export default useConfigs;
|
|
@ -1,40 +0,0 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import type { AdminConsoleConfig, Setting } from '@logto/schemas';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import type { RequestError } from './use-api';
|
||||
import useApi from './use-api';
|
||||
|
||||
const useSettings = () => {
|
||||
const { isAuthenticated, error: authError } = useLogto();
|
||||
const shouldFetch = isAuthenticated && !authError;
|
||||
const {
|
||||
data: settings,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<Setting, RequestError>(shouldFetch && '/api/settings');
|
||||
const api = useApi();
|
||||
|
||||
const updateSettings = async (delta: Partial<AdminConsoleConfig>) => {
|
||||
const updatedSettings = await api
|
||||
.patch('/api/settings', {
|
||||
json: {
|
||||
adminConsole: {
|
||||
...delta,
|
||||
},
|
||||
},
|
||||
})
|
||||
.json<Setting>();
|
||||
void mutate(updatedSettings);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading: !settings && !error,
|
||||
settings: settings?.adminConsole,
|
||||
error,
|
||||
mutate,
|
||||
updateSettings,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSettings;
|
|
@ -11,7 +11,7 @@ import ModalLayout from '@/components/ModalLayout';
|
|||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
|
@ -30,7 +30,7 @@ type Props = {
|
|||
};
|
||||
|
||||
const CreateForm = ({ onClose }: Props) => {
|
||||
const { updateSettings } = useSettings();
|
||||
const { updateConfigs } = useConfigs();
|
||||
const [createdApp, setCreatedApp] = useState<Application>();
|
||||
const [isGetStartedModalOpen, setIsGetStartedModalOpen] = useState(false);
|
||||
const {
|
||||
|
@ -58,7 +58,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
const createdApp = await api.post('/api/applications', { json: data }).json<Application>();
|
||||
setCreatedApp(createdApp);
|
||||
setIsGetStartedModalOpen(true);
|
||||
void updateSettings({ applicationCreated: true });
|
||||
void updateConfigs({ applicationCreated: true });
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,7 +16,7 @@ import IconButton from '@/components/IconButton';
|
|||
import Markdown from '@/components/Markdown';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||
import { safeParseJson } from '@/utilities/json';
|
||||
|
||||
|
@ -33,7 +33,7 @@ type Props = {
|
|||
const Guide = ({ connector, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const { updateSettings } = useSettings();
|
||||
const { updateConfigs } = useConfigs();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
|
||||
const { language } = i18next;
|
||||
|
@ -89,7 +89,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
await updateSettings({
|
||||
await updateConfigs({
|
||||
...conditional(!isSocialConnector && { passwordlessConfigured: true }),
|
||||
...conditional(isSocialConnector && { socialSignInConfigured: true }),
|
||||
});
|
||||
|
|
|
@ -19,8 +19,8 @@ import SocialDark from '@/assets/images/social-dark.svg';
|
|||
import Social from '@/assets/images/social.svg';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
type GetStartedMetadata = {
|
||||
|
@ -36,7 +36,7 @@ type GetStartedMetadata = {
|
|||
|
||||
const useGetStartedMetadata = () => {
|
||||
const documentationUrl = useDocumentationUrl();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const { configs, updateConfigs } = useConfigs();
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === AppearanceMode.LightMode;
|
||||
const { data: demoApp, error } = useSWR<Application, RequestError>(
|
||||
|
@ -63,10 +63,10 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card1_subtitle',
|
||||
icon: isLightMode ? CheckDemo : CheckDemoDark,
|
||||
buttonText: 'general.check_out',
|
||||
isComplete: settings?.demoChecked,
|
||||
isComplete: configs?.demoChecked,
|
||||
isHidden: hideDemo,
|
||||
onClick: async () => {
|
||||
void updateSettings({ demoChecked: true });
|
||||
void updateConfigs({ demoChecked: true });
|
||||
window.open('/demo-app', '_blank');
|
||||
},
|
||||
},
|
||||
|
@ -76,7 +76,7 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card2_subtitle',
|
||||
icon: isLightMode ? CreateApp : CreateAppDark,
|
||||
buttonText: 'general.create',
|
||||
isComplete: settings?.applicationCreated,
|
||||
isComplete: configs?.applicationCreated,
|
||||
onClick: () => {
|
||||
navigate('/applications/create');
|
||||
},
|
||||
|
@ -87,7 +87,7 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card3_subtitle',
|
||||
icon: isLightMode ? Customize : CustomizeDark,
|
||||
buttonText: 'general.customize',
|
||||
isComplete: settings?.signInExperienceCustomized,
|
||||
isComplete: configs?.signInExperienceCustomized,
|
||||
onClick: () => {
|
||||
navigate('/sign-in-experience');
|
||||
},
|
||||
|
@ -98,7 +98,7 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card4_subtitle',
|
||||
icon: isLightMode ? Passwordless : PasswordlessDark,
|
||||
buttonText: 'general.set_up',
|
||||
isComplete: settings?.passwordlessConfigured,
|
||||
isComplete: configs?.passwordlessConfigured,
|
||||
onClick: () => {
|
||||
navigate(`/connectors/${ConnectorsTabs.Passwordless}`);
|
||||
},
|
||||
|
@ -109,7 +109,7 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card5_subtitle',
|
||||
icon: isLightMode ? Social : SocialDark,
|
||||
buttonText: 'general.add',
|
||||
isComplete: settings?.socialSignInConfigured,
|
||||
isComplete: configs?.socialSignInConfigured,
|
||||
onClick: () => {
|
||||
navigate(`/connectors/${ConnectorsTabs.Social}`);
|
||||
},
|
||||
|
@ -120,9 +120,9 @@ const useGetStartedMetadata = () => {
|
|||
subtitle: 'get_started.card6_subtitle',
|
||||
icon: isLightMode ? FurtherReadings : FurtherReadingsDark,
|
||||
buttonText: 'general.check_out',
|
||||
isComplete: settings?.furtherReadingsChecked,
|
||||
isComplete: configs?.furtherReadingsChecked,
|
||||
onClick: () => {
|
||||
void updateSettings({ furtherReadingsChecked: true });
|
||||
void updateConfigs({ furtherReadingsChecked: true });
|
||||
window.open(`${documentationUrl}/docs/tutorials/get-started/further-readings/`, '_blank');
|
||||
},
|
||||
},
|
||||
|
@ -134,13 +134,13 @@ const useGetStartedMetadata = () => {
|
|||
hideDemo,
|
||||
isLightMode,
|
||||
navigate,
|
||||
settings?.applicationCreated,
|
||||
settings?.demoChecked,
|
||||
settings?.furtherReadingsChecked,
|
||||
settings?.passwordlessConfigured,
|
||||
settings?.signInExperienceCustomized,
|
||||
settings?.socialSignInConfigured,
|
||||
updateSettings,
|
||||
configs?.applicationCreated,
|
||||
configs?.demoChecked,
|
||||
configs?.furtherReadingsChecked,
|
||||
configs?.passwordlessConfigured,
|
||||
configs?.signInExperienceCustomized,
|
||||
configs?.socialSignInConfigured,
|
||||
updateConfigs,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -12,7 +12,7 @@ import CardTitle from '@/components/CardTitle';
|
|||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
|
@ -34,7 +34,7 @@ type Props = {
|
|||
const GuideModal = ({ isOpen, onClose }: Props) => {
|
||||
const { data } = useSWR<SignInExperience>('/api/sign-in-exp');
|
||||
const { data: preferences, update: updatePreferences } = useUserPreferences();
|
||||
const { updateSettings } = useSettings();
|
||||
const { updateConfigs } = useConfigs();
|
||||
const methods = useForm<SignInExperienceForm>();
|
||||
const {
|
||||
reset,
|
||||
|
@ -68,7 +68,7 @@ const GuideModal = ({ isOpen, onClose }: Props) => {
|
|||
api.patch('/api/sign-in-exp', {
|
||||
json: signInExperienceParser.toRemoteModel(formData),
|
||||
}),
|
||||
updateSettings({ signInExperienceCustomized: true }),
|
||||
updateConfigs({ signInExperienceCustomized: true }),
|
||||
]);
|
||||
|
||||
onClose();
|
||||
|
@ -76,7 +76,7 @@ const GuideModal = ({ isOpen, onClose }: Props) => {
|
|||
|
||||
const onSkip = async () => {
|
||||
setIsLoading(true);
|
||||
await updateSettings({ signInExperienceCustomized: true });
|
||||
await updateConfigs({ signInExperienceCustomized: true });
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
|||
import { SignInExperiencePage } from '@/consts/page-tabs';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
|
||||
import Preview from './components/Preview';
|
||||
|
@ -40,7 +40,7 @@ const SignInExperience = () => {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { tab } = useParams();
|
||||
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
||||
const { settings, error: settingsError, updateSettings, mutate: mutateSettings } = useSettings();
|
||||
const { configs, error: configsError, updateConfigs, mutate: mutateConfigs } = useConfigs();
|
||||
const { error: languageError, isLoading: isLoadingLanguages } = useUiLanguages();
|
||||
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
|
||||
|
||||
|
@ -79,7 +79,7 @@ const SignInExperience = () => {
|
|||
.json<SignInExperienceType>();
|
||||
void mutate(updatedData);
|
||||
setDataToCompare(undefined);
|
||||
await updateSettings({ signInExperienceCustomized: true });
|
||||
await updateConfigs({ signInExperienceCustomized: true });
|
||||
toast.success(t('general.saved'));
|
||||
};
|
||||
|
||||
|
@ -100,23 +100,23 @@ const SignInExperience = () => {
|
|||
await saveData();
|
||||
});
|
||||
|
||||
if ((!settings && !settingsError) || (!data && !error) || isLoadingLanguages) {
|
||||
if ((!configs && !configsError) || (!data && !error) || isLoadingLanguages) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (!settings && settingsError) {
|
||||
return <div>{settingsError.body?.message ?? settingsError.message}</div>;
|
||||
if (!configs && configsError) {
|
||||
return <div>{configsError.body?.message ?? configsError.message}</div>;
|
||||
}
|
||||
|
||||
if (languageError) {
|
||||
return <div>{languageError.body?.message ?? languageError.message}</div>;
|
||||
}
|
||||
|
||||
if (!settings?.signInExperienceCustomized) {
|
||||
if (!configs?.signInExperienceCustomized) {
|
||||
return (
|
||||
<Welcome
|
||||
mutate={() => {
|
||||
void mutateSettings();
|
||||
void mutateConfigs();
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { VerificationCodeType } from '@logto/connector-kit';
|
||||
import type { Application, Passcode, Resource, Role, Scope, Setting } from '@logto/schemas';
|
||||
import type {
|
||||
AdminConsoleData,
|
||||
Application,
|
||||
Passcode,
|
||||
Resource,
|
||||
Role,
|
||||
Scope,
|
||||
} from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
|
||||
export * from './connector.js';
|
||||
|
@ -65,17 +72,13 @@ export const mockRole: Role = {
|
|||
description: 'admin',
|
||||
};
|
||||
|
||||
export const mockSetting: Setting = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'foo setting',
|
||||
adminConsole: {
|
||||
demoChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
passwordlessConfigured: false,
|
||||
socialSignInConfigured: false,
|
||||
furtherReadingsChecked: false,
|
||||
},
|
||||
export const mockAdminConsoleData: AdminConsoleData = {
|
||||
demoChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
passwordlessConfigured: false,
|
||||
socialSignInConfigured: false,
|
||||
furtherReadingsChecked: false,
|
||||
};
|
||||
|
||||
export const mockPasscode: Passcode = {
|
||||
|
|
|
@ -48,7 +48,7 @@ export const buildUpdateWhereWithPool =
|
|||
*/
|
||||
return sql`
|
||||
${fields[key]}=
|
||||
coalesce(${fields[key]},'{}'::jsonb)|| ${convertToPrimitiveOrSql(key, value)}
|
||||
coalesce(${fields[key]},'{}'::jsonb) || ${convertToPrimitiveOrSql(key, value)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
25
packages/core/src/queries/logto-config.ts
Normal file
25
packages/core/src/queries/logto-config.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { AdminConsoleData } from '@logto/schemas';
|
||||
import { AdminConsoleConfigKey, LogtoConfigs } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||
|
||||
export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
|
||||
const getAdminConsoleConfig = async () =>
|
||||
pool.one<Record<string, unknown>>(sql`
|
||||
select ${fields.value} from ${table}
|
||||
where ${fields.key} = ${AdminConsoleConfigKey.AdminConsole}
|
||||
`);
|
||||
|
||||
const updateAdminConsoleConfig = async (value: Partial<AdminConsoleData>) =>
|
||||
pool.one<Record<string, unknown>>(sql`
|
||||
update ${table}
|
||||
set ${fields.value}=coalesce(${fields.value},'{}'::jsonb) || ${sql.jsonb(value)}
|
||||
where ${fields.key} = ${AdminConsoleConfigKey.AdminConsole}
|
||||
returning ${fields.value}
|
||||
`);
|
||||
|
||||
return { getAdminConsoleConfig, updateAdminConsoleConfig };
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
import { Settings } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { mockSetting } from '#src/__mocks__/index.js';
|
||||
import type { QueryType } from '#src/utils/test-utils.js';
|
||||
import { expectSqlAssert } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
const pool = createMockPool({
|
||||
query: async (sql, values) => {
|
||||
return mockQuery(sql, values);
|
||||
},
|
||||
});
|
||||
|
||||
const { defaultSettingId, createSettingQueries } = await import('./setting.js');
|
||||
const { getSetting, updateSetting } = createSettingQueries(pool);
|
||||
|
||||
describe('setting query', () => {
|
||||
const { table, fields } = convertToIdentifiers(Settings);
|
||||
const databaseValue = { ...mockSetting, adminConsole: JSON.stringify(mockSetting.adminConsole) };
|
||||
|
||||
it('getSetting', async () => {
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([defaultSettingId]);
|
||||
|
||||
return createMockQueryResult([databaseValue]);
|
||||
});
|
||||
|
||||
await expect(getSetting()).resolves.toEqual(databaseValue);
|
||||
});
|
||||
|
||||
it('updateSetting', async () => {
|
||||
const { adminConsole } = mockSetting;
|
||||
|
||||
const expectSql = sql`
|
||||
update ${table}
|
||||
set
|
||||
${fields.adminConsole}=
|
||||
coalesce("admin_console",'{}'::jsonb)|| $1
|
||||
where ${fields.id}=$2
|
||||
returning *
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([JSON.stringify(adminConsole), defaultSettingId]);
|
||||
|
||||
return createMockQueryResult([databaseValue]);
|
||||
});
|
||||
|
||||
await expect(updateSetting({ adminConsole })).resolves.toEqual(databaseValue);
|
||||
});
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import type { Setting, CreateSetting } from '@logto/schemas';
|
||||
import { Settings } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js';
|
||||
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
||||
|
||||
export const defaultSettingId = 'default';
|
||||
|
||||
export const createSettingQueries = (pool: CommonQueryMethods) => {
|
||||
const getSetting = async () =>
|
||||
buildFindEntityByIdWithPool(pool)<CreateSetting, Setting>(Settings)(defaultSettingId);
|
||||
|
||||
const updateSetting = async (setting: Partial<OmitAutoSetFields<CreateSetting>>) => {
|
||||
return buildUpdateWhereWithPool(pool)<CreateSetting, Setting>(Settings, true)({
|
||||
set: setting,
|
||||
where: { id: defaultSettingId },
|
||||
jsonbMode: 'merge',
|
||||
});
|
||||
};
|
||||
|
||||
return { getSetting, updateSetting };
|
||||
};
|
|
@ -15,12 +15,12 @@ import dashboardRoutes from './dashboard.js';
|
|||
import hookRoutes from './hook.js';
|
||||
import interactionRoutes from './interaction/index.js';
|
||||
import logRoutes from './log.js';
|
||||
import logtoConfigRoutes from './logto-config.js';
|
||||
import phraseRoutes from './phrase.js';
|
||||
import resourceRoutes from './resource.js';
|
||||
import roleRoutes from './role.js';
|
||||
import roleScopeRoutes from './role.scope.js';
|
||||
import samlAssertionHandlerRoutes from './saml-assertion-handler.js';
|
||||
import settingRoutes from './setting.js';
|
||||
import signInExperiencesRoutes from './sign-in-experience/index.js';
|
||||
import statusRoutes from './status.js';
|
||||
import swaggerRoutes from './swagger.js';
|
||||
|
@ -35,7 +35,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
const managementRouter: AuthedRouter = new Router();
|
||||
managementRouter.use(koaAuth(tenant.envSet, managementResourceScope.name));
|
||||
applicationRoutes(managementRouter, tenant);
|
||||
settingRoutes(managementRouter, tenant);
|
||||
logtoConfigRoutes(managementRouter, tenant);
|
||||
connectorRoutes(managementRouter, tenant);
|
||||
resourceRoutes(managementRouter, tenant);
|
||||
signInExperiencesRoutes(managementRouter, tenant);
|
||||
|
|
39
packages/core/src/routes/logto-config.test.ts
Normal file
39
packages/core/src/routes/logto-config.test.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { AdminConsoleData } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockAdminConsoleData } from '#src/__mocks__/index.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const logtoConfigs = {
|
||||
getAdminConsoleConfig: async () => ({ value: mockAdminConsoleData }),
|
||||
updateAdminConsoleConfig: async (data: Partial<AdminConsoleData>) => ({
|
||||
value: {
|
||||
...mockAdminConsoleData,
|
||||
...data,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const settingRoutes = await pickDefault(import('./logto-config.js'));
|
||||
|
||||
describe('configs routes', () => {
|
||||
const roleRequester = createRequester({
|
||||
authedRoutes: settingRoutes,
|
||||
tenantContext: new MockTenant(undefined, { logtoConfigs }),
|
||||
});
|
||||
|
||||
it('GET /configs/admin-console', async () => {
|
||||
const response = await roleRequester.get('/configs/admin-console');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockAdminConsoleData);
|
||||
});
|
||||
|
||||
it('PATCH /configs/admin-console', async () => {
|
||||
const demoChecked = !mockAdminConsoleData.demoChecked;
|
||||
const response = await roleRequester.patch('/configs/admin-console').send({ demoChecked });
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ ...mockAdminConsoleData, demoChecked });
|
||||
});
|
||||
});
|
35
packages/core/src/routes/logto-config.ts
Normal file
35
packages/core/src/routes/logto-config.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { adminConsoleDataGuard } from '@logto/schemas';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function logtoConfigRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { getAdminConsoleConfig, updateAdminConsoleConfig } = queries.logtoConfigs;
|
||||
|
||||
router.get(
|
||||
'/configs/admin-console',
|
||||
koaGuard({ response: adminConsoleDataGuard, status: 200 }),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await getAdminConsoleConfig();
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/configs/admin-console',
|
||||
koaGuard({
|
||||
body: adminConsoleDataGuard.partial(),
|
||||
response: adminConsoleDataGuard,
|
||||
status: 200,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await updateAdminConsoleConfig(ctx.guard.body);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import type { Setting, CreateSetting } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockSetting } from '#src/__mocks__/index.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const settings = {
|
||||
getSetting: async (): Promise<Setting> => mockSetting,
|
||||
updateSetting: async (data: Partial<CreateSetting>): Promise<Setting> => ({
|
||||
...mockSetting,
|
||||
...data,
|
||||
}),
|
||||
};
|
||||
|
||||
const settingRoutes = await pickDefault(import('./setting.js'));
|
||||
|
||||
describe('settings routes', () => {
|
||||
const roleRequester = createRequester({
|
||||
authedRoutes: settingRoutes,
|
||||
tenantContext: new MockTenant(undefined, { settings }),
|
||||
});
|
||||
|
||||
it('GET /settings', async () => {
|
||||
const response = await roleRequester.get('/settings');
|
||||
expect(response.status).toEqual(200);
|
||||
const { id, ...rest } = mockSetting;
|
||||
expect(response.body).toEqual(rest);
|
||||
});
|
||||
|
||||
it('PATCH /settings', async () => {
|
||||
const { adminConsole } = mockSetting;
|
||||
|
||||
const response = await roleRequester.patch('/settings').send({
|
||||
adminConsole,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
tenantId: 'fake_tenant',
|
||||
adminConsole,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
import { Settings } from '@logto/schemas';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function settingRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { getSetting, updateSetting } = queries.settings;
|
||||
|
||||
router.get('/settings', async (ctx, next) => {
|
||||
const { id, ...rest } = await getSetting();
|
||||
ctx.body = rest;
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.patch(
|
||||
'/settings',
|
||||
koaGuard({
|
||||
body: Settings.createGuard.omit({ id: true }).deepPartial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { body: partialSettings } = ctx.guard;
|
||||
const { id, ...rest } = await updateSetting(partialSettings);
|
||||
ctx.body = rest;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -5,13 +5,13 @@ import { createApplicationsRolesQueries } from '#src/queries/applications-roles.
|
|||
import { createConnectorQueries } from '#src/queries/connector.js';
|
||||
import { createCustomPhraseQueries } from '#src/queries/custom-phrase.js';
|
||||
import { createLogQueries } from '#src/queries/log.js';
|
||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||
import { createOidcModelInstanceQueries } from '#src/queries/oidc-model-instance.js';
|
||||
import { createPasscodeQueries } from '#src/queries/passcode.js';
|
||||
import { createResourceQueries } from '#src/queries/resource.js';
|
||||
import { createRolesScopesQueries } from '#src/queries/roles-scopes.js';
|
||||
import { createRolesQueries } from '#src/queries/roles.js';
|
||||
import { createScopeQueries } from '#src/queries/scope.js';
|
||||
import { createSettingQueries } from '#src/queries/setting.js';
|
||||
import { createSignInExperienceQueries } from '#src/queries/sign-in-experience.js';
|
||||
import { createUserQueries } from '#src/queries/user.js';
|
||||
import { createUsersRolesQueries } from '#src/queries/users-roles.js';
|
||||
|
@ -27,7 +27,7 @@ export default class Queries {
|
|||
rolesScopes = createRolesScopesQueries(this.pool);
|
||||
roles = createRolesQueries(this.pool);
|
||||
scopes = createScopeQueries(this.pool);
|
||||
settings = createSettingQueries(this.pool);
|
||||
logtoConfigs = createLogtoConfigQueries(this.pool);
|
||||
signInExperiences = createSignInExperienceQueries(this.pool);
|
||||
users = createUserQueries(this.pool);
|
||||
usersRoles = createUsersRolesQueries(this.pool);
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
insert into _logto_configs (key, value)
|
||||
select 'adminConsole', admin_console from settings
|
||||
where id='default';
|
||||
`);
|
||||
await pool.query(sql`
|
||||
alter table _logto_configs
|
||||
add column tenant_id varchar(21) not null default 'default'
|
||||
references tenants (id) on update cascade on delete cascade;
|
||||
|
||||
create trigger set_tenant_id before insert on _logto_configs
|
||||
for each row execute procedure set_tenant_id();
|
||||
`);
|
||||
await pool.query(sql`
|
||||
alter table _logto_configs
|
||||
alter column tenant_id drop default;
|
||||
`);
|
||||
await pool.query(sql`drop table settings cascade;`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
create table settings (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
id varchar(21) not null,
|
||||
admin_console jsonb not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create index settings__id
|
||||
on settings (tenant_id, id);
|
||||
|
||||
create trigger set_tenant_id before insert on settings
|
||||
for each row execute procedure set_tenant_id();
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
insert into settings (id, admin_console)
|
||||
select 'default', value from _logto_configs
|
||||
where key='adminConsole';
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
delete from _logto_configs
|
||||
where key='adminConsole';
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
alter table _logto_configs
|
||||
drop column tenant_id,
|
||||
drop trigger set_tenant_id;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -0,0 +1,39 @@
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
create table systems (
|
||||
key varchar(256) not null,
|
||||
value jsonb not null default '{}'::jsonb,
|
||||
primary key (key)
|
||||
);
|
||||
|
||||
alter table _logto_configs rename to logto_configs;
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
insert into systems (key, value)
|
||||
select key, value from logto_configs
|
||||
where key='alterationState';
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
delete from logto_configs
|
||||
where key='alterationState';
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
insert into _logto_configs (key, value)
|
||||
select key, value from systems
|
||||
where key='alterationState';
|
||||
drop table systems;
|
||||
alter table logto_configs rename to _logto_configs;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -163,18 +163,6 @@ export enum AppearanceMode {
|
|||
DarkMode = 'dark',
|
||||
}
|
||||
|
||||
export const adminConsoleConfigGuard = z.object({
|
||||
// Get started challenges
|
||||
demoChecked: z.boolean(),
|
||||
applicationCreated: z.boolean(),
|
||||
signInExperienceCustomized: z.boolean(),
|
||||
passwordlessConfigured: z.boolean(),
|
||||
socialSignInConfigured: z.boolean(),
|
||||
furtherReadingsChecked: z.boolean(),
|
||||
});
|
||||
|
||||
export type AdminConsoleConfig = z.infer<typeof adminConsoleConfigGuard>;
|
||||
|
||||
/**
|
||||
* Phrases
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export * from './application.js';
|
||||
export * from './resource.js';
|
||||
export * from './setting.js';
|
||||
export * from './logto-config.js';
|
||||
export * from './sign-in-experience.js';
|
||||
export * from './roles.js';
|
||||
export * from './scope.js';
|
||||
|
|
22
packages/schemas/src/seeds/logto-config.ts
Normal file
22
packages/schemas/src/seeds/logto-config.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { CreateLogtoConfig } from '../db-entries/index.js';
|
||||
import { AppearanceMode } from '../foundations/index.js';
|
||||
import type { AdminConsoleData } from '../types/index.js';
|
||||
import { AdminConsoleConfigKey } from '../types/index.js';
|
||||
|
||||
export const createDefaultAdminConsoleConfig = (): Readonly<{
|
||||
key: AdminConsoleConfigKey;
|
||||
value: AdminConsoleData;
|
||||
}> =>
|
||||
Object.freeze({
|
||||
key: AdminConsoleConfigKey.AdminConsole,
|
||||
value: {
|
||||
language: 'en',
|
||||
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||
demoChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
passwordlessConfigured: false,
|
||||
socialSignInConfigured: false,
|
||||
furtherReadingsChecked: false,
|
||||
},
|
||||
} satisfies CreateLogtoConfig);
|
|
@ -1,21 +0,0 @@
|
|||
import type { CreateSetting } from '../db-entries/index.js';
|
||||
import { AppearanceMode } from '../foundations/index.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
export const defaultSettingId = 'default';
|
||||
|
||||
export const createDefaultSetting = (): Readonly<CreateSetting> =>
|
||||
Object.freeze({
|
||||
tenantId: defaultTenantId,
|
||||
id: defaultSettingId,
|
||||
adminConsole: {
|
||||
language: 'en',
|
||||
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||
demoChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
passwordlessConfigured: false,
|
||||
socialSignInConfigured: false,
|
||||
furtherReadingsChecked: false,
|
||||
},
|
||||
});
|
|
@ -39,17 +39,48 @@ export const logtoOidcConfigGuard: Readonly<{
|
|||
[LogtoOidcConfigKey.CookieKeys]: z.string().array(),
|
||||
});
|
||||
|
||||
// Admin console config
|
||||
export const adminConsoleDataGuard = z.object({
|
||||
// Get started challenges
|
||||
demoChecked: z.boolean(),
|
||||
applicationCreated: z.boolean(),
|
||||
signInExperienceCustomized: z.boolean(),
|
||||
passwordlessConfigured: z.boolean(),
|
||||
socialSignInConfigured: z.boolean(),
|
||||
furtherReadingsChecked: z.boolean(),
|
||||
});
|
||||
|
||||
export type AdminConsoleData = z.infer<typeof adminConsoleDataGuard>;
|
||||
|
||||
export enum AdminConsoleConfigKey {
|
||||
AdminConsole = 'adminConsole',
|
||||
}
|
||||
|
||||
export type AdminConsoleConfigType = {
|
||||
[AdminConsoleConfigKey.AdminConsole]: AdminConsoleData;
|
||||
};
|
||||
|
||||
export const adminConsoleConfigGuard: Readonly<{
|
||||
[key in AdminConsoleConfigKey]: ZodType<AdminConsoleConfigType[key]>;
|
||||
}> = Object.freeze({
|
||||
[AdminConsoleConfigKey.AdminConsole]: adminConsoleDataGuard,
|
||||
});
|
||||
|
||||
// Summary
|
||||
export type LogtoConfigKey = AlterationStateKey | LogtoOidcConfigKey;
|
||||
export type LogtoConfigType = AlterationStateType | LogtoOidcConfigType;
|
||||
export type LogtoConfigGuard = typeof alterationStateGuard & typeof logtoOidcConfigGuard;
|
||||
export type LogtoConfigKey = AlterationStateKey | LogtoOidcConfigKey | AdminConsoleConfigKey;
|
||||
export type LogtoConfigType = AlterationStateType | LogtoOidcConfigType | AdminConsoleConfigType;
|
||||
export type LogtoConfigGuard = typeof alterationStateGuard &
|
||||
typeof logtoOidcConfigGuard &
|
||||
typeof adminConsoleConfigGuard;
|
||||
|
||||
export const logtoConfigKeys: readonly LogtoConfigKey[] = Object.freeze([
|
||||
...Object.values(AlterationStateKey),
|
||||
...Object.values(LogtoOidcConfigKey),
|
||||
...Object.values(AdminConsoleConfigKey),
|
||||
]);
|
||||
|
||||
export const logtoConfigGuards: LogtoConfigGuard = Object.freeze({
|
||||
...alterationStateGuard,
|
||||
...logtoOidcConfigGuard,
|
||||
...adminConsoleConfigGuard,
|
||||
});
|
||||
|
|
10
packages/schemas/tables/logto_configs.sql
Normal file
10
packages/schemas/tables/logto_configs.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
create table logto_configs (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
key varchar(256) not null,
|
||||
value jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb,
|
||||
primary key (tenant_id, key)
|
||||
);
|
||||
|
||||
create trigger set_tenant_id before insert on logto_configs
|
||||
for each row execute procedure set_tenant_id();
|
|
@ -1,13 +0,0 @@
|
|||
create table settings (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
id varchar(21) not null,
|
||||
admin_console jsonb /* @use AdminConsoleConfig */ not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create index settings__id
|
||||
on settings (tenant_id, id);
|
||||
|
||||
create trigger set_tenant_id before insert on settings
|
||||
for each row execute procedure set_tenant_id();
|
|
@ -1,4 +1,4 @@
|
|||
create table _logto_configs (
|
||||
create table systems (
|
||||
key varchar(256) not null,
|
||||
value jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb,
|
||||
primary key (key)
|
Loading…
Add table
Reference in a new issue