feat: more models + migrations (wip)

vega
NGPixel 10 hours ago
parent dc25db1139
commit dc4b84dc82
No known key found for this signature in database

@ -33,9 +33,13 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [5432, 8000],
"forwardPorts": [3000, 5432, 8000],
"portsAttributes": {
"3000": {
"label": "Application",
"onAutoForward": "silent"
},
"5432": {
"label": "PostgreSQL",
"onAutoForward": "silent"

@ -2,3 +2,4 @@ audit = false
fund = false
save-exact = true
save-prefix = ""
update-notifier = false

@ -20,24 +20,70 @@ async function routes (app, options) {
return { hello: 'world' }
})
/**
* CREATE SITE
*/
app.post('/', {
config: {
// permissions: ['create:sites', 'manage:sites']
},
schema: {
summary: 'Create a new site',
tags: ['Sites'],
body: {
type: 'object',
required: ['name', 'hostname'],
required: ['hostname', 'title'],
properties: {
name: { type: 'string' },
hostname: { type: 'string' }
hostname: {
type: 'string',
minLength: 1,
maxLength: 255,
pattern: '^(\\*|[a-z0-9.-]+)$'
},
title: {
type: 'string',
minLength: 1,
maxLength: 255
}
},
examples: [
{
hostname: 'wiki.example.org',
title: 'My Wiki Site'
}
]
},
response: {
200: {
description: 'Site created successfully',
type: 'object',
properties: {
message: {
type: 'string'
},
id: {
type: 'string',
format: 'uuid'
}
}
}
}
}
}, async (req, reply) => {
return { hello: 'world' }
const result = await WIKI.models.sites.createSite(req.body.hostname, { title: req.body.title })
return {
message: 'Site created successfully.',
id: result.id
}
})
/**
* UPDATE SITE
*/
app.put('/:siteId', {
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Update a site',
tags: ['Sites']
@ -50,6 +96,9 @@ async function routes (app, options) {
* DELETE SITE
*/
app.delete('/:siteId', {
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Delete a site',
tags: ['Sites'],
@ -71,7 +120,9 @@ async function routes (app, options) {
}
}, async (req, reply) => {
try {
if (await WIKI.models.sites.deleteSite(req.params.siteId)) {
if (await WIKI.models.sites.countSites() <= 1) {
reply.conflict('Cannot delete the last site. At least 1 site must exist at all times.')
} else if (await WIKI.models.sites.deleteSite(req.params.siteId)) {
reply.code(204)
} else {
reply.badRequest('Site does not exist.')

@ -140,6 +140,7 @@ export default {
await WIKI.models.sites.init(ids)
await WIKI.models.groups.init(ids)
await WIKI.models.authentication.init(ids)
await WIKI.models.users.init(ids)
},
/**
* Subscribe to HA propagation events

@ -1,57 +0,0 @@
CREATE TABLE "authentication" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"module" varchar(255) NOT NULL,
"isEnabled" boolean DEFAULT false NOT NULL,
"displayName" varchar(255) DEFAULT '' NOT NULL,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"registration" boolean DEFAULT false NOT NULL,
"allowedEmailRegex" varchar(255) DEFAULT '' NOT NULL,
"autoEnrollGroups" uuid[] DEFAULT '{}'
);
--> statement-breakpoint
CREATE TABLE "groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"permissions" jsonb NOT NULL,
"rules" jsonb NOT NULL,
"redirectOnLogin" varchar(255) DEFAULT '' NOT NULL,
"redirectOnFirstLogin" varchar(255) DEFAULT '' NOT NULL,
"redirectOnLogout" varchar(255) DEFAULT '' NOT NULL,
"isSystem" boolean DEFAULT false NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "settings" (
"key" varchar(255) PRIMARY KEY NOT NULL,
"value" jsonb DEFAULT '{}'::jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"hostname" varchar(255) NOT NULL,
"isEnabled" boolean DEFAULT false NOT NULL,
"config" jsonb NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "sites_hostname_unique" UNIQUE("hostname")
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"auth" jsonb DEFAULT '{}'::jsonb NOT NULL,
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
"passkeys" jsonb DEFAULT '{}'::jsonb NOT NULL,
"prefs" jsonb DEFAULT '{}'::jsonb NOT NULL,
"hasAvatar" boolean DEFAULT false NOT NULL,
"isActive" boolean DEFAULT false NOT NULL,
"isSystem" boolean DEFAULT false NOT NULL,
"isVerified" boolean DEFAULT false NOT NULL,
"lastLoginAt" timestamp,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE INDEX "lastLoginAt_idx" ON "users" USING btree ("lastLoginAt");

@ -0,0 +1,10 @@
{
"id": "061e8c84-e05e-40b0-a074-7a56bd794fc7",
"prevIds": [
"00000000-0000-0000-0000-000000000000"
],
"version": "8",
"dialect": "postgres",
"ddl": [],
"renames": []
}

@ -0,0 +1,291 @@
CREATE TYPE "assetKind" AS ENUM('document', 'image', 'other');--> statement-breakpoint
CREATE TYPE "jobHistoryState" AS ENUM('active', 'completed', 'failed', 'interrupted');--> statement-breakpoint
CREATE TYPE "pagePublishState" AS ENUM('draft', 'published', 'scheduled');--> statement-breakpoint
CREATE TYPE "treeNavigationMode" AS ENUM('inherit', 'override', 'overrideExact', 'hide', 'hideExact');--> statement-breakpoint
CREATE TYPE "treeType" AS ENUM('folder', 'page', 'asset');--> statement-breakpoint
CREATE TABLE "apiKeys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"name" varchar(255) NOT NULL,
"key" text NOT NULL,
"expiration" timestamp DEFAULT now() NOT NULL,
"isRevoked" boolean DEFAULT false NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "assets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"fileName" varchar(255) NOT NULL,
"fileExt" varchar(255) NOT NULL,
"isSystem" boolean DEFAULT false NOT NULL,
"kind" "assetKind" DEFAULT 'other'::"assetKind" NOT NULL,
"mimeType" varchar(255) DEFAULT 'application/octet-stream' NOT NULL,
"fileSize" bigint,
"meta" jsonb DEFAULT '{}' NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"data" bytea,
"preview" bytea,
"storageInfo" jsonb,
"authorId" uuid NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "authentication" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"module" varchar(255) NOT NULL,
"isEnabled" boolean DEFAULT false NOT NULL,
"displayName" varchar(255) DEFAULT '' NOT NULL,
"config" jsonb DEFAULT '{}' NOT NULL,
"registration" boolean DEFAULT false NOT NULL,
"allowedEmailRegex" varchar(255) DEFAULT '' NOT NULL,
"autoEnrollGroups" uuid[] DEFAULT '{}'::uuid[]
);
--> statement-breakpoint
CREATE TABLE "blocks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"block" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"description" varchar(255) NOT NULL,
"icon" varchar(255) NOT NULL,
"isEnabled" boolean DEFAULT false NOT NULL,
"isCustom" boolean DEFAULT false NOT NULL,
"config" jsonb DEFAULT '{}' NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"name" varchar(255) NOT NULL,
"permissions" jsonb NOT NULL,
"rules" jsonb NOT NULL,
"redirectOnLogin" varchar(255) DEFAULT '' NOT NULL,
"redirectOnFirstLogin" varchar(255) DEFAULT '' NOT NULL,
"redirectOnLogout" varchar(255) DEFAULT '' NOT NULL,
"isSystem" boolean DEFAULT false NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "jobHistory" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"task" varchar(255) NOT NULL,
"state" "jobHistoryState" NOT NULL,
"useWorker" boolean DEFAULT false NOT NULL,
"wasScheduled" boolean DEFAULT false NOT NULL,
"payload" jsonb NOT NULL,
"attempt" integer DEFAULT 1 NOT NULL,
"maxRetries" integer DEFAULT 0 NOT NULL,
"lastErrorMessage" text,
"executedBy" varchar(255),
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"completedAt" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "jobLock" (
"key" varchar(255) PRIMARY KEY,
"lastCheckedBy" varchar(255),
"lastCheckedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "jobSchedule" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"task" varchar(255) NOT NULL,
"cron" varchar(255) NOT NULL,
"type" varchar(255) DEFAULT 'system' NOT NULL,
"payload" jsonb NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "jobs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"task" varchar(255) NOT NULL,
"useWorker" boolean DEFAULT false NOT NULL,
"payload" jsonb NOT NULL,
"retries" integer DEFAULT 0 NOT NULL,
"maxRetries" integer DEFAULT 0 NOT NULL,
"waitUntil" timestamp,
"isScheduled" boolean DEFAULT false NOT NULL,
"createdBy" varchar(255),
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "locales" (
"code" varchar(255) PRIMARY KEY,
"name" varchar(255) NOT NULL,
"nativeName" varchar(255) NOT NULL,
"language" varchar(8) NOT NULL,
"region" varchar(3) NOT NULL,
"script" varchar(4) NOT NULL,
"isRTL" boolean DEFAULT false NOT NULL,
"strings" jsonb DEFAULT '[]' NOT NULL,
"completeness" integer DEFAULT 0 NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "navigation" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"items" jsonb DEFAULT '[]' NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"locale" ltree NOT NULL,
"path" varchar(255) NOT NULL,
"hash" varchar(255) NOT NULL,
"alias" varchar(255),
"title" varchar(255) NOT NULL,
"description" varchar(255),
"icon" varchar(255),
"publishState" "pagePublishState" DEFAULT 'draft'::"pagePublishState" NOT NULL,
"publishStartDate" timestamp,
"publishEndDate" timestamp,
"config" jsonb DEFAULT '{}' NOT NULL,
"relations" jsonb DEFAULT '[]' NOT NULL,
"content" text,
"render" text,
"searchContent" text,
"ts" tsvector,
"tags" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"toc" jsonb,
"editor" varchar(255) NOT NULL,
"contentType" varchar(255) NOT NULL,
"isBrowsable" boolean DEFAULT true NOT NULL,
"isSearchable" boolean DEFAULT true NOT NULL,
"isSearchableComputed" boolean GENERATED ALWAYS AS ("pages"."publishState" != 'draft' AND "pages"."isSearchable") STORED,
"password" varchar(255),
"ratingScore" integer DEFAULT 0 NOT NULL,
"ratingCount" timestamp DEFAULT now() NOT NULL,
"scripts" jsonb DEFAULT '{}' NOT NULL,
"historyData" jsonb DEFAULT '{}' NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"authorId" uuid NOT NULL,
"creatorId" uuid NOT NULL,
"ownerId" uuid NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "settings" (
"key" varchar(255) PRIMARY KEY,
"value" jsonb DEFAULT '{}' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"hostname" varchar(255) NOT NULL UNIQUE,
"isEnabled" boolean DEFAULT false NOT NULL,
"config" jsonb NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tags" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"tag" varchar(255) NOT NULL,
"usageCount" integer DEFAULT 0 NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tree" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"folderPath" ltree,
"fileName" varchar(255) NOT NULL,
"hash" varchar(255) NOT NULL,
"tree" "treeType" NOT NULL,
"locale" ltree NOT NULL,
"title" varchar(255) NOT NULL,
"navigationMode" "treeNavigationMode" DEFAULT 'inherit'::"treeNavigationMode" NOT NULL,
"navigationId" uuid,
"tags" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"meta" jsonb DEFAULT '{}' NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"siteId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "userAvatars" (
"id" uuid PRIMARY KEY,
"data" bytea NOT NULL
);
--> statement-breakpoint
CREATE TABLE "userGroups" (
"userId" uuid,
"groupId" uuid,
CONSTRAINT "userGroups_pkey" PRIMARY KEY("userId","groupId")
);
--> statement-breakpoint
CREATE TABLE "userKeys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"kind" varchar(255) NOT NULL,
"token" varchar(255) NOT NULL,
"meta" jsonb DEFAULT '{}' NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"validUntil" timestamp NOT NULL,
"userId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"email" varchar(255) NOT NULL UNIQUE,
"name" varchar(255) NOT NULL,
"auth" jsonb DEFAULT '{}' NOT NULL,
"meta" jsonb DEFAULT '{}' NOT NULL,
"passkeys" jsonb DEFAULT '{}' NOT NULL,
"prefs" jsonb DEFAULT '{}' NOT NULL,
"hasAvatar" boolean DEFAULT false NOT NULL,
"isActive" boolean DEFAULT false NOT NULL,
"isSystem" boolean DEFAULT false NOT NULL,
"isVerified" boolean DEFAULT false NOT NULL,
"lastLoginAt" timestamp,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE INDEX "assets_siteId_idx" ON "assets" ("siteId");--> statement-breakpoint
CREATE INDEX "blocks_siteId_idx" ON "blocks" ("siteId");--> statement-breakpoint
CREATE INDEX "locales_language_idx" ON "locales" ("language");--> statement-breakpoint
CREATE INDEX "navigation_siteId_idx" ON "navigation" ("siteId");--> statement-breakpoint
CREATE INDEX "pages_authorId_idx" ON "pages" ("authorId");--> statement-breakpoint
CREATE INDEX "pages_creatorId_idx" ON "pages" ("creatorId");--> statement-breakpoint
CREATE INDEX "pages_ownerId_idx" ON "pages" ("ownerId");--> statement-breakpoint
CREATE INDEX "pages_siteId_idx" ON "pages" ("siteId");--> statement-breakpoint
CREATE INDEX "pages_ts_idx" ON "pages" USING gin ("ts");--> statement-breakpoint
CREATE INDEX "pages_tags_idx" ON "pages" USING gin ("tags");--> statement-breakpoint
CREATE INDEX "pages_isSearchableComputed_idx" ON "pages" ("isSearchableComputed");--> statement-breakpoint
CREATE INDEX "tags_siteId_idx" ON "tags" ("siteId");--> statement-breakpoint
CREATE UNIQUE INDEX "tags_composite_idx" ON "tags" ("siteId","tag");--> statement-breakpoint
CREATE INDEX "tree_folderpath_idx" ON "tree" ("folderPath");--> statement-breakpoint
CREATE INDEX "tree_folderpath_gist_idx" ON "tree" USING gist ("folderPath");--> statement-breakpoint
CREATE INDEX "tree_fileName_idx" ON "tree" ("fileName");--> statement-breakpoint
CREATE INDEX "tree_hash_idx" ON "tree" ("hash");--> statement-breakpoint
CREATE INDEX "tree_type_idx" ON "tree" ("tree");--> statement-breakpoint
CREATE INDEX "tree_locale_idx" ON "tree" USING gist ("locale");--> statement-breakpoint
CREATE INDEX "tree_navigationMode_idx" ON "tree" ("navigationMode");--> statement-breakpoint
CREATE INDEX "tree_navigationId_idx" ON "tree" ("navigationId");--> statement-breakpoint
CREATE INDEX "tree_tags_idx" ON "tree" USING gin ("tags");--> statement-breakpoint
CREATE INDEX "tree_siteId_idx" ON "tree" ("siteId");--> statement-breakpoint
CREATE INDEX "userGroups_userId_idx" ON "userGroups" ("userId");--> statement-breakpoint
CREATE INDEX "userGroups_groupId_idx" ON "userGroups" ("groupId");--> statement-breakpoint
CREATE INDEX "userGroups_composite_idx" ON "userGroups" ("userId","groupId");--> statement-breakpoint
CREATE INDEX "userKeys_userId_idx" ON "userKeys" ("userId");--> statement-breakpoint
CREATE INDEX "users_lastLoginAt_idx" ON "users" ("lastLoginAt");--> statement-breakpoint
ALTER TABLE "assets" ADD CONSTRAINT "assets_authorId_users_id_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id");--> statement-breakpoint
ALTER TABLE "assets" ADD CONSTRAINT "assets_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "blocks" ADD CONSTRAINT "blocks_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "navigation" ADD CONSTRAINT "navigation_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "pages" ADD CONSTRAINT "pages_authorId_users_id_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id");--> statement-breakpoint
ALTER TABLE "pages" ADD CONSTRAINT "pages_creatorId_users_id_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id");--> statement-breakpoint
ALTER TABLE "pages" ADD CONSTRAINT "pages_ownerId_users_id_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id");--> statement-breakpoint
ALTER TABLE "pages" ADD CONSTRAINT "pages_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "tags" ADD CONSTRAINT "tags_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "tree" ADD CONSTRAINT "tree_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
ALTER TABLE "userGroups" ADD CONSTRAINT "userGroups_userId_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "userGroups" ADD CONSTRAINT "userGroups_groupId_groups_id_fkey" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "userKeys" ADD CONSTRAINT "userKeys_userId_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id");

File diff suppressed because it is too large Load Diff

@ -1,18 +0,0 @@
{
"id": "061e8c84-e05e-40b0-a074-7a56bd794fc7",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {},
"enums": {},
"schemas": {},
"views": {},
"sequences": {},
"roles": {},
"policies": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

@ -1,379 +0,0 @@
{
"id": "8e212503-f07f-43d5-8e3d-9a3b9869b3cb",
"prevId": "061e8c84-e05e-40b0-a074-7a56bd794fc7",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.authentication": {
"name": "authentication",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"module": {
"name": "module",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"isEnabled": {
"name": "isEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"displayName": {
"name": "displayName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"registration": {
"name": "registration",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"allowedEmailRegex": {
"name": "allowedEmailRegex",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"autoEnrollGroups": {
"name": "autoEnrollGroups",
"type": "uuid[]",
"primaryKey": false,
"notNull": false,
"default": "'{}'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.groups": {
"name": "groups",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"permissions": {
"name": "permissions",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"rules": {
"name": "rules",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"redirectOnLogin": {
"name": "redirectOnLogin",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"redirectOnFirstLogin": {
"name": "redirectOnFirstLogin",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"redirectOnLogout": {
"name": "redirectOnLogout",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"isSystem": {
"name": "isSystem",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"value": {
"name": "value",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"hostname": {
"name": "hostname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"isEnabled": {
"name": "isEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sites_hostname_unique": {
"name": "sites_hostname_unique",
"nullsNotDistinct": false,
"columns": [
"hostname"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"auth": {
"name": "auth",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"meta": {
"name": "meta",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"passkeys": {
"name": "passkeys",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"prefs": {
"name": "prefs",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"hasAvatar": {
"name": "hasAvatar",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"isSystem": {
"name": "isSystem",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"isVerified": {
"name": "isVerified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"lastLoginAt": {
"name": "lastLoginAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"lastLoginAt_idx": {
"name": "lastLoginAt_idx",
"columns": [
{
"expression": "lastLoginAt",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

@ -1,20 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1768857200779,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1769316465987,
"tag": "0001_main",
"breakpoints": true
}
]
}

@ -1,7 +1,56 @@
import { boolean, index, jsonb, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
import { defineRelations, sql } from 'drizzle-orm'
import { bigint, boolean, bytea, customType, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex, uuid, varchar } from 'drizzle-orm/pg-core'
// == CUSTOM TYPES =====================
const ltree = customType({
dataType () {
return 'ltree'
}
})
const tsvector = customType({
dataType () {
return 'tsvector'
}
})
// == TABLES ===========================
// API KEYS ----------------------------
export const apiKeys = pgTable('apiKeys', {
id: uuid().primaryKey().defaultRandom(),
name: varchar({ length: 255 }).notNull(),
key: text().notNull(),
expiration: timestamp().notNull().defaultNow(),
isRevoked: boolean().notNull().default(false),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow()
})
// ASSETS ------------------------------
export const assetKindEnum = pgEnum('assetKind', ['document', 'image', 'other'])
export const assets = pgTable('assets', {
id: uuid().primaryKey().defaultRandom(),
fileName: varchar({ length: 255 }).notNull(),
fileExt: varchar({ length: 255 }).notNull(),
isSystem: boolean().notNull().default(false),
kind: assetKindEnum().notNull().default('other'),
mimeType: varchar({ length: 255 }).notNull().default('application/octet-stream'),
fileSize: bigint({ mode: 'number' }), // in bytes
meta: jsonb().notNull().default({}),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
data: bytea(),
preview: bytea(),
storageInfo: jsonb(),
authorId: uuid().notNull().references(() => users.id),
siteId: uuid().notNull().references(() => sites.id),
}, (table) => [
index('assets_siteId_idx').on(table.siteId)
])
// AUTHENTICATION ----------------------
export const authenticationTable = pgTable('authentication', {
export const authentication = pgTable('authentication', {
id: uuid().primaryKey().defaultRandom(),
module: varchar({ length: 255 }).notNull(),
isEnabled: boolean().notNull().default(false),
@ -12,8 +61,23 @@ export const authenticationTable = pgTable('authentication', {
autoEnrollGroups: uuid().array().default([])
})
// BLOCKS ------------------------------
export const blocks = pgTable('blocks', {
id: uuid().primaryKey().defaultRandom(),
block: varchar({ length: 255 }).notNull(),
name: varchar({ length: 255 }).notNull(),
description: varchar({ length: 255 }).notNull(),
icon: varchar({ length: 255 }).notNull(),
isEnabled: boolean().notNull().default(false),
isCustom: boolean().notNull().default(false),
config: jsonb().notNull().default({}),
siteId: uuid().notNull().references(() => sites.id),
}, (table) => [
index('blocks_siteId_idx').on(table.siteId)
])
// GROUPS ------------------------------
export const groupsTable = pgTable('groups', {
export const groups = pgTable('groups', {
id: uuid().primaryKey().defaultRandom(),
name: varchar({ length: 255 }).notNull(),
permissions: jsonb().notNull(),
@ -26,14 +90,139 @@ export const groupsTable = pgTable('groups', {
updatedAt: timestamp().notNull().defaultNow()
})
// JOB HISTORY -------------------------
export const jobHistoryStateEnum = pgEnum('jobHistoryState', ['active', 'completed', 'failed', 'interrupted'])
export const jobHistory = pgTable('jobHistory', {
id: uuid().primaryKey().defaultRandom(),
task: varchar({ length: 255 }).notNull(),
state: jobHistoryStateEnum().notNull(),
useWorker: boolean().notNull().default(false),
wasScheduled: boolean().notNull().default(false),
payload: jsonb().notNull(),
attempt: integer().notNull().default(1),
maxRetries: integer().notNull().default(0),
lastErrorMessage: text(),
executedBy: varchar({ length: 255 }),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
completedAt: timestamp().notNull()
})
// JOB SCHEDULE ------------------------
export const jobSchedule = pgTable('jobSchedule', {
id: uuid().primaryKey().defaultRandom(),
task: varchar({ length: 255 }).notNull(),
cron: varchar({ length: 255 }).notNull(),
type: varchar({ length: 255 }).notNull().default('system'),
payload: jsonb().notNull(),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow()
})
// JOB LOCK ----------------------------
export const jobLock = pgTable('jobLock', {
key: varchar({ length: 255 }).primaryKey(),
lastCheckedBy: varchar({ length: 255 }),
lastCheckedAt: timestamp().notNull().defaultNow()
})
// JOBS --------------------------------
export const jobs = pgTable('jobs', {
id: uuid().primaryKey().defaultRandom(),
task: varchar({ length: 255 }).notNull(),
useWorker: boolean().notNull().default(false),
payload: jsonb().notNull(),
retries: integer().notNull().default(0),
maxRetries: integer().notNull().default(0),
waitUntil: timestamp(),
isScheduled: boolean().notNull().default(false),
createdBy: varchar({ length: 255 }),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow()
})
// LOCALES -----------------------------
export const locales = pgTable('locales', {
code: varchar({ length: 255 }).primaryKey(),
name: varchar({ length: 255 }).notNull(),
nativeName: varchar({ length: 255 }).notNull(),
language: varchar({ length: 8 }).notNull(), // Unicode language subtag
region: varchar({ length: 3 }).notNull(), // Unicode region subtag
script: varchar({ length: 4 }).notNull(), // Unicode script subtag
isRTL: boolean().notNull().default(false),
strings: jsonb().notNull().default([]),
completeness: integer().notNull().default(0),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow()
}, (table) => [
index('locales_language_idx').on(table.language)
])
// NAVIGATION --------------------------
export const navigation = pgTable('navigation', {
id: uuid().primaryKey().defaultRandom(),
items: jsonb().notNull().default([]),
siteId: uuid().notNull().references(() => sites.id),
}, (table) => [
index('navigation_siteId_idx').on(table.siteId)
])
// PAGES ------------------------------
export const pagePublishStateEnum = pgEnum('pagePublishState', ['draft', 'published', 'scheduled'])
export const pages = pgTable('pages', {
id: uuid().primaryKey().defaultRandom(),
locale: ltree('locale').notNull(),
path: varchar({ length: 255 }).notNull(),
hash: varchar({ length: 255 }).notNull(),
alias: varchar({ length: 255 }),
title: varchar({ length: 255 }).notNull(),
description: varchar({ length: 255 }),
icon: varchar({ length: 255 }),
publishState: pagePublishStateEnum('publishState').notNull().default('draft'),
publishStartDate: timestamp(),
publishEndDate: timestamp(),
config: jsonb().notNull().default({}),
relations: jsonb().notNull().default([]),
content: text(),
render: text(),
searchContent: text(),
ts: tsvector('ts'),
tags: text().array().notNull().default(sql`ARRAY[]::text[]`),
toc: jsonb(),
editor: varchar({ length: 255 }).notNull(),
contentType: varchar({ length: 255 }).notNull(),
isBrowsable: boolean().notNull().default(true),
isSearchable: boolean().notNull().default(true),
isSearchableComputed: boolean('isSearchableComputed').generatedAlwaysAs(() => sql`${pages.publishState} != 'draft' AND ${pages.isSearchable}`),
password: varchar({ length: 255 }),
ratingScore: integer().notNull().default(0),
ratingCount: timestamp().notNull().defaultNow(),
scripts: jsonb().notNull().default({}),
historyData: jsonb().notNull().default({}),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
authorId: uuid().notNull().references(() => users.id),
creatorId: uuid().notNull().references(() => users.id),
ownerId: uuid().notNull().references(() => users.id),
siteId: uuid().notNull().references(() => sites.id),
}, (table) => [
index('pages_authorId_idx').on(table.authorId),
index('pages_creatorId_idx').on(table.creatorId),
index('pages_ownerId_idx').on(table.ownerId),
index('pages_siteId_idx').on(table.siteId),
index('pages_ts_idx').using('gin', table.ts),
index('pages_tags_idx').using('gin', table.tags),
index('pages_isSearchableComputed_idx').on(table.isSearchableComputed)
])
// SETTINGS ----------------------------
export const settingsTable = pgTable('settings', {
export const settings = pgTable('settings', {
key: varchar({ length: 255 }).notNull().primaryKey(),
value: jsonb().notNull().default({})
})
// SITES -------------------------------
export const sitesTable = pgTable('sites', {
export const sites = pgTable('sites', {
id: uuid().primaryKey().defaultRandom(),
hostname: varchar({ length: 255 }).notNull().unique(),
isEnabled: boolean().notNull().default(false),
@ -41,8 +230,71 @@ export const sitesTable = pgTable('sites', {
createdAt: timestamp().notNull().defaultNow()
})
// TAGS --------------------------------
export const tags = pgTable('tags', {
id: uuid().primaryKey().defaultRandom(),
tag: varchar({ length: 255 }).notNull(),
usageCount: integer().notNull().default(0),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
siteId: uuid().notNull().references(() => sites.id)
}, (table) => [
index('tags_siteId_idx').on(table.siteId),
uniqueIndex('tags_composite_idx').on(table.siteId, table.tag)
])
// TREE --------------------------------
export const treeTypeEnum = pgEnum('treeType', ['folder', 'page', 'asset'])
export const treeNavigationModeEnum = pgEnum('treeNavigationMode', ['inherit', 'override', 'overrideExact', 'hide', 'hideExact'])
export const tree = pgTable('tree', {
id: uuid().primaryKey().defaultRandom(),
folderPath: ltree('folderPath'),
fileName: varchar({ length: 255 }).notNull(),
hash: varchar({ length: 255 }).notNull(),
type: treeTypeEnum('tree').notNull(),
locale: ltree('locale').notNull(),
title: varchar({ length: 255 }).notNull(),
navigationMode: treeNavigationModeEnum('navigationMode').notNull().default('inherit'),
navigationId: uuid(),
tags: text().array().notNull().default(sql`ARRAY[]::text[]`),
meta: jsonb().notNull().default({}),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
siteId: uuid().notNull().references(() => sites.id)
}, (table) => [
index('tree_folderpath_idx').on(table.folderPath),
index('tree_folderpath_gist_idx').using('gist', table.folderPath),
index('tree_fileName_idx').on(table.fileName),
index('tree_hash_idx').on(table.hash),
index('tree_type_idx').on(table.type),
index('tree_locale_idx').using('gist', table.locale),
index('tree_navigationMode_idx').on(table.navigationMode),
index('tree_navigationId_idx').on(table.navigationId),
index('tree_tags_idx').using('gin', table.tags),
index('tree_siteId_idx').on(table.siteId)
])
// USER AVATARS ------------------------
export const userAvatars = pgTable('userAvatars', {
id: uuid().primaryKey(),
data: bytea().notNull()
})
// USER KEYS ---------------------------
export const userKeys = pgTable('userKeys', {
id: uuid().primaryKey().defaultRandom(),
kind: varchar({ length: 255 }).notNull(),
token: varchar({ length: 255 }).notNull(),
meta: jsonb().notNull().default({}),
createdAt: timestamp().notNull().defaultNow(),
validUntil: timestamp().notNull(),
userId: uuid().notNull().references(() => users.id)
}, (table) => [
index('userKeys_userId_idx').on(table.userId)
])
// USERS -------------------------------
export const usersTable = pgTable('users', {
export const users = pgTable('users', {
id: uuid().primaryKey().defaultRandom(),
email: varchar({ length: 255 }).notNull().unique(),
name: varchar({ length: 255 }).notNull(),
@ -58,5 +310,34 @@ export const usersTable = pgTable('users', {
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow()
}, (table) => [
index('lastLoginAt_idx').on(table.lastLoginAt)
index('users_lastLoginAt_idx').on(table.lastLoginAt)
])
// == RELATION TABLES ==================
// USER GROUPS -------------------------
export const userGroups = pgTable('userGroups', {
userId: uuid().notNull().references(() => users.id, { onDelete: 'cascade' }),
groupId: uuid().notNull().references(() => groups.id, { onDelete: 'cascade' })
}, (table) => [
primaryKey({ columns: [table.userId, table.groupId] }),
index('userGroups_userId_idx').on(table.userId),
index('userGroups_groupId_idx').on(table.groupId),
index('userGroups_composite_idx').on(table.userId, table.groupId)
])
// == RELATIONS ========================
export const relations = defineRelations({ users, groups, userGroups },
r => ({
users: {
groups: r.many.groups({
from: r.users.id.through(r.userGroups.userId),
to: r.groups.id.through(r.userGroups.groupId)
})
},
groups: {
members: r.many.users()
}
})
)

@ -84,27 +84,41 @@ WIKI.logger.info('=======================================')
WIKI.logger.info('Initializing...')
WIKI.logger.info(`Running node.js ${process.version} [ OK ]`)
WIKI.dbManager = (await import('./core/db.mjs')).default
WIKI.db = await dbManager.init()
WIKI.models = (await import('./models/index.mjs')).default
try {
if (await WIKI.configSvc.loadFromDb()) {
WIKI.logger.info('Settings merged with DB successfully [ OK ]')
} else {
WIKI.logger.warn('No settings found in DB. Initializing with defaults...')
await WIKI.configSvc.initDbValues()
if (!(await WIKI.configSvc.loadFromDb())) {
throw new Error('Settings table is empty! Could not initialize [ ERROR ]')
// ----------------------------------------
// Pre-Boot Sequence
// ----------------------------------------
async function preBoot () {
WIKI.dbManager = (await import('./core/db.mjs')).default
WIKI.db = await dbManager.init()
WIKI.models = (await import('./models/index.mjs')).default
try {
if (await WIKI.configSvc.loadFromDb()) {
WIKI.logger.info('Settings merged with DB successfully [ OK ]')
} else {
WIKI.logger.warn('No settings found in DB. Initializing with defaults...')
await WIKI.configSvc.initDbValues()
if (!(await WIKI.configSvc.loadFromDb())) {
throw new Error('Settings table is empty! Could not initialize [ ERROR ]')
}
}
} catch (err) {
WIKI.logger.error('Database Initialization Error: ' + err.message)
if (WIKI.IS_DEBUG) {
WIKI.logger.error(err)
}
process.exit(1)
}
} catch (err) {
WIKI.logger.error('Database Initialization Error: ' + err.message)
if (WIKI.IS_DEBUG) {
WIKI.logger.error(err)
}
process.exit(1)
}
// ----------------------------------------
// Post-Boot Sequence
// ----------------------------------------
async function postBoot () {
await WIKI.models.sites.reloadCache()
}
// ----------------------------------------
@ -131,7 +145,9 @@ async function initHTTPServer () {
]
},
bodyLimit: WIKI.config.bodyParserLimit || 5242880, // 5mb
logger: true,
logger: {
level: 'error'
},
trustProxy: WIKI.config.security.securityTrustProxy ?? false,
routerOptions: {
ignoreTrailingSlash: true
@ -152,7 +168,7 @@ async function initHTTPServer () {
// ----------------------------------------
WIKI.server.on(gracefulServer.SHUTTING_DOWN, () => {
WIKI.logger.info('Shutting down HTTP Server... [ PENDING ]')
WIKI.logger.info('Shutting down HTTP Server... [ STOPPING ]')
})
WIKI.server.on(gracefulServer.SHUTDOWN, (err) => {
@ -271,12 +287,41 @@ async function initHTTPServer () {
routePrefix: '/_swagger'
})
// ----------------------------------------
// Permissions
// ----------------------------------------
app.addHook('preHandler', (req, reply, done) => {
if (req.routeOptions.config?.permissions?.length > 0) {
// Unauthenticated / No Permissions
if (!req.user?.isAuthenticated || !(req.user.permissions?.length > 0)) {
return reply.unauthorized()
}
// Is Root Admin?
if (!req.user.permissions.includes('manage:system')) {
// Check for at least 1 permission
const isAllowed = req.routeOptions.config.permissions.some(perms => {
// Check for all permissions
if (Array.isArray(perms)) {
return perms.every(perm => req.user.permissions?.some(p => p === perm))
} else {
return req.user.permissions?.some(p => p === perms)
}
})
// Forbidden
if (!isAllowed) {
return reply.forbidden()
}
}
}
done()
})
// ----------------------------------------
// SEO
// ----------------------------------------
app.addHook('onRequest', (req, reply, done) => {
console.log(req.raw.url)
const [urlPath, urlQuery] = req.raw.url.split('?')
if (urlPath.length > 1 && urlPath.endsWith('/')) {
const newPath = urlPath.slice(0, -1)
@ -366,7 +411,7 @@ async function initHTTPServer () {
WIKI.logger.info('HTTP Server: [ RUNNING ]')
WIKI.server.setReady()
} catch (err) {
app.log.error(err)
WIKI.logger.error(err)
process.exit(1)
}
}
@ -385,7 +430,9 @@ async function initHTTPServer () {
// })
// ----------------------------------------
// Start HTTP Server
// Initialization Sequence
// ----------------------------------------
initHTTPServer()
await preBoot()
await initHTTPServer()
await postBoot()

@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import yaml from 'js-yaml'
import { parseModuleProps } from '../helpers/common.mjs'
import { authenticationTable } from '../db/schema.mjs'
import { authentication as authenticationTable } from '../db/schema.mjs'
/**
* Authentication model

@ -1,5 +1,5 @@
import { v4 as uuid } from 'uuid'
import { groupsTable } from '../db/schema.mjs'
import { groups as groupsTable } from '../db/schema.mjs'
/**
* Groups model

@ -2,10 +2,12 @@ import { authentication } from './authentication.mjs'
import { groups } from './groups.mjs'
import { settings } from './settings.mjs'
import { sites } from './sites.mjs'
import { users } from './users.mjs'
export default {
authentication,
groups,
settings,
sites
sites,
users
}

@ -1,4 +1,4 @@
import { settingsTable } from '../db/schema.mjs'
import { settings as settingsTable } from '../db/schema.mjs'
import { has, reduce, set } from 'lodash-es'
import { pem2jwk } from 'pem-jwk'
import crypto from 'node:crypto'

@ -1,5 +1,5 @@
import { defaultsDeep, keyBy } from 'lodash-es'
import { sitesTable } from '../db/schema.mjs'
import { sites as sitesTable } from '../db/schema.mjs'
import { eq } from 'drizzle-orm'
/**
@ -8,7 +8,7 @@ import { eq } from 'drizzle-orm'
class Sites {
async getSiteByHostname ({ hostname, forceReload = false }) {
if (forceReload) {
await WIKI.db.sites.reloadCache()
await WIKI.models.sites.reloadCache()
}
const siteId = WIKI.sitesMappings[hostname] || WIKI.sitesMappings['*']
if (siteId) {
@ -19,7 +19,7 @@ class Sites {
async reloadCache () {
WIKI.logger.info('Reloading site configurations...')
const sites = await WIKI.db.sites.query().orderBy('id')
const sites = await WIKI.db.select().from(sitesTable).orderBy(sitesTable.id)
WIKI.sites = keyBy(sites, 'id')
WIKI.sitesMappings = {}
for (const site of sites) {
@ -29,7 +29,7 @@ class Sites {
}
async createSite (hostname, config) {
const newSite = await WIKI.db.sites.query().insertAndFetch({
const result = await WIKI.db.insert(sitesTable).values({
hostname,
isEnabled: true,
config: defaultsDeep(config, {
@ -126,34 +126,36 @@ class Sites {
normalizeFilename: true
}
})
})
}).returning({ id: sitesTable.id })
WIKI.logger.debug(`Creating new root navigation for site ${newSite.id}`)
const newSite = result[0]
await WIKI.db.navigation.query().insert({
id: newSite.id,
siteId: newSite.id,
items: []
})
// WIKI.logger.debug(`Creating new root navigation for site ${newSite.id}`)
WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`)
// await WIKI.db.navigation.query().insert({
// id: newSite.id,
// siteId: newSite.id,
// items: []
// })
await WIKI.db.storage.query().insert({
module: 'db',
siteId: newSite.id,
isEnabled: true,
contentTypes: {
activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
largeThreshold: '5MB'
},
assetDelivery: {
streaming: true,
directAccess: false
},
state: {
current: 'ok'
}
})
// WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`)
// await WIKI.db.storage.query().insert({
// module: 'db',
// siteId: newSite.id,
// isEnabled: true,
// contentTypes: {
// activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
// largeThreshold: '5MB'
// },
// assetDelivery: {
// streaming: true,
// directAccess: false
// },
// state: {
// current: 'ok'
// }
// })
return newSite
}
@ -168,6 +170,10 @@ class Sites {
return Boolean(deletedResult.rowCount > 0)
}
async countSites () {
return WIKI.db.$count(sitesTable)
}
async init (ids) {
WIKI.logger.info('Inserting default site...')

@ -0,0 +1,76 @@
/* global WIKI */
import bcrypt from 'bcryptjs'
import { userGroups as userGroupsTable, users as usersTable } from '../db/schema.mjs'
/**
* Users model
*/
class Users {
async init (ids) {
WIKI.logger.info('Inserting default users...')
await WIKI.db.insert(usersTable).values([
{
id: ids.userAdminId,
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
auth: {
[ids.authModuleId]: {
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
mustChangePwd: !process.env.ADMIN_PASS,
restrictLogin: false,
tfaIsActive: false,
tfaRequired: false,
tfaSecret: ''
}
},
name: 'Administrator',
isSystem: false,
isActive: true,
isVerified: true,
meta: {
location: '',
jobTitle: '',
pronouns: ''
},
prefs: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h',
appearance: 'site',
cvd: 'none'
}
},
{
id: ids.userGuestId,
email: 'guest@example.com',
auth: {},
name: 'Guest',
isSystem: true,
isActive: true,
isVerified: true,
meta: {},
prefs: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h',
appearance: 'site',
cvd: 'none'
}
}
])
await WIKI.db.insert(userGroupsTable).values([
{
userId: ids.userAdminId,
groupId: ids.groupAdminId
},
{
userId: ids.userGuestId,
groupId: ids.groupGuestId
}
])
}
}
export const users = new Users()

File diff suppressed because it is too large Load Diff

@ -12,7 +12,8 @@
"dev": "cd .. && nodemon backend --watch backend --ext mjs,js,json",
"ncu": "ncu -i",
"ncu-u": "ncu -u",
"db-generate": "drizzle-kit generate --dialect=postgresql --schema=./db/schema.mjs --out=./db/migrations --name=main"
"db-generate": "drizzle-kit generate --dialect=postgresql --schema=./db/schema.mjs --out=./db/migrations --name=main",
"db-up": "drizzle-kit up --dialect=postgresql --out=./db/migrations"
},
"repository": {
"type": "git",
@ -49,30 +50,30 @@
"@fastify/swagger": "9.6.1",
"@fastify/swagger-ui": "5.2.4",
"@fastify/view": "11.1.1",
"@gquittet/graceful-server": "6.0.2",
"@gquittet/graceful-server": "6.0.3",
"ajv-formats": "3.0.1",
"bcryptjs": "3.0.3",
"drizzle-orm": "0.45.1",
"drizzle-orm": "1.0.0-beta.15-859cf75",
"fastify": "5.7.1",
"fastify-favicon": "5.0.0",
"lodash-es": "4.17.22",
"lodash-es": "4.17.23",
"luxon": "3.7.2",
"nanoid": "5.1.6",
"pem-jwk": "2.0.0",
"pg": "8.16.3",
"pg": "8.17.2",
"pg-pubsub": "0.8.1",
"pug": "3.0.3",
"semver": "7.7.3",
"uuid": "13.0.0"
},
"devDependencies": {
"drizzle-kit": "0.31.8",
"eslint": "9.32.0",
"drizzle-kit": "1.0.0-beta.15-859cf75",
"eslint": "9.39.2",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "7.2.1",
"neostandard": "0.12.2",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"npm-check-updates": "19.3.1"
},
"collective": {

@ -13,7 +13,7 @@ port: 3000
# ---------------------------------------------------------------------
# Database
# ---------------------------------------------------------------------
# PostgreSQL 12 or later required
# PostgreSQL 16 or later required
db:
host: localhost
@ -41,37 +41,6 @@ db:
#######################################################################
# Do not change unless you know what you are doing!
# ---------------------------------------------------------------------
# SSL/TLS Settings
# ---------------------------------------------------------------------
# Consider using a reverse proxy (e.g. nginx) if you require more
# advanced options than those provided below.
ssl:
enabled: false
port: 3443
# Provider to use, possible values: custom, letsencrypt
provider: custom
# ++++++ For custom only ++++++
# Certificate format, either 'pem' or 'pfx':
format: pem
# Using PEM format:
key: path/to/key.pem
cert: path/to/cert.pem
# Using PFX format:
pfx: path/to/cert.pfx
# Passphrase when using encrypted PEM / PFX keys (default: null):
passphrase: null
# Diffie Hellman parameters, with key length being greater or equal
# to 1024 bits (default: null):
dhparam: null
# ++++++ For letsencrypt only ++++++
domain: wiki.yourdomain.com
subscriberEmail: admin@example.com
# ---------------------------------------------------------------------
# Database Pool Options
# ---------------------------------------------------------------------
@ -116,10 +85,10 @@ dataPath: ./data
# ---------------------------------------------------------------------
# Body Parser Limit
# ---------------------------------------------------------------------
# Maximum size of API requests body that can be parsed. Does not affect
# file uploads.
# Maximum size of API requests body that can be parsed, in bytes.
# Does not affect file uploads. (default: 5242880 (5mb))
bodyParserLimit: 5mb
bodyParserLimit: 5242880
# ---------------------------------------------------------------------
# Scheduler

Loading…
Cancel
Save