mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
feat(core): add domain routes (#3892)
This commit is contained in:
parent
43dd5e6d11
commit
0edd549365
43 changed files with 448 additions and 0 deletions
23
packages/core/src/__mocks__/domain.ts
Normal file
23
packages/core/src/__mocks__/domain.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { type Domain, type DomainResponse } from '@logto/schemas';
|
||||
|
||||
export const mockNanoIdForDomain = 'random_string';
|
||||
|
||||
export const mockCreatedAtForDomain = 1_650_969_000_000;
|
||||
|
||||
export const mockTenantIdForHook = 'fake_tenant';
|
||||
|
||||
export const mockDomainResponse: DomainResponse = {
|
||||
id: mockNanoIdForDomain,
|
||||
domain: 'logto.example.com',
|
||||
status: 'pending',
|
||||
errorMessage: null,
|
||||
dnsRecords: [],
|
||||
};
|
||||
|
||||
export const mockDomain: Domain = {
|
||||
...mockDomainResponse,
|
||||
tenantId: mockTenantIdForHook,
|
||||
cloudflareData: null,
|
||||
updatedAt: mockCreatedAtForDomain,
|
||||
createdAt: mockCreatedAtForDomain,
|
||||
};
|
|
@ -13,6 +13,7 @@ import { ApplicationType } from '@logto/schemas';
|
|||
export * from './connector.js';
|
||||
export * from './sign-in-experience.js';
|
||||
export * from './user.js';
|
||||
export * from './domain.js';
|
||||
|
||||
export const mockApplication: Application = {
|
||||
tenantId: 'fake_tenant',
|
||||
|
|
56
packages/core/src/queries/domains.ts
Normal file
56
packages/core/src/queries/domains.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import type { CreateDomain, Domain } from '@logto/schemas';
|
||||
import { Domains } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import { convertToIdentifiers, manyRows } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js';
|
||||
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
||||
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Domains);
|
||||
|
||||
export const createDomainsQueries = (pool: CommonQueryMethods) => {
|
||||
const findAllDomains = async () =>
|
||||
manyRows(
|
||||
pool.query<Domain>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
`)
|
||||
);
|
||||
|
||||
const findDomainById = buildFindEntityByIdWithPool(pool)(Domains);
|
||||
|
||||
const insertDomain = buildInsertIntoWithPool(pool)(Domains, {
|
||||
returning: true,
|
||||
});
|
||||
|
||||
const updateDomain = buildUpdateWhereWithPool(pool)(Domains, true);
|
||||
|
||||
const updateDomainById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateDomain>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'replace'
|
||||
) => updateDomain({ set, where: { id }, jsonbMode });
|
||||
|
||||
const deleteDomainById = async (id: string) => {
|
||||
const { rowCount } = await pool.query(sql`
|
||||
delete from ${table}
|
||||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError(Domains.table, id);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findAllDomains,
|
||||
findDomainById,
|
||||
insertDomain,
|
||||
updateDomainById,
|
||||
deleteDomainById,
|
||||
};
|
||||
};
|
69
packages/core/src/routes/domain.test.ts
Normal file
69
packages/core/src/routes/domain.test.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { type Domain, type CreateDomain } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockDomain, mockDomainResponse } from '#src/__mocks__/domain.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const domains = {
|
||||
findAllDomains: jest.fn(async (): Promise<Domain[]> => [mockDomain]),
|
||||
insertDomain: async (data: CreateDomain): Promise<Domain> => ({
|
||||
...mockDomain,
|
||||
...data,
|
||||
}),
|
||||
findDomainById: async (id: string): Promise<Domain> => {
|
||||
const domain = [mockDomain].find((domain) => domain.id === id);
|
||||
if (!domain) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
return domain;
|
||||
},
|
||||
deleteDomainById: jest.fn(),
|
||||
};
|
||||
|
||||
const tenantContext = new MockTenant(undefined, { domains });
|
||||
|
||||
const domainRoutes = await pickDefault(import('./domain.js'));
|
||||
|
||||
describe('domain routes', () => {
|
||||
const domainRequest = createRequester({ authedRoutes: domainRoutes, tenantContext });
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('GET /domains', async () => {
|
||||
const response = await domainRequest.get('/domains');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([mockDomainResponse]);
|
||||
});
|
||||
|
||||
it('GET /domains/:id', async () => {
|
||||
const response = await domainRequest.get(`/domains/${mockDomain.id}`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchObject(mockDomainResponse);
|
||||
});
|
||||
|
||||
it('POST /domains', async () => {
|
||||
domains.findAllDomains.mockResolvedValueOnce([]);
|
||||
const response = await domainRequest.post('/domains').send({ domain: 'test.com' });
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body.id).toBeTruthy();
|
||||
expect(response.body.domain).toEqual('test.com');
|
||||
});
|
||||
|
||||
it('POST /domains should fail when there is already a domain', async () => {
|
||||
await expect(
|
||||
domainRequest.post('/domains').send({ domain: mockDomain.domain })
|
||||
).resolves.toHaveProperty('status', 422);
|
||||
});
|
||||
|
||||
it('DELETE /domains/:id', async () => {
|
||||
await expect(domainRequest.delete(`/domains/${mockDomain.id}`)).resolves.toHaveProperty(
|
||||
'status',
|
||||
204
|
||||
);
|
||||
});
|
||||
});
|
90
packages/core/src/routes/domain.ts
Normal file
90
packages/core/src/routes/domain.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Domains, domainResponseGuard, domainSelectFields } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { pick } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function domainRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
domains: { findAllDomains, findDomainById, insertDomain, deleteDomainById },
|
||||
} = queries;
|
||||
|
||||
router.get(
|
||||
'/domains',
|
||||
koaGuard({ response: domainResponseGuard.array(), status: 200 }),
|
||||
async (ctx, next) => {
|
||||
const domains = await findAllDomains();
|
||||
ctx.body = domains.map((domain) => pick(domain, ...domainSelectFields));
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/domains/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
response: domainResponseGuard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
} = ctx.guard;
|
||||
|
||||
const domain = await findDomainById(id);
|
||||
|
||||
ctx.body = pick(domain, ...domainSelectFields);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/domains',
|
||||
koaGuard({
|
||||
body: Domains.createGuard.pick({ domain: true }),
|
||||
response: domainResponseGuard,
|
||||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const existingDomains = await findAllDomains();
|
||||
assertThat(
|
||||
existingDomains.length === 0,
|
||||
new RequestError({
|
||||
code: 'domain.limit_to_one_domain',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
const domain = await insertDomain({
|
||||
...ctx.guard.body,
|
||||
id: generateStandardId(),
|
||||
});
|
||||
|
||||
ctx.status = 201;
|
||||
ctx.body = pick(domain, ...domainSelectFields);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/domains/:id',
|
||||
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
await deleteDomainById(id);
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -18,6 +18,7 @@ import authnRoutes from './authn.js';
|
|||
import connectorRoutes from './connector/index.js';
|
||||
import customPhraseRoutes from './custom-phrase.js';
|
||||
import dashboardRoutes from './dashboard.js';
|
||||
import domainRoutes from './domain.js';
|
||||
import hookRoutes from './hook.js';
|
||||
import interactionRoutes from './interaction/index.js';
|
||||
import logRoutes from './log.js';
|
||||
|
@ -56,6 +57,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
hookRoutes(managementRouter, tenant);
|
||||
verificationCodeRoutes(managementRouter, tenant);
|
||||
userAssetsRoutes(managementRouter, tenant);
|
||||
domainRoutes(managementRouter, tenant);
|
||||
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
wellKnownRoutes(anonymousRouter, tenant);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createApplicationQueries } from '#src/queries/application.js';
|
|||
import { createApplicationsRolesQueries } from '#src/queries/applications-roles.js';
|
||||
import { createConnectorQueries } from '#src/queries/connector.js';
|
||||
import { createCustomPhraseQueries } from '#src/queries/custom-phrase.js';
|
||||
import { createDomainsQueries } from '#src/queries/domains.js';
|
||||
import { createHooksQueries } from '#src/queries/hooks.js';
|
||||
import { createLogQueries } from '#src/queries/log.js';
|
||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||
|
@ -37,6 +38,7 @@ export default class Queries {
|
|||
applicationsRoles = createApplicationsRolesQueries(this.pool);
|
||||
verificationStatuses = createVerificationStatusQueries(this.pool);
|
||||
hooks = createHooksQueries(this.pool);
|
||||
domains = createDomainsQueries(this.pool);
|
||||
|
||||
constructor(
|
||||
public readonly pool: CommonQueryMethods,
|
||||
|
|
22
packages/integration-tests/src/api/domain.ts
Normal file
22
packages/integration-tests/src/api/domain.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { DomainResponse } from '@logto/schemas';
|
||||
|
||||
import { generateDomain } from '#src/utils.js';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
|
||||
export const createDomain = async (domain?: string) =>
|
||||
authedAdminApi
|
||||
.post('domains', {
|
||||
json: {
|
||||
domain: domain ?? generateDomain(),
|
||||
},
|
||||
})
|
||||
.json<DomainResponse>();
|
||||
|
||||
export const getDomains = async () => authedAdminApi.get('domains').json<DomainResponse[]>();
|
||||
|
||||
export const getDomain = async (domainId: string) =>
|
||||
authedAdminApi.get(`domains/${domainId}`).json<DomainResponse>();
|
||||
|
||||
export const deleteDomain = async (domainId: string) =>
|
||||
authedAdminApi.delete(`domains/${domainId}`);
|
|
@ -7,5 +7,6 @@ export * from './logs.js';
|
|||
export * from './dashboard.js';
|
||||
export * from './interaction.js';
|
||||
export * from './logto-config.js';
|
||||
export * from './domain.js';
|
||||
|
||||
export { default as api, authedAdminApi } from './api.js';
|
||||
|
|
54
packages/integration-tests/src/tests/api/domains.test.ts
Normal file
54
packages/integration-tests/src/tests/api/domains.test.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { HTTPError } from 'got';
|
||||
|
||||
import { createDomain, deleteDomain, getDomain, getDomains } from '#src/api/domain.js';
|
||||
import { generateDomain } from '#src/utils.js';
|
||||
|
||||
describe('domains', () => {
|
||||
afterEach(async () => {
|
||||
const domains = await getDomains();
|
||||
await Promise.all(domains.map(async (domain) => deleteDomain(domain.id)));
|
||||
});
|
||||
|
||||
it('should get domains list successfully', async () => {
|
||||
await createDomain();
|
||||
const domains = await getDomains();
|
||||
|
||||
expect(domains.length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create domain successfully', async () => {
|
||||
const domainName = generateDomain();
|
||||
const domain = await createDomain(domainName);
|
||||
|
||||
expect(domain.domain).toBe(domainName);
|
||||
});
|
||||
|
||||
it('should fail when already has a domain', async () => {
|
||||
await createDomain();
|
||||
|
||||
const response = await createDomain().catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(422);
|
||||
});
|
||||
|
||||
it('should get domain detail successfully', async () => {
|
||||
const createdDomain = await createDomain();
|
||||
const domain = await getDomain(createdDomain.id);
|
||||
|
||||
expect(domain.domain).toBe(createdDomain.domain);
|
||||
});
|
||||
|
||||
it('should return 404 if domain does not exist', async () => {
|
||||
const response = await getDomain('non_existent_domain').catch((error: unknown) => error);
|
||||
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should delete domain successfully', async () => {
|
||||
const domain = await createDomain();
|
||||
|
||||
await deleteDomain(domain.id);
|
||||
|
||||
const response = await getDomain(domain.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ export const generateResourceIndicator = () => `https://${crypto.randomUUID()}.l
|
|||
export const generateEmail = () => `${crypto.randomUUID().toLowerCase()}@logto.io`;
|
||||
export const generateScopeName = () => `sc:${crypto.randomUUID()}`;
|
||||
export const generateRoleName = () => `role_${crypto.randomUUID()}`;
|
||||
export const generateDomain = () => `${crypto.randomUUID().toLowerCase().slice(0, 5)}.example.com`;
|
||||
|
||||
export const generatePhone = (isE164?: boolean) => {
|
||||
const plus = isE164 ? '+' : '';
|
||||
|
|
5
packages/phrases/src/locales/de/errors/domain.ts
Normal file
5
packages/phrases/src/locales/de/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Sie können nur eine benutzerdefinierte Domain haben.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/en/errors/domain.ts
Normal file
5
packages/phrases/src/locales/en/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'You can only have one custom domain.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/es/errors/domain.ts
Normal file
5
packages/phrases/src/locales/es/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Solo puedes tener un dominio personalizado.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/fr/errors/domain.ts
Normal file
5
packages/phrases/src/locales/fr/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domaine = {
|
||||
limit_to_one_domain: "Vous ne pouvez avoir qu'un seul domaine personnalisé.",
|
||||
};
|
||||
|
||||
export default domaine;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/it/errors/domain.ts
Normal file
5
packages/phrases/src/locales/it/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Puoi avere solo un dominio personalizzato.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ja/errors/domain.ts
Normal file
5
packages/phrases/src/locales/ja/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'カスタムドメインは1つしか持てません。',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ko/errors/domain.ts
Normal file
5
packages/phrases/src/locales/ko/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: '하나의 맞춤 도메인만 사용할 수 있습니다.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pl-pl/errors/domain.ts
Normal file
5
packages/phrases/src/locales/pl-pl/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Możesz mieć tylko jedną niestandardową domenę.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pt-br/errors/domain.ts
Normal file
5
packages/phrases/src/locales/pt-br/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pt-pt/errors/domain.ts
Normal file
5
packages/phrases/src/locales/pt-pt/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Você só pode ter um domínio personalizado.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ru/errors/domain.ts
Normal file
5
packages/phrases/src/locales/ru/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Вы можете использовать только один пользовательский домен.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/tr-tr/errors/domain.ts
Normal file
5
packages/phrases/src/locales/tr-tr/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: 'Sadece bir özel alan adınız olabilir.',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-cn/errors/domain.ts
Normal file
5
packages/phrases/src/locales/zh-cn/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: '仅限一个自定义域名。',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-hk/errors/domain.ts
Normal file
5
packages/phrases/src/locales/zh-hk/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: '您只能有一个自定义域名。',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-tw/errors/domain.ts
Normal file
5
packages/phrases/src/locales/zh-tw/errors/domain.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const domain = {
|
||||
limit_to_one_domain: '您只能擁有一個自訂網域。',
|
||||
};
|
||||
|
||||
export default domain;
|
|
@ -1,5 +1,6 @@
|
|||
import auth from './auth.js';
|
||||
import connector from './connector.js';
|
||||
import domain from './domain.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
|
@ -38,6 +39,7 @@ const errors = {
|
|||
storage,
|
||||
resource,
|
||||
hook,
|
||||
domain,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
21
packages/schemas/src/types/domain.ts
Normal file
21
packages/schemas/src/types/domain.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { type z } from 'zod';
|
||||
|
||||
import { Domains } from '../db-entries/index.js';
|
||||
|
||||
export const domainSelectFields = Object.freeze([
|
||||
'id',
|
||||
'domain',
|
||||
'status',
|
||||
'errorMessage',
|
||||
'dnsRecords',
|
||||
] as const);
|
||||
|
||||
export const domainResponseGuard = Domains.guard.pick({
|
||||
id: true,
|
||||
domain: true,
|
||||
status: true,
|
||||
errorMessage: true,
|
||||
dnsRecords: true,
|
||||
});
|
||||
|
||||
export type DomainResponse = z.infer<typeof domainResponseGuard>;
|
|
@ -18,3 +18,4 @@ export * from './service-log.js';
|
|||
export * from './theme.js';
|
||||
export * from './cookie.js';
|
||||
export * from './dashboard.js';
|
||||
export * from './domain.js';
|
||||
|
|
Loading…
Add table
Reference in a new issue