diff --git a/.husky/pre-commit b/.husky/pre-commit index dff836df..72c4429b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm exec lint-staged \ No newline at end of file +npm test diff --git a/package.json b/package.json index 2de24072..08ef9f48 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:integration": "cross-env DATABASE_LOCATION=:memory: vitest run --project integration", "test:e2e": "playwright test", "---- DB ------------------------------------------------------------": "", - "setupdb": "pnpm migrate && pnpm db-types", + "setupdb": "pnpm migrate:latest && pnpm db-types", "migrate": "tsx ./services/src/kysely/migrator.ts", "migrate:latest": "pnpm run migrate -- latest", "migrate:up": "pnpm run migrate -- up", @@ -43,6 +43,7 @@ "tailwind-merge": "^2.3.0", "tailwind-variants": "^0.2.1", "typesafe-utils": "^1.16.2", + "uuid": "^9.0.1", "zod": "^3.23.5" }, "devDependencies": { @@ -58,6 +59,7 @@ "@types/jsonwebtoken": "^9.0.6", "@types/minimist": "^1.2.5", "@types/node": "^20.12.7", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.6.0", "@typescript-eslint/parser": "^7.6.0", "autoprefixer": "^10.4.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf59e798..025be49a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: typesafe-utils: specifier: ^1.16.2 version: 1.16.2 + uuid: + specifier: ^9.0.1 + version: 9.0.1 zod: specifier: ^3.23.5 version: 3.23.8 @@ -99,6 +102,9 @@ importers: '@types/node': specifier: ^20.12.7 version: 20.13.0 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 '@typescript-eslint/eslint-plugin': specifier: ^7.6.0 version: 7.11.0(@typescript-eslint/parser@7.11.0(eslint@9.4.0)(typescript@5.4.5))(eslint@9.4.0)(typescript@5.4.5) @@ -765,6 +771,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validator@13.11.10': resolution: {integrity: sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==} @@ -2134,10 +2143,6 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - lucide-svelte@0.378.0: resolution: {integrity: sha512-T7hV1sfOc94AWE5GOJ6r9wGEsR4h4TJr8d4Z0sM8O0e3IBcmeIvEGRAA6jCp7NGy4PeGrn5Tju6Y2JwJQntNrQ==} peerDependencies: @@ -2797,11 +2802,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true - semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} engines: {node: '>=10'} @@ -3251,6 +3251,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + valibot@0.30.0: resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} @@ -3931,6 +3935,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/uuid@9.0.8': {} + '@types/validator@13.11.10': optional: true @@ -5340,7 +5346,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.2 - semver: 7.6.0 + semver: 7.6.2 just-clone@6.2.0: {} @@ -5455,10 +5461,6 @@ snapshots: lru-cache@10.2.2: {} - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - lucide-svelte@0.378.0(svelte@4.2.17): dependencies: svelte: 4.2.17 @@ -6063,10 +6065,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.0: - dependencies: - lru-cache: 6.0.0 - semver@7.6.2: {} set-blocking@2.0.0: {} @@ -6596,6 +6594,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@9.0.1: {} + valibot@0.30.0: optional: true diff --git a/services/package.json b/services/package.json index 87afd26a..22c12e98 100644 --- a/services/package.json +++ b/services/package.json @@ -7,9 +7,11 @@ "kysely": "^0.27.3", "pino": "^9.0.0", "pino-pretty": "^11.0.0", + "uuid": "^9.0.1", "zod": "^3.23.5" }, "devDependencies": { + "@types/uuid": "^9.0.8", "vite": "^5.2.11", "vitest": "^1.5.3" }, diff --git a/services/src/auth/api-access-repository.integration.test.ts b/services/src/auth/api-access-repository.integration.test.ts new file mode 100644 index 00000000..e6e99f47 --- /dev/null +++ b/services/src/auth/api-access-repository.integration.test.ts @@ -0,0 +1,134 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { db } from '../db/database' +import { runMigration } from '../db/database-migration-util' +import { createProject } from '../project/project.repository' +import { + createApiAccess, + deleteApiAccess, + getApiAccessForProject, + projectHasKey, + setApiAccessName +} from './api-access.repository' +import type { ProjectId } from '../project/project' + +beforeEach(async () => { + db.reset() + await runMigration() +}) + +describe('ApiKey Repository', () => { + let projectId: ProjectId + beforeEach(async () => { + projectId = (await createProject({ name: 'demo project name' })).id as ProjectId + }) + + describe('createApiAccess', () => { + it('should create an api key given an existing project', async () => { + const result = await createApiAccess(projectId) + expect(result).toBeTruthy() + expect(false).toBeFalsy() + }) + }) + + describe('getApiAccessForProject', () => { + it('should fetch all apiKeys for a given project', async () => { + const access1 = await createApiAccess(projectId) + const access2 = await createApiAccess(projectId) + + const keys = (await getApiAccessForProject(projectId)).map((it) => it.apikey) + expect(keys).toHaveLength(2) + expect(keys).includes(access1.apikey) + expect(keys).includes(access2.apikey) + }) + + it('should retrieve the name of the apiKey when it is created with one', async () => { + const name = 'some key name' + const key = await createApiAccess(projectId, name) + const result = await getApiAccessForProject(projectId) + + expect(result.find((it) => it.apikey === key.apikey)?.name).toBe(name) + }) + + it('should no longer find deleted keys', async () => { + const key = await createApiAccess(projectId) + + const keysBeforeDelete = await getApiAccessForProject(projectId) + expect(keysBeforeDelete.map((it) => it.apikey)).toContain(key.apikey) + + await deleteApiAccess(projectId, key.apikey) + const keysAfterDelete = await getApiAccessForProject(projectId) + expect(keysAfterDelete.map((it) => it.apikey).includes(key.apikey)).toBe(false) + }) + + it('should return an empty list in case the project does not exist', async () => { + const keys = await getApiAccessForProject(projectId) + expect(keys).toHaveLength(0) + }) + + it('should return an empty list if there are no keys', async () => { + const result = await getApiAccessForProject(projectId) + expect(result).toHaveLength(0) + }) + }) + + describe('setApiAccessName', () => { + it('should be able to set a name when previously there was none', async () => { + const key = await createApiAccess(projectId) + const initialRetrieval = await getApiAccessForProject(projectId).then((it) => + it.find((apiKey) => apiKey.apikey === key.apikey) + ) + + expect(initialRetrieval?.name).toBeFalsy() + const updatedName = 'some new apiKeyName' + await setApiAccessName(key.id, updatedName) + + const secondRetrieval = await getApiAccessForProject(projectId).then((it) => + it.find((apiKey) => apiKey.apikey === key.apikey) + ) + expect(secondRetrieval?.name).toBe(updatedName) + }) + + it('should be able to set the name', async () => { + const initialName = 'my personal api key' + const key = await createApiAccess(projectId, initialName) + const initialRetrieval = await getApiAccessForProject(projectId).then((it) => + it.find((apiKey) => apiKey.apikey === key.apikey) + ) + + expect(initialRetrieval?.name).toBe(initialName) + const updatedName = 'some new apiKeyName' + await setApiAccessName(key.id, updatedName) + + const secondRetrieval = await getApiAccessForProject(projectId).then((it) => + it.find((apiKey) => apiKey.apikey === key.apikey) + ) + expect(secondRetrieval?.name).toBe(updatedName) + }) + }) + + describe('projectHasKey', () => { + it('should return true if there is a key for the project', async () => { + const key = await createApiAccess(projectId) + const result = await projectHasKey(projectId, key.apikey) + expect(result).toBe(true) + }) + + it('should return false if the project does not have the corresponding key', async () => { + const result = await projectHasKey(projectId, 'nonexiststant-id') + expect(result).toBe(false) + }) + + it('should return false if the project does not exist', async () => { + const result = await projectHasKey(4242, 'nonexiststant-id') + expect(result).toBe(false) + }) + + it('should return false if key and project exist, but do not match', async () => { + const otherProjectId = (await createProject({ name: 'another Project' })).id as ProjectId + const key = await createApiAccess(projectId) + + const result = await projectHasKey(otherProjectId, key.apikey) + expect(result).toBe(false) + }) + }) +}) diff --git a/services/src/auth/api-access.repository.ts b/services/src/auth/api-access.repository.ts new file mode 100644 index 00000000..751ca301 --- /dev/null +++ b/services/src/auth/api-access.repository.ts @@ -0,0 +1,53 @@ +import { db } from '../db/database' +import type { ApiKeyCreationParams, SelectableApiKey } from './api-access' +import { v4 as uuid } from 'uuid' + +export function createApiAccess( + projectId: number, + name: string | null = null +): Promise { + const insertKey: ApiKeyCreationParams = { + apikey: uuid(), + name, + project_id: projectId + } + + return db + .insertInto('apiaccess') + .values(insertKey) + .returningAll() + .executeTakeFirstOrThrow(() => new Error('Error creating Api Access Key')) +} + +export function getApiAccessForProject(projectId: number): Promise { + return db.selectFrom('apiaccess').selectAll().where('project_id', '==', projectId).execute() +} + +export async function setApiAccessName(keyId: number, name: string): Promise { + await db + .updateTable('apiaccess') + .set({ + name + }) + .where('id', '==', keyId) + .execute() +} + +export async function projectHasKey(projectId: number, apikey: string): Promise { + const result = await db + .selectFrom('apiaccess') + .selectAll() + .where('project_id', '==', projectId) + .where('apikey', '==', apikey) + .execute() + + return !!result.length +} + +export async function deleteApiAccess(projectId: number, apikey: string): Promise { + await db + .deleteFrom('apiaccess') + .where('project_id', '==', projectId) + .where('apikey', '==', apikey) + .execute() +} diff --git a/services/src/auth/api-access.service.ts b/services/src/auth/api-access.service.ts new file mode 100644 index 00000000..0c416bd9 --- /dev/null +++ b/services/src/auth/api-access.service.ts @@ -0,0 +1,27 @@ +import type { ProjectId } from '../project/project' +import { type ApiAccess, type ApiAccessId, type ApiKey, apiAccessSchema } from './api-access' +import { + createApiAccess, + getApiAccessForProject, + projectHasKey, + setApiAccessName +} from './api-access.repository' + +export const checkApiKeyAccess = async (apiKey: ApiKey, projectId: ProjectId): Promise => + projectHasKey(projectId, apiKey) + +export const addApiAccess = async (projectId: ProjectId): Promise => { + const key = await createApiAccess(projectId) + + return apiAccessSchema.parse(key) +} + +export const changeApiAccessName = async (apiAccessId: ApiAccessId, name: string) => { + await setApiAccessName(apiAccessId, name) +} + +export const listApiAccessForProject = async (projectId: ProjectId): Promise => { + const queryResult = await getApiAccessForProject(projectId) + + return apiAccessSchema.array().parse(queryResult) +} diff --git a/services/src/auth/api-access.ts b/services/src/auth/api-access.ts new file mode 100644 index 00000000..7583464b --- /dev/null +++ b/services/src/auth/api-access.ts @@ -0,0 +1,22 @@ +import type { Insertable, Selectable } from 'kysely' +import type { Apiaccess } from 'kysely-codegen' +import { z } from 'zod' + +export type ApiKeyCreationParams = Insertable> +export type SelectableApiKey = Selectable + +const apiAccessIdSchema = z.number().brand('api-access') +export type ApiAccessId = z.infer + +export const apiKeySchema = z.string().uuid().brand('api-key') +export type ApiKey = z.infer + +export const apiAccessSchema = z.object({ + id: apiAccessIdSchema, + apikey: apiKeySchema, + name: z.string(), + project_id: z.number(), + created_at: z.date(), + updated_at: z.date() +}) +export type ApiAccess = z.infer diff --git a/services/src/kysely/migrations/2024-04-28T09:42:38Z_init.ts b/services/src/kysely/migrations/2024-04-28T09:42:38Z_init.ts index 50f2a5a3..77d74b1e 100644 --- a/services/src/kysely/migrations/2024-04-28T09:42:38Z_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09:42:38Z_init.ts @@ -11,7 +11,7 @@ export async function up(db: Kysely): Promise { await createTableMigration(db, 'projects') .addColumn('name', 'text', (col) => col.unique().notNull()) .addColumn('base_language', 'integer', (col) => - col.references('languages.id').onDelete('restrict').notNull() + col.references('languages.id').onDelete('restrict') ) .execute() diff --git a/services/src/kysely/migrations/2024-05-16T19:00:00Z_apikey.ts b/services/src/kysely/migrations/2024-05-16T19:00:00Z_apikey.ts new file mode 100644 index 00000000..a11e4a2d --- /dev/null +++ b/services/src/kysely/migrations/2024-05-16T19:00:00Z_apikey.ts @@ -0,0 +1,16 @@ +import { Kysely } from 'kysely' +import { createTableMigration } from '../migration.util' + +export async function up(db: Kysely): Promise { + await createTableMigration(db, 'apiaccess') + .addColumn('apikey', 'text', (col) => col.unique().notNull()) + .addColumn('name', 'text') + .addColumn('project_id', 'integer', (col) => + col.references('projects.id').onDelete('cascade').notNull() + ) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('apiaccess').execute() +} diff --git a/services/src/project/project-repository.integration.test.ts b/services/src/project/project-repository.integration.test.ts new file mode 100644 index 00000000..8379aff1 --- /dev/null +++ b/services/src/project/project-repository.integration.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import type { PojectCreationParams, SelectableProject } from './project' +import { db } from '../db/database' +import { runMigration } from '../db/database-migration-util' +import { createProject, deleteProjectById, getProjectById } from './project.repository' + +const projectCreationObject: PojectCreationParams = { + name: 'some project name' +} + +beforeEach(async () => { + db.reset() + await runMigration() +}) + +describe('Project Repository', () => { + describe('createProject', () => { + it('should create a project with the correct attributes', async () => { + await createProject(projectCreationObject) + + const projects = await db.selectFrom('projects').selectAll().execute() + expect(projects).toHaveLength(1) + + const project = projects[0] as SelectableProject + + expect(project).toMatchObject(projectCreationObject) + expect(project.id).toBeTypeOf('number') + }) + }) + + describe('getProjectById', () => { + it('should get a created project with its ID', async () => { + const createdProject = await createProject(projectCreationObject) + + const retrievedProject = await getProjectById(createdProject.id) + + expect(retrievedProject.id).toBe(createdProject.id) + }) + }) + + describe('deleteProjectById', () => { + it('should delete a project based on its ID', async () => { + const createdProject = await createProject(projectCreationObject) + + const retrievedProject = await getProjectById(createdProject.id) + expect(retrievedProject).toBeTruthy() + + await deleteProjectById(createdProject.id) + await expect(() => getProjectById(createdProject.id)).rejects.toThrowError() + }) + }) +}) diff --git a/services/src/project/project.repository.ts b/services/src/project/project.repository.ts new file mode 100644 index 00000000..ff403b2b --- /dev/null +++ b/services/src/project/project.repository.ts @@ -0,0 +1,24 @@ +import { db } from '../db/database' +import type { PojectCreationParams } from './project' + +export function createProject(projectParams: PojectCreationParams) { + return db + .insertInto('projects') + .values({ + ...projectParams + }) + .returningAll() + .executeTakeFirstOrThrow(() => new Error('Error creating Project')) +} + +export function getProjectById(projectId: number) { + return db + .selectFrom('projects') + .selectAll() + .where('projects.id', '==', projectId) + .executeTakeFirstOrThrow(() => new Error(`Could not find Project with ID "${projectId}"`)) +} + +export function deleteProjectById(projectId: number) { + return db.deleteFrom('projects').where('projects.id', '==', projectId).execute() +} diff --git a/services/src/project/project.ts b/services/src/project/project.ts new file mode 100644 index 00000000..b48efff3 --- /dev/null +++ b/services/src/project/project.ts @@ -0,0 +1,20 @@ +import type { Insertable, Selectable } from 'kysely' +import type { Projects } from 'kysely-codegen' +import { z } from 'zod' + +export type PojectCreationParams = Insertable< + Omit +> +export type SelectableProject = Selectable + +const projectIdSchema = z.number().brand('projectId') +export type ProjectId = z.infer + +const projectSchema = z.object({ + id: projectIdSchema, + created_at: z.string(), + updated_at: z.string(), + name: z.string(), + base_language: z.number().nullable() +}) +export type Project = z.infer diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e62952f2..945caea5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,7 +4,7 @@ import { TOKEN_NAME, parseTokenToJwt } from 'services/auth/token' import type { UserAuthCredentials } from 'services/user/user' import { getUserAuthCredentials } from 'services/user/user-auth-service' -const PUBLIC_ROUTES = ['/', '/login', '/signup'] +const PUBLIC_ROUTES = [/^\/$/, /^\/login$/, /^\/signup$/, /^\/api\/*/] export const handle: Handle = async ({ event, resolve }) => { const { locals, cookies, url } = event @@ -25,7 +25,7 @@ export const handle: Handle = async ({ event, resolve }) => { locals.user = undefined } - if (PUBLIC_ROUTES.includes(pathname) || userAuthCredentials) { + if (userAuthCredentials || PUBLIC_ROUTES.some((it) => it.test(pathname))) { return await resolve(event) } diff --git a/src/lib/server/request-utils.ts b/src/lib/server/request-utils.ts new file mode 100644 index 00000000..dfa8cc64 --- /dev/null +++ b/src/lib/server/request-utils.ts @@ -0,0 +1,12 @@ +import { error } from '@sveltejs/kit' +import type { z } from 'zod' + +export const validateRequestBody = async (req: Request, schema: z.ZodSchema): Promise => { + const body = await req.json() + const validationResult = schema.safeParse(body) + if (!validationResult.success) { + error(400, 'Invalid request') + } + + return validationResult.data +} diff --git a/src/routes/(api)/api/[project]/[lang]/translations/+server.ts b/src/routes/(api)/api/[project]/[lang]/translations/+server.ts new file mode 100644 index 00000000..f7fac802 --- /dev/null +++ b/src/routes/(api)/api/[project]/[lang]/translations/+server.ts @@ -0,0 +1,22 @@ +import { validateRequestBody } from '$lib/server/request-utils' +import type { ProjectId } from 'services/project/project' +import { authorize } from '../../../api-utils' +import { translationPOSTRequestSchema } from '../../../api.model' +import type { RequestHandler } from './$types' + +export const POST: RequestHandler = async ({ params, request }) => { + await authorize(request, +params.project as ProjectId) + const newTranslations = validateRequestBody(request, translationPOSTRequestSchema) + + return new Response( + `getting POST for translations on project "${params.project}" and lang "${params.lang}" with body "${JSON.stringify(newTranslations)}"` + ) +} + +export const GET: RequestHandler = async ({ params, request }) => { + await authorize(request, +params.project as ProjectId) + + return new Response( + `getting GET for translations on project "${params.project}" and lang "${params.lang}"` + ) +} diff --git a/src/routes/(api)/api/[project]/config/+server.ts b/src/routes/(api)/api/[project]/config/+server.ts new file mode 100644 index 00000000..15a06166 --- /dev/null +++ b/src/routes/(api)/api/[project]/config/+server.ts @@ -0,0 +1,14 @@ +import { validateRequestBody } from '$lib/server/request-utils' +import type { ProjectId } from 'services/project/project' +import { authorize } from '../../api-utils' +import { projectConfigPOSTRequestSchema } from '../../api.model' +import type { RequestHandler } from './$types' + +export const POST: RequestHandler = async ({ params, request }) => { + await authorize(request, +params.project as ProjectId) + const config = await validateRequestBody(request, projectConfigPOSTRequestSchema) + + return new Response( + `getting post for config on project "${params.project}" with payload "${JSON.stringify(config)}"` + ) +} diff --git a/src/routes/(api)/api/api-utils.ts b/src/routes/(api)/api/api-utils.ts new file mode 100644 index 00000000..9638ca7c --- /dev/null +++ b/src/routes/(api)/api/api-utils.ts @@ -0,0 +1,23 @@ +import { checkApiKeyAccess } from 'services/auth/api-access.service' +import { error } from '@sveltejs/kit' +import { apiKeySchema } from 'services/auth/api-access' +import type { ProjectId } from 'services/project/project' + +export const authorize = async (req: Request, projectId: ProjectId) => { + if (req.headers.get('Authorization')) { + error(401, 'No API key provided in the Authorization header') + } + + const parsedApiKey = apiKeySchema.safeParse(req.headers.get('Authorization')) + if (parsedApiKey.error) { + error(400, 'The provided API key does not conform to the correct schema') + } + + const hasAccess = await checkApiKeyAccess(parsedApiKey.data, projectId) + if (!hasAccess) { + error( + 403, + 'The provided API key is invalid, the project does not exist, or the provided key does not grant access to the project' + ) + } +} diff --git a/src/routes/(api)/api/api.model.ts b/src/routes/(api)/api/api.model.ts new file mode 100644 index 00000000..7bece5b7 --- /dev/null +++ b/src/routes/(api)/api/api.model.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +const translationKeySchema = z.string().brand('translation-key') +export type TranslationKey = z.infer + +const translationValueSchema = z.string().brand('translation-value') +export type TranslationValue = z.infer + +const addTranslationCommandSchema = z.object({ + key: translationKeySchema, + value: translationValueSchema +}) +export type AddTranslationCommand = z.infer + +export const translationPOSTRequestSchema = addTranslationCommandSchema.array() + +export const translationsDELETERequestSchema = translationKeySchema.array() + +const frontendAdapterSchema = z.enum(['typesafe-i18n', 'other']) + +export const projectConfigPOSTRequestSchema = z.object({ + frontendAdapter: frontendAdapterSchema +})