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,
|
type: ApplicationType.Protected,
|
||||||
description: null,
|
description: null,
|
||||||
oidcClientMetadata: {
|
oidcClientMetadata: {
|
||||||
redirectUris: [],
|
redirectUris: ['https://mock.protected.dev/callback'],
|
||||||
postLogoutRedirectUris: [],
|
postLogoutRedirectUris: ['https://mock.protected.dev'],
|
||||||
},
|
},
|
||||||
customClientMetadata: {
|
customClientMetadata: {
|
||||||
corsAllowedOrigins: ['http://localhost:3000', 'http://localhost:3001', 'https://logto.dev'],
|
corsAllowedOrigins: ['http://localhost:3000', 'http://localhost:3001', 'https://logto.dev'],
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { createMockUtils } from '@logto/shared/esm';
|
import { createMockUtils } from '@logto/shared/esm';
|
||||||
|
|
||||||
import { mockProtectedApplication } from '#src/__mocks__/index.js';
|
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';
|
import SystemContext from '#src/tenants/SystemContext.js';
|
||||||
|
|
||||||
const { jest } = import.meta;
|
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 { createProtectedAppLibrary } = await import('./protected-app.js');
|
||||||
|
|
||||||
const findApplicationById = jest.fn(async () => mockProtectedApplication);
|
const findApplicationById = jest.fn(async () => mockProtectedApplication);
|
||||||
const { syncAppConfigsToRemote } = createProtectedAppLibrary(
|
const findApplicationByProtectedAppHost = jest.fn();
|
||||||
new MockQueries({ applications: { findApplicationById } })
|
const { syncAppConfigsToRemote, checkAndBuildProtectedAppData } = createProtectedAppLibrary(
|
||||||
|
new MockQueries({ applications: { findApplicationById, findApplicationByProtectedAppHost } })
|
||||||
);
|
);
|
||||||
|
|
||||||
const protectedAppConfigProviderConfig = {
|
const protectedAppConfigProviderConfig = {
|
||||||
|
@ -26,6 +32,7 @@ const protectedAppConfigProviderConfig = {
|
||||||
namespaceIdentifier: 'fake_namespace_id',
|
namespaceIdentifier: 'fake_namespace_id',
|
||||||
keyName: 'fake_key_name',
|
keyName: 'fake_key_name',
|
||||||
apiToken: '',
|
apiToken: '',
|
||||||
|
domain: 'protected.app',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
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 { 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 type Queries from '#src/tenants/Queries.js';
|
||||||
import SystemContext from '#src/tenants/SystemContext.js';
|
import SystemContext from '#src/tenants/SystemContext.js';
|
||||||
import assertThat from '#src/utils/assert-that.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) => {
|
export const createProtectedAppLibrary = (queries: Queries) => {
|
||||||
const {
|
const {
|
||||||
applications: { findApplicationById },
|
applications: { findApplicationById, findApplicationByProtectedAppHost },
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
const syncAppConfigsToRemote = async (applicationId: string): Promise<void> => {
|
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 {
|
return {
|
||||||
syncAppConfigsToRemote,
|
syncAppConfigsToRemote,
|
||||||
deleteRemoteAppConfigs,
|
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 { convertToIdentifiers, convertToPrimitiveOrSql, excludeAutoSetFields } from '@logto/shared';
|
||||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||||
import { snakeCase } from 'snake-case';
|
import { snakeCase } from 'snake-case';
|
||||||
|
@ -22,6 +22,7 @@ const { createApplicationQueries } = await import('./application.js');
|
||||||
const {
|
const {
|
||||||
findTotalNumberOfApplications,
|
findTotalNumberOfApplications,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
|
findApplicationByProtectedAppHost,
|
||||||
insertApplication,
|
insertApplication,
|
||||||
updateApplicationById,
|
updateApplicationById,
|
||||||
deleteApplicationById,
|
deleteApplicationById,
|
||||||
|
@ -66,6 +67,27 @@ describe('application query', () => {
|
||||||
await findApplicationById(id);
|
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 () => {
|
it('insertApplication', async () => {
|
||||||
const keys = excludeAutoSetFields(Applications.fieldKeys);
|
const keys = excludeAutoSetFields(Applications.fieldKeys);
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,14 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
||||||
|
|
||||||
const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications);
|
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, {
|
const insertApplication = buildInsertIntoWithPool(pool)(Applications, {
|
||||||
returning: true,
|
returning: true,
|
||||||
});
|
});
|
||||||
|
@ -231,6 +239,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
||||||
findApplications,
|
findApplications,
|
||||||
findTotalNumberOfApplications,
|
findTotalNumberOfApplications,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
|
findApplicationByProtectedAppHost,
|
||||||
insertApplication,
|
insertApplication,
|
||||||
updateApplication,
|
updateApplication,
|
||||||
updateApplicationById,
|
updateApplicationById,
|
||||||
|
|
|
@ -13,6 +13,11 @@ const findApplicationById = jest.fn(async () => mockApplication);
|
||||||
const deleteApplicationById = jest.fn();
|
const deleteApplicationById = jest.fn();
|
||||||
const syncAppConfigsToRemote = jest.fn();
|
const syncAppConfigsToRemote = jest.fn();
|
||||||
const deleteRemoteAppConfigs = jest.fn();
|
const deleteRemoteAppConfigs = jest.fn();
|
||||||
|
const checkAndBuildProtectedAppData = jest.fn(async () => {
|
||||||
|
const { oidcClientMetadata, protectedAppMetadata } = mockProtectedApplication;
|
||||||
|
|
||||||
|
return { oidcClientMetadata, protectedAppMetadata };
|
||||||
|
});
|
||||||
const updateApplicationById = jest.fn(
|
const updateApplicationById = jest.fn(
|
||||||
async (_, data: Partial<CreateApplication>): Promise<Application> => ({
|
async (_, data: Partial<CreateApplication>): Promise<Application> => ({
|
||||||
...mockApplication,
|
...mockApplication,
|
||||||
|
@ -46,7 +51,11 @@ const tenantContext = new MockTenant(
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
quota: createMockQuotaLibrary(),
|
quota: createMockQuotaLibrary(),
|
||||||
protectedApps: { syncAppConfigsToRemote, deleteRemoteAppConfigs },
|
protectedApps: {
|
||||||
|
syncAppConfigsToRemote,
|
||||||
|
deleteRemoteAppConfigs,
|
||||||
|
checkAndBuildProtectedAppData,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -109,7 +118,7 @@ describe('application route', () => {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
protectedAppMetadata: {
|
protectedAppMetadata: {
|
||||||
host: protectedAppMetadata?.host,
|
subDomain: 'mock',
|
||||||
origin: protectedAppMetadata?.origin,
|
origin: protectedAppMetadata?.origin,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {
|
||||||
applicationCreateGuard,
|
applicationCreateGuard,
|
||||||
applicationPatchGuard,
|
applicationPatchGuard,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { buildProtectedAppData } from './utils.js';
|
|
||||||
|
|
||||||
const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
|
const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
|
||||||
roles.some(({ role: { name } }) => name === InternalRole.Admin);
|
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({
|
const application = await insertApplication({
|
||||||
id: generateStandardId(),
|
id: generateStandardId(),
|
||||||
secret: generateStandardSecret(),
|
secret: generateStandardSecret(),
|
||||||
|
@ -161,7 +158,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
||||||
...conditional(
|
...conditional(
|
||||||
rest.type === ApplicationType.Protected &&
|
rest.type === ApplicationType.Protected &&
|
||||||
protectedAppMetadata &&
|
protectedAppMetadata &&
|
||||||
buildProtectedAppData(protectedAppMetadata)
|
(await protectedApps.checkAndBuildProtectedAppData(protectedAppMetadata))
|
||||||
),
|
),
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,7 +31,7 @@ const applicationCreateGuardWithProtectedAppMetadata = originalApplicationCreate
|
||||||
.extend({
|
.extend({
|
||||||
protectedAppMetadata: z
|
protectedAppMetadata: z
|
||||||
.object({
|
.object({
|
||||||
host: z.string(),
|
subDomain: z.string(),
|
||||||
origin: z.string(),
|
origin: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.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}`,
|
Authorization: `Bearer ${auth.apiToken}`,
|
||||||
},
|
},
|
||||||
throwHttpErrors: false,
|
throwHttpErrors: false,
|
||||||
json: {
|
json: value,
|
||||||
metadata: {},
|
|
||||||
value: JSON.stringify(value),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ describe('admin console application', () => {
|
||||||
const applicationName = 'test-protected-app';
|
const applicationName = 'test-protected-app';
|
||||||
const metadata = {
|
const metadata = {
|
||||||
origin: 'https://example.com',
|
origin: 'https://example.com',
|
||||||
host: 'example.protected.app',
|
subDomain: 'example',
|
||||||
};
|
};
|
||||||
|
|
||||||
const application = await createApplication(applicationName, ApplicationType.Protected, {
|
const application = await createApplication(applicationName, ApplicationType.Protected, {
|
||||||
|
@ -63,11 +63,35 @@ describe('admin console application', () => {
|
||||||
expect(application.name).toBe(applicationName);
|
expect(application.name).toBe(applicationName);
|
||||||
expect(application.type).toBe(ApplicationType.Protected);
|
expect(application.type).toBe(ApplicationType.Protected);
|
||||||
expect(application.protectedAppMetadata).toHaveProperty('origin', metadata.origin);
|
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');
|
expect(application.protectedAppMetadata).toHaveProperty('sessionDuration');
|
||||||
await deleteApplication(application.id);
|
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 () => {
|
it('should throw error when creating a protected application with invalid type', async () => {
|
||||||
await expectRejects(createApplication('test-create-app', ApplicationType.Protected), {
|
await expectRejects(createApplication('test-create-app', ApplicationType.Protected), {
|
||||||
code: 'application.protected_app_metadata_is_required',
|
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 () => {
|
it('should update application details for protected app successfully', async () => {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
origin: 'https://example.com',
|
origin: 'https://example.com',
|
||||||
host: 'example.protected.app',
|
subDomain: 'example',
|
||||||
};
|
};
|
||||||
|
|
||||||
const application = await createApplication('test-update-app', ApplicationType.Protected, {
|
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.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -11,6 +11,9 @@ const application = {
|
||||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -16,6 +16,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -17,6 +17,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -15,6 +15,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -14,6 +14,11 @@ const application = {
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
/** UNTRANSLATED */
|
/** UNTRANSLATED */
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
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);
|
export default Object.freeze(application);
|
||||||
|
|
|
@ -158,6 +158,8 @@ export const protectedAppConfigProviderDataGuard = z.object({
|
||||||
namespaceIdentifier: z.string(),
|
namespaceIdentifier: z.string(),
|
||||||
/* Key prefix for protected app config */
|
/* Key prefix for protected app config */
|
||||||
keyName: z.string(),
|
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"
|
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 './id.js';
|
||||||
export * from './user-display-name.js';
|
export * from './user-display-name.js';
|
||||||
export * from './phone.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