mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(core,shared,phrases): check subdomain when creating protected app (#5217)
This commit is contained in:
parent
400ee914d6
commit
e1bbbd9ebf
30 changed files with 285 additions and 39 deletions
|
@ -51,8 +51,8 @@ export const mockProtectedApplication: Application = {
|
|||
type: ApplicationType.Protected,
|
||||
description: null,
|
||||
oidcClientMetadata: {
|
||||
redirectUris: [],
|
||||
postLogoutRedirectUris: [],
|
||||
redirectUris: ['https://mock.protected.dev/callback'],
|
||||
postLogoutRedirectUris: ['https://mock.protected.dev'],
|
||||
},
|
||||
customClientMetadata: {
|
||||
corsAllowedOrigins: ['http://localhost:3000', 'http://localhost:3001', 'https://logto.dev'],
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { createMockUtils } from '@logto/shared/esm';
|
||||
|
||||
import { mockProtectedApplication } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import {
|
||||
defaultProtectedAppPageRules,
|
||||
defaultProtectedAppSessionDuration,
|
||||
} from '#src/routes/applications/constants.js';
|
||||
import SystemContext from '#src/tenants/SystemContext.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
@ -17,8 +22,9 @@ const { MockQueries } = await import('#src/test-utils/tenant.js');
|
|||
const { createProtectedAppLibrary } = await import('./protected-app.js');
|
||||
|
||||
const findApplicationById = jest.fn(async () => mockProtectedApplication);
|
||||
const { syncAppConfigsToRemote } = createProtectedAppLibrary(
|
||||
new MockQueries({ applications: { findApplicationById } })
|
||||
const findApplicationByProtectedAppHost = jest.fn();
|
||||
const { syncAppConfigsToRemote, checkAndBuildProtectedAppData } = createProtectedAppLibrary(
|
||||
new MockQueries({ applications: { findApplicationById, findApplicationByProtectedAppHost } })
|
||||
);
|
||||
|
||||
const protectedAppConfigProviderConfig = {
|
||||
|
@ -26,6 +32,7 @@ const protectedAppConfigProviderConfig = {
|
|||
namespaceIdentifier: 'fake_namespace_id',
|
||||
keyName: 'fake_key_name',
|
||||
apiToken: '',
|
||||
domain: 'protected.app',
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -72,3 +79,43 @@ describe('syncAppConfigsToRemote()', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAndBuildProtectedAppData()', () => {
|
||||
const origin = 'https://example.com';
|
||||
|
||||
it('should throw if subdomain is invalid', async () => {
|
||||
await expect(checkAndBuildProtectedAppData({ subDomain: 'a-', origin })).rejects.toThrowError(
|
||||
new RequestError({
|
||||
code: 'application.invalid_subdomain',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if subdomain is not available', async () => {
|
||||
findApplicationByProtectedAppHost.mockResolvedValueOnce(mockProtectedApplication);
|
||||
await expect(checkAndBuildProtectedAppData({ subDomain: 'a', origin })).rejects.toThrowError(
|
||||
new RequestError({
|
||||
code: 'application.protected_application_subdomain_exists',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return data if subdomain is available', async () => {
|
||||
const subDomain = 'a';
|
||||
const host = `${subDomain}.${protectedAppConfigProviderConfig.domain}`;
|
||||
await expect(checkAndBuildProtectedAppData({ subDomain, origin })).resolves.toEqual({
|
||||
protectedAppMetadata: {
|
||||
host,
|
||||
origin,
|
||||
sessionDuration: defaultProtectedAppSessionDuration,
|
||||
pageRules: defaultProtectedAppPageRules,
|
||||
},
|
||||
oidcClientMetadata: {
|
||||
redirectUris: [`https://${host}/callback`],
|
||||
postLogoutRedirectUris: [`https://${host}`],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { type Application } from '@logto/schemas';
|
||||
import { isValidSubdomain } from '@logto/shared';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import {
|
||||
defaultProtectedAppPageRules,
|
||||
defaultProtectedAppSessionDuration,
|
||||
} from '#src/routes/applications/constants.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import SystemContext from '#src/tenants/SystemContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
@ -28,7 +36,7 @@ const deleteRemoteAppConfigs = async (host: string): Promise<void> => {
|
|||
|
||||
export const createProtectedAppLibrary = (queries: Queries) => {
|
||||
const {
|
||||
applications: { findApplicationById },
|
||||
applications: { findApplicationById, findApplicationByProtectedAppHost },
|
||||
} = queries;
|
||||
|
||||
const syncAppConfigsToRemote = async (applicationId: string): Promise<void> => {
|
||||
|
@ -58,8 +66,59 @@ export const createProtectedAppLibrary = (queries: Queries) => {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build application data for protected app
|
||||
* check if subdomain is valid
|
||||
* generate host based on subdomain
|
||||
* generate default protectedAppMetadata based on host and origin
|
||||
* generate redirectUris and postLogoutRedirectUris based on host
|
||||
*/
|
||||
const checkAndBuildProtectedAppData = async ({
|
||||
subDomain,
|
||||
origin,
|
||||
}: {
|
||||
subDomain: string;
|
||||
origin: string;
|
||||
}): Promise<Pick<Application, 'protectedAppMetadata' | 'oidcClientMetadata'>> => {
|
||||
assertThat(
|
||||
isValidSubdomain(subDomain),
|
||||
new RequestError({
|
||||
code: 'application.invalid_subdomain',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
// Skip for integration test, use empty value instead
|
||||
const { domain } = EnvSet.values.isIntegrationTest ? { domain: '' } : await getProviderConfig();
|
||||
const host = `${subDomain}.${domain}`;
|
||||
|
||||
const application = await findApplicationByProtectedAppHost(host);
|
||||
|
||||
assertThat(
|
||||
!application,
|
||||
new RequestError({
|
||||
code: 'application.protected_application_subdomain_exists',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
protectedAppMetadata: {
|
||||
host,
|
||||
origin,
|
||||
sessionDuration: defaultProtectedAppSessionDuration,
|
||||
pageRules: defaultProtectedAppPageRules,
|
||||
},
|
||||
oidcClientMetadata: {
|
||||
redirectUris: [`https://${host}/callback`],
|
||||
postLogoutRedirectUris: [`https://${host}`],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
syncAppConfigsToRemote,
|
||||
deleteRemoteAppConfigs,
|
||||
checkAndBuildProtectedAppData,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Applications } from '@logto/schemas';
|
||||
import { ApplicationType, Applications } from '@logto/schemas';
|
||||
import { convertToIdentifiers, convertToPrimitiveOrSql, excludeAutoSetFields } from '@logto/shared';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
@ -22,6 +22,7 @@ const { createApplicationQueries } = await import('./application.js');
|
|||
const {
|
||||
findTotalNumberOfApplications,
|
||||
findApplicationById,
|
||||
findApplicationByProtectedAppHost,
|
||||
insertApplication,
|
||||
updateApplicationById,
|
||||
deleteApplicationById,
|
||||
|
@ -66,6 +67,27 @@ describe('application query', () => {
|
|||
await findApplicationById(id);
|
||||
});
|
||||
|
||||
it('findApplicationByProtectedAppHost', async () => {
|
||||
const host = 'host.protected.app';
|
||||
const rowData = { host };
|
||||
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.protectedAppMetadata}->>'host' = $1
|
||||
and ${fields.type} = $2
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([host, ApplicationType.Protected]);
|
||||
|
||||
return createMockQueryResult([rowData]);
|
||||
});
|
||||
|
||||
await findApplicationByProtectedAppHost(host);
|
||||
});
|
||||
|
||||
it('insertApplication', async () => {
|
||||
const keys = excludeAutoSetFields(Applications.fieldKeys);
|
||||
|
||||
|
|
|
@ -130,6 +130,14 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
|
||||
const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications);
|
||||
|
||||
const findApplicationByProtectedAppHost = async (host: string) =>
|
||||
pool.maybeOne<Application>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.protectedAppMetadata}->>'host' = ${host}
|
||||
and ${fields.type} = ${ApplicationType.Protected}
|
||||
`);
|
||||
|
||||
const insertApplication = buildInsertIntoWithPool(pool)(Applications, {
|
||||
returning: true,
|
||||
});
|
||||
|
@ -231,6 +239,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
findApplications,
|
||||
findTotalNumberOfApplications,
|
||||
findApplicationById,
|
||||
findApplicationByProtectedAppHost,
|
||||
insertApplication,
|
||||
updateApplication,
|
||||
updateApplicationById,
|
||||
|
|
|
@ -13,6 +13,11 @@ const findApplicationById = jest.fn(async () => mockApplication);
|
|||
const deleteApplicationById = jest.fn();
|
||||
const syncAppConfigsToRemote = jest.fn();
|
||||
const deleteRemoteAppConfigs = jest.fn();
|
||||
const checkAndBuildProtectedAppData = jest.fn(async () => {
|
||||
const { oidcClientMetadata, protectedAppMetadata } = mockProtectedApplication;
|
||||
|
||||
return { oidcClientMetadata, protectedAppMetadata };
|
||||
});
|
||||
const updateApplicationById = jest.fn(
|
||||
async (_, data: Partial<CreateApplication>): Promise<Application> => ({
|
||||
...mockApplication,
|
||||
|
@ -46,7 +51,11 @@ const tenantContext = new MockTenant(
|
|||
undefined,
|
||||
{
|
||||
quota: createMockQuotaLibrary(),
|
||||
protectedApps: { syncAppConfigsToRemote, deleteRemoteAppConfigs },
|
||||
protectedApps: {
|
||||
syncAppConfigsToRemote,
|
||||
deleteRemoteAppConfigs,
|
||||
checkAndBuildProtectedAppData,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -109,7 +118,7 @@ describe('application route', () => {
|
|||
name,
|
||||
type,
|
||||
protectedAppMetadata: {
|
||||
host: protectedAppMetadata?.host,
|
||||
subDomain: 'mock',
|
||||
origin: protectedAppMetadata?.origin,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
applicationCreateGuard,
|
||||
applicationPatchGuard,
|
||||
} from './types.js';
|
||||
import { buildProtectedAppData } from './utils.js';
|
||||
|
||||
const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
|
||||
roles.some(({ role: { name } }) => name === InternalRole.Admin);
|
||||
|
@ -152,8 +151,6 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO LOG-7794: check and add domain to Cloudflare
|
||||
|
||||
const application = await insertApplication({
|
||||
id: generateStandardId(),
|
||||
secret: generateStandardSecret(),
|
||||
|
@ -161,7 +158,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
...conditional(
|
||||
rest.type === ApplicationType.Protected &&
|
||||
protectedAppMetadata &&
|
||||
buildProtectedAppData(protectedAppMetadata)
|
||||
(await protectedApps.checkAndBuildProtectedAppData(protectedAppMetadata))
|
||||
),
|
||||
...rest,
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@ const applicationCreateGuardWithProtectedAppMetadata = originalApplicationCreate
|
|||
.extend({
|
||||
protectedAppMetadata: z
|
||||
.object({
|
||||
host: z.string(),
|
||||
subDomain: z.string(),
|
||||
origin: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { defaultProtectedAppPageRules, defaultProtectedAppSessionDuration } from './constants.js';
|
||||
|
||||
/**
|
||||
* Build application data for protected app
|
||||
* generate default protectedAppMetadata based on host and origin
|
||||
* generate redirectUris and postLogoutRedirectUris based on host
|
||||
*/
|
||||
export const buildProtectedAppData = ({ host, origin }: { host: string; origin: string }) => ({
|
||||
protectedAppMetadata: {
|
||||
host,
|
||||
origin,
|
||||
sessionDuration: defaultProtectedAppSessionDuration,
|
||||
pageRules: defaultProtectedAppPageRules,
|
||||
},
|
||||
oidcClientMetadata: {
|
||||
redirectUris: [`https://${host}/callback`],
|
||||
postLogoutRedirectUris: [`https://${host}`],
|
||||
},
|
||||
});
|
|
@ -45,10 +45,7 @@ export const updateProtectedAppSiteConfigs = async (
|
|||
Authorization: `Bearer ${auth.apiToken}`,
|
||||
},
|
||||
throwHttpErrors: false,
|
||||
json: {
|
||||
metadata: {},
|
||||
value: JSON.stringify(value),
|
||||
},
|
||||
json: value,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('admin console application', () => {
|
|||
const applicationName = 'test-protected-app';
|
||||
const metadata = {
|
||||
origin: 'https://example.com',
|
||||
host: 'example.protected.app',
|
||||
subDomain: 'example',
|
||||
};
|
||||
|
||||
const application = await createApplication(applicationName, ApplicationType.Protected, {
|
||||
|
@ -63,11 +63,35 @@ describe('admin console application', () => {
|
|||
expect(application.name).toBe(applicationName);
|
||||
expect(application.type).toBe(ApplicationType.Protected);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('origin', metadata.origin);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('host', metadata.host);
|
||||
expect(application.protectedAppMetadata?.host).toContain(metadata.subDomain);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('sessionDuration');
|
||||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should throw error when creating protected application with existing subdomain', async () => {
|
||||
const applicationName = 'test-protected-app';
|
||||
const metadata = {
|
||||
origin: 'https://example.com',
|
||||
subDomain: 'example',
|
||||
};
|
||||
|
||||
const application = await createApplication(applicationName, ApplicationType.Protected, {
|
||||
// @ts-expect-error the create guard has been modified
|
||||
protectedAppMetadata: metadata,
|
||||
});
|
||||
await expectRejects(
|
||||
createApplication('test-create-app', ApplicationType.Protected, {
|
||||
// @ts-expect-error the create guard has been modified
|
||||
protectedAppMetadata: metadata,
|
||||
}),
|
||||
{
|
||||
code: 'application.protected_application_subdomain_exists',
|
||||
statusCode: 422,
|
||||
}
|
||||
);
|
||||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should throw error when creating a protected application with invalid type', async () => {
|
||||
await expectRejects(createApplication('test-create-app', ApplicationType.Protected), {
|
||||
code: 'application.protected_app_metadata_is_required',
|
||||
|
@ -105,7 +129,7 @@ describe('admin console application', () => {
|
|||
it('should update application details for protected app successfully', async () => {
|
||||
const metadata = {
|
||||
origin: 'https://example.com',
|
||||
host: 'example.protected.app',
|
||||
subDomain: 'example',
|
||||
};
|
||||
|
||||
const application = await createApplication('test-update-app', ApplicationType.Protected, {
|
||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -11,6 +11,9 @@ const application = {
|
|||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
|||
protected_application_only: 'The feature is only available for protected applications.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||
/** UNTRANSLATED */
|
||||
protected_application_subdomain_exists:
|
||||
'The subdomain of Protected application is already in use.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_subdomain: 'Invalid subdomain.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -158,6 +158,8 @@ export const protectedAppConfigProviderDataGuard = z.object({
|
|||
namespaceIdentifier: z.string(),
|
||||
/* Key prefix for protected app config */
|
||||
keyName: z.string(),
|
||||
/* The default domain (e.g protected.app) for the protected app */
|
||||
domain: z.string(),
|
||||
apiToken: z.string(), // Requires account permission for "KV Storage Edit"
|
||||
});
|
||||
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from './ttl-cache.js';
|
|||
export * from './id.js';
|
||||
export * from './user-display-name.js';
|
||||
export * from './phone.js';
|
||||
export * from './sub-domain.js';
|
||||
|
|
18
packages/shared/src/utils/sub-domain.test.ts
Normal file
18
packages/shared/src/utils/sub-domain.test.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { isValidSubdomain } from './sub-domain.js';
|
||||
|
||||
describe('isValidSubdomain()', () => {
|
||||
it('should return true for valid subdomains', () => {
|
||||
expect(isValidSubdomain('a')).toBe(true);
|
||||
expect(isValidSubdomain('1')).toBe(true);
|
||||
expect(isValidSubdomain('a1')).toBe(true);
|
||||
expect(isValidSubdomain('a1-b2')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid subdomains', () => {
|
||||
expect(isValidSubdomain('')).toBe(false);
|
||||
expect(isValidSubdomain('a1-')).toBe(false);
|
||||
expect(isValidSubdomain('-a1')).toBe(false);
|
||||
expect(isValidSubdomain('a1-')).toBe(false);
|
||||
expect(isValidSubdomain('a1.b')).toBe(false);
|
||||
});
|
||||
});
|
7
packages/shared/src/utils/sub-domain.ts
Normal file
7
packages/shared/src/utils/sub-domain.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Checks if the subdomain string is valid
|
||||
* @param subdomain subdomain string
|
||||
* @returns boolean indicating whether the subdomain is valid
|
||||
*/
|
||||
export const isValidSubdomain = (subdomain: string): boolean =>
|
||||
/^([\da-z]([\da-z-]{0,61}[\da-z])?)$/i.test(subdomain);
|
Loading…
Reference in a new issue