0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor: fix third-party app experience branding (#6223)

This commit is contained in:
Gao Sun 2024-07-12 14:04:43 +08:00 committed by GitHub
parent 3d1f0c93ae
commit ba875b417c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 76 additions and 11 deletions

View file

@ -45,7 +45,10 @@ export type ImageField<FormContext extends FieldValues> = {
type Props<FormContext extends FieldValues> = {
/** The condensed title when user assets service is available. */
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 control: Control<FormContext>;
readonly register: UseFormRegister<FormContext>;

View file

@ -147,7 +147,7 @@ export const createSignInExperienceLibrary = (
return;
}
return pick(found, 'branding', 'color');
return pick(found, 'branding', 'color', 'type', 'isThirdParty');
};
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 {
...deepmerge(
deepmerge(signInExperience, appSignInExperience ?? {}),
deepmerge(signInExperience, getAppSignInExperience()),
organizationOverride ?? {}
),
socialConnectors,

View file

@ -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 { buildInsertIntoWithPool } from '#src/database/insert-into.js';
@ -10,12 +16,22 @@ const createApplicationSignInExperienceQueries = (pool: CommonQueryMethods) => {
returning: true,
});
const safeFindSignInExperienceByApplicationId = async (applicationId: string) => {
const { table, fields } = convertToIdentifiers(ApplicationSignInExperiences);
type ApplicationSignInExperienceReturn = ApplicationSignInExperience &
Pick<Application, 'type' | 'isThirdParty'>;
return pool.maybeOne<ApplicationSignInExperience>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
const safeFindSignInExperienceByApplicationId = async (
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}
join ${applications.table} on ${fields.applicationId}=${applications.fields.id}
where ${fields.applicationId}=${applicationId}
`);
};

View file

@ -3,13 +3,20 @@
*/
import { ConnectorType } from '@logto/connector-kit';
import { ApplicationType, type Branding, type Color, SignInIdentifier } from '@logto/schemas';
import { pick } from '@silverhand/essentials';
import {
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 { createApplication, deleteApplication } from '#src/api/application.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 { OrganizationApiTest } from '#src/helpers/organization.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('dark', organizationBranding.darkLogoUrl, 'rgb(255, 0, 255)');
await deleteApplication(application.id);
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', () => {
beforeAll(async () => {
await updateSignInExperience({
@ -261,6 +298,7 @@ describe('overrides', () => {
expect(faviconElement).toBe(appBranding.favicon);
expect(appleFavicon).toBe(appBranding.favicon);
await deleteApplication(application.id);
await experience.page.close();
});
});