mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
refactor: fix third-party app experience branding (#6223)
This commit is contained in:
parent
3d1f0c93ae
commit
ba875b417c
4 changed files with 76 additions and 11 deletions
|
@ -45,7 +45,10 @@ export type ImageField<FormContext extends FieldValues> = {
|
||||||
type Props<FormContext extends FieldValues> = {
|
type Props<FormContext extends FieldValues> = {
|
||||||
/** The condensed title when user assets service is available. */
|
/** The condensed title when user assets service is available. */
|
||||||
readonly uploadTitle: React.ComponentProps<typeof FormField>['title'];
|
readonly uploadTitle: React.ComponentProps<typeof FormField>['title'];
|
||||||
/** The tooltip to show for all the fields. */
|
/**
|
||||||
|
* When user assets service is available, the tip will be displayed for the `uploadTitle`;
|
||||||
|
* otherwise, it will be displayed for each text input.
|
||||||
|
*/
|
||||||
readonly tip?: React.ComponentProps<typeof FormField>['tip'];
|
readonly tip?: React.ComponentProps<typeof FormField>['tip'];
|
||||||
readonly control: Control<FormContext>;
|
readonly control: Control<FormContext>;
|
||||||
readonly register: UseFormRegister<FormContext>;
|
readonly register: UseFormRegister<FormContext>;
|
||||||
|
|
|
@ -147,7 +147,7 @@ export const createSignInExperienceLibrary = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pick(found, 'branding', 'color');
|
return pick(found, 'branding', 'color', 'type', 'isThirdParty');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFullSignInExperience = async ({
|
const getFullSignInExperience = async ({
|
||||||
|
@ -223,9 +223,17 @@ export const createSignInExperienceLibrary = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get the branding and color from the app sign-in experience if it is not a third-party app. */
|
||||||
|
const getAppSignInExperience = () => {
|
||||||
|
if (!appSignInExperience || appSignInExperience.isThirdParty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return pick(appSignInExperience, 'branding', 'color');
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...deepmerge(
|
...deepmerge(
|
||||||
deepmerge(signInExperience, appSignInExperience ?? {}),
|
deepmerge(signInExperience, getAppSignInExperience()),
|
||||||
organizationOverride ?? {}
|
organizationOverride ?? {}
|
||||||
),
|
),
|
||||||
socialConnectors,
|
socialConnectors,
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { ApplicationSignInExperiences, type ApplicationSignInExperience } from '@logto/schemas';
|
import {
|
||||||
|
type Application,
|
||||||
|
ApplicationSignInExperiences,
|
||||||
|
Applications,
|
||||||
|
type ApplicationSignInExperience,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
import { type Nullable } from '@silverhand/essentials';
|
||||||
import { sql, type CommonQueryMethods } from '@silverhand/slonik';
|
import { sql, type CommonQueryMethods } from '@silverhand/slonik';
|
||||||
|
|
||||||
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
||||||
|
@ -10,12 +16,22 @@ const createApplicationSignInExperienceQueries = (pool: CommonQueryMethods) => {
|
||||||
returning: true,
|
returning: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const safeFindSignInExperienceByApplicationId = async (applicationId: string) => {
|
type ApplicationSignInExperienceReturn = ApplicationSignInExperience &
|
||||||
const { table, fields } = convertToIdentifiers(ApplicationSignInExperiences);
|
Pick<Application, 'type' | 'isThirdParty'>;
|
||||||
|
|
||||||
return pool.maybeOne<ApplicationSignInExperience>(sql`
|
const safeFindSignInExperienceByApplicationId = async (
|
||||||
select ${sql.join(Object.values(fields), sql`, `)}
|
applicationId: string
|
||||||
|
): Promise<Nullable<ApplicationSignInExperienceReturn>> => {
|
||||||
|
const applications = convertToIdentifiers(Applications, true);
|
||||||
|
const { table, fields } = convertToIdentifiers(ApplicationSignInExperiences, true);
|
||||||
|
|
||||||
|
return pool.maybeOne<ApplicationSignInExperienceReturn>(sql`
|
||||||
|
select
|
||||||
|
${sql.join(Object.values(fields), sql`, `)},
|
||||||
|
${applications.fields.type},
|
||||||
|
${applications.fields.isThirdParty}
|
||||||
from ${table}
|
from ${table}
|
||||||
|
join ${applications.table} on ${fields.applicationId}=${applications.fields.id}
|
||||||
where ${fields.applicationId}=${applicationId}
|
where ${fields.applicationId}=${applicationId}
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,13 +3,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConnectorType } from '@logto/connector-kit';
|
import { ConnectorType } from '@logto/connector-kit';
|
||||||
import { ApplicationType, type Branding, type Color, SignInIdentifier } from '@logto/schemas';
|
import {
|
||||||
import { pick } from '@silverhand/essentials';
|
ApplicationType,
|
||||||
|
type Branding,
|
||||||
|
type Color,
|
||||||
|
SignInIdentifier,
|
||||||
|
type FullSignInExperience,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
import { appendPath, pick } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
import api from '#src/api/api.js';
|
||||||
import { setApplicationSignInExperience } from '#src/api/application-sign-in-experience.js';
|
import { setApplicationSignInExperience } from '#src/api/application-sign-in-experience.js';
|
||||||
import { createApplication, deleteApplication } from '#src/api/application.js';
|
import { createApplication, deleteApplication } from '#src/api/application.js';
|
||||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||||
import { demoAppRedirectUri, demoAppUrl } from '#src/constants.js';
|
import { demoAppRedirectUri, demoAppUrl, logtoUrl } from '#src/constants.js';
|
||||||
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
|
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
|
||||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||||
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
||||||
|
@ -194,9 +201,39 @@ describe('overrides', () => {
|
||||||
|
|
||||||
await expectMatchBranding('light', organizationBranding.logoUrl, 'rgb(0, 0, 255)');
|
await expectMatchBranding('light', organizationBranding.logoUrl, 'rgb(0, 0, 255)');
|
||||||
await expectMatchBranding('dark', organizationBranding.darkLogoUrl, 'rgb(255, 0, 255)');
|
await expectMatchBranding('dark', organizationBranding.darkLogoUrl, 'rgb(255, 0, 255)');
|
||||||
|
await deleteApplication(application.id);
|
||||||
await experience.page.close();
|
await experience.page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not use app-level branding when the app is an third-party app', async () => {
|
||||||
|
const application = await createApplication(
|
||||||
|
'Sign-in experience override',
|
||||||
|
ApplicationType.Traditional,
|
||||||
|
{
|
||||||
|
isThirdParty: true,
|
||||||
|
oidcClientMetadata: {
|
||||||
|
redirectUris: [demoAppRedirectUri],
|
||||||
|
postLogoutRedirectUris: [demoAppRedirectUri],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await setApplicationSignInExperience(application.id, {
|
||||||
|
color: appColor,
|
||||||
|
branding: appBranding,
|
||||||
|
});
|
||||||
|
|
||||||
|
// It's hard to simulate third-party apps because their type is "Traditional" while our demo
|
||||||
|
// app is an SPA. Only test the API response here.
|
||||||
|
const experience = await api
|
||||||
|
.get(appendPath(new URL(logtoUrl), 'api/.well-known/sign-in-exp'))
|
||||||
|
.json<FullSignInExperience>();
|
||||||
|
|
||||||
|
expect(experience.branding).toEqual(omniBranding);
|
||||||
|
|
||||||
|
await deleteApplication(application.id);
|
||||||
|
});
|
||||||
|
|
||||||
describe('override fallback', () => {
|
describe('override fallback', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await updateSignInExperience({
|
await updateSignInExperience({
|
||||||
|
@ -261,6 +298,7 @@ describe('overrides', () => {
|
||||||
expect(faviconElement).toBe(appBranding.favicon);
|
expect(faviconElement).toBe(appBranding.favicon);
|
||||||
expect(appleFavicon).toBe(appBranding.favicon);
|
expect(appleFavicon).toBe(appBranding.favicon);
|
||||||
|
|
||||||
|
await deleteApplication(application.id);
|
||||||
await experience.page.close();
|
await experience.page.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue