mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor: move console sie to database (#3171)
This commit is contained in:
parent
14fe40e846
commit
6858f3c8dc
44 changed files with 258 additions and 323 deletions
1
.github/workflows/integration-test.yml
vendored
1
.github/workflows/integration-test.yml
vendored
|
@ -97,6 +97,7 @@ jobs:
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
# continue-on-error: true # Uncomment this line to debug
|
||||||
run: |
|
run: |
|
||||||
cd tests/packages/integration-tests
|
cd tests/packages/integration-tests
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { readdir, readFile } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultSignInExperience,
|
|
||||||
createDefaultAdminConsoleConfig,
|
createDefaultAdminConsoleConfig,
|
||||||
defaultTenantId,
|
defaultTenantId,
|
||||||
adminTenantId,
|
adminTenantId,
|
||||||
defaultManagementApi,
|
defaultManagementApi,
|
||||||
createAdminDataInAdminTenant,
|
createAdminDataInAdminTenant,
|
||||||
createMeApiInAdminTenant,
|
createMeApiInAdminTenant,
|
||||||
|
createDefaultSignInExperience,
|
||||||
|
createAdminTenantSignInExperience,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { Hooks, Tenants } from '@logto/schemas/models';
|
import { Hooks, Tenants } from '@logto/schemas/models';
|
||||||
import type { DatabaseTransactionConnection } from 'slonik';
|
import type { DatabaseTransactionConnection } from 'slonik';
|
||||||
|
@ -123,7 +124,10 @@ export const seedTables = async (
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
||||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
connection.query(
|
||||||
|
insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences')
|
||||||
|
),
|
||||||
|
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
|
||||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import type { InteractionResults } from 'oidc-provider';
|
import type { InteractionResults } from 'oidc-provider';
|
||||||
import type Provider from 'oidc-provider';
|
import type Provider from 'oidc-provider';
|
||||||
import { errors } from 'oidc-provider';
|
|
||||||
|
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
|
|
||||||
|
@ -45,27 +44,3 @@ export const saveUserFirstConsentedAppId = async (
|
||||||
await updateUserById(userId, { applicationId });
|
await updateUserById(userId, { applicationId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApplicationIdFromInteraction = async (
|
|
||||||
ctx: Context,
|
|
||||||
provider: Provider
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
const interaction = await provider
|
|
||||||
.interactionDetails(ctx.req, ctx.res)
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
// Should not block if interaction is not found
|
|
||||||
if (error instanceof errors.SessionNotFound) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!interaction?.params) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof interaction.params.client_id === 'string'
|
|
||||||
? interaction.params.client_id
|
|
||||||
: undefined;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { builtInLanguages } from '@logto/phrases-ui';
|
import { builtInLanguages } from '@logto/phrases-ui';
|
||||||
import type { Branding, LanguageInfo, SignInExperience } from '@logto/schemas';
|
import type { Branding, LanguageInfo, SignInExperience } from '@logto/schemas';
|
||||||
import {
|
import { ConnectorType, BrandingStyle } from '@logto/schemas';
|
||||||
adminTenantId,
|
|
||||||
SignInMode,
|
|
||||||
ConnectorType,
|
|
||||||
BrandingStyle,
|
|
||||||
adminConsoleApplicationId,
|
|
||||||
adminConsoleSignInExperience,
|
|
||||||
demoAppApplicationId,
|
|
||||||
} from '@logto/schemas';
|
|
||||||
import { deduplicate } from '@silverhand/essentials';
|
import { deduplicate } from '@silverhand/essentials';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
@ -69,49 +61,20 @@ export const createSignInExperienceLibrary = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSignInExperienceForApplication = async (
|
const getSignInExperience = async (): Promise<SignInExperience> => {
|
||||||
applicationId?: string
|
const raw = await findDefaultSignInExperience();
|
||||||
): Promise<SignInExperience & { notification?: string }> => {
|
const { branding } = raw;
|
||||||
// Hard code Admin Console sign-in methods settings.
|
|
||||||
if (applicationId === adminConsoleApplicationId) {
|
|
||||||
return {
|
|
||||||
...adminConsoleSignInExperience,
|
|
||||||
tenantId: adminTenantId,
|
|
||||||
branding: {
|
|
||||||
...adminConsoleSignInExperience.branding,
|
|
||||||
slogan: i18next.t('admin_console.welcome.title'),
|
|
||||||
},
|
|
||||||
termsOfUseUrl: null,
|
|
||||||
languageInfo: {
|
|
||||||
autoDetect: true,
|
|
||||||
fallbackLanguage: 'en',
|
|
||||||
},
|
|
||||||
signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register,
|
|
||||||
socialSignInConnectorTargets: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const signInExperience = await findDefaultSignInExperience();
|
// Alter sign-in experience dynamic configs
|
||||||
|
return Object.freeze({
|
||||||
// Insert Demo App Notification
|
...raw,
|
||||||
if (applicationId === demoAppApplicationId) {
|
branding: { ...branding, slogan: branding.slogan && i18next.t(branding.slogan) },
|
||||||
const { socialSignInConnectorTargets } = signInExperience;
|
});
|
||||||
|
|
||||||
const notification = i18next.t('demo_app.notification');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...signInExperience,
|
|
||||||
socialSignInConnectorTargets,
|
|
||||||
notification,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return signInExperience;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateLanguageInfo,
|
validateLanguageInfo,
|
||||||
removeUnavailableSocialConnectorTargets,
|
removeUnavailableSocialConnectorTargets,
|
||||||
getSignInExperienceForApplication,
|
getSignInExperience,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,10 +9,6 @@ const { mockEsmWithActual } = createMockUtils(jest);
|
||||||
|
|
||||||
const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector];
|
const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector];
|
||||||
|
|
||||||
await mockEsmWithActual('#src/libraries/session.js', () => ({
|
|
||||||
getApplicationIdFromInteraction: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { validateSignUp } = await import('./sign-up.js');
|
const { validateSignUp } = await import('./sign-up.js');
|
||||||
|
|
||||||
describe('validate sign-up', () => {
|
describe('validate sign-up', () => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
import { userClaims } from '@logto/core-kit';
|
import { userClaims } from '@logto/core-kit';
|
||||||
import { CustomClientMetadataKey } from '@logto/schemas';
|
import { CustomClientMetadataKey, demoAppApplicationId } from '@logto/schemas';
|
||||||
import Provider, { errors, ResourceServer } from 'oidc-provider';
|
import Provider, { errors, ResourceServer } from 'oidc-provider';
|
||||||
import snakecaseKeys from 'snakecase-keys';
|
import snakecaseKeys from 'snakecase-keys';
|
||||||
|
|
||||||
|
@ -129,9 +129,16 @@ export default function initOidc(envSet: EnvSet, queries: Queries, libraries: Li
|
||||||
},
|
},
|
||||||
interactions: {
|
interactions: {
|
||||||
url: (_, interaction) => {
|
url: (_, interaction) => {
|
||||||
|
const appendParameters = (path: string) => {
|
||||||
|
// `notification` is for showing a text banner on the homepage
|
||||||
|
return interaction.params.client_id === demoAppApplicationId
|
||||||
|
? path + `?notification=demo_app.notification`
|
||||||
|
: path;
|
||||||
|
};
|
||||||
|
|
||||||
switch (interaction.prompt.name) {
|
switch (interaction.prompt.name) {
|
||||||
case 'login': {
|
case 'login': {
|
||||||
return routes.signIn.credentials;
|
return appendParameters(routes.signIn.credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'consent': {
|
case 'consent': {
|
||||||
|
|
|
@ -3,9 +3,9 @@ const signIn = '/sign-in';
|
||||||
export const routes = Object.freeze({
|
export const routes = Object.freeze({
|
||||||
signIn: {
|
signIn: {
|
||||||
credentials: signIn,
|
credentials: signIn,
|
||||||
consent: signIn + '/consent',
|
consent: `${signIn}/consent`,
|
||||||
},
|
},
|
||||||
});
|
} as const);
|
||||||
|
|
||||||
export const verificationTimeout = 10 * 60; // 10 mins.
|
export const verificationTimeout = 10 * 60; // 10 mins.
|
||||||
export const continueSignInTimeout = 10 * 60; // 10 mins.
|
export const continueSignInTimeout = 10 * 60; // 10 mins.
|
||||||
|
|
|
@ -55,7 +55,7 @@ jest.useFakeTimers().setSystemTime(now);
|
||||||
describe('submit action', () => {
|
describe('submit action', () => {
|
||||||
const tenant = new MockTenant(
|
const tenant = new MockTenant(
|
||||||
undefined,
|
undefined,
|
||||||
{ users: userQueries },
|
{ users: userQueries, signInExperiences: { updateDefaultSignInExperience: jest.fn() } },
|
||||||
{ users: userLibraries, connectors: { getLogtoConnectorById } }
|
{ users: userLibraries, connectors: { getLogtoConnectorById } }
|
||||||
);
|
);
|
||||||
const ctx = {
|
const ctx = {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { User, Profile } from '@logto/schemas';
|
import type { User, Profile } from '@logto/schemas';
|
||||||
import {
|
import {
|
||||||
|
SignInMode,
|
||||||
UserRole,
|
UserRole,
|
||||||
getManagementApiAdminName,
|
getManagementApiAdminName,
|
||||||
defaultTenantId,
|
defaultTenantId,
|
||||||
|
@ -151,6 +152,7 @@ export default async function submitInteraction(
|
||||||
log?: LogEntry
|
log?: LogEntry
|
||||||
) {
|
) {
|
||||||
const { hasActiveUsers, findUserById, updateUserById } = queries.users;
|
const { hasActiveUsers, findUserById, updateUserById } = queries.users;
|
||||||
|
const { updateDefaultSignInExperience } = queries.signInExperiences;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
users: { generateUserId, insertUser },
|
users: { generateUserId, insertUser },
|
||||||
|
@ -164,7 +166,7 @@ export default async function submitInteraction(
|
||||||
|
|
||||||
const { client_id } = ctx.interactionDetails.params;
|
const { client_id } = ctx.interactionDetails.params;
|
||||||
|
|
||||||
const createAdminUser =
|
const isCreatingFirstAdminUser =
|
||||||
getTenantId(ctx.URL) === adminTenantId &&
|
getTenantId(ctx.URL) === adminTenantId &&
|
||||||
String(client_id) === adminConsoleApplicationId &&
|
String(client_id) === adminConsoleApplicationId &&
|
||||||
!(await hasActiveUsers());
|
!(await hasActiveUsers());
|
||||||
|
@ -174,9 +176,15 @@ export default async function submitInteraction(
|
||||||
id,
|
id,
|
||||||
...upsertProfile,
|
...upsertProfile,
|
||||||
},
|
},
|
||||||
createAdminUser ? [UserRole.User, getManagementApiAdminName(defaultTenantId)] : []
|
isCreatingFirstAdminUser ? [UserRole.User, getManagementApiAdminName(defaultTenantId)] : []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// In OSS, we need to limit sign-in experience to "sign-in only" once
|
||||||
|
// the first admin has been create since we don't want other unexpected registrations
|
||||||
|
if (isCreatingFirstAdminUser) {
|
||||||
|
await updateDefaultSignInExperience({ signInMode: SignInMode.SignIn });
|
||||||
|
}
|
||||||
|
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
log?.append({ userId: id });
|
log?.append({ userId: id });
|
||||||
|
|
|
@ -117,7 +117,7 @@ const tenantContext = new MockTenant(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
signInExperiences: {
|
signInExperiences: {
|
||||||
getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience),
|
getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
import type { SignInExperience } from '@logto/schemas';
|
||||||
import { conditional } from '@silverhand/essentials';
|
|
||||||
import type { MiddlewareType } from 'koa';
|
import type { MiddlewareType } from 'koa';
|
||||||
|
|
||||||
import type { SignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
|
import type { SignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
|
||||||
|
@ -11,21 +10,14 @@ export type WithInteractionSieContext<ContextT> = WithInteractionDetailsContext<
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
||||||
getSignInExperienceForApplication,
|
getSignInExperience,
|
||||||
}: SignInExperienceLibrary): MiddlewareType<
|
}: SignInExperienceLibrary): MiddlewareType<
|
||||||
StateT,
|
StateT,
|
||||||
WithInteractionSieContext<ContextT>,
|
WithInteractionSieContext<ContextT>,
|
||||||
ResponseT
|
ResponseT
|
||||||
> {
|
> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const { interactionDetails } = ctx;
|
const signInExperience = await getSignInExperience();
|
||||||
|
|
||||||
const signInExperience = await getSignInExperienceForApplication(
|
|
||||||
conditional(
|
|
||||||
typeof interactionDetails.params.client_id === 'string' &&
|
|
||||||
interactionDetails.params.client_id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.signInExperience = signInExperience;
|
ctx.signInExperience = signInExperience;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import zhCN from '@logto/phrases-ui/lib/locales/zh-cn.js';
|
import zhCN from '@logto/phrases-ui/lib/locales/zh-cn.js';
|
||||||
import type { CustomPhrase, SignInExperience } from '@logto/schemas';
|
import type { CustomPhrase, SignInExperience } from '@logto/schemas';
|
||||||
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
|
|
||||||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
||||||
|
|
||||||
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
|
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
|
||||||
|
@ -44,9 +43,8 @@ const { findAllCustomLanguageTags } = customPhrases;
|
||||||
|
|
||||||
const getPhrases = jest.fn(async () => zhCN);
|
const getPhrases = jest.fn(async () => zhCN);
|
||||||
|
|
||||||
const interactionDetails = jest.fn();
|
|
||||||
const tenantContext = new MockTenant(
|
const tenantContext = new MockTenant(
|
||||||
createMockProvider(interactionDetails),
|
createMockProvider(),
|
||||||
{ customPhrases, signInExperiences: { findDefaultSignInExperience } },
|
{ customPhrases, signInExperiences: { findDefaultSignInExperience } },
|
||||||
{ phrases: { getPhrases } }
|
{ phrases: { getPhrases } }
|
||||||
);
|
);
|
||||||
|
@ -59,64 +57,11 @@ const phraseRequest = createRequester({
|
||||||
tenantContext,
|
tenantContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the application is admin-console', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
interactionDetails.mockResolvedValueOnce({
|
|
||||||
params: { client_id: adminConsoleApplicationId },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call interactionDetails', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(interactionDetails).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call findDefaultSignInExperience', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(findDefaultSignInExperience).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call detectLanguage', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(detectLanguageSpy).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call findAllCustomLanguageTags', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(findAllCustomLanguageTags).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getPhrases with fallback language from Admin Console sign-in experience', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(getPhrases).toBeCalledTimes(1);
|
|
||||||
expect(getPhrases).toBeCalledWith(adminConsoleSignInExperience.languageInfo.fallbackLanguage, [
|
|
||||||
customizedLanguage,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the application is not admin-console', () => {
|
describe('when the application is not admin-console', () => {
|
||||||
beforeEach(() => {
|
|
||||||
interactionDetails.mockResolvedValue({
|
|
||||||
params: {},
|
|
||||||
jti: 'jti',
|
|
||||||
client_id: 'mockApplicationId',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call interactionDetails', async () => {
|
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
|
||||||
expect(interactionDetails).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call findDefaultSignInExperience', async () => {
|
it('should call findDefaultSignInExperience', async () => {
|
||||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||||
expect(findDefaultSignInExperience).toBeCalledTimes(1);
|
expect(findDefaultSignInExperience).toBeCalledTimes(1);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||||
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
|
|
||||||
import { object, string } from 'zod';
|
import { object, string } from 'zod';
|
||||||
|
|
||||||
import detectLanguage from '#src/i18n/detect-language.js';
|
import detectLanguage from '#src/i18n/detect-language.js';
|
||||||
|
@ -8,7 +7,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||||
|
|
||||||
export default function phraseRoutes<T extends AnonymousRouter>(
|
export default function phraseRoutes<T extends AnonymousRouter>(
|
||||||
...[router, { provider, queries, libraries }]: RouterInitArgs<T>
|
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
customPhrases: { findAllCustomLanguageTags },
|
customPhrases: { findAllCustomLanguageTags },
|
||||||
|
@ -16,11 +15,7 @@ export default function phraseRoutes<T extends AnonymousRouter>(
|
||||||
} = queries;
|
} = queries;
|
||||||
const { getPhrases } = libraries.phrases;
|
const { getPhrases } = libraries.phrases;
|
||||||
|
|
||||||
const getLanguageInfo = async (applicationId: unknown) => {
|
const getLanguageInfo = async () => {
|
||||||
if (applicationId === adminConsoleApplicationId) {
|
|
||||||
return adminConsoleSignInExperience.languageInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { languageInfo } = await findDefaultSignInExperience();
|
const { languageInfo } = await findDefaultSignInExperience();
|
||||||
|
|
||||||
return languageInfo;
|
return languageInfo;
|
||||||
|
@ -34,17 +29,11 @@ export default function phraseRoutes<T extends AnonymousRouter>(
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const interaction = await provider
|
|
||||||
.interactionDetails(ctx.req, ctx.res)
|
|
||||||
// Should not block when failed to get interaction
|
|
||||||
.catch(() => null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: { lng },
|
query: { lng },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
const applicationId = interaction?.params.client_id;
|
const { autoDetect, fallbackLanguage } = await getLanguageInfo();
|
||||||
const { autoDetect, fallbackLanguage } = await getLanguageInfo(applicationId);
|
|
||||||
|
|
||||||
const targetLanguage = lng ? [lng] : [];
|
const targetLanguage = lng ? [lng] : [];
|
||||||
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
|
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
import {
|
|
||||||
SignInMode,
|
|
||||||
adminConsoleApplicationId,
|
|
||||||
adminConsoleSignInExperience,
|
|
||||||
} from '@logto/schemas';
|
|
||||||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -100,26 +95,4 @@ describe('GET /.well-known/sign-in-exp', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return admin console settings', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(provider, 'interactionDetails')
|
|
||||||
// @ts-expect-error
|
|
||||||
.mockResolvedValue({ params: { client_id: adminConsoleApplicationId } });
|
|
||||||
const response = await sessionRequest.get('/.well-known/sign-in-exp');
|
|
||||||
expect(response.status).toEqual(200);
|
|
||||||
|
|
||||||
expect(response.body).toMatchObject({
|
|
||||||
...adminConsoleSignInExperience,
|
|
||||||
tenantId: 'admin',
|
|
||||||
branding: {
|
|
||||||
...adminConsoleSignInExperience.branding,
|
|
||||||
slogan: 'admin_console.welcome.title',
|
|
||||||
},
|
|
||||||
termsOfUseUrl: null,
|
|
||||||
languageInfo: mockSignInExperience.languageInfo,
|
|
||||||
socialConnectors: [],
|
|
||||||
signInMode: SignInMode.SignIn,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||||
import { ConnectorType } from '@logto/connector-kit';
|
import { ConnectorType } from '@logto/connector-kit';
|
||||||
import { adminConsoleApplicationId, adminTenantId } from '@logto/schemas';
|
import { adminTenantId } from '@logto/schemas';
|
||||||
import etag from 'etag';
|
import etag from 'etag';
|
||||||
|
|
||||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import { getApplicationIdFromInteraction } from '#src/libraries/session.js';
|
|
||||||
|
|
||||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||||
|
|
||||||
export default function wellKnownRoutes<T extends AnonymousRouter>(
|
export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
...[router, { provider, libraries, id }]: RouterInitArgs<T>
|
...[router, { libraries, id }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
signInExperiences: { getSignInExperienceForApplication },
|
signInExperiences: { getSignInExperience },
|
||||||
connectors: { getLogtoConnectors },
|
connectors: { getLogtoConnectors },
|
||||||
} = libraries;
|
} = libraries;
|
||||||
|
|
||||||
|
@ -34,10 +33,8 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
router.get(
|
router.get(
|
||||||
'/.well-known/sign-in-exp',
|
'/.well-known/sign-in-exp',
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const applicationId = await getApplicationIdFromInteraction(ctx, provider);
|
|
||||||
|
|
||||||
const [signInExperience, logtoConnectors] = await Promise.all([
|
const [signInExperience, logtoConnectors] = await Promise.all([
|
||||||
getSignInExperienceForApplication(applicationId),
|
getSignInExperience(),
|
||||||
getLogtoConnectors(),
|
getLogtoConnectors(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -46,10 +43,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
|
email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
|
||||||
};
|
};
|
||||||
|
|
||||||
const socialConnectors =
|
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
|
||||||
applicationId === adminConsoleApplicationId
|
|
||||||
? []
|
|
||||||
: signInExperience.socialSignInConnectorTargets.reduce<
|
|
||||||
Array<ConnectorMetadata & { id: string }>
|
Array<ConnectorMetadata & { id: string }>
|
||||||
>((previous, connectorTarget) => {
|
>((previous, connectorTarget) => {
|
||||||
const connectors = logtoConnectors.filter(
|
const connectors = logtoConnectors.filter(
|
||||||
|
|
|
@ -3,7 +3,7 @@ const config = {
|
||||||
transform: {},
|
transform: {},
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
setupFilesAfterEnv: ['./jest.setup.js'],
|
setupFilesAfterEnv: ['./jest.setup.js', './jest.setup.api.js'],
|
||||||
roots: ['./lib'],
|
roots: ['./lib'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
||||||
|
|
7
packages/integration-tests/jest.setup.api.js
Normal file
7
packages/integration-tests/jest.setup.api.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { authedAdminTenantApi } from './lib/api/api.js';
|
||||||
|
|
||||||
|
// We need to update this before tests otherwise Logto will update SignInMode for admin tenant
|
||||||
|
// The update logic should be
|
||||||
|
await authedAdminTenantApi.patch('sign-in-exp', {
|
||||||
|
json: { signInMode: 'SignInAndRegister' },
|
||||||
|
});
|
|
@ -5,7 +5,6 @@ export * from './sign-in-experience.js';
|
||||||
export * from './admin-user.js';
|
export * from './admin-user.js';
|
||||||
export * from './logs.js';
|
export * from './logs.js';
|
||||||
export * from './dashboard.js';
|
export * from './dashboard.js';
|
||||||
export * from './wellknown.js';
|
|
||||||
export * from './interaction.js';
|
export * from './interaction.js';
|
||||||
|
|
||||||
export { default as api, authedAdminApi } from './api.js';
|
export { default as api, authedAdminApi } from './api.js';
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
|
||||||
|
|
||||||
import api from './api.js';
|
|
||||||
|
|
||||||
export const getWellKnownSignInExperience = (interactionCookie: string) =>
|
|
||||||
api
|
|
||||||
.get('.well-known/sign-in-exp', {
|
|
||||||
headers: {
|
|
||||||
cookie: interactionCookie,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.json<SignInExperience>();
|
|
|
@ -76,7 +76,7 @@ export default class MockClient {
|
||||||
|
|
||||||
// Note: should redirect to sign-in page
|
// Note: should redirect to sign-in page
|
||||||
assert(
|
assert(
|
||||||
response.statusCode === 303 && response.headers.location === '/sign-in',
|
response.statusCode === 303 && response.headers.location?.startsWith('/sign-in'),
|
||||||
new Error('Visit sign in uri failed')
|
new Error('Visit sign in uri failed')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
36
packages/integration-tests/src/tests/api/well-known.test.ts
Normal file
36
packages/integration-tests/src/tests/api/well-known.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import type { SignInExperience } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { adminTenantApi } from '#src/api/api.js';
|
||||||
|
import { api } from '#src/api/index.js';
|
||||||
|
|
||||||
|
describe('.well-known api', () => {
|
||||||
|
it('get /.well-known/sign-in-exp for console', async () => {
|
||||||
|
const response = await adminTenantApi.get('.well-known/sign-in-exp').json<SignInExperience>();
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
signUp: {
|
||||||
|
identifiers: ['username'],
|
||||||
|
password: true,
|
||||||
|
verify: false,
|
||||||
|
},
|
||||||
|
signIn: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
identifier: 'username',
|
||||||
|
password: true,
|
||||||
|
verificationCode: false,
|
||||||
|
isPasswordPrimary: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
signInMode: 'SignInAndRegister',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get /.well-known/sign-in-exp for general app', async () => {
|
||||||
|
const response = await api.get('.well-known/sign-in-exp').json<SignInExperience>();
|
||||||
|
|
||||||
|
// Should support sign-in and register
|
||||||
|
expect(response).toMatchObject({ signInMode: 'SignInAndRegister' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,49 +0,0 @@
|
||||||
import { adminConsoleApplicationId } from '@logto/schemas';
|
|
||||||
import { assert } from '@silverhand/essentials';
|
|
||||||
|
|
||||||
import { getWellKnownSignInExperience } from '#src/api/index.js';
|
|
||||||
import MockClient from '#src/client/index.js';
|
|
||||||
import { adminConsoleRedirectUri } from '#src/constants.js';
|
|
||||||
|
|
||||||
describe('wellknown api', () => {
|
|
||||||
it('get /.well-known/sign-in-exp for AC', async () => {
|
|
||||||
const client = new MockClient({ appId: adminConsoleApplicationId });
|
|
||||||
await client.initSession(adminConsoleRedirectUri);
|
|
||||||
|
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
|
||||||
|
|
||||||
const response = await getWellKnownSignInExperience(client.interactionCookie);
|
|
||||||
|
|
||||||
expect(response).toMatchObject({
|
|
||||||
signUp: {
|
|
||||||
identifiers: ['username'],
|
|
||||||
password: true,
|
|
||||||
verify: false,
|
|
||||||
},
|
|
||||||
signIn: {
|
|
||||||
methods: [
|
|
||||||
{
|
|
||||||
identifier: 'username',
|
|
||||||
password: true,
|
|
||||||
verificationCode: false,
|
|
||||||
isPasswordPrimary: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signInMode: 'SignIn',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('get /.well-known/sign-in-exp for general app', async () => {
|
|
||||||
const client = new MockClient();
|
|
||||||
|
|
||||||
await client.initSession();
|
|
||||||
|
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
|
||||||
|
|
||||||
const response = await getWellKnownSignInExperience(client.interactionCookie);
|
|
||||||
|
|
||||||
// Should support sign-in and register
|
|
||||||
expect(response).toMatchObject({ signInMode: 'SignInAndRegister' });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -112,6 +112,10 @@ const translation = {
|
||||||
unknown: 'Unbekannter Fehler. Versuche es später noch einmal.',
|
unknown: 'Unbekannter Fehler. Versuche es später noch einmal.',
|
||||||
invalid_session: 'Die Sitzung ist ungültig. Bitte melde dich erneut an.',
|
invalid_session: 'Die Sitzung ist ungültig. Bitte melde dich erneut an.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Nutze dein existierendes Admin Konto oder erstelle ein neues Konto um dich in die Demo App einzuloggen.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const de: LocalePhrase = Object.freeze({
|
const de: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -107,6 +107,10 @@ const translation = {
|
||||||
unknown: 'Unknown error. Please try again later.',
|
unknown: 'Unknown error. Please try again later.',
|
||||||
invalid_session: 'Session not found. Please go back and sign in again.',
|
invalid_session: 'Session not found. Please go back and sign in again.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Use your default admin account or create a new account to sign in to the demo app.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const en = Object.freeze({
|
const en = Object.freeze({
|
||||||
|
|
|
@ -113,6 +113,10 @@ const translation = {
|
||||||
invalid_session:
|
invalid_session:
|
||||||
'Session non trouvée. Veuillez revenir en arrière et vous connecter à nouveau.',
|
'Session non trouvée. Veuillez revenir en arrière et vous connecter à nouveau.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Utilisez votre compte administrateur par défaut ou créez un nouveau compte pour vous connecter à la démo.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fr: LocalePhrase = Object.freeze({
|
const fr: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -106,6 +106,10 @@ const translation = {
|
||||||
unknown: '알 수 없는 오류가 발생했어요. 잠시 후에 시도해 주세요.',
|
unknown: '알 수 없는 오류가 발생했어요. 잠시 후에 시도해 주세요.',
|
||||||
invalid_session: '세션을 찾을 수 없어요. 다시 로그인해 주세요.',
|
invalid_session: '세션을 찾을 수 없어요. 다시 로그인해 주세요.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'체험 App에 로그인 하기 위해 관리자 정보 또는 게정을 새로 생성하여 로그인해보세요.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ko: LocalePhrase = Object.freeze({
|
const ko: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -110,6 +110,10 @@ const translation = {
|
||||||
unknown: 'Erro desconhecido. Por favor, tente novamente mais tarde.',
|
unknown: 'Erro desconhecido. Por favor, tente novamente mais tarde.',
|
||||||
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Use sua conta de administrador padrão ou crie uma nova conta para entrar no aplicativo de demonstração.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ptBR: LocalePhrase = Object.freeze({
|
const ptBR: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -108,6 +108,10 @@ const translation = {
|
||||||
unknown: 'Erro desconhecido. Por favor, tente novamente mais tarde.',
|
unknown: 'Erro desconhecido. Por favor, tente novamente mais tarde.',
|
||||||
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Use a sua conta de administrador padrão ou crie uma nova conta para entrar na app de demonstração.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ptPT: LocalePhrase = Object.freeze({
|
const ptPT: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -111,6 +111,10 @@ const translation = {
|
||||||
unknown: 'Неизвестная ошибка. Пожалуйста, повторите попытку позднее.',
|
unknown: 'Неизвестная ошибка. Пожалуйста, повторите попытку позднее.',
|
||||||
invalid_session: 'Сессия не найдена. Пожалуйста, войдите снова.',
|
invalid_session: 'Сессия не найдена. Пожалуйста, войдите снова.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'Nutze dein existierendes Admin Konto oder erstelle ein neues Konto um dich in die Demo App einzuloggen.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ru: LocalePhrase = Object.freeze({
|
const ru: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -108,6 +108,9 @@ const translation = {
|
||||||
unknown: 'Bilinmeyen hata. Lütfen daha sonra tekrar deneyiniz.',
|
unknown: 'Bilinmeyen hata. Lütfen daha sonra tekrar deneyiniz.',
|
||||||
invalid_session: 'Oturum bulunamadı. Lütfen geri dönüp tekrar giriş yapınız.',
|
invalid_session: 'Oturum bulunamadı. Lütfen geri dönüp tekrar giriş yapınız.',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification: 'Demo uygulamaya giriş yapmak için yönetici konsolunu kullanınız.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const trTR: LocalePhrase = Object.freeze({
|
const trTR: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -104,6 +104,10 @@ const translation = {
|
||||||
unknown: '未知错误,请稍后重试。',
|
unknown: '未知错误,请稍后重试。',
|
||||||
invalid_session: '未找到会话,请返回并重新登录。',
|
invalid_session: '未找到会话,请返回并重新登录。',
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification:
|
||||||
|
'管理控制台的用户名和密码是 demo app 的默认登录方式。点击下方创建账号或用现有账号登录。',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const zhCN: LocalePhrase = Object.freeze({
|
const zhCN: LocalePhrase = Object.freeze({
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'Nutze dein existierendes Admin Konto oder erstelle ein neues Konto um dich in die Demo App einzuloggen.',
|
|
||||||
title: 'Du hast dich erfolgreich in der Demo App angemeldet!',
|
title: 'Du hast dich erfolgreich in der Demo App angemeldet!',
|
||||||
subtitle: 'Here is your log in information:',
|
subtitle: 'Here is your log in information:',
|
||||||
username: 'Benutzername: ',
|
username: 'Benutzername: ',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'Use your default admin account or create a new account to sign in to the demo app.',
|
|
||||||
title: "You've successfully signed in the demo app!",
|
title: "You've successfully signed in the demo app!",
|
||||||
subtitle: 'Here is your log in information:',
|
subtitle: 'Here is your log in information:',
|
||||||
username: 'Username: ',
|
username: 'Username: ',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'Utilisez votre compte administrateur par défaut ou créez un nouveau compte pour vous connecter à la démo.',
|
|
||||||
title: 'Vous avez réussi à vous connecter à la démo !',
|
title: 'Vous avez réussi à vous connecter à la démo !',
|
||||||
subtitle: 'Voici vos informations de connexion :',
|
subtitle: 'Voici vos informations de connexion :',
|
||||||
username: "Nom d'utilisateur : ",
|
username: "Nom d'utilisateur : ",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification: '체험 App에 로그인 하기 위해 관리자 정보 또는 게정을 새로 생성하여 로그인해보세요.',
|
|
||||||
title: '성공적으로 체험 App에 로그인되었어요!',
|
title: '성공적으로 체험 App에 로그인되었어요!',
|
||||||
subtitle: '여기 로그인 정보가 있어요:',
|
subtitle: '여기 로그인 정보가 있어요:',
|
||||||
username: '사용자 이름: ',
|
username: '사용자 이름: ',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'Use sua conta de administrador padrão ou crie uma nova conta para entrar no aplicativo de demonstração.',
|
|
||||||
title: 'Você se inscreveu com sucesso no aplicativo de demonstração!',
|
title: 'Você se inscreveu com sucesso no aplicativo de demonstração!',
|
||||||
subtitle: 'Aqui estão suas informações de login:',
|
subtitle: 'Aqui estão suas informações de login:',
|
||||||
username: 'Nome de usuário: ',
|
username: 'Nome de usuário: ',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'Use a sua conta de administrador padrão ou crie uma nova conta para entrar na app de demonstração.',
|
|
||||||
title: 'Entrou com sucesso na app de demonstração!',
|
title: 'Entrou com sucesso na app de demonstração!',
|
||||||
subtitle: 'Aqui estão as suas informações de login:',
|
subtitle: 'Aqui estão as suas informações de login:',
|
||||||
username: 'Utilizador: ',
|
username: 'Utilizador: ',
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification: 'Demo uygulamaya giriş yapmak için yönetici konsolunu kullanınız.',
|
|
||||||
title: 'Demo uygulamaya başarıyla giriş yaptınız!',
|
title: 'Demo uygulamaya başarıyla giriş yaptınız!',
|
||||||
subtitle: 'Sisteme giriş bilgileriniz:',
|
subtitle: 'Sisteme giriş bilgileriniz:',
|
||||||
username: 'Kullanıcı Adı: ',
|
username: 'Kullanıcı Adı: ',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
const demo_app = {
|
const demo_app = {
|
||||||
notification:
|
|
||||||
'管理控制台的用户名和密码是 demo app 的默认登录方式。点击下方创建账号或用现有账号登录。',
|
|
||||||
title: '恭喜!你已成功登录到示例应用!',
|
title: '恭喜!你已成功登录到示例应用!',
|
||||||
subtitle: '以下是本次登录的用户信息:',
|
subtitle: '以下是本次登录的用户信息:',
|
||||||
username: '用户名:',
|
username: '用户名:',
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { sql } from 'slonik';
|
||||||
|
|
||||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
const defaultTenantId = 'default';
|
|
||||||
|
|
||||||
const alteration: AlterationScript = {
|
const alteration: AlterationScript = {
|
||||||
up: async (pool) => {
|
up: async (pool) => {
|
||||||
const isCi = process.env.CI;
|
const isCi = process.env.CI;
|
||||||
|
@ -44,7 +42,7 @@ const alteration: AlterationScript = {
|
||||||
'Logto demo app.',
|
'Logto demo app.',
|
||||||
'SPA',
|
'SPA',
|
||||||
'{ "redirectUris": [], "postLogoutRedirectUris": [] }'::jsonb
|
'{ "redirectUris": [], "postLogoutRedirectUris": [] }'::jsonb
|
||||||
)
|
);
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { generateDarkColor } from '@logto/core-kit';
|
||||||
|
import { sql } from 'slonik';
|
||||||
|
|
||||||
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
|
const defaultPrimaryColor = '#6139F6';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
tenantId: 'admin',
|
||||||
|
id: 'default',
|
||||||
|
color: {
|
||||||
|
primaryColor: defaultPrimaryColor,
|
||||||
|
isDarkModeEnabled: true,
|
||||||
|
darkPrimaryColor: generateDarkColor(defaultPrimaryColor),
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
|
style: 'Logo_Slogan',
|
||||||
|
logoUrl: 'https://logto.io/logo.svg',
|
||||||
|
darkLogoUrl: 'https://logto.io/logo-dark.svg',
|
||||||
|
slogan: 'admin_console.welcome.title',
|
||||||
|
},
|
||||||
|
languageInfo: {
|
||||||
|
autoDetect: true,
|
||||||
|
fallbackLanguage: 'en',
|
||||||
|
},
|
||||||
|
termsOfUseUrl: null,
|
||||||
|
signUp: {
|
||||||
|
identifiers: ['username'],
|
||||||
|
password: true,
|
||||||
|
verify: false,
|
||||||
|
},
|
||||||
|
signIn: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
identifier: 'username',
|
||||||
|
password: true,
|
||||||
|
verificationCode: false,
|
||||||
|
isPasswordPrimary: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socialSignInConnectorTargets: [],
|
||||||
|
signInMode: 'Register',
|
||||||
|
customCss: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const alteration: AlterationScript = {
|
||||||
|
up: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
insert into sign_in_experiences (
|
||||||
|
tenant_id,
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
branding,
|
||||||
|
language_info,
|
||||||
|
terms_of_use_url,
|
||||||
|
sign_up,
|
||||||
|
sign_in,
|
||||||
|
social_sign_in_connector_targets,
|
||||||
|
sign_in_mode,
|
||||||
|
custom_css
|
||||||
|
) values (
|
||||||
|
${data.tenantId},
|
||||||
|
${data.id},
|
||||||
|
${sql.jsonb(data.color)},
|
||||||
|
${sql.jsonb(data.branding)},
|
||||||
|
${sql.jsonb(data.languageInfo)},
|
||||||
|
${data.termsOfUseUrl},
|
||||||
|
${sql.jsonb(data.signUp)},
|
||||||
|
${sql.jsonb(data.signIn)},
|
||||||
|
${sql.jsonb(data.socialSignInConnectorTargets)},
|
||||||
|
${data.signInMode},
|
||||||
|
${data.customCss}
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
delete from sign_in_experiences
|
||||||
|
where tenant_id = 'admin'
|
||||||
|
and id = 'default';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default alteration;
|
|
@ -3,7 +3,7 @@ import { generateDarkColor } from '@logto/core-kit';
|
||||||
import type { SignInExperience } from '../db-entries/index.js';
|
import type { SignInExperience } from '../db-entries/index.js';
|
||||||
import { SignInMode } from '../db-entries/index.js';
|
import { SignInMode } from '../db-entries/index.js';
|
||||||
import { BrandingStyle, SignInIdentifier } from '../foundations/index.js';
|
import { BrandingStyle, SignInIdentifier } from '../foundations/index.js';
|
||||||
import { defaultTenantId } from './tenant.js';
|
import { adminTenantId, defaultTenantId } from './tenant.js';
|
||||||
|
|
||||||
const defaultPrimaryColor = '#6139F6';
|
const defaultPrimaryColor = '#6139F6';
|
||||||
|
|
||||||
|
@ -49,16 +49,19 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly<Sig
|
||||||
/** @deprecated Use `createDefaultSignInExperience()` instead. */
|
/** @deprecated Use `createDefaultSignInExperience()` instead. */
|
||||||
export const defaultSignInExperience = createDefaultSignInExperience(defaultTenantId);
|
export const defaultSignInExperience = createDefaultSignInExperience(defaultTenantId);
|
||||||
|
|
||||||
export const adminConsoleSignInExperience: Readonly<SignInExperience> = Object.freeze({
|
export const createAdminTenantSignInExperience = (): Readonly<SignInExperience> =>
|
||||||
|
Object.freeze({
|
||||||
...defaultSignInExperience,
|
...defaultSignInExperience,
|
||||||
|
tenantId: adminTenantId,
|
||||||
color: {
|
color: {
|
||||||
...defaultSignInExperience.color,
|
...defaultSignInExperience.color,
|
||||||
isDarkModeEnabled: true,
|
isDarkModeEnabled: true,
|
||||||
},
|
},
|
||||||
|
signInMode: SignInMode.Register,
|
||||||
branding: {
|
branding: {
|
||||||
style: BrandingStyle.Logo_Slogan,
|
style: BrandingStyle.Logo_Slogan,
|
||||||
logoUrl: 'https://logto.io/logo.svg',
|
logoUrl: 'https://logto.io/logo.svg',
|
||||||
darkLogoUrl: 'https://logto.io/logo-dark.svg',
|
darkLogoUrl: 'https://logto.io/logo-dark.svg',
|
||||||
slogan: 'admin_console.welcome.title', // TODO: @simeng should we programmatically support an i18n key for slogan?
|
slogan: 'admin_console.welcome.title',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties, ForwardedRef } from 'react';
|
import type { CSSProperties, ForwardedRef, ReactNode } from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import InfoIcon from '@/assets/icons/info-icon.svg';
|
import InfoIcon from '@/assets/icons/info-icon.svg';
|
||||||
|
@ -10,7 +10,7 @@ import * as styles from './index.module.scss';
|
||||||
/* eslint-disable react/require-default-props */
|
/* eslint-disable react/require-default-props */
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
message: string;
|
message: ReactNode;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
import { useContext, useState, useEffect, useCallback, useRef } from 'react';
|
import type { Nullable } from '@silverhand/essentials';
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { AppNotification as Notification } from '@/components/Notification';
|
import { AppNotification as Notification } from '@/components/Notification';
|
||||||
import { PageContext } from '@/hooks/use-page-context';
|
|
||||||
import usePlatform from '@/hooks/use-platform';
|
import usePlatform from '@/hooks/use-platform';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const AppNotification = () => {
|
const AppNotification = () => {
|
||||||
const { isMobile } = usePlatform();
|
const { isMobile } = usePlatform();
|
||||||
const { experienceSettings } = useContext(PageContext);
|
const [notification, setNotification] = useState<Nullable<string>>(null);
|
||||||
const [notification, setNotification] = useState<string>();
|
|
||||||
const [topOffset, setTopOffset] = useState<number>();
|
const [topOffset, setTopOffset] = useState<number>();
|
||||||
const eleRef = useRef<HTMLDivElement>(null);
|
const eleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
|
||||||
if (experienceSettings?.notification) {
|
|
||||||
setNotification(experienceSettings.notification);
|
|
||||||
}
|
|
||||||
}, [experienceSettings]);
|
|
||||||
|
|
||||||
const adjustNotificationPosition = useCallback(() => {
|
const adjustNotificationPosition = useCallback(() => {
|
||||||
const mainEleOffsetTop = document.querySelector('main')?.offsetTop;
|
const mainEleOffsetTop = document.querySelector('main')?.offsetTop;
|
||||||
|
@ -30,6 +25,10 @@ const AppNotification = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNotification(new URLSearchParams(window.location.search).get('notification'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!notification || isMobile) {
|
if (!notification || isMobile) {
|
||||||
return;
|
return;
|
||||||
|
@ -45,7 +44,7 @@ const AppNotification = () => {
|
||||||
}, [adjustNotificationPosition, isMobile, notification]);
|
}, [adjustNotificationPosition, isMobile, notification]);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setNotification('');
|
setNotification(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
|
@ -56,7 +55,7 @@ const AppNotification = () => {
|
||||||
<Notification
|
<Notification
|
||||||
ref={eleRef}
|
ref={eleRef}
|
||||||
className={styles.appNotification}
|
className={styles.appNotification}
|
||||||
message={notification}
|
message={<Trans t={t}>{notification}</Trans>}
|
||||||
style={isMobile ? undefined : { top: topOffset }}
|
style={isMobile ? undefined : { top: topOffset }}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>,
|
/>,
|
||||||
|
|
Loading…
Reference in a new issue