From d1c41b4111753b0505d34891f85c2c0855e6f7e0 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 26 Jul 2026 01:05:30 +0000 Subject: [PATCH] refactor: wire remaining admin views --- backend/api/apiKeys.ts | 192 + backend/api/authentication.ts | 297 +- backend/api/hooks.ts | 320 ++ backend/api/index.ts | 8 + backend/api/schemas/apiKey.ts | 60 + backend/api/schemas/authentication.ts | 152 + backend/api/schemas/extension.ts | 40 + backend/api/schemas/flags.ts | 17 + backend/api/schemas/hook.ts | 107 + backend/api/schemas/security.ts | 96 + backend/api/system.ts | 483 +- backend/base.yml | 3 - backend/core/db.ts | 29 +- .../20260725221449_main/migration.sql | 1 + .../20260725221449_main/snapshot.json | 3731 ++++++++++++++++ .../20260725221505_main/migration.sql | 2 + .../20260725221505_main/snapshot.json | 3757 ++++++++++++++++ .../20260726001017_main/migration.sql | 15 + .../20260726001017_main/snapshot.json | 3939 +++++++++++++++++ backend/db/schema.ts | 31 +- backend/helpers/common.ts | 4 + backend/helpers/jwt.ts | 108 + backend/helpers/security.ts | 61 + backend/index.ts | 93 +- backend/locales/en.json | 41 +- backend/models/apiKeys.ts | 212 + backend/models/authentication.ts | 319 +- backend/models/extensions.ts | 197 + backend/models/flags.ts | 96 + backend/models/hooks.ts | 307 ++ backend/models/index.ts | 12 + backend/models/search.ts | 152 + backend/models/security.ts | 172 + backend/models/settings.ts | 7 +- backend/models/users.ts | 54 +- .../authentication/local/definition.yml | 6 +- backend/modules/extensions/git/definition.yml | 12 + .../modules/extensions/pandoc/definition.yml | 10 + .../extensions/puppeteer/definition.yml | 13 + .../modules/extensions/sharp/definition.yml | 15 + backend/tasks/simple/dispatch-webhook.ts | 13 + backend/tasks/simple/rebuild-search-index.ts | 9 + backend/types/fastify.d.ts | 9 + frontend/src/components/ApiKeyCopyDialog.vue | 31 +- .../src/components/ApiKeyCreateDialog.vue | 93 +- .../src/components/ApiKeyRevokeDialog.vue | 35 +- .../src/components/WebhookDeleteDialog.vue | 35 +- frontend/src/components/WebhookEditDialog.vue | 173 +- frontend/src/pages/AdminApi.vue | 135 +- frontend/src/pages/AdminAuth.vue | 500 +-- frontend/src/pages/AdminExtensions.vue | 75 +- frontend/src/pages/AdminFlags.vue | 114 +- frontend/src/pages/AdminSearch.vue | 127 +- frontend/src/pages/AdminSecurity.vue | 143 +- frontend/src/pages/AdminWebhooks.vue | 26 +- frontend/src/stores/admin.js | 2 +- frontend/src/stores/flags.js | 11 +- 57 files changed, 15803 insertions(+), 899 deletions(-) create mode 100644 backend/api/apiKeys.ts create mode 100644 backend/api/hooks.ts create mode 100644 backend/api/schemas/apiKey.ts create mode 100644 backend/api/schemas/authentication.ts create mode 100644 backend/api/schemas/extension.ts create mode 100644 backend/api/schemas/flags.ts create mode 100644 backend/api/schemas/hook.ts create mode 100644 backend/api/schemas/security.ts create mode 100644 backend/db/migrations/20260725221449_main/migration.sql create mode 100644 backend/db/migrations/20260725221449_main/snapshot.json create mode 100644 backend/db/migrations/20260725221505_main/migration.sql create mode 100644 backend/db/migrations/20260725221505_main/snapshot.json create mode 100644 backend/db/migrations/20260726001017_main/migration.sql create mode 100644 backend/db/migrations/20260726001017_main/snapshot.json create mode 100644 backend/helpers/jwt.ts create mode 100644 backend/helpers/security.ts create mode 100644 backend/models/apiKeys.ts create mode 100644 backend/models/extensions.ts create mode 100644 backend/models/flags.ts create mode 100644 backend/models/hooks.ts create mode 100644 backend/models/search.ts create mode 100644 backend/models/security.ts create mode 100644 backend/modules/extensions/git/definition.yml create mode 100644 backend/modules/extensions/pandoc/definition.yml create mode 100644 backend/modules/extensions/puppeteer/definition.yml create mode 100644 backend/modules/extensions/sharp/definition.yml create mode 100644 backend/tasks/simple/dispatch-webhook.ts create mode 100644 backend/tasks/simple/rebuild-search-index.ts diff --git a/backend/api/apiKeys.ts b/backend/api/apiKeys.ts new file mode 100644 index 000000000..048d26b29 --- /dev/null +++ b/backend/api/apiKeys.ts @@ -0,0 +1,192 @@ +import type { FastifyInstance } from 'fastify' +import type { KeyExpiration } from '../models/apiKeys.ts' + +/** + * API Keys Routes + */ +async function routes(app: FastifyInstance) { + /** + * LIST API KEYS + */ + app.get( + '/', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List all API keys', + description: + 'Revoked and expired keys are listed too, so that the admin area can show their state.', + tags: ['API Keys'], + response: { + 200: { + description: 'List of API keys', + type: 'array', + items: { $ref: 'ApiKey#' } + } + } + } + }, + async () => { + return WIKI.models.apiKeys.getKeys() + } + ) + + /** + * CREATE API KEY + */ + app.post<{ + Body: { name: string; expiration: KeyExpiration; groups: string[] } + }>( + '/', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Create a new API key', + description: + 'The response carries the token, which is the only time it can be read: only its last characters are stored. The key holds the combined permissions of the groups given.', + tags: ['API Keys'], + body: { + type: 'object', + required: ['name', 'expiration', 'groups'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + description: 'What the key is for.' + }, + expiration: { $ref: 'ApiKeyExpiration#' }, + groups: { + type: 'array', + minItems: 1, + description: + 'Groups whose permissions the key carries. The guests group is not accepted.', + items: { + type: 'string', + format: 'uuid' + } + } + } + }, + response: { + 200: { + description: 'API key created successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + }, + id: { + type: 'string', + format: 'uuid' + }, + key: { + type: 'string', + description: 'The token. Shown once and never again.' + } + } + } + } + } + }, + async (req, reply) => { + if (!/^[^<>"]+$/.test(req.body.name)) { + return reply.badRequest('Key name contains invalid characters.') + } + + // -> A key inherits group permissions, so every group must exist; a stale client should not + // silently mint a key with fewer permissions than the operator picked + const known = await WIKI.models.groups.getAllGroups() + for (const groupId of req.body.groups) { + if (!known.some((g) => g.id === groupId)) { + return reply.badRequest('One of the groups does not exist.') + } + // -> Guests are anonymous visitors: a key holding their permissions grants nothing a caller + // could not already do without one + if (groupId === WIKI.data.systemIds.guestsGroupId) { + return reply.badRequest('The guests group cannot be used for API keys.') + } + } + + const { id, key } = await WIKI.models.apiKeys.createKey({ + name: req.body.name, + expiration: req.body.expiration, + groups: req.body.groups + }) + + return { + ok: true, + message: 'API key created successfully.', + id, + key + } + } + ) + + /** + * REVOKE API KEY + */ + app.post<{ Params: { keyId: string } }>( + '/:keyId/revoke', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Revoke an API key', + description: + 'Permanent: the key stays listed as revoked and stops authenticating on the next request. Keys are never deleted, so the record of what existed is kept.', + tags: ['API Keys'], + params: { + type: 'object', + properties: { + keyId: { + type: 'string', + format: 'uuid' + } + }, + required: ['keyId'] + }, + response: { + 200: { + description: 'API key revoked successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + const key = await WIKI.models.apiKeys.getKeyById(req.params.keyId) + if (!key) { + return reply.notFound('API key does not exist.') + } + if (key.isRevoked) { + return reply.conflict('This API key is already revoked.') + } + + await WIKI.models.apiKeys.revokeKey(key.id) + + return { + ok: true, + message: 'API key revoked successfully.' + } + } + ) +} + +export default routes diff --git a/backend/api/authentication.ts b/backend/api/authentication.ts index 94f0c14b7..c3fecb982 100644 --- a/backend/api/authentication.ts +++ b/backend/api/authentication.ts @@ -192,7 +192,10 @@ async function routes(app: FastifyInstance) { if (err.message.startsWith('ERR_')) { return reply.badRequest(err.message) } else { - WIKI.logger.info(err) // TODO: change to debug once stable + // -> An unexpected failure, reported to the client as a generic one. The detail is behind + // the authDebug flag rather than logged on every failed login. + WIKI.logger.debug(err) + WIKI.models.flags.authDebug(`Login failed unexpectedly: ${err.message}`) return reply.badRequest('ERR_LOGIN_FAILED') } } @@ -266,14 +269,306 @@ async function routes(app: FastifyInstance) { } } catch (err: any) { if (err.message.startsWith('ERR_')) { + WIKI.models.flags.authDebug(`Password change from login rejected: ${err.message}`) return reply.badRequest(err.message) } else { WIKI.logger.debug(err) + WIKI.models.flags.authDebug(`Password change from login failed: ${err.message}`) return reply.badRequest('ERR_CHANGE_PASSWORD_FAILED') } } } ) + + /** + * LIST AUTHENTICATION MODULES + */ + app.get( + '/authentication/modules', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List the authentication modules available on this server', + description: + 'Read from `modules/authentication` at startup, so installing a module means dropping it on disk and restarting. Modules that declare themselves unavailable are not listed.', + tags: ['Authentication'], + response: { + 200: { + description: 'List of authentication modules', + type: 'array', + items: { $ref: 'AuthModule#' } + } + } + } + }, + async () => { + return WIKI.models.authentication.getModules() + } + ) + + /** + * LIST CONFIGURED STRATEGIES + */ + app.get( + '/authentication/strategies', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List the configured authentication strategies', + description: + 'Instance-wide, i.e. every strategy regardless of which sites offer it. Which of them a given site shows on its login screen, and in what order, is part of that site’s configuration. Configuration values include any secrets a module stores, hence the `manage:system` requirement.', + tags: ['Authentication'], + response: { + 200: { + description: 'List of configured strategies', + type: 'array', + items: { $ref: 'AuthStrategy#' } + } + } + } + }, + async () => { + return WIKI.models.authentication.getActiveStrategies() + } + ) + + /** + * GET CONFIGURED STRATEGY + */ + app.get<{ Params: { strategyId: string } }>( + '/authentication/strategies/:strategyId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Get a single configured authentication strategy', + tags: ['Authentication'], + params: { + type: 'object', + properties: { + strategyId: { + type: 'string', + format: 'uuid' + } + }, + required: ['strategyId'] + }, + response: { + 200: { $ref: 'AuthStrategy#' } + } + } + }, + async (req, reply) => { + const strategy = await WIKI.models.authentication.getStrategyById(req.params.strategyId) + if (!strategy) { + return reply.notFound('Authentication strategy does not exist.') + } + return strategy + } + ) + + /** + * CREATE STRATEGY + */ + app.post<{ Body: Record }>( + '/authentication/strategies', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Configure a new authentication strategy', + description: + 'A module can be configured more than once, so that two instances of the same provider can coexist. A new strategy is not offered by any site until that site adds it to its login screen.', + tags: ['Authentication'], + body: { + allOf: [{ $ref: 'AuthStrategyInput#' }, { required: ['module'] }] + }, + response: { + 200: { + description: 'Strategy created successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + }, + id: { + type: 'string', + format: 'uuid' + } + } + } + } + } + }, + async (req, reply) => { + const mod = WIKI.models.authentication.getModule(req.body.module) + if (!mod) { + return reply.badRequest(`There is no authentication module named "${req.body.module}".`) + } + + const invalid = + (await WIKI.models.authentication.validateStrategy({ + module: req.body.module, + displayName: req.body.displayName, + isEnabled: req.body.isEnabled, + allowedEmailRegex: req.body.allowedEmailRegex, + autoEnrollGroups: req.body.autoEnrollGroups + })) ?? WIKI.models.authentication.validateConfig(req.body.module, req.body.config) + if (invalid) { + return reply.badRequest(invalid) + } + + const id = await WIKI.models.authentication.createStrategy(req.body as any) + + return { + ok: true, + message: 'Authentication strategy created successfully.', + id + } + } + ) + + /** + * UPDATE STRATEGY + */ + app.put<{ Params: { strategyId: string }; Body: Record }>( + '/authentication/strategies/:strategyId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Update an authentication strategy', + description: + 'Accepts any subset of the fields, except `module`, which is fixed once a strategy exists. The strategies are reloaded on success, so a configuration change applies to the next login rather than after a restart.', + tags: ['Authentication'], + params: { + type: 'object', + properties: { + strategyId: { + type: 'string', + format: 'uuid' + } + }, + required: ['strategyId'] + }, + body: { $ref: 'AuthStrategyInput#' }, + response: { + 200: { + description: 'Strategy updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + const current = await WIKI.models.authentication.getStrategyById(req.params.strategyId) + if (!current) { + return reply.notFound('Authentication strategy does not exist.') + } + if (req.body.module !== undefined && req.body.module !== current.module) { + return reply.badRequest('The module of an existing strategy cannot be changed.') + } + + const patch: Record = {} + for (const field of [ + 'displayName', + 'isEnabled', + 'registration', + 'allowedEmailRegex', + 'autoEnrollGroups', + 'config' + ] as const) { + if (req.body[field] !== undefined) { + patch[field] = req.body[field] + } + } + if (Object.keys(patch).length < 1) { + return reply.badRequest('No strategy fields provided to update.') + } + + const invalid = + (await WIKI.models.authentication.validateStrategy({ + id: current.id, + module: current.module, + ...patch + })) ?? WIKI.models.authentication.validateConfig(current.module, patch.config) + if (invalid) { + return reply.badRequest(invalid) + } + + if (!(await WIKI.models.authentication.updateStrategy(req.params.strategyId, patch))) { + return reply.internalServerError('Failed to update the authentication strategy.') + } + + return { + ok: true, + message: 'Authentication strategy updated successfully.' + } + } + ) + + /** + * DELETE STRATEGY + */ + app.delete<{ Params: { strategyId: string } }>( + '/authentication/strategies/:strategyId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Delete an authentication strategy', + description: + 'Also removes it from every site’s login screen. The built-in local strategy cannot be deleted: every account stores its password under that strategy ID, so removing it would leave no way in.', + tags: ['Authentication'], + params: { + type: 'object', + properties: { + strategyId: { + type: 'string', + format: 'uuid' + } + }, + required: ['strategyId'] + }, + response: { + 204: { + description: 'Strategy deleted successfully' + } + } + } + }, + async (req, reply) => { + const strategy = await WIKI.models.authentication.getStrategyById(req.params.strategyId) + if (!strategy) { + return reply.notFound('Authentication strategy does not exist.') + } + if (strategy.id === WIKI.data.systemIds.localAuthId) { + return reply.conflict('The built-in local strategy cannot be deleted.') + } + + await WIKI.models.authentication.deleteStrategy(req.params.strategyId) + return reply.code(204).send() + } + ) } export default routes diff --git a/backend/api/hooks.ts b/backend/api/hooks.ts new file mode 100644 index 000000000..7e911c803 --- /dev/null +++ b/backend/api/hooks.ts @@ -0,0 +1,320 @@ +import type { FastifyInstance } from 'fastify' +import { EMITTED_EVENTS, HOOK_EVENTS } from '../models/hooks.ts' + +interface HookBody { + name?: string + events?: string[] + url?: string + includeMetadata?: boolean + includeContent?: boolean + acceptUntrusted?: boolean + authHeader?: string +} + +/** + * Reject what the admin area's own validation rejects, so the API is not the looser of the two + */ +function invalidReason(body: HookBody, { partial }: { partial: boolean }): string | null { + if (body.name !== undefined && !/^[^<>"]+$/.test(body.name)) { + return 'The webhook name contains invalid characters.' + } + if (body.url !== undefined) { + let parsed: URL + try { + parsed = new URL(body.url) + } catch { + return 'The URL is not valid.' + } + if (!['http:', 'https:'].includes(parsed.protocol)) { + return 'The URL must be an http or https address.' + } + } + if (!partial && (body.events?.length ?? 0) < 1) { + return 'At least one event is required.' + } + if (body.events !== undefined && body.events.length < 1) { + return 'At least one event is required.' + } + return null +} + +/** + * Webhooks API Routes + */ +async function routes(app: FastifyInstance) { + /** + * LIST WEBHOOKS + */ + app.get( + '/', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List all webhooks', + tags: ['Webhooks'], + response: { + 200: { + description: 'List of webhooks', + type: 'array', + items: { $ref: 'Hook#' } + } + } + } + }, + async () => { + return WIKI.models.hooks.getHooks() + } + ) + + /** + * LIST AVAILABLE EVENTS + */ + app.get( + '/events', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List the events a webhook can subscribe to', + description: + 'Only `user:join` and `user:login` are emitted at the moment. Pages, assets, comments and logout are not implemented yet, so a subscription to those is stored but never triggered.', + tags: ['Webhooks'], + response: { + 200: { + description: 'List of event keys', + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string' + }, + isEmitted: { + type: 'boolean', + description: 'Whether anything in the server currently emits this event.' + } + } + } + } + } + } + }, + async () => { + return HOOK_EVENTS.map((key) => ({ key, isEmitted: EMITTED_EVENTS.includes(key) })) + } + ) + + /** + * GET WEBHOOK + */ + app.get<{ Params: { hookId: string } }>( + '/:hookId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Get a single webhook', + tags: ['Webhooks'], + params: { + type: 'object', + properties: { + hookId: { + type: 'string', + format: 'uuid' + } + }, + required: ['hookId'] + }, + response: { + 200: { $ref: 'Hook#' } + } + } + }, + async (req, reply) => { + const hook = await WIKI.models.hooks.getHookById(req.params.hookId) + if (!hook) { + return reply.notFound('Webhook does not exist.') + } + return hook + } + ) + + /** + * CREATE WEBHOOK + */ + app.post<{ Body: HookBody }>( + '/', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Create a new webhook', + tags: ['Webhooks'], + // -> The same shape as an update, with the three fields a webhook cannot exist without + body: { + allOf: [{ $ref: 'HookInput#' }, { required: ['name', 'events', 'url'] }] + }, + response: { + 200: { + description: 'Webhook created successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + }, + id: { + type: 'string', + format: 'uuid' + } + } + } + } + } + }, + async (req, reply) => { + const invalid = invalidReason(req.body, { partial: false }) + if (invalid) { + return reply.badRequest(invalid) + } + + const id = await WIKI.models.hooks.createHook({ + name: req.body.name!, + events: req.body.events!, + url: req.body.url!, + includeMetadata: req.body.includeMetadata, + includeContent: req.body.includeContent, + acceptUntrusted: req.body.acceptUntrusted, + authHeader: req.body.authHeader + }) + + return { + ok: true, + message: 'Webhook created successfully.', + id + } + } + ) + + /** + * UPDATE WEBHOOK + */ + app.put<{ Params: { hookId: string }; Body: HookBody }>( + '/:hookId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Update a webhook', + description: + 'Accepts any subset of the fields. Changing the URL, the events or the authentication header resets the webhook to pending, since the last outcome no longer describes the new configuration.', + tags: ['Webhooks'], + params: { + type: 'object', + properties: { + hookId: { + type: 'string', + format: 'uuid' + } + }, + required: ['hookId'] + }, + body: { $ref: 'HookInput#' }, + response: { + 200: { + description: 'Webhook updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + if (!(await WIKI.models.hooks.getHookById(req.params.hookId))) { + return reply.notFound('Webhook does not exist.') + } + const invalid = invalidReason(req.body, { partial: true }) + if (invalid) { + return reply.badRequest(invalid) + } + const patch: Record = {} + for (const field of [ + 'name', + 'events', + 'url', + 'includeMetadata', + 'includeContent', + 'acceptUntrusted', + 'authHeader' + ] as const) { + if (req.body[field] !== undefined) { + patch[field] = req.body[field] + } + } + if (Object.keys(patch).length < 1) { + return reply.badRequest('No webhook fields provided to update.') + } + + await WIKI.models.hooks.updateHook(req.params.hookId, patch) + + return { + ok: true, + message: 'Webhook updated successfully.' + } + } + ) + + /** + * DELETE WEBHOOK + */ + app.delete<{ Params: { hookId: string } }>( + '/:hookId', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Delete a webhook', + tags: ['Webhooks'], + params: { + type: 'object', + properties: { + hookId: { + type: 'string', + format: 'uuid' + } + }, + required: ['hookId'] + }, + response: { + 204: { + description: 'Webhook deleted successfully' + } + } + } + }, + async (req, reply) => { + if (!(await WIKI.models.hooks.deleteHook(req.params.hookId))) { + return reply.notFound('Webhook does not exist.') + } + return reply.code(204).send() + } + ) +} + +export default routes diff --git a/backend/api/index.ts b/backend/api/index.ts index 67d2007b7..db703dd00 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -5,17 +5,25 @@ import type { FastifyInstance } from 'fastify' */ async function routes(app: FastifyInstance) { // Register schemas + await import('./schemas/apiKey.ts').then((m) => m.registerSchemas(app)) + await import('./schemas/authentication.ts').then((m) => m.registerSchemas(app)) await import('./schemas/block.ts').then((m) => m.registerSchemas(app)) + await import('./schemas/extension.ts').then((m) => m.registerSchemas(app)) + await import('./schemas/flags.ts').then((m) => m.registerSchemas(app)) await import('./schemas/group.ts').then((m) => m.registerSchemas(app)) + await import('./schemas/hook.ts').then((m) => m.registerSchemas(app)) await import('./schemas/mail.ts').then((m) => m.registerSchemas(app)) await import('./schemas/scheduler.ts').then((m) => m.registerSchemas(app)) + await import('./schemas/security.ts').then((m) => m.registerSchemas(app)) await import('./schemas/site.ts').then((m) => m.registerSchemas(app)) await import('./schemas/user.ts').then((m) => m.registerSchemas(app)) // Register routes + app.register(import('./apiKeys.ts'), { prefix: '/api-keys' }) app.register(import('./authentication.ts')) app.register(import('./blocks.ts')) app.register(import('./groups.ts'), { prefix: '/groups' }) + app.register(import('./hooks.ts'), { prefix: '/hooks' }) app.register(import('./locales.ts'), { prefix: '/locales' }) app.register(import('./mail.ts'), { prefix: '/mail' }) app.register(import('./pages.ts')) diff --git a/backend/api/schemas/apiKey.ts b/backend/api/schemas/apiKey.ts new file mode 100644 index 000000000..1a27e7e0a --- /dev/null +++ b/backend/api/schemas/apiKey.ts @@ -0,0 +1,60 @@ +import type { FastifyInstance } from 'fastify' +import { KEY_EXPIRATIONS } from '../../models/apiKeys.ts' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * API KEY - Metadata only; the token itself exists once, in the create response + */ + app.addSchema({ + $id: 'ApiKey', + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid' + }, + name: { + type: 'string' + }, + keyShort: { + type: 'string', + description: 'Last characters of the token, to tell keys apart. The token is not stored.' + }, + groups: { + type: 'array', + description: 'IDs of the groups this key draws its permissions from.', + items: { + type: 'string', + format: 'uuid' + } + }, + expiration: { + type: 'string', + format: 'date-time', + description: 'RFC 3339 Date Time' + }, + isRevoked: { + type: 'boolean' + }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'RFC 3339 Date Time' + }, + updatedAt: { + type: 'string', + format: 'date-time', + description: 'RFC 3339 Date Time' + } + } + }) + + /** + * API KEY EXPIRATION - The lifetimes a new key can be given + */ + app.addSchema({ + $id: 'ApiKeyExpiration', + type: 'string', + enum: Object.keys(KEY_EXPIRATIONS) + }) +} diff --git a/backend/api/schemas/authentication.ts b/backend/api/schemas/authentication.ts new file mode 100644 index 000000000..eb88583a6 --- /dev/null +++ b/backend/api/schemas/authentication.ts @@ -0,0 +1,152 @@ +import type { FastifyInstance } from 'fastify' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * AUTH MODULE - An authentication module as found on disk + */ + app.addSchema({ + $id: 'AuthModule', + type: 'object', + properties: { + key: { + type: 'string', + description: 'Directory name under `modules/authentication`.' + }, + title: { + type: 'string' + }, + description: { + type: 'string' + }, + logo: { + type: 'string' + }, + icon: { + type: 'string' + }, + color: { + type: 'string' + }, + vendor: { + type: 'string' + }, + website: { + type: 'string' + }, + isAvailable: { + type: 'boolean' + }, + useForm: { + type: 'boolean', + description: 'Whether logging in through it means submitting a username and password.' + }, + usernameType: { + type: 'string' + }, + props: { + type: 'object', + additionalProperties: true, + description: + 'The module configuration, declared in its `definition.yml`: each entry carries a `type`, `title`, `hint`, `default` and the display hints the admin area renders a control from. A `readOnly` prop is shown but cannot be changed, and is silently kept at its stored value when written to.' + }, + refs: { + type: 'object', + additionalProperties: true, + description: + 'Read-only values the administrator needs to configure the other side, such as a callback URL. `{host}` and `{id}` are placeholders for the wiki origin and the strategy ID.' + } + } + }) + + /** + * AUTH STRATEGY - A configured instance of a module + */ + app.addSchema({ + $id: 'AuthStrategy', + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid' + }, + module: { + type: 'string', + description: 'Key of the module this strategy is an instance of.' + }, + displayName: { + type: 'string' + }, + isEnabled: { + type: 'boolean' + }, + registration: { + type: 'boolean' + }, + allowedEmailRegex: { + type: 'string' + }, + autoEnrollGroups: { + type: 'array', + items: { + type: 'string', + format: 'uuid' + } + }, + config: { + type: 'object', + additionalProperties: true, + description: + 'Values for the module props, completed with the module defaults for any prop that has none stored yet.' + } + } + }) + + /** + * AUTH STRATEGY INPUT - Used both ways: to create a strategy, and as a partial update + */ + app.addSchema({ + $id: 'AuthStrategyInput', + type: 'object', + properties: { + module: { + type: 'string', + maxLength: 255, + description: + 'Only on create, and only a module that exists on disk. Cannot be changed after.' + }, + displayName: { + type: 'string', + maxLength: 255, + description: 'Defaults to the module title on create.' + }, + isEnabled: { + type: 'boolean' + }, + registration: { + type: 'boolean', + description: 'Stored but not enforced: self-registration is not implemented yet.' + }, + allowedEmailRegex: { + type: 'string', + maxLength: 255, + description: + 'Must be a valid regular expression. Stored but not enforced, as it only applies to self-registration.' + }, + autoEnrollGroups: { + type: 'array', + items: { + type: 'string', + format: 'uuid' + }, + description: + 'Groups a self-registered user would join. The guests group is refused. Stored but not enforced, as above.' + }, + config: { + type: 'object', + additionalProperties: true, + description: + 'Values for the module props. Validated against what the module declares: an unknown key is dropped, a wrong type is refused, and a read-only prop keeps its stored value.' + } + } + }) +} diff --git a/backend/api/schemas/extension.ts b/backend/api/schemas/extension.ts new file mode 100644 index 000000000..e10369ec6 --- /dev/null +++ b/backend/api/schemas/extension.ts @@ -0,0 +1,40 @@ +import type { FastifyInstance } from 'fastify' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * EXTENSION - Optional third-party tooling, with its state on this system + */ + app.addSchema({ + $id: 'Extension', + type: 'object', + properties: { + key: { + type: 'string', + description: 'Directory name under `modules/extensions`.' + }, + title: { + type: 'string' + }, + description: { + type: 'string' + }, + website: { + type: 'string', + description: 'Where the extension itself is documented. Empty when not declared.' + }, + isInstalled: { + type: 'boolean', + description: 'Whether it was found on this system. Always false when incompatible.' + }, + isInstallable: { + type: 'boolean', + description: + 'Whether the admin area can install it, rather than it being installed by hand.' + }, + isCompatible: { + type: 'boolean', + description: 'Whether this platform and architecture can run it at all.' + } + } + }) +} diff --git a/backend/api/schemas/flags.ts b/backend/api/schemas/flags.ts new file mode 100644 index 000000000..532cd6537 --- /dev/null +++ b/backend/api/schemas/flags.ts @@ -0,0 +1,17 @@ +import type { FastifyInstance } from 'fastify' +import { FLAGS } from '../../models/flags.ts' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * SYSTEM FLAGS - Used both ways: as the response, and as a partial update body + * + * Built from the model's own list, so a new flag is exposed and documented by declaring it there. + */ + app.addSchema({ + $id: 'SystemFlags', + type: 'object', + properties: Object.fromEntries( + Object.entries(FLAGS).map(([key, description]) => [key, { type: 'boolean', description }]) + ) + }) +} diff --git a/backend/api/schemas/hook.ts b/backend/api/schemas/hook.ts new file mode 100644 index 000000000..a868107ea --- /dev/null +++ b/backend/api/schemas/hook.ts @@ -0,0 +1,107 @@ +import type { FastifyInstance } from 'fastify' +import { HOOK_EVENTS } from '../../models/hooks.ts' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * HOOK INPUT - The writable fields, used for both create and update + */ + app.addSchema({ + $id: 'HookInput', + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255 + }, + events: { + type: 'array', + minItems: 1, + items: { + type: 'string', + enum: HOOK_EVENTS + } + }, + url: { + type: 'string', + maxLength: 2048, + description: 'Where to POST the event. Must be an http or https address.' + }, + includeMetadata: { + type: 'boolean', + description: 'Include the event metadata, such as a page title and author.' + }, + includeContent: { + type: 'boolean', + description: 'Include the full content, e.g. a page body. Payloads can get large.' + }, + acceptUntrusted: { + type: 'boolean', + description: 'Skip TLS certificate validation for this endpoint.' + }, + authHeader: { + type: 'string', + maxLength: 2048, + description: 'Sent verbatim as the Authorization header.' + } + } + }) + + /** + * HOOK - A webhook with the outcome of its last delivery + */ + app.addSchema({ + $id: 'Hook', + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid' + }, + name: { + type: 'string' + }, + events: { + type: 'array', + items: { type: 'string' } + }, + url: { + type: 'string' + }, + includeMetadata: { + type: 'boolean' + }, + includeContent: { + type: 'boolean' + }, + acceptUntrusted: { + type: 'boolean' + }, + authHeader: { + type: 'string', + nullable: true + }, + state: { + type: 'string', + enum: ['pending', 'success', 'error'], + description: + '`pending` until an event reaches it, then the outcome of the most recent delivery.' + }, + lastErrorMessage: { + type: 'string', + nullable: true, + description: 'Why the last delivery failed. Null unless the state is `error`.' + }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'RFC 3339 Date Time' + }, + updatedAt: { + type: 'string', + format: 'date-time', + description: 'RFC 3339 Date Time' + } + } + }) +} diff --git a/backend/api/schemas/security.ts b/backend/api/schemas/security.ts new file mode 100644 index 000000000..a22aef25d --- /dev/null +++ b/backend/api/schemas/security.ts @@ -0,0 +1,96 @@ +import type { FastifyInstance } from 'fastify' +import { CORS_MODES } from '../../helpers/security.ts' + +export async function registerSchemas(app: FastifyInstance): Promise { + /** + * SECURITY CONFIG - Used both ways: as the response, and as a partial update body + */ + app.addSchema({ + $id: 'SecurityConfig', + type: 'object', + properties: { + corsMode: { + type: 'string', + enum: CORS_MODES, + description: + '`OFF` sends no CORS headers at all, i.e. same-origin only. `REFLECT` echoes the request origin back.' + }, + corsConfig: { + type: 'string', + maxLength: 8192, + description: + 'Hostnames, one per line or comma-separated, for `HOSTNAMES` mode; a regular expression for `REGEX` mode. Ignored otherwise.' + }, + enforceCsp: { + type: 'boolean' + }, + cspDirectives: { + type: 'string', + maxLength: 8192, + description: "Directives separated by `;`, e.g. `default-src 'self'; img-src * data:`." + }, + enforceHsts: { + type: 'boolean' + }, + hstsDuration: { + type: 'integer', + minimum: 0, + description: 'Seconds. Must be greater than zero when HSTS is enforced.' + }, + disallowFloc: { + type: 'boolean', + description: 'Sends `Permissions-Policy: interest-cohort=()`.' + }, + disallowIframe: { + type: 'boolean', + description: '`X-Frame-Options: DENY` when on, `SAMEORIGIN` when off.' + }, + enforceSameOriginReferrerPolicy: { + type: 'boolean', + description: '`Referrer-Policy: same-origin` when on, `no-referrer` when off.' + }, + disallowOpenRedirect: { + type: 'boolean', + description: 'Stored, but nothing redirects on user input yet.' + }, + forceAssetDownload: { + type: 'boolean', + description: 'Stored, but asset serving is not implemented yet.' + }, + trustProxy: { + type: 'boolean', + description: 'Whether to trust `X-Forwarded-*` headers.' + }, + uploadMaxFileSize: { + type: 'integer', + minimum: 1, + description: 'Bytes. Stored, but there is no upload endpoint yet.' + }, + uploadMaxFiles: { + type: 'integer', + minimum: 1, + description: 'Stored, but there is no upload endpoint yet.' + }, + uploadScanSVG: { + type: 'boolean', + description: 'Stored, but there is no upload endpoint yet.' + }, + authJwtAudience: { + type: 'string', + maxLength: 255, + description: + 'Audience claim of issued tokens. Changing it invalidates every API key already issued.' + }, + authJwtExpiration: { + type: 'string', + maxLength: 16, + description: 'Duration, e.g. `30m`.' + }, + authJwtRenewablePeriod: { + type: 'string', + maxLength: 16, + description: 'Duration, e.g. `14d`.' + } + } + }) +} diff --git a/backend/api/system.ts b/backend/api/system.ts index 4a8f86aa7..ad4d0e449 100644 --- a/backend/api/system.ts +++ b/backend/api/system.ts @@ -56,6 +56,10 @@ async function routes(app: FastifyInstance) { isMailConfigured: { type: 'boolean' }, + isApiEnabled: { + type: 'boolean', + description: 'Whether API keys are accepted.' + }, isMetricsEnabled: { type: 'boolean', description: 'Whether the Prometheus metrics endpoint is turned on.' @@ -117,6 +121,7 @@ async function routes(app: FastifyInstance) { groupsTotal: await WIKI.db.$count(groupsTable), hostname: os.hostname(), httpPort: 0, + isApiEnabled: WIKI.config.api.isEnabled === true, isMailConfigured: WIKI.config?.mail?.host?.length > 2, isMetricsEnabled: WIKI.config.metrics.isEnabled === true, isSchedulerHealthy: await WIKI.models.jobs.isHealthy(), @@ -147,19 +152,428 @@ async function routes(app: FastifyInstance) { { schema: { summary: 'System Flags', + description: + 'Readable without authentication: the frontend needs `experimental` before anyone has logged in, to know which unfinished features to reveal. A flag must therefore never carry anything sensitive.', + tags: ['System'], + response: { + 200: { $ref: 'SystemFlags#' } + } + } + }, + async () => { + return WIKI.models.flags.getFlags() + } + ) + + /** + * UPDATE SYSTEM FLAGS + */ + app.put<{ Body: Record }>( + '/flags', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Update the system flags', + description: + 'Accepts any subset of the flags. All of them take effect immediately, without a restart: `authDebug` and `sqlLog` write to the server log at info level, and `experimental` is picked up by the frontend on its next load.', + tags: ['System'], + body: { $ref: 'SystemFlags#' }, + response: { + 200: { + description: 'System flags updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + const patch = WIKI.models.flags.pickFlags(req.body) + if (Object.keys(patch).length < 1) { + return reply.badRequest('No system flags provided to update.') + } + if (!(await WIKI.models.flags.updateFlags(patch))) { + return reply.internalServerError('Failed to save the system flags.') + } + + return { + ok: true, + message: 'System flags updated successfully.' + } + } + ) + + /** + * GET SECURITY CONFIGURATION + */ + app.get( + '/security', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Get the security configuration', + description: + 'The JWT fields come from the `auth` settings, which are the ones actually in force. Most of the rest is applied when the HTTP server starts, so changing it takes effect on the next restart.', + tags: ['System'], + response: { + 200: { $ref: 'SecurityConfig#' } + } + } + }, + async () => { + return WIKI.models.security.getConfig() + } + ) + + /** + * UPDATE SECURITY CONFIGURATION + */ + app.put<{ Body: Record }>( + '/security', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Update the security configuration', + description: + 'Accepts any subset of the fields. Changing the JWT audience invalidates every API key already issued, since a key carries the audience it was signed with. Header, CORS and proxy settings are read when the HTTP server starts and therefore apply after a restart.', + tags: ['System'], + body: { $ref: 'SecurityConfig#' }, + response: { + 200: { + description: 'Security configuration updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + const patch = WIKI.models.security.pickFields(req.body) + if (Object.keys(patch).length < 1) { + return reply.badRequest('No security settings provided to update.') + } + + const invalid = WIKI.models.security.validate(patch) + if (invalid) { + return reply.badRequest(invalid) + } + + if (!(await WIKI.models.security.updateConfig(patch))) { + return reply.internalServerError('Failed to save the security configuration.') + } + + return { + ok: true, + message: 'Security configuration updated successfully.' + } + } + ) + + /** + * GET SEARCH CONFIGURATION + */ + app.get( + '/search', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Get the search configuration', + description: + 'Search is postgres full-text. `availableDictionaries` lists the text search configurations this database has, which is what a locale may be mapped to.', + tags: ['System'], + response: { + 200: { + description: 'Search configuration', + type: 'object', + properties: { + termHighlighting: { + type: 'boolean' + }, + dictOverrides: { + type: 'object', + description: + 'Locale code to postgres dictionary, e.g. `{ "en": "english" }`. Overrides the built-in mapping.', + additionalProperties: { type: 'string' } + }, + availableDictionaries: { + type: 'array', + description: 'Dictionary names this postgres installation knows.', + items: { type: 'string' } + } + } + } + } + } + }, + async () => { + return { + ...WIKI.models.search.getConfig(), + availableDictionaries: await WIKI.models.search.getAvailableDictionaries() + } + } + ) + + /** + * UPDATE SEARCH CONFIGURATION + */ + app.put<{ Body: { termHighlighting?: boolean; dictOverrides?: Record } }>( + '/search', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Update the search configuration', + description: + 'Every dictionary named in `dictOverrides` must exist in this database, otherwise indexing would fail later, long after the setting was accepted. Changing a mapping affects pages the next time they are indexed — rebuild the index to apply it to existing content.', + tags: ['System'], + body: { + type: 'object', + properties: { + termHighlighting: { + type: 'boolean' + }, + dictOverrides: { + type: 'object', + description: 'Locale code to postgres dictionary. Replaces the stored mapping.', + additionalProperties: { type: 'string' } + } + } + }, + response: { + 200: { + description: 'Search configuration updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + if (req.body.termHighlighting === undefined && req.body.dictOverrides === undefined) { + return reply.badRequest('No search settings provided to update.') + } + + if (req.body.dictOverrides) { + const available = await WIKI.models.search.getAvailableDictionaries() + for (const [locale, dictionary] of Object.entries(req.body.dictOverrides)) { + if (!/^[a-z]{2,3}(?:[-_][A-Za-z]{2,4})?$/.test(locale)) { + return reply.badRequest(`"${locale}" is not a valid locale code.`) + } + if (!available.includes(dictionary)) { + return reply.badRequest( + `"${dictionary}" is not a text search dictionary in this database.` + ) + } + } + } + + const previousConfig = WIKI.config.search + WIKI.config.search = { + ...previousConfig, + ...(req.body.termHighlighting !== undefined && { + termHighlighting: req.body.termHighlighting + }), + ...(req.body.dictOverrides !== undefined && { dictOverrides: req.body.dictOverrides }) + } + + if (!(await WIKI.configSvc.saveToDb(['search']))) { + WIKI.config.search = previousConfig + return reply.internalServerError('Failed to save the search configuration.') + } + + return { + ok: true, + message: 'Search configuration updated successfully.' + } + } + ) + + /** + * REBUILD SEARCH INDEX + */ + app.post( + '/search/rebuild', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Rebuild the search index', + description: + 'Queues a job that recomputes the search vector of every page from its stored content, using the dictionary mapping in force. Runs in the background: the response only says the job was queued.', tags: ['System'], response: { 200: { - description: 'System Flags', + description: 'Rebuild queued successfully', type: 'object', properties: { - experimental: { + ok: { type: 'boolean' }, - authDebug: { + message: { + type: 'string' + }, + id: { + type: 'string', + format: 'uuid', + description: 'ID of the queued job, which the scheduler view lists.' + } + } + } + } + } + }, + async (req, reply) => { + const added = await WIKI.scheduler.addJob({ task: 'rebuildSearchIndex' }) + if (!added?.id) { + return reply.internalServerError('The scheduler could not queue the rebuild.') + } + return { + ok: true, + message: 'Search index rebuild queued successfully.', + id: added.id + } + } + ) + + /** + * LIST EXTENSIONS + */ + app.get( + '/extensions', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'List optional extensions', + description: + 'Third-party tooling that unlocks extra functionality, with whether each one is present on this system. Detection runs per request, so installing a tool shows up without a restart.', + tags: ['System'], + response: { + 200: { + description: 'List of extensions', + type: 'array', + items: { $ref: 'Extension#' } + } + } + } + }, + async () => { + return WIKI.models.extensions.getExtensions() + } + ) + + /** + * INSTALL EXTENSION + */ + app.post<{ Params: { extensionKey: string } }>( + '/extensions/:extensionKey/install', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Install an extension', + description: + 'Only extensions flagged `isInstallable` can be installed from here. None currently are: Git and Pandoc come from the operating system, Sharp and Puppeteer are optional dependencies, so both are installed outside the application and this answers 409 pointing at the documentation.', + tags: ['System'], + params: { + type: 'object', + properties: { + extensionKey: { + type: 'string', + maxLength: 255 + } + }, + required: ['extensionKey'] + }, + response: { + 200: { + description: 'Extension installed successfully', + type: 'object', + properties: { + ok: { type: 'boolean' }, - sqlLog: { + message: { + type: 'string' + } + } + } + } + } + }, + async (req, reply) => { + const definition = WIKI.models.extensions.getDefinition(req.params.extensionKey) + if (!definition) { + return reply.notFound('Extension does not exist.') + } + if (!WIKI.models.extensions.isCompatible(definition)) { + return reply.conflict('This extension is not compatible with this system.') + } + if (definition.isInstallable !== true) { + return reply.conflict( + `${definition.title} must be installed manually. See the documentation for instructions.` + ) + } + + // -> No extension declares itself installable yet; an installer belongs with the extension + // that needs it, next to its definition + return reply.notImplemented('Installing this extension is not implemented yet.') + } + ) + + /** + * GET API STATE + */ + app.get( + '/api', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Get the API state', + description: + 'Whether API keys are accepted. While this is off, every request presenting a key is rejected, no matter how valid the key is.', + tags: ['System'], + response: { + 200: { + description: 'API state', + type: 'object', + properties: { + isEnabled: { type: 'boolean' } } @@ -168,7 +582,66 @@ async function routes(app: FastifyInstance) { } }, async () => { - return WIKI.config.flags + return { isEnabled: WIKI.config.api.isEnabled === true } + } + ) + + /** + * SET API STATE + */ + app.put<{ Body: { isEnabled: boolean } }>( + '/api', + { + config: { + permissions: ['manage:system'] + }, + schema: { + summary: 'Turn the API on or off', + description: + 'Turning it off stops every API key from authenticating, without revoking any of them. Session-authenticated requests, i.e. the admin area itself, are unaffected.', + tags: ['System'], + body: { + type: 'object', + required: ['isEnabled'], + properties: { + isEnabled: { + type: 'boolean' + } + } + }, + response: { + 200: { + description: 'API state updated successfully', + type: 'object', + properties: { + ok: { + type: 'boolean' + }, + message: { + type: 'string' + }, + isEnabled: { + type: 'boolean' + } + } + } + } + } + }, + async (req, reply) => { + const previousConfig = WIKI.config.api + WIKI.config.api = { ...previousConfig, isEnabled: req.body.isEnabled } + + if (!(await WIKI.configSvc.saveToDb(['api']))) { + WIKI.config.api = previousConfig + return reply.internalServerError('Failed to save the API state.') + } + + return { + ok: true, + message: req.body.isEnabled ? 'API enabled successfully.' : 'API disabled successfully.', + isEnabled: req.body.isEnabled + } } ) diff --git a/backend/base.yml b/backend/base.yml index e3a0cf5c1..265253fb7 100644 --- a/backend/base.yml +++ b/backend/base.yml @@ -64,12 +64,9 @@ defaults: uploadScanSVG: true disallowIframe: true uploadMaxFiles: 20 - authJwtAudience: 'urn:wiki.js' - authJwtExpiration: '30m' uploadMaxFileSize: 10485760 forceAssetDownload: true disallowOpenRedirect: true - authJwtRenewablePeriod: '14d' enforceSameOriginReferrerPolicy: true flags: experimental: false diff --git a/backend/core/db.ts b/backend/core/db.ts index 37e7c772e..18e8170cf 100644 --- a/backend/core/db.ts +++ b/backend/core/db.ts @@ -9,19 +9,36 @@ import { parse } from 'pg-connection-string' import semver from 'semver' import { relations } from '../db/relations.ts' +import { flags } from '../models/flags.ts' import { createDeferred } from '../helpers/common.ts' // import migrationSource from '../db/migrator-source.js' // const migrateFromLegacy = require('../db/legacy') +/** + * Query logger, consulted by Drizzle on every query. + * + * The decision is made per query rather than when the instance is built, so that the `sqlLog` system + * flag can be turned on in the admin area and take effect on the next query — a logger chosen at boot + * would need a restart. + */ +const queryLogger = { + logQuery(query: string, params: unknown[]): void { + if (!flags.isEnabled('sqlLog') && !WIKI.config.dev?.logQueries) { + return + } + WIKI.logger.info(`[SQL] ${query}${params.length > 0 ? ` -- ${JSON.stringify(params)}` : ''}`) + } +} + /** * Build the Drizzle instance. * - * The two branches are spelled out rather than spreading a conditional `{ logger: true }` into a - * single call: a spread in the config literal collapses the inferred relations to `EmptyRelations`, - * which would untype the whole `db.query.*` relational API. + * `logger` is passed unconditionally rather than spread in from a conditional: a spread in the config + * literal collapses the inferred relations to `EmptyRelations`, which would untype the whole + * `db.query.*` relational API. */ -function createDb(client: Pool, logQueries: boolean) { - return logQueries ? drizzle({ client, relations, logger: true }) : drizzle({ client, relations }) +function createDb(client: Pool) { + return drizzle({ client, relations, logger: queryLogger }) } /** The Drizzle instance, as returned by `init()` and exposed as `WIKI.db`. */ @@ -117,7 +134,7 @@ export default { options: `-c search_path=${WIKI.config.db.schema}` }) - const db = createDb(this.pool, Boolean(WIKI.config.dev?.logQueries)) + const db = createDb(this.pool) // Connect await this.connect(db) diff --git a/backend/db/migrations/20260725221449_main/migration.sql b/backend/db/migrations/20260725221449_main/migration.sql new file mode 100644 index 000000000..5925ebbea --- /dev/null +++ b/backend/db/migrations/20260725221449_main/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "apiKeys" DROP COLUMN "key"; \ No newline at end of file diff --git a/backend/db/migrations/20260725221449_main/snapshot.json b/backend/db/migrations/20260725221449_main/snapshot.json new file mode 100644 index 000000000..17555396a --- /dev/null +++ b/backend/db/migrations/20260725221449_main/snapshot.json @@ -0,0 +1,3731 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "9104fb01-a5cd-4e9f-bba7-e9b86dd6fa68", + "prevIds": [ + "91c32c3a-9a9e-4693-b10f-694beca33f26" + ], + "ddl": [ + { + "values": [ + "document", + "image", + "other" + ], + "name": "assetKind", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "completed", + "failed", + "interrupted" + ], + "name": "jobHistoryState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "draft", + "published", + "scheduled" + ], + "name": "pagePublishState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inherit", + "override", + "overrideExact", + "hide", + "hideExact" + ], + "name": "treeNavigationMode", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "folder", + "page", + "asset" + ], + "name": "treeType", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apiKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "assets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "authentication", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "blocks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "groups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobHistory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobLock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobSchedule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "locales", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "navigation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sites", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tree", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userAvatars", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userGroups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "expiration", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRevoked", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileExt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "assetKind", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'other'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'application/octet-stream'", + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileSize", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storageInfo", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "displayName", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "registration", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "allowedEmailRegex", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "autoEnrollGroups", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "block", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isCustom", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rules", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnFirstLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogout", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jobHistoryState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "wasScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorMessage", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "executedBy", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCheckedBy", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cron", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "retries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "waitUntil", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nativeName", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(8)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "region", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(4)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "script", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRTL", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "strings", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "completeness", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "items", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alias", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "pagePublishState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "publishState", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishStartDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishEndDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "relations", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "render", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "searchContent", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "toc", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "editor", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isBrowsable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isSearchable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "\"pages\".\"publishState\" != 'draft' AND \"pages\".\"isSearchable\"", + "type": "stored" + }, + "identity": null, + "name": "isSearchableComputed", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "ratingScore", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "ratingCount", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "scripts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "historyData", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creatorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hostname", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tag", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "usageCount", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderPath", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tree", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeNavigationMode", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inherit'", + "generated": null, + "identity": null, + "name": "navigationMode", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "navigationId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "validUntil", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "passkeys", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "prefs", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasAvatar", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isVerified", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastLoginAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "assets_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blocks_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "locales_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "locales" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "navigation_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_authorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "creatorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_creatorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ownerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_ownerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ts", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_ts_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isSearchableComputed", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_isSearchableComputed_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_folderpath_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_folderpath_gist_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_fileName_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tree", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_locale_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationMode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationMode_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "tree_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_groupId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userKeys_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userKeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastLoginAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_lastLoginAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "blocks_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "navigation_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "creatorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_creatorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_ownerId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "sessions_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tags_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tree_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "groupId" + ], + "schemaTo": "public", + "tableTo": "groups", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_groupId_groups_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "userKeys_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userKeys" + }, + { + "columns": [ + "userId", + "groupId" + ], + "nameExplicit": false, + "name": "userGroups_pkey", + "entityType": "pks", + "schema": "public", + "table": "userGroups" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apiKeys_pkey", + "schema": "public", + "table": "apiKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "assets_pkey", + "schema": "public", + "table": "assets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "authentication_pkey", + "schema": "public", + "table": "authentication", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blocks_pkey", + "schema": "public", + "table": "blocks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "groups_pkey", + "schema": "public", + "table": "groups", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobHistory_pkey", + "schema": "public", + "table": "jobHistory", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "jobLock_pkey", + "schema": "public", + "table": "jobLock", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobSchedule_pkey", + "schema": "public", + "table": "jobSchedule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobs_pkey", + "schema": "public", + "table": "jobs", + "entityType": "pks" + }, + { + "columns": [ + "code" + ], + "nameExplicit": false, + "name": "locales_pkey", + "schema": "public", + "table": "locales", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "navigation_pkey", + "schema": "public", + "table": "navigation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pages_pkey", + "schema": "public", + "table": "pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "schema": "public", + "table": "sessions", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "settings_pkey", + "schema": "public", + "table": "settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sites_pkey", + "schema": "public", + "table": "sites", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tags_pkey", + "schema": "public", + "table": "tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tree_pkey", + "schema": "public", + "table": "tree", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userAvatars_pkey", + "schema": "public", + "table": "userAvatars", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userKeys_pkey", + "schema": "public", + "table": "userKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "hostname" + ], + "nullsNotDistinct": false, + "name": "sites_hostname_key", + "schema": "public", + "table": "sites", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/db/migrations/20260725221505_main/migration.sql b/backend/db/migrations/20260725221505_main/migration.sql new file mode 100644 index 000000000..ff2a165b4 --- /dev/null +++ b/backend/db/migrations/20260725221505_main/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "apiKeys" ADD COLUMN "keyShort" varchar(8) NOT NULL;--> statement-breakpoint +ALTER TABLE "apiKeys" ADD COLUMN "groups" jsonb DEFAULT '[]' NOT NULL; \ No newline at end of file diff --git a/backend/db/migrations/20260725221505_main/snapshot.json b/backend/db/migrations/20260725221505_main/snapshot.json new file mode 100644 index 000000000..2d4ed5a2b --- /dev/null +++ b/backend/db/migrations/20260725221505_main/snapshot.json @@ -0,0 +1,3757 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "39f9e578-5956-45d5-bdc5-a27d2452b9bc", + "prevIds": [ + "9104fb01-a5cd-4e9f-bba7-e9b86dd6fa68" + ], + "ddl": [ + { + "values": [ + "document", + "image", + "other" + ], + "name": "assetKind", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "completed", + "failed", + "interrupted" + ], + "name": "jobHistoryState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "draft", + "published", + "scheduled" + ], + "name": "pagePublishState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inherit", + "override", + "overrideExact", + "hide", + "hideExact" + ], + "name": "treeNavigationMode", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "folder", + "page", + "asset" + ], + "name": "treeType", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apiKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "assets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "authentication", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "blocks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "groups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobHistory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobLock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobSchedule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "locales", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "navigation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sites", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tree", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userAvatars", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userGroups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "varchar(8)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "keyShort", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "groups", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "expiration", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRevoked", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileExt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "assetKind", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'other'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'application/octet-stream'", + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileSize", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storageInfo", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "displayName", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "registration", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "allowedEmailRegex", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "autoEnrollGroups", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "block", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isCustom", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rules", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnFirstLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogout", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jobHistoryState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "wasScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorMessage", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "executedBy", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCheckedBy", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cron", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "retries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "waitUntil", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nativeName", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(8)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "region", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(4)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "script", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRTL", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "strings", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "completeness", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "items", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alias", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "pagePublishState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "publishState", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishStartDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishEndDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "relations", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "render", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "searchContent", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "toc", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "editor", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isBrowsable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isSearchable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "\"pages\".\"publishState\" != 'draft' AND \"pages\".\"isSearchable\"", + "type": "stored" + }, + "identity": null, + "name": "isSearchableComputed", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "ratingScore", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "ratingCount", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "scripts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "historyData", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creatorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hostname", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tag", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "usageCount", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderPath", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tree", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeNavigationMode", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inherit'", + "generated": null, + "identity": null, + "name": "navigationMode", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "navigationId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "validUntil", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "passkeys", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "prefs", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasAvatar", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isVerified", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastLoginAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "assets_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blocks_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "locales_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "locales" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "navigation_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_authorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "creatorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_creatorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ownerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_ownerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ts", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_ts_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isSearchableComputed", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_isSearchableComputed_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_folderpath_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_folderpath_gist_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_fileName_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tree", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_locale_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationMode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationMode_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "tree_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_groupId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userKeys_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userKeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastLoginAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_lastLoginAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "blocks_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "navigation_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "creatorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_creatorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_ownerId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "sessions_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tags_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tree_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "groupId" + ], + "schemaTo": "public", + "tableTo": "groups", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_groupId_groups_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "userKeys_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userKeys" + }, + { + "columns": [ + "userId", + "groupId" + ], + "nameExplicit": false, + "name": "userGroups_pkey", + "entityType": "pks", + "schema": "public", + "table": "userGroups" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apiKeys_pkey", + "schema": "public", + "table": "apiKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "assets_pkey", + "schema": "public", + "table": "assets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "authentication_pkey", + "schema": "public", + "table": "authentication", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blocks_pkey", + "schema": "public", + "table": "blocks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "groups_pkey", + "schema": "public", + "table": "groups", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobHistory_pkey", + "schema": "public", + "table": "jobHistory", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "jobLock_pkey", + "schema": "public", + "table": "jobLock", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobSchedule_pkey", + "schema": "public", + "table": "jobSchedule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobs_pkey", + "schema": "public", + "table": "jobs", + "entityType": "pks" + }, + { + "columns": [ + "code" + ], + "nameExplicit": false, + "name": "locales_pkey", + "schema": "public", + "table": "locales", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "navigation_pkey", + "schema": "public", + "table": "navigation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pages_pkey", + "schema": "public", + "table": "pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "schema": "public", + "table": "sessions", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "settings_pkey", + "schema": "public", + "table": "settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sites_pkey", + "schema": "public", + "table": "sites", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tags_pkey", + "schema": "public", + "table": "tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tree_pkey", + "schema": "public", + "table": "tree", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userAvatars_pkey", + "schema": "public", + "table": "userAvatars", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userKeys_pkey", + "schema": "public", + "table": "userKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "hostname" + ], + "nullsNotDistinct": false, + "name": "sites_hostname_key", + "schema": "public", + "table": "sites", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/db/migrations/20260726001017_main/migration.sql b/backend/db/migrations/20260726001017_main/migration.sql new file mode 100644 index 000000000..f800f8e6c --- /dev/null +++ b/backend/db/migrations/20260726001017_main/migration.sql @@ -0,0 +1,15 @@ +CREATE TYPE "hookState" AS ENUM('pending', 'success', 'error');--> statement-breakpoint +CREATE TABLE "hooks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" varchar(255) NOT NULL, + "events" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "url" text NOT NULL, + "includeMetadata" boolean DEFAULT true NOT NULL, + "includeContent" boolean DEFAULT false NOT NULL, + "acceptUntrusted" boolean DEFAULT false NOT NULL, + "authHeader" text, + "state" "hookState" DEFAULT 'pending'::"hookState" NOT NULL, + "lastErrorMessage" text, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); diff --git a/backend/db/migrations/20260726001017_main/snapshot.json b/backend/db/migrations/20260726001017_main/snapshot.json new file mode 100644 index 000000000..ed4ca56d4 --- /dev/null +++ b/backend/db/migrations/20260726001017_main/snapshot.json @@ -0,0 +1,3939 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2edb30b9-3a30-45f4-a267-0751f92e8505", + "prevIds": [ + "39f9e578-5956-45d5-bdc5-a27d2452b9bc" + ], + "ddl": [ + { + "values": [ + "document", + "image", + "other" + ], + "name": "assetKind", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "success", + "error" + ], + "name": "hookState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "active", + "completed", + "failed", + "interrupted" + ], + "name": "jobHistoryState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "draft", + "published", + "scheduled" + ], + "name": "pagePublishState", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "inherit", + "override", + "overrideExact", + "hide", + "hideExact" + ], + "name": "treeNavigationMode", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "folder", + "page", + "asset" + ], + "name": "treeType", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "apiKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "assets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "authentication", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "blocks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "groups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "hooks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobHistory", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobLock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobSchedule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "jobs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "locales", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "navigation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sites", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tree", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userAvatars", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userGroups", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "userKeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "varchar(8)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "keyShort", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "groups", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "expiration", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRevoked", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "apiKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileExt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "assetKind", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'other'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'application/octet-stream'", + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileSize", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storageInfo", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "assets" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "displayName", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "registration", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "allowedEmailRegex", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "autoEnrollGroups", + "entityType": "columns", + "schema": "public", + "table": "authentication" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "block", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isCustom", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "blocks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rules", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnFirstLogin", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "redirectOnLogout", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "groups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "events", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "includeMetadata", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "includeContent", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "acceptUntrusted", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authHeader", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "hookState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorMessage", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "hooks" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jobHistoryState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "wasScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastErrorMessage", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "executedBy", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "jobHistory" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastCheckedBy", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastCheckedAt", + "entityType": "columns", + "schema": "public", + "table": "jobLock" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cron", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobSchedule" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "useWorker", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "retries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "waitUntil", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isScheduled", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "jobs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nativeName", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(8)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "region", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "varchar(4)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "script", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isRTL", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "strings", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "completeness", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "locales" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "items", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "navigation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alias", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "pagePublishState", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "publishState", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishStartDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishEndDate", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "relations", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "render", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "searchContent", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "toc", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "editor", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentType", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isBrowsable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isSearchable", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": { + "as": "\"pages\".\"publishState\" != 'draft' AND \"pages\".\"isSearchable\"", + "type": "stored" + }, + "identity": null, + "name": "isSearchableComputed", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "ratingScore", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "ratingCount", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "scripts", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "historyData", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creatorId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ownerId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "pages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "settings" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hostname", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isEnabled", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "sites" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tag", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "usageCount", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folderPath", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileName", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeType", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tree", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "ltree", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "treeNavigationMode", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'inherit'", + "generated": null, + "identity": null, + "name": "navigationMode", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "navigationId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "ARRAY[]", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "siteId", + "entityType": "columns", + "schema": "public", + "table": "tree" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "userAvatars" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupId", + "entityType": "columns", + "schema": "public", + "table": "userGroups" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "validUntil", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "userKeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "meta", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "passkeys", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "prefs", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasAvatar", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isActive", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isSystem", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isVerified", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastLoginAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "assets_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blocks_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "locales_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "locales" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "navigation_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_authorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "creatorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_creatorId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ownerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_ownerId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ts", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_ts_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "pages_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isSearchableComputed", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pages_isSearchableComputed_idx", + "entityType": "indexes", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tags_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_folderpath_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "folderPath", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_folderpath_gist_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_fileName_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tree", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gist", + "concurrently": false, + "name": "tree_locale_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationMode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationMode_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "navigationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_navigationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "tree_tags_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "siteId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tree_siteId_idx", + "entityType": "indexes", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_groupId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userGroups_composite_idx", + "entityType": "indexes", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "userKeys_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "userKeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastLoginAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_lastLoginAt_idx", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "assets_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "assets" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "blocks_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blocks" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "navigation_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "navigation" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_authorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "creatorId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_creatorId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "ownerId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_ownerId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "pages_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "pages" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "sessions_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tags_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tags" + }, + { + "nameExplicit": false, + "columns": [ + "siteId" + ], + "schemaTo": "public", + "tableTo": "sites", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "tree_siteId_sites_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tree" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "groupId" + ], + "schemaTo": "public", + "tableTo": "groups", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "userGroups_groupId_groups_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userGroups" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "userKeys_userId_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "userKeys" + }, + { + "columns": [ + "userId", + "groupId" + ], + "nameExplicit": false, + "name": "userGroups_pkey", + "entityType": "pks", + "schema": "public", + "table": "userGroups" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "apiKeys_pkey", + "schema": "public", + "table": "apiKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "assets_pkey", + "schema": "public", + "table": "assets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "authentication_pkey", + "schema": "public", + "table": "authentication", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blocks_pkey", + "schema": "public", + "table": "blocks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "groups_pkey", + "schema": "public", + "table": "groups", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "hooks_pkey", + "schema": "public", + "table": "hooks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobHistory_pkey", + "schema": "public", + "table": "jobHistory", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "jobLock_pkey", + "schema": "public", + "table": "jobLock", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobSchedule_pkey", + "schema": "public", + "table": "jobSchedule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "jobs_pkey", + "schema": "public", + "table": "jobs", + "entityType": "pks" + }, + { + "columns": [ + "code" + ], + "nameExplicit": false, + "name": "locales_pkey", + "schema": "public", + "table": "locales", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "navigation_pkey", + "schema": "public", + "table": "navigation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pages_pkey", + "schema": "public", + "table": "pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "schema": "public", + "table": "sessions", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "settings_pkey", + "schema": "public", + "table": "settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sites_pkey", + "schema": "public", + "table": "sites", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tags_pkey", + "schema": "public", + "table": "tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tree_pkey", + "schema": "public", + "table": "tree", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userAvatars_pkey", + "schema": "public", + "table": "userAvatars", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "userKeys_pkey", + "schema": "public", + "table": "userKeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "hostname" + ], + "nullsNotDistinct": false, + "name": "sites_hostname_key", + "schema": "public", + "table": "sites", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/db/schema.ts b/backend/db/schema.ts index 999e7c263..70714da23 100644 --- a/backend/db/schema.ts +++ b/backend/db/schema.ts @@ -36,7 +36,13 @@ const tsvector = customType({ export const apiKeys = pgTable('apiKeys', { id: uuid().primaryKey().defaultRandom(), name: varchar({ length: 255 }).notNull(), - key: text().notNull(), + // -> Only the tail of the token, to tell keys apart in the admin list. The token itself is a + // signed JWT shown once at creation and never stored: it is a bearer credential, and + // verification needs the public key plus this row's state, not the token. + keyShort: varchar({ length: 8 }).notNull(), + // -> IDs of the groups whose permissions the key carries. Resolved on every request, so editing a + // group immediately affects the keys pointing at it. + groups: jsonb().notNull().default([]), expiration: timestamp().notNull().defaultNow(), isRevoked: boolean().notNull().default(false), createdAt: timestamp().notNull().defaultNow(), @@ -116,6 +122,29 @@ export const groups = pgTable('groups', { updatedAt: timestamp().notNull().defaultNow() }) +// HOOKS ------------------------------- +export const hookStateEnum = pgEnum('hookState', ['pending', 'success', 'error']) +export const hooks = pgTable('hooks', { + id: uuid().primaryKey().defaultRandom(), + name: varchar({ length: 255 }).notNull(), + // -> Event keys such as `page:create`, matched against what the server emits + events: text() + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), + url: text().notNull(), + includeMetadata: boolean().notNull().default(true), + includeContent: boolean().notNull().default(false), + acceptUntrusted: boolean().notNull().default(false), + // -> Sent verbatim as the Authorization header, so it holds whatever secret the remote expects + authHeader: text(), + // -> Outcome of the most recent delivery, which is what the admin list shows + state: hookStateEnum().notNull().default('pending'), + lastErrorMessage: text(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow() +}) + // JOB HISTORY ------------------------- export const jobHistoryStateEnum = pgEnum('jobHistoryState', [ 'active', diff --git a/backend/helpers/common.ts b/backend/helpers/common.ts index 170661b15..bd509f96a 100644 --- a/backend/helpers/common.ts +++ b/backend/helpers/common.ts @@ -112,6 +112,7 @@ export interface ModulePropDefinition { enumDisplay?: string multiline?: boolean sensitive?: boolean + readOnly?: boolean icon?: string order?: number if?: unknown[] @@ -127,6 +128,8 @@ export interface ModuleProp { enumDisplay: string multiline: boolean sensitive: boolean + /** Shown but not editable — the module declares something this server cannot currently change. */ + readOnly: boolean icon: string order: number if: unknown[] @@ -149,6 +152,7 @@ export function parseModuleProps( enumDisplay: def.enumDisplay || 'select', multiline: def.multiline || false, sensitive: def.sensitive || false, + readOnly: def.readOnly || false, icon: def.icon || 'rename', order: def.order || 100, if: def.if ?? [] diff --git a/backend/helpers/jwt.ts b/backend/helpers/jwt.ts new file mode 100644 index 000000000..6b673541a --- /dev/null +++ b/backend/helpers/jwt.ts @@ -0,0 +1,108 @@ +import crypto from 'node:crypto' + +/** + * Minimal RS256 JWT signing and verification. + * + * Wiki.js generates an RSA keypair during installation and keeps it in `config.auth.certs`, so + * tokens are signed with that key rather than with a shared secret. Only the RS256 algorithm is + * accepted on the way in — a token asking for `none`, or for an HMAC algorithm that would turn the + * public key into a signing secret, is rejected outright. + */ + +export interface JwtClaims { + [claim: string]: any + /** Audience. Compared against the expected one during verification. */ + aud?: string + /** Expiry, in seconds since the epoch. Required by `verifyJwt`. */ + exp?: number + /** Issued at, in seconds since the epoch. */ + iat?: number +} + +function encodeSegment(value: object): string { + return Buffer.from(JSON.stringify(value)).toString('base64url') +} + +function decodeSegment(segment: string): any { + return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8')) +} + +/** Seconds since the epoch, the unit JWT uses for `iat` / `exp`. */ +export function epochSeconds(instant: Temporal.Instant = Temporal.Now.instant()): number { + return Math.floor(instant.epochMilliseconds / 1000) +} + +/** + * Sign a set of claims. + * + * @param privateKey A key object, or a PEM string for an unencrypted key. The installation key is + * passphrase-protected, so callers pass a `KeyObject` built with the passphrase. + */ +export function signJwt(claims: JwtClaims, privateKey: crypto.KeyObject | string): string { + const payload = `${encodeSegment({ alg: 'RS256', typ: 'JWT' })}.${encodeSegment(claims)}` + const signature = crypto.sign('RSA-SHA256', Buffer.from(payload), privateKey) + return `${payload}.${signature.toString('base64url')}` +} + +/** + * Verify a token and return its claims. + * + * Throws with a specific message on every failure — a malformed token, a bad signature, an expired + * token or the wrong audience — so callers can log the reason without inspecting the token again. + */ +export function verifyJwt( + token: string, + publicKey: crypto.KeyObject | string, + { audience }: { audience?: string } = {} +): JwtClaims { + const segments = token.split('.') + if (segments.length !== 3) { + throw new Error('Token is malformed.') + } + const [encodedHeader, encodedClaims, encodedSignature] = segments as [string, string, string] + + let header: any + let claims: JwtClaims + try { + header = decodeSegment(encodedHeader) + claims = decodeSegment(encodedClaims) + } catch { + throw new Error('Token is malformed.') + } + if (header?.alg !== 'RS256') { + throw new Error('Token algorithm is not supported.') + } + if (!claims || typeof claims !== 'object') { + throw new Error('Token is malformed.') + } + + let isValid = false + try { + isValid = crypto.verify( + 'RSA-SHA256', + Buffer.from(`${encodedHeader}.${encodedClaims}`), + publicKey, + Buffer.from(encodedSignature, 'base64url') + ) + } catch { + // -> A signature that is not even well-formed lands here rather than returning false + isValid = false + } + if (!isValid) { + throw new Error('Token signature is invalid.') + } + + // -> A token with no expiry would be valid forever; treat its absence as a failure rather than as + // permission to skip the check + if (typeof claims.exp !== 'number') { + throw new Error('Token has no expiration.') + } + if (epochSeconds() >= claims.exp) { + throw new Error('Token has expired.') + } + if (audience && claims.aud !== audience) { + throw new Error('Token audience does not match.') + } + + return claims +} diff --git a/backend/helpers/security.ts b/backend/helpers/security.ts new file mode 100644 index 000000000..0bda50191 --- /dev/null +++ b/backend/helpers/security.ts @@ -0,0 +1,61 @@ +/** + * Helpers turning the security settings an operator edits in the admin area into the shapes the + * HTTP plugins expect. + */ + +/** CORS modes offered by the admin area, in the order they appear there. */ +export const CORS_MODES = ['OFF', 'REFLECT', 'HOSTNAMES', 'REGEX'] as const +export type CorsMode = (typeof CORS_MODES)[number] + +/** + * Turn a Content-Security-Policy string into helmet's directives object. + * + * `default-src 'self'; img-src * data:` becomes + * `{ 'default-src': ["'self'"], 'img-src': ['*', 'data:'] }`. A directive with no value, such as + * `upgrade-insecure-requests`, maps to an empty list, which is how helmet expresses it too. + */ +export function parseCspDirectives(value: string): Record { + const directives: Record = {} + for (const chunk of value.split(';')) { + const parts = chunk.trim().split(/\s+/).filter(Boolean) + const name = parts.shift() + if (!name) { + continue + } + directives[name.toLowerCase()] = parts + } + return directives +} + +/** + * The `origin` option for `@fastify/cors`, from the configured mode. + * + * `false` means no CORS headers at all, i.e. same-origin only, which is both the `OFF` mode and what + * anything unrecognised degrades to — a misconfiguration should not end up more permissive than the + * operator asked for. + */ +export function corsOrigin(security: { + corsMode?: string + corsConfig?: string +}): boolean | string[] | RegExp { + switch (security.corsMode) { + case 'REFLECT': + return true + case 'HOSTNAMES': + return (security.corsConfig ?? '') + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter(Boolean) + case 'REGEX': + try { + return new RegExp(security.corsConfig ?? '') + } catch (err: any) { + WIKI.logger.warn( + `The CORS regex pattern is invalid (${err.message}) — falling back to same-origin only.` + ) + return false + } + default: + return false + } +} diff --git a/backend/index.ts b/backend/index.ts index f05540cf2..2b6a380d0 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -33,6 +33,7 @@ import configSvc from './core/config.ts' import dbManager from './core/db.ts' import logger from './core/logger.ts' import scheduler from './core/scheduler.ts' +import { corsOrigin, parseCspDirectives } from './helpers/security.ts' const nanoid = customAlphabet('1234567890abcdef', 10) @@ -145,6 +146,11 @@ async function postBoot() { await WIKI.models.blocks.refreshFromDisk() await WIKI.models.blocks.syncAllSites() + // -> Optional third-party tooling: report what is available, since features silently degrade + // without it + await WIKI.models.extensions.refreshFromDisk() + await WIKI.models.extensions.logState() + await WIKI.dbManager.subscribeToNotifications() await WIKI.scheduler.start() } @@ -192,7 +198,9 @@ async function initHTTPServer() { logger: { level: 'error' }, - trustProxy: WIKI.config.security.securityTrustProxy ?? false, + // -> `securityTrustProxy` was the 2.x name: the setting is `trustProxy`, so this read never + // matched and the option was permanently off no matter what the admin area showed + trustProxy: WIKI.config.security.trustProxy ?? false, routerOptions: { ignoreTrailingSlash: true } @@ -227,21 +235,43 @@ async function initHTTPServer() { // Security // ---------------------------------------- + // -> Every setting below comes from the admin area's security view. They are read once, here, so a + // change takes effect on the next restart — the view says as much. + const security = WIKI.config.security + app.register(fastifyHelmet, { - contentSecurityPolicy: false, // TODO: Make it configurable - strictTransportSecurity: WIKI.config.security.securityHSTS - ? { - maxAge: WIKI.config.security.securityHSTSDuration, - includeSubDomains: true - } - : false + contentSecurityPolicy: + security.enforceCsp && security.cspDirectives + ? { directives: parseCspDirectives(security.cspDirectives), useDefaults: false } + : false, + strictTransportSecurity: + security.enforceHsts && security.hstsDuration > 0 + ? { + maxAge: security.hstsDuration, + includeSubDomains: true + } + : false, + // -> Helmet's own default is `sameorigin`, which is also what this setting turned off means + xFrameOptions: { action: security.disallowIframe ? 'deny' : 'sameorigin' }, + referrerPolicy: security.enforceSameOriginReferrerPolicy + ? { policy: 'same-origin' } + : { policy: 'no-referrer' } }) app.register(fastifyCors, { - origin: '*', // TODO: Make it configurable + origin: corsOrigin(security), methods: ['GET', 'HEAD', 'POST', 'OPTIONS'] }) + if (security.disallowFloc) { + // -> Helmet dropped its FLoC helper once the proposal was withdrawn, but opting out still costs + // one header and the setting exists + app.addHook('onSend', (req, reply, payload, done) => { + reply.header('Permissions-Policy', 'interest-cohort=()') + done(null, payload) + }) + } + // ---------------------------------------- // Public Assets // ---------------------------------------- @@ -377,6 +407,36 @@ async function initHTTPServer() { logo: {} as any }) + // ---------------------------------------- + // API Key Authentication + // ---------------------------------------- + + app.decorateRequest('apiKey', null) + + app.addHook('onRequest', async (req, reply) => { + // -> Bearer tokens authenticate API calls only; everything else is cookie-authenticated. Note + // that the session is deliberately left untouched: writing to it would have @fastify/session + // persist a session row for every scraped request. + if (!req.url.startsWith('/_api/')) { + return + } + const header = req.headers.authorization + if (!header?.startsWith('Bearer ')) { + return + } + const token = header.slice('Bearer '.length).trim() + if (!token) { + return + } + try { + req.apiKey = await WIKI.models.apiKeys.verify(token) + } catch (err: any) { + // -> Say why: the caller holds the credential and can act on "revoked" or "expired" + WIKI.logger.debug(`Rejected an API key: ${err.message}`) + return reply.unauthorized(err.message) + } + }) + // ---------------------------------------- // Permissions // ---------------------------------------- @@ -384,19 +444,26 @@ async function initHTTPServer() { app.addHook('preHandler', (req, reply, done) => { const routePermissions = req.routeOptions.config?.permissions if (routePermissions && routePermissions.length > 0) { + // -> A verified API key stands in for a session, carrying the permissions of the groups it was + // issued for + const permissions = req.apiKey + ? req.apiKey.permissions + : req.session?.authenticated + ? req.session.permissions + : null // Unauthenticated / No Permissions - if (!req.session?.authenticated || !(req.session?.permissions?.length ?? 0)) { + if (!permissions || permissions.length < 1) { return reply.unauthorized() } // Is Root Admin? - if (!req.session.permissions!.includes('manage:system')) { + if (!permissions.includes('manage:system')) { // Check for at least 1 permission const isAllowed = routePermissions.some((perms) => { // Check for all permissions if (Array.isArray(perms)) { - return perms.every((perm) => req.session.permissions?.some((p) => p === perm)) + return perms.every((perm) => permissions.some((p) => p === perm)) } else { - return req.session.permissions?.some((p) => p === perms) + return permissions.some((p) => p === perms) } }) // Forbidden diff --git a/backend/locales/en.json b/backend/locales/en.json index 047c534c7..d846c3ac6 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -7,9 +7,12 @@ "admin.analytics.saveSuccess": "Analytics configuration saved successfully", "admin.analytics.subtitle": "Add analytics and tracking tools to your wiki", "admin.analytics.title": "Analytics", + "admin.api.copyFailed": "Could not copy the key to the clipboard.", "admin.api.copyKeyTitle": "Copy API Key", + "admin.api.copySuccess": "API key copied to the clipboard.", "admin.api.createInvalidData": "Some fields are missing or have invalid data.", "admin.api.createSuccess": "API Key created successfully.", + "admin.api.createdOn": "Created on {date}", "admin.api.disableButton": "Disable API", "admin.api.disabled": "API Disabled", "admin.api.enableButton": "Enable API", @@ -19,6 +22,9 @@ "admin.api.expiration30d": "30 days", "admin.api.expiration3y": "3 years", "admin.api.expiration90d": "90 days", + "admin.api.expired": "Expired", + "admin.api.expiredHint": "This key is past its expiration date and can no longer be used.", + "admin.api.expiresOn": "Expires on {date}", "admin.api.groupSelected": "Use {group} group permissions", "admin.api.groupsMissing": "You must select at least 1 group for this key.", "admin.api.groupsSelected": "Use permissions from {count} groups", @@ -29,6 +35,8 @@ "admin.api.headerName": "Name", "admin.api.headerRevoke": "Revoke", "admin.api.key": "API Key", + "admin.api.keyEndingIn": "Ending in {suffix}", + "admin.api.loadFailed": "Failed to load API keys.", "admin.api.nameInvalidChars": "Key name has invalid characters.", "admin.api.nameMissing": "Key name is missing.", "admin.api.newKeyButton": "New API Key", @@ -51,6 +59,7 @@ "admin.api.noKeyInfo": "No API keys have been generated yet.", "admin.api.none": "There are no API keys yet.", "admin.api.permissionGroups": "Group Permissions", + "admin.api.permissionsFrom": "Permissions from {groups}", "admin.api.refreshSuccess": "List of API keys has been refreshed.", "admin.api.revoke": "Revoke", "admin.api.revokeConfirm": "Revoke API Key?", @@ -62,10 +71,13 @@ "admin.api.title": "API Access", "admin.api.toggleStateDisabledSuccess": "API has been disabled successfully.", "admin.api.toggleStateEnabledSuccess": "API has been enabled successfully.", + "admin.api.toggleStateFailed": "Failed to switch the API state.", "admin.approval.title": "Approvals", "admin.audit.title": "Audit Log", "admin.auth.activeStrategies": "Active Strategies", + "admin.auth.addFailed": "Failed to add the strategy.", "admin.auth.addStrategy": "Add Strategy", + "admin.auth.addSuccess": "{strategy} has been added.", "admin.auth.allowedEmailRegex": "Allowed Email Address Regex", "admin.auth.allowedEmailRegexHint": "(optional) Only allow users to register with an email address that matches the regex expression.", "admin.auth.allowedWebOrigins": "Allowed Web Origins", @@ -74,6 +86,11 @@ "admin.auth.callbackUrl": "Callback URL / Redirect URI", "admin.auth.configReference": "Configuration Reference", "admin.auth.configReferenceSubtitle": "Some strategies may require some configuration values to be set on your provider. These are provided for reference only and may not be needed by the current strategy.", + "admin.auth.deleteConfirm": "Are you sure you want to delete the {strategy} strategy? Users who can only sign in through it will lose access.", + "admin.auth.deleteFailed": "Failed to delete the strategy.", + "admin.auth.deleteLocalForbidden": "Every account is registered against the local strategy, so it cannot be deleted.", + "admin.auth.deleteStrategy": "Delete Strategy", + "admin.auth.deleteSuccess": "{strategy} has been deleted.", "admin.auth.displayName": "Display Name", "admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.", "admin.auth.emailValidation": "Email Validation", @@ -87,13 +104,17 @@ "admin.auth.info": "Info", "admin.auth.infoName": "Name", "admin.auth.infoNameHint": "Display name for this strategy.", + "admin.auth.loadFailed": "Failed to load the authentication configuration.", "admin.auth.loginUrl": "Login URL", "admin.auth.logoutUrl": "Logout URL", "admin.auth.noConfigOption": "This strategy has no configuration options you can modify.", + "admin.auth.noModulesToAdd": "No other authentication module is installed on this server.", "admin.auth.refreshSuccess": "List of strategies has been refreshed.", "admin.auth.registration": "Registration", "admin.auth.registrationHint": "Allow any user successfully authorized by the strategy to access the wiki.", "admin.auth.registrationLocalHint": "Whether to allow guests to register new accounts.", + "admin.auth.registrationNotEnforced": "Saved but not enforced yet: self-registration is not implemented.", + "admin.auth.saveFailed": "Failed to save {strategy}.", "admin.auth.saveSuccess": "Authentication configuration saved successfully.", "admin.auth.security": "Security", "admin.auth.siteUrlNotSetup": "You must set a valid {siteUrl} first! Click on {general} in the left sidebar.", @@ -203,19 +224,22 @@ "admin.extensions.installingHint": "This may take a while depending on your server.", "admin.extensions.instructions": "Instructions", "admin.extensions.instructionsHint": "Must be installed manually", + "admin.extensions.loadFailed": "Failed to load extensions.", "admin.extensions.reinstall": "Reinstall", "admin.extensions.requiresSharp": "Requires Sharp extension", "admin.extensions.subtitle": "Install extensions for extra functionality", "admin.extensions.title": "Extensions", "admin.flags.advanced.hint": "Set custom configuration flags. Note that all values are public to all users! Do not insert senstive data.", "admin.flags.advanced.label": "Custom Configuration", + "admin.flags.advanced.notImplemented": "The editor for custom flags is not available yet, and nothing reads custom keys so far.", "admin.flags.authDebug.hint": "Log detailed debug info of all login / registration attempts.", "admin.flags.authDebug.label": "Auth Debug", "admin.flags.experimental.hint": "Enable unstable / unfinished features. DO NOT enable in a production environment!", "admin.flags.experimental.label": "Experimental Features", - "admin.flags.getTokenHint": "Copy your current authentication token for use in GraphQL API testing.", - "admin.flags.getTokenLabel": "Get Current Token", + "admin.flags.loadFailed": "Failed to fetch system flags.", + "admin.flags.saveFailed": "Failed to save system flags.", "admin.flags.saveSuccess": "Flags have been updated successfully.", + "admin.flags.serverLogNotice": "Auth Debug and SQL Query Logging take effect immediately and write to the server log.", "admin.flags.sqlLog.hint": "Log all queries made to the database to console.", "admin.flags.sqlLog.label": "SQL Query Logging", "admin.flags.subtitle": "Low-level system flags for debugging or experimental purposes", @@ -591,14 +615,20 @@ "admin.search.configSaveSuccess": "Search engine configuration saved successfully.", "admin.search.dictOverrides": "PostgreSQL Dictionary Mapping Overrides", "admin.search.dictOverridesHint": "JSON object of 2 letters locale codes and their PostgreSQL dictionary association. e.g. {0}", + "admin.search.dictOverridesInvalidJSON": "The dictionary mapping is not valid JSON. {reason}", + "admin.search.dictOverridesNotAnObject": "The dictionary mapping must be a JSON object, e.g. { \"en\": \"english\" }.", + "admin.search.dictOverridesUnknown": "{dictionary} is not a PostgreSQL dictionary available in this database (mapped to {locale}).", "admin.search.engineConfig": "Engine Configuration", "admin.search.engineNoConfig": "This engine has no configuration options you can modify.", "admin.search.highlighting": "Enable Term Highlighting", "admin.search.highlightingHint": "Whether to show the highlighted terms in search results. There is a slight performance impact when enabled.", "admin.search.indexRebuildSuccess": "Index rebuilt successfully.", "admin.search.listRefreshSuccess": "List of search engines has been refreshed.", + "admin.search.loadFailed": "Failed to load the search configuration.", + "admin.search.rebuildFailed": "Failed to queue a search index rebuild.", "admin.search.rebuildIndex": "Rebuild Index", "admin.search.rebuildInitSuccess": "A search index rebuild has been initiated and will start shortly.", + "admin.search.saveFailed": "Failed to save the search configuration.", "admin.search.saveSuccess": "Search engine configuration saved successfully", "admin.search.searchEngine": "Search Engine", "admin.search.subtitle": "Configure the search capabilities of your wiki", @@ -631,13 +661,17 @@ "admin.security.jwt": "JWT Configuration", "admin.security.jwtAudience": "JWT Audience", "admin.security.jwtAudienceHint": "Audience URN used in JWT issued upon login. Usually your domain name. (e.g. urn:your.domain.com)", + "admin.security.loadFailed": "Failed to load the security configuration.", "admin.security.loginScreen": "Login Screen", "admin.security.maxUploadBatch": "Max Files per Upload", "admin.security.maxUploadBatchHint": "How many files can be uploaded in a single batch?", "admin.security.maxUploadBatchSuffix": "files", "admin.security.maxUploadSize": "Max Upload Size", "admin.security.maxUploadSizeHint": "The maximum size for a single file. Final value in base 2.", + "admin.security.maxUploadSizeInvalid": "The maximum upload size must be a positive value, e.g. 10 MB.", "admin.security.maxUploadSizeSuffix": "bytes", + "admin.security.restartRequired": "Header, CORS and proxy settings are applied when the server starts, so they take effect after a restart.", + "admin.security.saveFailed": "Failed to save the security configuration.", "admin.security.saveSuccess": "Security configuration updated successfully.", "admin.security.scanSVG": "Scan and Sanitize SVG Uploads", "admin.security.scanSVGHint": "Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.", @@ -652,6 +686,7 @@ "admin.security.trustProxyHint": "Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.", "admin.security.uploads": "Uploads", "admin.security.uploadsInfo": "These settings only affect Wiki.js. If you're using a reverse-proxy (e.g. nginx, Apache, Cloudflare), you must also change its settings to match.", + "admin.security.uploadsNotEnforced": "These limits are saved but not enforced yet: uploading is not implemented.", "admin.security.warn": "Make sure to understand the implications before turning on / off a security feature.", "admin.sites.activate": "Activate Site", "admin.sites.activateConfirm": "Are you sure you want activate site {siteTitle}? The site will become accessible to users with read access.", @@ -1144,6 +1179,7 @@ "admin.webhooks.eventEditComment": "Edit an existing comment", "admin.webhooks.eventEditPage": "Update an existing page", "admin.webhooks.eventNewComment": "Post a new comment", + "admin.webhooks.eventNotEmitted": "Not emitted yet — this part of the wiki is not implemented.", "admin.webhooks.eventRenameAsset": "Rename / move an asset", "admin.webhooks.eventRenamePage": "Rename / move a page", "admin.webhooks.eventUploadAsset": "Upload a new asset", @@ -1157,6 +1193,7 @@ "admin.webhooks.includeContentHint": "Should the payload include content (e.g. the full page body). Make sure that your remote endpoint can accept large payloads!", "admin.webhooks.includeMetadata": "Include Metadata", "admin.webhooks.includeMetadataHint": "Should the payload include metadata such as title, description, author, etc.", + "admin.webhooks.loadFailed": "Failed to load webhooks.", "admin.webhooks.nameInvalidChars": "The name contains invalid characters.", "admin.webhooks.nameMissing": "A name for this webhook is required.", "admin.webhooks.new": "New Webhook", diff --git a/backend/models/apiKeys.ts b/backend/models/apiKeys.ts new file mode 100644 index 000000000..e48941293 --- /dev/null +++ b/backend/models/apiKeys.ts @@ -0,0 +1,212 @@ +import crypto from 'node:crypto' +import { apiKeys as apiKeysTable, groups as groupsTable } from '../db/schema.ts' +import { desc, eq, inArray, sql } from 'drizzle-orm' +import { flatten, uniq } from 'es-toolkit/array' +import { epochSeconds, signJwt, verifyJwt } from '../helpers/jwt.ts' + +/** The lifetimes the admin area offers, as durations the API accepts. */ +export const KEY_EXPIRATIONS = { + '30d': { days: 30 }, + '90d': { days: 90 }, + '180d': { days: 180 }, + '1y': { years: 1 }, + '3y': { years: 3 } +} as const + +export type KeyExpiration = keyof typeof KEY_EXPIRATIONS + +/** An API key as exposed by the API. Never includes the token itself, which is not stored. */ +export interface ApiKey { + id: string + name: string + keyShort: string + groups: string[] + expiration: Date + isRevoked: boolean + createdAt: Date + updatedAt: Date +} + +/** What a verified key grants, resolved from its groups at request time. */ +export interface ApiKeyIdentity { + id: string + permissions: string[] +} + +/** Raised by `verify()` when a token is not usable, with a reason safe to return to the caller. */ +export class ApiKeyError extends Error {} + +const keySelection = { + id: apiKeysTable.id, + name: apiKeysTable.name, + keyShort: apiKeysTable.keyShort, + groups: apiKeysTable.groups, + expiration: apiKeysTable.expiration, + isRevoked: apiKeysTable.isRevoked, + createdAt: apiKeysTable.createdAt, + updatedAt: apiKeysTable.updatedAt +} + +/** + * API Keys model + * + * A key is an RS256 JWT signed with the installation keypair, carrying the key row's ID and the + * groups it draws permissions from. The token is shown once at creation and never stored: the + * signature proves authenticity, and the row is consulted for revocation and expiry. Permissions are + * resolved from the groups on every request, so changing a group takes effect immediately. + */ +class ApiKeys { + /** + * The signing key, built from the passphrase-protected PEM in `config.auth.certs` + */ + private privateKey(): crypto.KeyObject { + return crypto.createPrivateKey({ + key: WIKI.config.auth.certs.private, + passphrase: WIKI.config.auth.secret + }) + } + + /** + * Every key, newest first. Revoked and expired keys are kept: the admin list shows their state. + */ + async getKeys(): Promise { + const results = await WIKI.db + .select(keySelection) + .from(apiKeysTable) + .orderBy(desc(apiKeysTable.createdAt)) + return results as ApiKey[] + } + + /** + * Mint a new key. + * + * @returns The key row plus the token, which is the only time it exists outside the client + */ + async createKey({ + name, + expiration, + groups + }: { + name: string + expiration: KeyExpiration + groups: string[] + }): Promise<{ id: string; key: string }> { + const id = crypto.randomUUID() + const expiresAt = Temporal.Now.zonedDateTimeISO('UTC') + .add(KEY_EXPIRATIONS[expiration]) + .toInstant() + + const key = signJwt( + { + // -> `api` marks the token as a key rather than a user token, so the two can never be + // confused should user tokens ever be signed with the same keypair + api: 1, + id, + grp: groups, + aud: WIKI.config.auth.audience, + iat: epochSeconds(), + exp: epochSeconds(expiresAt) + }, + this.privateKey() + ) + + await WIKI.db.insert(apiKeysTable).values({ + id, + name, + keyShort: key.slice(-8), + groups, + expiration: new Date(expiresAt.epochMilliseconds), + isRevoked: false + }) + + return { id, key } + } + + /** + * A single key, or null if there is no such key + */ + async getKeyById(id: string): Promise { + const results = await WIKI.db + .select(keySelection) + .from(apiKeysTable) + .where(eq(apiKeysTable.id, id)) + .limit(1) + return (results[0] as ApiKey) ?? null + } + + /** + * Revoke a key, permanently. Tokens already handed out stop working on the next request. + * + * @returns Whether a key was revoked + */ + async revokeKey(id: string): Promise { + const result = await WIKI.db + .update(apiKeysTable) + .set({ isRevoked: true, updatedAt: sql`now()` }) + .where(eq(apiKeysTable.id, id)) + return (result.rowCount ?? 0) > 0 + } + + /** + * The union of the permissions held by the given groups. + * + * A group that no longer exists simply contributes nothing, so deleting a group narrows the keys + * pointing at it instead of breaking them. + */ + async resolvePermissions(groupIds: string[]): Promise { + if (groupIds.length < 1) { + return [] + } + const rows = await WIKI.db + .select({ permissions: groupsTable.permissions }) + .from(groupsTable) + .where(inArray(groupsTable.id, groupIds)) + return uniq(flatten(rows.map((r: any) => (r.permissions ?? []) as string[]))) + } + + /** + * Verify a bearer token and resolve what it grants. + * + * @throws ApiKeyError with a reason suitable for a 401 response + */ + async verify(token: string): Promise { + if (WIKI.config.api.isEnabled !== true) { + throw new ApiKeyError('The API is disabled.') + } + + let claims + try { + claims = verifyJwt(token, WIKI.config.auth.certs.public, { + audience: WIKI.config.auth.audience + }) + } catch (err: any) { + throw new ApiKeyError(err.message) + } + + if (claims.api !== 1 || typeof claims.id !== 'string') { + throw new ApiKeyError('Token is not an API key.') + } + + const key = await this.getKeyById(claims.id) + if (!key) { + throw new ApiKeyError('API key does not exist.') + } + if (key.isRevoked) { + throw new ApiKeyError('API key has been revoked.') + } + // -> The token carries its own expiry, but the row is what the admin area shows; a mismatch + // should fail closed rather than trust the token + if (Temporal.Instant.compare(key.expiration.toTemporalInstant(), Temporal.Now.instant()) <= 0) { + throw new ApiKeyError('API key has expired.') + } + + return { + id: key.id, + permissions: await this.resolvePermissions( + Array.isArray(claims.grp) ? (claims.grp as string[]) : [] + ) + } + } +} + +export const apiKeys = new ApiKeys() diff --git a/backend/models/authentication.ts b/backend/models/authentication.ts index 5e9ea7c96..512bcb380 100644 --- a/backend/models/authentication.ts +++ b/backend/models/authentication.ts @@ -1,11 +1,55 @@ import fs from 'node:fs/promises' import path from 'node:path' import yaml from 'js-yaml' -import { eq } from 'drizzle-orm' +import { asc, eq } from 'drizzle-orm' import { parseModuleProps } from '../helpers/common.ts' -import { authentication as authenticationTable } from '../db/schema.ts' +import { authentication as authenticationTable, groups as groupsTable } from '../db/schema.ts' +import type { ModuleProp } from '../helpers/common.ts' import type { SystemIds } from './types.ts' +/** An authentication module, as declared by its `definition.yml`. */ +export interface AuthModule { + key: string + title: string + description: string + logo?: string + icon?: string + color?: string + vendor?: string + website?: string + isAvailable: boolean + useForm: boolean + usernameType: string + props: Record + refs?: Record +} + +/** A configured instance of an authentication module. */ +export interface AuthStrategy { + id: string + module: string + displayName: string + isEnabled: boolean + registration: boolean + allowedEmailRegex: string + autoEnrollGroups: string[] + config: Record +} + +/** The module every wiki ships with. */ +const LOCAL_MODULE = 'local' + +/** + * Whether this is the strategy the instance was seeded with. + * + * Not merely "a local strategy": every account's password is stored under this exact strategy ID + * (see `models/users.ts`), so it is the one that cannot be disabled or deleted. A second instance of + * the local module holds no credentials and is as disposable as any other strategy. + */ +function isBuiltInLocal(id: string): boolean { + return id === WIKI.data.systemIds.localAuthId +} + /** * Authentication model */ @@ -21,6 +65,277 @@ class Authentication { .where(enabledOnly ? eq(authenticationTable.isEnabled, true) : undefined) } + /** + * The authentication modules found on disk, in the order the admin area lists them + */ + getModules(): AuthModule[] { + return [...((WIKI.data.authentication ?? []) as AuthModule[])].sort((a, b) => + a.key === LOCAL_MODULE ? -1 : b.key === LOCAL_MODULE ? 1 : a.title.localeCompare(b.title) + ) + } + + /** + * A single module definition, or null when nothing on disk declares that key + */ + getModule(key: string): AuthModule | null { + return this.getModules().find((m) => m.key === key) ?? null + } + + /** + * Every configured strategy, the built-in local one first, then alphabetically by display name. + * + * Config values are completed from the module's declared defaults, so a prop added to a module + * after a strategy was configured is returned with its default rather than as a missing key. + */ + async getActiveStrategies(): Promise { + const strategies = await WIKI.db + .select() + .from(authenticationTable) + .orderBy(asc(authenticationTable.displayName)) + return strategies + .map((stg) => ({ + ...stg, + autoEnrollGroups: stg.autoEnrollGroups ?? [], + config: this.buildConfig(stg.module, {}, stg.config as Record) + })) + .sort((a, b) => (isBuiltInLocal(a.id) ? -1 : isBuiltInLocal(b.id) ? 1 : 0)) + } + + /** + * A single configured strategy, or null if there is no such strategy + */ + async getStrategyById(id: string): Promise { + return (await this.getActiveStrategies()).find((stg) => stg.id === id) ?? null + } + + /** + * Merge incoming config values onto the ones already stored, keeping only what the module declares. + * + * Read-only props are never taken from the client: they are declarations of something the server + * does not support changing, so the stored value (or the module default) always wins. + */ + buildConfig( + moduleKey: string, + incoming: Record = {}, + existing: Record = {} + ): Record { + const props = this.getModule(moduleKey)?.props ?? {} + const config: Record = {} + for (const [key, prop] of Object.entries(props)) { + const current = existing[key] !== undefined ? existing[key] : prop.default + config[key] = prop.readOnly || incoming[key] === undefined ? current : incoming[key] + } + return config + } + + /** + * Check incoming config values against what the module declares. + * + * The props are a runtime declaration read from a YAML file, so no JSON Schema can cover them — + * without this, a boolean prop would happily store the string `"maybe"`. + * + * @returns The reason it is invalid, or null when it is fine + */ + validateConfig(moduleKey: string, incoming: Record = {}): string | null { + const props = this.getModule(moduleKey)?.props ?? {} + for (const [key, value] of Object.entries(incoming)) { + const prop = props[key] + // -> Unknown keys are dropped by buildConfig rather than refused: a module losing a prop must + // not make the admin area unable to save + if (!prop || prop.readOnly || value === undefined) { + continue + } + if (prop.enum) { + // -> Enum entries are declared as `value` or `value|label` + const allowed = prop.enum.map((entry) => entry.split('|')[0]) + if (!allowed.includes(`${value}`)) { + return `"${value}" is not a valid value for ${prop.title}.` + } + continue + } + switch (prop.type) { + case 'boolean': + if (typeof value !== 'boolean') { + return `${prop.title} must be true or false.` + } + break + case 'number': + if (typeof value !== 'number' || !Number.isFinite(value)) { + return `${prop.title} must be a number.` + } + break + default: + if (typeof value !== 'string') { + return `${prop.title} must be a string.` + } + } + } + return null + } + + /** + * Check the fields shared by every strategy, whichever module it uses. + * + * @param strategy The values as they will end up stored, i.e. already merged with the current ones + * @returns The reason it is invalid, or null when it is fine + */ + async validateStrategy(strategy: { + /** Omitted when the strategy does not exist yet, i.e. on create. */ + id?: string + module: string + displayName?: string + isEnabled?: boolean + allowedEmailRegex?: string + autoEnrollGroups?: string[] + }): Promise { + if (strategy.displayName !== undefined && strategy.displayName.trim().length < 1) { + return 'The display name cannot be empty.' + } + if (strategy.id && isBuiltInLocal(strategy.id) && strategy.isEnabled === false) { + return 'The built-in local strategy cannot be disabled, as it would leave no way to log in.' + } + if (strategy.allowedEmailRegex) { + try { + new RegExp(strategy.allowedEmailRegex) + } catch (err: any) { + return `The allowed email pattern is not a valid regular expression: ${err.message}` + } + } + if (strategy.autoEnrollGroups && strategy.autoEnrollGroups.length > 0) { + if (strategy.autoEnrollGroups.includes(WIKI.data.systemIds.guestsGroupId)) { + return 'The guests group cannot be used for auto-enrollment.' + } + const existing = await WIKI.db.select({ id: groupsTable.id }).from(groupsTable) + const existingIds = existing.map((g) => g.id) + const unknown = strategy.autoEnrollGroups.find((id) => !existingIds.includes(id)) + if (unknown) { + return `Group ${unknown} does not exist.` + } + } + return null + } + + /** + * Configure a new instance of a module + * + * @returns The new strategy's ID + */ + async createStrategy(values: { + module: string + displayName?: string + isEnabled?: boolean + registration?: boolean + allowedEmailRegex?: string + autoEnrollGroups?: string[] + config?: Record + }): Promise { + const mod = this.getModule(values.module)! + const result = await WIKI.db + .insert(authenticationTable) + .values({ + module: values.module, + displayName: values.displayName?.trim() || mod.title, + isEnabled: values.isEnabled ?? true, + registration: values.registration ?? false, + allowedEmailRegex: values.allowedEmailRegex ?? '', + autoEnrollGroups: values.autoEnrollGroups ?? [], + config: this.buildConfig(values.module, values.config) + }) + .returning({ id: authenticationTable.id }) + + await this.activateStrategies() + return result[0].id + } + + /** + * Update a configured strategy. + * + * The strategies are reloaded afterwards, so a config change takes effect on the next login rather + * than on the next restart. + * + * @returns Whether a strategy was updated + */ + async updateStrategy( + id: string, + patch: { + displayName?: string + isEnabled?: boolean + registration?: boolean + allowedEmailRegex?: string + autoEnrollGroups?: string[] + config?: Record + } + ): Promise { + const current = await this.getStrategyById(id) + if (!current) { + return false + } + + const values: Partial = {} + if (patch.displayName !== undefined) { + values.displayName = patch.displayName.trim() + } + if (patch.isEnabled !== undefined) { + values.isEnabled = patch.isEnabled + } + if (patch.registration !== undefined) { + values.registration = patch.registration + } + if (patch.allowedEmailRegex !== undefined) { + values.allowedEmailRegex = patch.allowedEmailRegex + } + if (patch.autoEnrollGroups !== undefined) { + values.autoEnrollGroups = patch.autoEnrollGroups + } + if (patch.config !== undefined) { + values.config = this.buildConfig(current.module, patch.config, current.config) + } + if (Object.keys(values).length < 1) { + return false + } + + const result = await WIKI.db + .update(authenticationTable) + .set(values) + .where(eq(authenticationTable.id, id)) + if ((result.rowCount ?? 0) < 1) { + return false + } + + await this.activateStrategies() + return true + } + + /** + * Delete a configured strategy, and stop every site from offering it. + * + * Users whose only credentials belong to this strategy lose their way in, which is why the built-in + * local strategy — the one every account is seeded against — cannot be deleted. + * + * @returns Whether a strategy was deleted + */ + async deleteStrategy(id: string): Promise { + const result = await WIKI.db.delete(authenticationTable).where(eq(authenticationTable.id, id)) + if ((result.rowCount ?? 0) < 1) { + return false + } + + // -> Sites keep their own ordered list of strategy IDs, which would otherwise keep a dangling one + for (const site of await WIKI.models.sites.getAllSites()) { + const configured = ((site.config as Record)?.authStrategies ?? []) as Array<{ + id: string + }> + if (configured.some((s) => s.id === id)) { + await WIKI.models.sites.updateSite(site.id, { + config: { authStrategies: configured.filter((s) => s.id !== id) } + }) + } + } + + await this.activateStrategies() + return true + } + async refreshStrategiesFromDisk(): Promise { try { // -> Fetch definitions from disk diff --git a/backend/models/extensions.ts b/backend/models/extensions.ts new file mode 100644 index 000000000..88b029517 --- /dev/null +++ b/backend/models/extensions.ts @@ -0,0 +1,197 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import yaml from 'js-yaml' + +/** How an extension's presence on this system is detected. */ +export interface ExtensionDetection { + /** `command` looks for an executable on PATH, `module` for a resolvable npm package. */ + type: 'command' | 'module' + value: string +} + +/** An extension as declared by its `definition.yml`. */ +export interface ExtensionDefinition { + key: string + title: string + description: string + website?: string + detect: ExtensionDetection + /** Architectures the extension can run on. Any architecture when absent. */ + architectures?: string[] + /** Platforms the extension can run on. Any platform when absent. */ + platforms?: string[] + /** Whether the admin area can install it, as opposed to it being installed by hand. */ + isInstallable: boolean +} + +/** An extension plus its state on this system, as exposed by the API. */ +export interface ExtensionState { + key: string + title: string + description: string + website: string + isInstalled: boolean + isInstallable: boolean + isCompatible: boolean +} + +/** + * Whether an executable of this name exists on PATH. + * + * Walks PATH rather than shelling out to `which` / `where`, which is both faster and free of any + * quoting concerns around the name being looked up. + */ +async function commandExists(command: string): Promise { + const dirs = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean) + // -> On Windows the name on disk carries an extension, e.g. `git.exe` + const suffixes = + process.platform === 'win32' + ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) + : [''] + + for (const dir of dirs) { + for (const suffix of suffixes) { + try { + await fs.access(path.join(dir, `${command}${suffix}`), fs.constants.X_OK) + return true + } catch { + // -> Not in this directory, or not executable by us; keep looking + } + } + } + return false +} + +/** + * Whether an npm package is installed in the backend's `node_modules`. + * + * Not `import()`: optional dependencies like Sharp load native binaries, which is expensive and can + * fail for reasons that have nothing to do with the package being there. Not `import.meta.resolve` + * either — it caches package.json lookups, so a package removed after being resolved once keeps + * reporting as present until the server restarts, which is the misleading direction here. Reading the + * manifest is cheap and always current. + */ +async function moduleExists(specifier: string): Promise { + try { + await fs.access(path.join(WIKI.SERVERPATH, 'node_modules', specifier, 'package.json')) + return true + } catch { + return false + } +} + +/** + * Extensions model + * + * Optional third-party tooling that unlocks extra functionality — a Git binary, Pandoc, Sharp, + * Puppeteer. Each lives in `modules/extensions//definition.yml`, which declares how to detect + * it and what it is compatible with. Nothing here installs anything: these are installed with the + * system package manager or as optional dependencies, which is what the admin area links out to. + */ +class Extensions { + /** Definitions read from disk, refreshed by `refreshFromDisk()`. */ + definitions: ExtensionDefinition[] = [] + + /** + * Load the extension definitions from disk. + */ + async refreshFromDisk(): Promise { + const extensionsPath = path.join(WIKI.SERVERPATH, 'modules/extensions') + const definitions: ExtensionDefinition[] = [] + try { + for (const dir of await fs.readdir(extensionsPath)) { + const raw = await fs.readFile(path.join(extensionsPath, dir, 'definition.yml'), 'utf8') + const parsed = yaml.load(raw) as ExtensionDefinition + // -> The directory name is the key, as it is for every other module type + parsed.key = dir + definitions.push(parsed) + } + this.definitions = definitions.sort((a, b) => a.title.localeCompare(b.title)) + WIKI.logger.info(`Found ${this.definitions.length} extensions [ OK ]`) + } catch (err: any) { + this.definitions = [] + WIKI.logger.warn(`Could not read the extension definitions at ${extensionsPath} [ SKIPPED ]`) + WIKI.logger.warn(err.message) + } + } + + /** + * Whether this system can run the extension at all, regardless of whether it is installed + */ + isCompatible(definition: ExtensionDefinition): boolean { + if (definition.architectures && !definition.architectures.includes(os.arch())) { + return false + } + if (definition.platforms && !definition.platforms.includes(process.platform)) { + return false + } + return true + } + + /** + * Whether the extension is present on this system + */ + async isInstalled(definition: ExtensionDefinition): Promise { + switch (definition.detect?.type) { + case 'command': + return commandExists(definition.detect.value) + case 'module': + return moduleExists(definition.detect.value) + default: + WIKI.logger.warn(`Extension ${definition.key} has no usable detection method.`) + return false + } + } + + /** + * Every extension with its current state. + * + * Detection runs on each call rather than being cached at boot, so that installing a tool and + * hitting refresh in the admin area reflects reality without restarting the server. + */ + async getExtensions(): Promise { + const results: ExtensionState[] = [] + for (const definition of this.definitions) { + const isCompatible = this.isCompatible(definition) + results.push({ + key: definition.key, + title: definition.title, + description: definition.description, + website: definition.website ?? '', + // -> An incompatible extension cannot be present, and skipping the check keeps a pointless + // PATH walk out of the way + isInstalled: isCompatible ? await this.isInstalled(definition) : false, + isInstallable: definition.isInstallable === true, + isCompatible + }) + } + return results + } + + /** + * A single definition, or null if there is no extension with this key + */ + getDefinition(key: string): ExtensionDefinition | null { + return this.definitions.find((d) => d.key === key) ?? null + } + + /** + * Log which extensions were found, the way the other module types report at boot + */ + async logState(): Promise { + for (const extension of await this.getExtensions()) { + if (!extension.isCompatible) { + WIKI.logger.info( + `Extension ${extension.key} is not compatible with this system. [ SKIPPED ]` + ) + } else if (extension.isInstalled) { + WIKI.logger.info(`Extension ${extension.key} is installed. [ OK ]`) + } else { + WIKI.logger.info(`Extension ${extension.key} was not found on this system. [ SKIPPED ]`) + } + } + } +} + +export const extensions = new Extensions() diff --git a/backend/models/flags.ts b/backend/models/flags.ts new file mode 100644 index 000000000..c8781d1f7 --- /dev/null +++ b/backend/models/flags.ts @@ -0,0 +1,96 @@ +/** + * The system flags, and what enabling each one actually does. + * + * Flags are read live: nothing here needs a restart, and every one of them has an effect somewhere in + * the running server. Anything added to this list needs a consumer, otherwise the admin area offers a + * switch that changes nothing. + */ +export const FLAGS = { + /** Consumed by the frontend, which reveals unfinished features when it is on. */ + experimental: 'Unfinished features are offered in the interface.', + /** Consumed by `models/users.ts` and `api/authentication.ts` via `authDebug()` below. */ + authDebug: 'Login and account creation attempts are logged in detail.', + /** Consumed by the query logger in `core/db.ts`. */ + sqlLog: 'Every database query is logged.' +} as const + +export type Flag = keyof typeof FLAGS + +export const FLAG_KEYS = Object.keys(FLAGS) as Flag[] + +/** + * Flags model + * + * Low-level switches for debugging and for unfinished features, stored in the `flags` settings blob. + * They are readable without authentication — the frontend needs `experimental` before anyone has + * logged in — so a flag must never carry anything sensitive. + */ +class Flags { + /** + * Every flag, with anything missing from the stored blob reported as off + */ + getFlags(): Record { + const flags = WIKI.config.flags ?? {} + return Object.fromEntries(FLAG_KEYS.map((key) => [key, flags[key] === true])) as Record< + Flag, + boolean + > + } + + /** + * Whether a single flag is on. + * + * Reads the config directly on every call, so flipping a flag takes effect immediately — including + * on the other instances of a cluster, which reload their config when this one saves. + */ + isEnabled(flag: Flag): boolean { + return WIKI.config.flags?.[flag] === true + } + + /** + * Keep only the flags this model owns, dropping anything else a client sends + */ + pickFlags(body: Record): Partial> { + const patch: Partial> = {} + for (const key of FLAG_KEYS) { + if (body[key] !== undefined) { + patch[key] = body[key] === true + } + } + return patch + } + + /** + * Save a patch of flags, leaving the ones it does not mention alone + * + * @returns Whether the flags were saved + */ + async updateFlags(patch: Partial>): Promise { + const previous = WIKI.config.flags + WIKI.config.flags = { ...previous, ...patch } + + if (!(await WIKI.configSvc.saveToDb(['flags']))) { + WIKI.config.flags = previous + return false + } + + for (const [key, value] of Object.entries(patch)) { + WIKI.logger.info(`System flag ${key} is now ${value ? 'enabled' : 'disabled'}.`) + } + return true + } + + /** + * Log an authentication detail, but only while the auth debug flag is on. + * + * At info level rather than debug, because the default log level is info: sending these to debug + * would mean turning the flag on and seeing nothing. + */ + authDebug(message: string): void { + if (this.isEnabled('authDebug')) { + WIKI.logger.info(`[AUTH] ${message}`) + } + } +} + +export const flags = new Flags() diff --git a/backend/models/hooks.ts b/backend/models/hooks.ts new file mode 100644 index 000000000..98f3090cd --- /dev/null +++ b/backend/models/hooks.ts @@ -0,0 +1,307 @@ +import http from 'node:http' +import https from 'node:https' +import { hooks as hooksTable } from '../db/schema.ts' +import { desc, eq, sql } from 'drizzle-orm' + +/** + * The events a webhook can subscribe to, as offered by the admin area. + * + * Only the user events have emit points today — pages, assets and comments are not implemented yet, + * so subscribing to them stores a subscription that nothing triggers. + */ +export const HOOK_EVENTS = [ + 'page:create', + 'page:edit', + 'page:rename', + 'page:delete', + 'asset:upload', + 'asset:edit', + 'asset:rename', + 'asset:delete', + 'comment:new', + 'comment:edit', + 'comment:delete', + 'user:join', + 'user:login', + 'user:logout' +] as const + +export type HookEvent = (typeof HOOK_EVENTS)[number] + +/** + * The events something in the server actually emits today. + * + * Kept as an explicit list rather than inferred from the prefix: `user:logout` looks like it belongs + * here, but there is no logout route yet. Add an event here when you add its `emit()` call. + */ +export const EMITTED_EVENTS: HookEvent[] = ['user:join', 'user:login'] + +/** A webhook as exposed by the API. */ +export interface Hook { + id: string + name: string + events: string[] + url: string + includeMetadata: boolean + includeContent: boolean + acceptUntrusted: boolean + authHeader: string | null + state: 'pending' | 'success' | 'error' + lastErrorMessage: string | null + createdAt: Date + updatedAt: Date +} + +/** How long a remote endpoint has to answer before the delivery counts as failed. */ +const DELIVERY_TIMEOUT = 15000 + +const hookSelection = { + id: hooksTable.id, + name: hooksTable.name, + events: hooksTable.events, + url: hooksTable.url, + includeMetadata: hooksTable.includeMetadata, + includeContent: hooksTable.includeContent, + acceptUntrusted: hooksTable.acceptUntrusted, + authHeader: hooksTable.authHeader, + state: hooksTable.state, + lastErrorMessage: hooksTable.lastErrorMessage, + createdAt: hooksTable.createdAt, + updatedAt: hooksTable.updatedAt +} + +/** + * POST a JSON body, with control over certificate validation. + * + * `node:https` rather than `fetch`: a webhook may legitimately point at an endpoint with a + * self-signed certificate, and per-request TLS options are not expressible through fetch. + */ +function postJson( + url: string, + body: string, + { authHeader, acceptUntrusted }: { authHeader?: string | null; acceptUntrusted: boolean } +): Promise<{ statusCode: number }> { + return new Promise((resolve, reject) => { + let target: URL + try { + target = new URL(url) + } catch { + reject(new Error(`"${url}" is not a valid URL.`)) + return + } + const transport = target.protocol === 'http:' ? http : https + + const req = transport.request( + target, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(body), + 'user-agent': `Wiki.js/${WIKI.version}`, + ...(authHeader ? { authorization: authHeader } : {}) + }, + timeout: DELIVERY_TIMEOUT, + ...(target.protocol === 'https:' && acceptUntrusted ? { rejectUnauthorized: false } : {}) + }, + (res) => { + // -> The body is irrelevant, but it has to be drained for the socket to be released + res.resume() + res.on('end', () => resolve({ statusCode: res.statusCode ?? 0 })) + } + ) + req.on('timeout', () => { + req.destroy(new Error(`The endpoint did not respond within ${DELIVERY_TIMEOUT / 1000}s.`)) + }) + req.on('error', reject) + req.end(body) + }) +} + +/** + * Hooks model + * + * Webhooks POST a JSON body to a remote endpoint when something happens. Delivery goes through the + * scheduler rather than the request that triggered it: a slow or broken endpoint must not delay a + * user's action, and the scheduler already provides retries and a place to see failures. + */ +class Hooks { + /** + * Every webhook, newest first + */ + async getHooks(): Promise { + const results = await WIKI.db + .select(hookSelection) + .from(hooksTable) + .orderBy(desc(hooksTable.createdAt)) + return results as Hook[] + } + + /** + * A single webhook, or null if there is no such webhook + */ + async getHookById(id: string): Promise { + const results = await WIKI.db + .select(hookSelection) + .from(hooksTable) + .where(eq(hooksTable.id, id)) + .limit(1) + return (results[0] as Hook) ?? null + } + + /** + * Create a webhook. It starts out pending: no event has reached it yet. + * + * @returns The new webhook's ID + */ + async createHook(values: { + name: string + events: string[] + url: string + includeMetadata?: boolean + includeContent?: boolean + acceptUntrusted?: boolean + authHeader?: string + }): Promise { + const result = await WIKI.db + .insert(hooksTable) + .values({ + name: values.name, + events: values.events, + url: values.url, + includeMetadata: values.includeMetadata ?? true, + includeContent: values.includeContent ?? false, + acceptUntrusted: values.acceptUntrusted ?? false, + authHeader: values.authHeader ?? null, + state: 'pending' + }) + .returning({ id: hooksTable.id }) + return result[0].id + } + + /** + * Update a webhook. + * + * Changing where or what it sends resets the state to pending: the previous outcome says nothing + * about the new configuration. + * + * @returns Whether a webhook was updated + */ + async updateHook(id: string, patch: Record): Promise { + const values: Record = { ...patch, updatedAt: sql`now()` } + if (patch.url !== undefined || patch.events !== undefined || patch.authHeader !== undefined) { + values.state = 'pending' + values.lastErrorMessage = null + } + const result = await WIKI.db.update(hooksTable).set(values).where(eq(hooksTable.id, id)) + return (result.rowCount ?? 0) > 0 + } + + /** + * Delete a webhook + * + * @returns Whether a webhook was deleted + */ + async deleteHook(id: string): Promise { + const result = await WIKI.db.delete(hooksTable).where(eq(hooksTable.id, id)) + return (result.rowCount ?? 0) > 0 + } + + /** + * Queue a delivery for every webhook subscribed to an event. + * + * Safe to call from anywhere, including request handlers: it only writes jobs, and it never throws + * — a webhook problem must not fail the action that triggered it. + * + * @param data Event-specific payload. `metadata` and `content` are stripped per webhook, according + * to what each one asked for. + * @returns How many deliveries were queued + */ + async emit(event: HookEvent, data: Record = {}): Promise { + try { + const subscribed = await WIKI.db + .select({ + id: hooksTable.id, + includeMetadata: hooksTable.includeMetadata, + includeContent: hooksTable.includeContent + }) + .from(hooksTable) + .where(sql`${event} = ANY(${hooksTable.events})`) + + let queued = 0 + for (const hook of subscribed) { + const { metadata, content, ...rest } = data + const payload = { + ...rest, + ...(hook.includeMetadata && metadata !== undefined ? { metadata } : {}), + ...(hook.includeContent && content !== undefined ? { content } : {}) + } + const added = await WIKI.scheduler.addJob({ + task: 'dispatchWebhook', + payload: { hookId: hook.id, event, data: payload } + }) + if (added?.id) { + queued++ + } + } + return queued + } catch (err: any) { + WIKI.logger.warn(`Failed to queue webhook deliveries for ${event}: ${err.message}`) + return 0 + } + } + + /** + * Deliver one event to one webhook, recording the outcome on the webhook. + * + * Called by the `dispatchWebhook` task. Throws on failure so that the scheduler retries it. + */ + async deliver({ + hookId, + event, + data + }: { + hookId: string + event: string + data: Record + }): Promise { + const hook = await this.getHookById(hookId) + if (!hook) { + // -> Deleted between queueing and delivery; nothing to do and nothing to retry + WIKI.logger.info(`Webhook ${hookId} no longer exists, skipping delivery of ${event}.`) + return + } + + const body = JSON.stringify({ + event, + sentAt: Temporal.Now.instant().toString({ smallestUnit: 'millisecond' }), + instance: WIKI.INSTANCE_ID, + data + }) + + try { + const { statusCode } = await postJson(hook.url, body, { + authHeader: hook.authHeader, + acceptUntrusted: hook.acceptUntrusted + }) + if (statusCode < 200 || statusCode > 299) { + throw new Error(`The endpoint answered with HTTP ${statusCode}.`) + } + await WIKI.db + .update(hooksTable) + .set({ state: 'success', lastErrorMessage: null }) + .where(eq(hooksTable.id, hook.id)) + WIKI.logger.debug(`Delivered ${event} to webhook ${hook.name} [ OK ]`) + } catch (err: any) { + await WIKI.db + .update(hooksTable) + .set({ state: 'error', lastErrorMessage: err.message }) + .where(eq(hooksTable.id, hook.id)) + WIKI.logger.warn(`Failed to deliver ${event} to webhook ${hook.name}: ${err.message}`) + // -> Rethrown so the job fails and the scheduler retries with its usual backoff + throw err + } + } +} + +export const hooks = new Hooks() diff --git a/backend/models/index.ts b/backend/models/index.ts index 05e907bb2..cda3ab1f4 100644 --- a/backend/models/index.ts +++ b/backend/models/index.ts @@ -1,19 +1,31 @@ +import { apiKeys } from './apiKeys.ts' import { authentication } from './authentication.ts' import { blocks } from './blocks.ts' +import { extensions } from './extensions.ts' +import { flags } from './flags.ts' import { groups } from './groups.ts' +import { hooks } from './hooks.ts' import { jobs } from './jobs.ts' import { locales } from './locales.ts' +import { search } from './search.ts' +import { security } from './security.ts' import { sessions } from './sessions.ts' import { settings } from './settings.ts' import { sites } from './sites.ts' import { users } from './users.ts' export default { + apiKeys, authentication, blocks, + extensions, + flags, groups, + hooks, jobs, locales, + search, + security, sessions, settings, sites, diff --git a/backend/models/search.ts b/backend/models/search.ts new file mode 100644 index 000000000..88826bfee --- /dev/null +++ b/backend/models/search.ts @@ -0,0 +1,152 @@ +import { sql } from 'drizzle-orm' + +/** + * Locale to PostgreSQL text search dictionary, for the languages postgres ships a snowball stemmer + * for. Anything not listed here falls back to `simple`, which indexes words without stemming — still + * searchable, just without matching plurals and conjugations. + * + * An operator can override or extend this from the admin area, which is what `dictOverrides` is for. + */ +export const DEFAULT_DICTIONARIES: Record = { + ar: 'arabic', + ca: 'catalan', + da: 'danish', + de: 'german', + el: 'greek', + en: 'english', + es: 'spanish', + et: 'estonian', + eu: 'basque', + fi: 'finnish', + fr: 'french', + ga: 'irish', + hi: 'hindi', + hu: 'hungarian', + hy: 'armenian', + id: 'indonesian', + it: 'italian', + lt: 'lithuanian', + ne: 'nepali', + nl: 'dutch', + no: 'norwegian', + pt: 'portuguese', + ro: 'romanian', + ru: 'russian', + sr: 'serbian', + sv: 'swedish', + ta: 'tamil', + tr: 'turkish', + yi: 'yiddish' +} + +/** The dictionary used when a locale has no mapping, or when its mapping is not installed. */ +export const FALLBACK_DICTIONARY = 'simple' + +export interface SearchConfig { + termHighlighting: boolean + dictOverrides: Record +} + +/** What a rebuild did, per locale, so the caller can report something concrete. */ +export interface RebuildResult { + pages: number + locales: { locale: string; dictionary: string; pages: number }[] +} + +/** + * Search model + * + * Search is postgres full-text: every page carries a `ts` tsvector, indexed with GIN. Which + * dictionary builds that vector depends on the page's locale, which is why the mapping is + * configurable — using the wrong stemmer for a language quietly degrades results rather than + * failing. + */ +class Search { + /** + * The search configuration, with the shape the API and the admin area expect + */ + getConfig(): SearchConfig { + return { + termHighlighting: WIKI.config.search?.termHighlighting === true, + dictOverrides: (WIKI.config.search?.dictOverrides ?? {}) as Record + } + } + + /** + * The text search configurations this postgres actually has, e.g. `english`, `simple`. + * + * Used to validate what an operator maps a locale to: a name postgres does not know would make + * every `to_tsvector` call fail at rebuild time, long after the setting was saved. + */ + async getAvailableDictionaries(): Promise { + const rows = await WIKI.db.execute(sql`SELECT cfgname FROM pg_ts_config ORDER BY cfgname`) + return (rows.rows ?? rows).map((r: any) => r.cfgname as string) + } + + /** + * The dictionary to index a locale with, preferring the operator's override + * + * @param available Dictionary names postgres knows; an unknown mapping degrades to the fallback + */ + dictionaryForLocale(locale: string, available: string[]): string { + const { dictOverrides } = this.getConfig() + // -> Locales can be regional (`en-US`), while dictionaries are per language + const language = locale.split(/[-_]/)[0] ?? locale + const wanted = + dictOverrides[locale] ?? dictOverrides[language] ?? DEFAULT_DICTIONARIES[language] + if (wanted && available.includes(wanted)) { + return wanted + } + if (wanted) { + WIKI.logger.warn( + `Text search dictionary "${wanted}" for locale ${locale} is not installed — falling back to ${FALLBACK_DICTIONARY}.` + ) + } + return FALLBACK_DICTIONARY + } + + /** + * Recompute the search vector of every page. + * + * Grouped by locale, since the dictionary is chosen per locale. Title and description are weighted + * above the body so that a page whose title matches outranks one that merely mentions the term. + * + * Runs over every page rather than only searchable ones: whether a page shows up in results is + * decided at query time by `isSearchableComputed`, and keeping the vector current means flipping a + * page back to searchable needs no reindex. + */ + async rebuildIndex(): Promise { + const available = await this.getAvailableDictionaries() + const localeRows = await WIKI.db.execute( + sql`SELECT DISTINCT locale::text AS locale FROM pages ORDER BY locale` + ) + const locales = ((localeRows.rows ?? localeRows) as any[]).map((r) => r.locale as string) + + WIKI.logger.info(`Rebuilding the search index for ${locales.length} locale(s)...`) + const result: RebuildResult = { pages: 0, locales: [] } + + for (const locale of locales) { + const dictionary = this.dictionaryForLocale(locale, available) + // -> The dictionary name is an identifier in `to_tsvector`, and it is only ever one of the + // names postgres itself reported, so it cannot carry anything unexpected + const updated = await WIKI.db.execute(sql` + UPDATE pages SET ts = + setweight(to_tsvector(${sql.raw(`'${dictionary}'`)}, coalesce(title, '')), 'A') || + setweight(to_tsvector(${sql.raw(`'${dictionary}'`)}, coalesce(description, '')), 'B') || + setweight(to_tsvector(${sql.raw(`'${dictionary}'`)}, coalesce("searchContent", '')), 'C') + WHERE locale::text = ${locale} + `) + const pages = updated.rowCount ?? 0 + result.pages += pages + result.locales.push({ locale, dictionary, pages }) + WIKI.logger.info( + `Reindexed ${pages} page(s) in ${locale} using the ${dictionary} dictionary.` + ) + } + + WIKI.logger.info(`Search index rebuild completed: ${result.pages} page(s) [ OK ]`) + return result + } +} + +export const search = new Search() diff --git a/backend/models/security.ts b/backend/models/security.ts new file mode 100644 index 000000000..d8d6fa8b7 --- /dev/null +++ b/backend/models/security.ts @@ -0,0 +1,172 @@ +import { CORS_MODES, parseCspDirectives } from '../helpers/security.ts' + +/** Fields stored in the `security` settings blob. */ +export const SECURITY_FIELDS = [ + 'corsConfig', + 'corsMode', + 'cspDirectives', + 'disallowFloc', + 'disallowIframe', + 'disallowOpenRedirect', + 'enforceCsp', + 'enforceHsts', + 'enforceSameOriginReferrerPolicy', + 'forceAssetDownload', + 'hstsDuration', + 'trustProxy', + 'uploadMaxFileSize', + 'uploadMaxFiles', + 'uploadScanSVG' +] as const + +/** + * The JWT fields the admin area shows, mapped onto the `auth` settings they really live in. + * + * The `security` blob used to carry copies of these under the 2.x names, which nothing read — so the + * view was editing values with no effect. These are the keys the running server uses. + */ +export const AUTH_FIELD_MAP = { + authJwtAudience: 'audience', + authJwtExpiration: 'tokenExpiration', + authJwtRenewablePeriod: 'tokenRenewal' +} as const + +/** A duration as the admin area writes it: `30m`, `14d`, `1y`. */ +const DURATION_PATTERN = /^\d+[smhdwy]$/ + +/** + * Security model + * + * One flat surface for the admin area's security view, even though the values are stored in two + * settings blobs. Most of them are read when the HTTP server starts — see the `Security` section of + * `index.ts` — so saving them here takes effect on the next restart. + */ +class Security { + /** + * The security configuration as the admin area expects it + */ + getConfig(): Record { + const security = WIKI.config.security ?? {} + const config: Record = {} + for (const field of SECURITY_FIELDS) { + config[field] = security[field] + } + for (const [field, authKey] of Object.entries(AUTH_FIELD_MAP)) { + config[field] = WIKI.config.auth?.[authKey] + } + return config + } + + /** + * Keep only the fields this model owns, dropping anything else a client sends + */ + pickFields(body: Record): Record { + const patch: Record = {} + for (const field of [...SECURITY_FIELDS, ...Object.keys(AUTH_FIELD_MAP)]) { + if (body[field] !== undefined) { + patch[field] = body[field] + } + } + return patch + } + + /** + * Check a patch against the settings it will end up merged with. + * + * Merged rather than in isolation, because these fields constrain each other: turning CSP on with + * no directives, or picking the hostname whitelist mode without hostnames, would store a setting + * that quietly does nothing. + * + * @returns The reason it is invalid, or null when it is fine + */ + validate(patch: Record): string | null { + const merged = { ...this.getConfig(), ...patch } + + if (!CORS_MODES.includes(merged.corsMode)) { + return `"${merged.corsMode}" is not a valid CORS mode.` + } + if (merged.corsMode === 'REGEX') { + try { + new RegExp(merged.corsConfig ?? '') + } catch (err: any) { + return `The CORS regex pattern is invalid: ${err.message}` + } + } + if (merged.corsMode === 'HOSTNAMES') { + const hostnames = (merged.corsConfig ?? '') + .split(/[\n,]/) + .map((entry: string) => entry.trim()) + .filter(Boolean) + if (hostnames.length < 1) { + return 'The hostname whitelist mode needs at least one hostname.' + } + } + + if (merged.enforceCsp) { + if (Object.keys(parseCspDirectives(merged.cspDirectives ?? '')).length < 1) { + return 'Enforcing a Content-Security-Policy needs at least one directive.' + } + } + + if (merged.enforceHsts && !(merged.hstsDuration > 0)) { + return 'Enforcing HSTS needs a duration greater than zero.' + } + + for (const [field, label] of [ + ['authJwtExpiration', 'token expiration'], + ['authJwtRenewablePeriod', 'token renewal period'] + ] as const) { + if (!DURATION_PATTERN.test(merged[field] ?? '')) { + return `The ${label} must be a duration such as 30m, 12h or 14d.` + } + } + if (!merged.authJwtAudience || `${merged.authJwtAudience}`.trim().length < 1) { + return 'The JWT audience cannot be empty.' + } + + return null + } + + /** + * Save a validated patch, splitting it across the two settings blobs it belongs to. + * + * Both are written in one go and rolled back together, so a failure cannot leave the JWT settings + * updated while the rest is not. + * + * @returns Whether the settings were saved + */ + async updateConfig(patch: Record): Promise { + const previousSecurity = WIKI.config.security + const previousAuth = WIKI.config.auth + const keys: string[] = [] + + const securityPatch: Record = {} + const authPatch: Record = {} + for (const [field, value] of Object.entries(patch)) { + const authKey = AUTH_FIELD_MAP[field as keyof typeof AUTH_FIELD_MAP] + if (authKey) { + authPatch[authKey] = typeof value === 'string' ? value.trim() : value + } else { + securityPatch[field] = value + } + } + + if (Object.keys(securityPatch).length > 0) { + WIKI.config.security = { ...previousSecurity, ...securityPatch } + keys.push('security') + } + if (Object.keys(authPatch).length > 0) { + WIKI.config.auth = { ...previousAuth, ...authPatch } + keys.push('auth') + } + + if (!(await WIKI.configSvc.saveToDb(keys))) { + WIKI.config.security = previousSecurity + WIKI.config.auth = previousAuth + return false + } + return true + } +} + +export const security = new Security() diff --git a/backend/models/settings.ts b/backend/models/settings.ts index 53528b658..0a45eebe2 100644 --- a/backend/models/settings.ts +++ b/backend/models/settings.ts @@ -152,9 +152,10 @@ class Settings { forceAssetDownload: true, hstsDuration: 0, trustProxy: false, - authJwtAudience: 'urn:wiki.js', - authJwtExpiration: '30m', - authJwtRenewablePeriod: '14d', + // NOTE: the JWT audience, expiration and renewal period are deliberately absent here. + // They used to be duplicated under 2.x names (`authJwt*`) that nothing read, so the + // admin area edited values with no effect. They live in the `auth` settings above, + // which is what the server uses; the security view maps onto those. uploadMaxFileSize: 10485760, uploadMaxFiles: 20, uploadScanSVG: true diff --git a/backend/models/users.ts b/backend/models/users.ts index 05de9718b..03c679a86 100644 --- a/backend/models/users.ts +++ b/backend/models/users.ts @@ -289,6 +289,19 @@ class Users { if (groups.length > 0) { await this.setUserGroups(userId, groups) } + + WIKI.models.flags.authDebug( + `Created user ${userId} <${email.toLowerCase()}> in ${groups.length} group(s), mustChangePwd: ${mustChangePassword}, verified: ${isVerified}` + ) + + await WIKI.models.hooks.emit('user:join', { + userId, + metadata: { + name, + email: email.toLowerCase() + } + }) + return userId } @@ -509,8 +522,21 @@ class Users { }) } + // -> Never the password, flag or no flag + WIKI.models.flags.authDebug( + `Login attempt on site ${siteId} using ${str.module} strategy ${strategyId}${username ? ` as "${username}"` : ''} from ${ip}` + ) + // Authenticate - const user = await str.authenticate(context) + let user + try { + user = await str.authenticate(context) + } catch (err: any) { + WIKI.models.flags.authDebug( + `Strategy ${str.module} rejected the attempt${username ? ` for "${username}"` : ''}: ${err.message}` + ) + throw err + } // Perform post-login checks return this.afterLoginChecks( @@ -524,6 +550,7 @@ class Users { req ) } else { + WIKI.models.flags.authDebug(`Login attempt using unknown strategy ${strategyId} from ${ip}`) throw new Error('Invalid Strategy ID') } } @@ -590,6 +617,9 @@ class Users { strategyId } }) + WIKI.models.flags.authDebug( + `User ${user.id} <${user.email}> authenticated, but a 2FA code is required first` + ) return { nextAction: 'provideTfa', continuationToken: tfaToken, @@ -612,6 +642,9 @@ class Users { strategyId } }) + WIKI.models.flags.authDebug( + `User ${user.id} <${user.email}> authenticated, but must set up 2FA first` + ) return { nextAction: 'setupTfa', continuationToken: tfaToken, @@ -636,6 +669,9 @@ class Users { } }) + WIKI.models.flags.authDebug( + `User ${user.id} <${user.email}> authenticated, but must change their password first` + ) return { nextAction: 'changePassword', continuationToken: pwdChangeToken, @@ -650,6 +686,22 @@ class Users { // Set Session Data this.updateSession(user, req) + WIKI.models.flags.authDebug( + `User ${user.id} <${user.email}> logged in with ${user.groups.length} group(s) and ${req?.session?.permissions?.length ?? 0} permission(s), redirecting to ${redirect}` + ) + + // -> Only once the login has actually succeeded: an attempt stopped by 2FA or a forced password + // change is not a login yet + await WIKI.models.hooks.emit('user:login', { + userId: user.id, + strategyId, + ip: context.ip, + metadata: { + name: user.name, + email: user.email + } + }) + return { authenticated: true, nextAction: 'redirect', diff --git a/backend/modules/authentication/local/definition.yml b/backend/modules/authentication/local/definition.yml index 77478da23..385538873 100644 --- a/backend/modules/authentication/local/definition.yml +++ b/backend/modules/authentication/local/definition.yml @@ -14,9 +14,13 @@ props: enforceTfa: type: Boolean title: Enforce Two-Factor Authentication - hint: Users will be required to setup 2FA the first time they login and cannot be disabled by the user. + # Read-only until 2FA works end to end: `afterLoginChecks` reaches for a `generateTFA()` that does + # not exist, and there is no route to submit a code, so a login that needs 2FA can only fail. + # See the FIXME comments in models/users.ts. + hint: Not available yet — two-factor authentication is not implemented in this version. icon: pin-pad default: false + readOnly: true emailValidation: type: Boolean title: Email Validation diff --git a/backend/modules/extensions/git/definition.yml b/backend/modules/extensions/git/definition.yml new file mode 100644 index 000000000..6636a0ab8 --- /dev/null +++ b/backend/modules/extensions/git/definition.yml @@ -0,0 +1,12 @@ +key: git +title: Git +description: >- + Distributed version control system. Required for the Git storage module to synchronize content with + a remote repository. +website: 'https://git-scm.com' +# Detection: a `git` executable somewhere on PATH +detect: + type: command + value: git +# Installed with the operating system's package manager, not from here +isInstallable: false diff --git a/backend/modules/extensions/pandoc/definition.yml b/backend/modules/extensions/pandoc/definition.yml new file mode 100644 index 000000000..cc40c3a8e --- /dev/null +++ b/backend/modules/extensions/pandoc/definition.yml @@ -0,0 +1,10 @@ +key: pandoc +title: Pandoc +description: >- + Converts between markup formats. Required to import content from other wikis and formats such as + MediaWiki, AsciiDoc, Textile or DocBook. +website: 'https://pandoc.org' +detect: + type: command + value: pandoc +isInstallable: false diff --git a/backend/modules/extensions/puppeteer/definition.yml b/backend/modules/extensions/puppeteer/definition.yml new file mode 100644 index 000000000..49171f64d --- /dev/null +++ b/backend/modules/extensions/puppeteer/definition.yml @@ -0,0 +1,13 @@ +key: puppeteer +title: Puppeteer +description: >- + Headless Chromium browser. Required to export pages as PDF and to render content elements on the + server, such as Mermaid or PlantUML diagrams. +website: 'https://pptr.dev' +detect: + type: module + value: puppeteer +architectures: + - x64 + - arm64 +isInstallable: false diff --git a/backend/modules/extensions/sharp/definition.yml b/backend/modules/extensions/sharp/definition.yml new file mode 100644 index 000000000..1043e510d --- /dev/null +++ b/backend/modules/extensions/sharp/definition.yml @@ -0,0 +1,15 @@ +key: sharp +title: Sharp +description: >- + Processes and transforms images. Required to generate thumbnails of uploaded images and to resize + site assets such as logos. +website: 'https://sharp.pixelplumbing.com' +# Detection: an optional dependency, so resolvable from the backend's node_modules when present +detect: + type: module + value: sharp +# Prebuilt binaries are published for these architectures only +architectures: + - x64 + - arm64 +isInstallable: false diff --git a/backend/tasks/simple/dispatch-webhook.ts b/backend/tasks/simple/dispatch-webhook.ts new file mode 100644 index 000000000..eefd2ef16 --- /dev/null +++ b/backend/tasks/simple/dispatch-webhook.ts @@ -0,0 +1,13 @@ +/** + * Deliver one event to one webhook. + * + * Queued by `models/hooks.ts` → `emit()`, one job per subscribed webhook, so that a slow endpoint + * delays nothing else and a failing one is retried with the scheduler's backoff. + */ +export async function task(payload: { + hookId: string + event: string + data: Record +}): Promise { + await WIKI.models.hooks.deliver(payload) +} diff --git a/backend/tasks/simple/rebuild-search-index.ts b/backend/tasks/simple/rebuild-search-index.ts new file mode 100644 index 000000000..d2eb5aa02 --- /dev/null +++ b/backend/tasks/simple/rebuild-search-index.ts @@ -0,0 +1,9 @@ +/** + * Recompute the search vector of every page. + * + * Queued from the admin area's search view, and safe to run at any time: it only rewrites `pages.ts` + * from the content already stored on each page. + */ +export async function task(): Promise { + await WIKI.models.search.rebuildIndex() +} diff --git a/backend/types/fastify.d.ts b/backend/types/fastify.d.ts index 96b49e895..9529a7585 100644 --- a/backend/types/fastify.d.ts +++ b/backend/types/fastify.d.ts @@ -7,8 +7,17 @@ import 'fastify' import '@fastify/session' +import type { ApiKeyIdentity } from '../models/apiKeys.ts' declare module 'fastify' { + interface FastifyRequest { + /** + * Set by the API key hook in `index.ts` when a request carries a valid bearer key. Null for + * cookie-authenticated and anonymous requests. + */ + apiKey?: ApiKeyIdentity | null + } + interface Session { /** Set by `models/users.ts` → `updateSession()` once a login completes. */ authenticated?: boolean diff --git a/frontend/src/components/ApiKeyCopyDialog.vue b/frontend/src/components/ApiKeyCopyDialog.vue index 3cbb5cbec..4f9db2e9b 100644 --- a/frontend/src/components/ApiKeyCopyDialog.vue +++ b/frontend/src/components/ApiKeyCopyDialog.vue @@ -16,6 +16,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide', persistent) type='textarea' outlined :model-value='props.keyValue' + readonly dense hide-bottom-space :label='t(`admin.api.key`)' @@ -24,6 +25,16 @@ q-dialog(ref='dialogRef', @hide='onDialogHide', persistent) ) q-card-actions.card-actions q-space + //- The dialog is the only place this token ever appears, so copying it must not depend on + //- selecting a wrapped 700-character string by hand + q-btn.acrylic-btn( + flat + icon='las la-copy' + :label='t(`common.actions.copy`)' + color='primary' + padding='xs md' + @click='copyKey' + ) q-btn( unelevated :label='t(`common.actions.close`)' @@ -35,7 +46,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide', persistent) diff --git a/frontend/src/components/ApiKeyCreateDialog.vue b/frontend/src/components/ApiKeyCreateDialog.vue index ccc331bca..565f4923c 100644 --- a/frontend/src/components/ApiKeyCreateDialog.vue +++ b/frontend/src/components/ApiKeyCreateDialog.vue @@ -24,11 +24,12 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') q-item blueprint-icon.self-start(icon='schedule') q-item-section + //- Single-select: a key has one lifetime. It was declared `multiple` against a string + //- model, which showed the default as a stray chip and let several be picked at once. q-select( outlined :options='expirations' v-model='state.keyExpiration' - multiple map-options option-value='value' option-label='text' @@ -105,7 +106,6 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') diff --git a/frontend/src/pages/AdminExtensions.vue b/frontend/src/pages/AdminExtensions.vue index 06d218325..cb31a8b81 100644 --- a/frontend/src/pages/AdminExtensions.vue +++ b/frontend/src/pages/AdminExtensions.vue @@ -39,6 +39,8 @@ q-page.admin-extensions q-item-section q-item-label {{ext.title}} q-item-label(caption) {{ext.description}} + q-item-label(caption, v-if='ext.website') + a.text-primary(:href='ext.website', target='_blank', rel='noopener') {{ ext.website }} q-item-section(side) .row q-btn-group(unelevated) @@ -103,12 +105,10 @@ q-page.admin-extensions diff --git a/frontend/src/pages/AdminSearch.vue b/frontend/src/pages/AdminSearch.vue index 1cab1cad8..b713e6609 100644 --- a/frontend/src/pages/AdminSearch.vue +++ b/frontend/src/pages/AdminSearch.vue @@ -82,13 +82,11 @@ q-page.admin-flags