diff --git a/.dockerignore b/.dockerignore index 9473324..80d8499 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,6 @@ node_modules /.svelte-kit /build /data -/drizzle # OS .DS_Store diff --git a/.gitignore b/.gitignore index 7ccff94..310e494 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ node_modules /.svelte-kit /build /data -/drizzle # OS .DS_Store diff --git a/Dockerfile b/Dockerfile index d22f972..691038e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ -# Build Stage -FROM node:18-alpine AS build +# Base Image +FROM node:18-alpine AS base WORKDIR /app RUN npm install -g pnpm@8 - COPY pnpm-lock.yaml . + +# Build Stage +FROM base AS build RUN pnpm fetch COPY . . @@ -12,18 +14,16 @@ RUN pnpm install --offline RUN pnpm build # Deploy Stage -FROM node:18-alpine -WORKDIR /app - -RUN npm install -g pnpm@8 - -COPY pnpm-lock.yaml . +FROM base RUN pnpm fetch --prod COPY package.json . RUN pnpm install --offline --prod COPY --from=build /app/build ./build +COPY drizzle ./drizzle EXPOSE 3000 +ENV BODY_SIZE_LIMIT=Infinity + CMD ["node", "./build/index.js"] diff --git a/docker-compose.yaml b/docker-compose.yaml index fcd223f..a54230e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,10 +6,15 @@ services: volumes: - ./data:/app/data environment: + # ArkValut - DATABASE_URL=/app/data/database.sqlite - - JWT_SECRET=${JWT_SECRET:?} + - JWT_SECRET=${JWT_SECRET:?} # Required - JWT_ACCESS_TOKEN_EXPIRES - JWT_REFRESH_TOKEN_EXPIRES - PUBKEY_CHALLENGE_EXPIRES + # SvelteKit + - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} + - XFF_DEPTH=${TRUST_PROXY:-} + - NODE_ENV=${NODE_ENV:-production} ports: - ${PORT:-80}:3000 diff --git a/drizzle/0000_spicy_morgan_stark.sql b/drizzle/0000_spicy_morgan_stark.sql new file mode 100644 index 0000000..85ad972 --- /dev/null +++ b/drizzle/0000_spicy_morgan_stark.sql @@ -0,0 +1,67 @@ +CREATE TABLE `client` ( + `id` integer PRIMARY KEY NOT NULL, + `public_key` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `user_client` ( + `user_id` integer NOT NULL, + `client_id` integer NOT NULL, + `state` text DEFAULT 'challenging' NOT NULL, + PRIMARY KEY(`client_id`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `user_client_challenge` ( + `id` integer PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `client_id` integer NOT NULL, + `challenge` text NOT NULL, + `allowed_ip` text NOT NULL, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `client_master_encryption_key` ( + `user_id` integer NOT NULL, + `client_id` integer NOT NULL, + `master_encryption_key_version` integer NOT NULL, + `encrypted_master_encryption_key` text NOT NULL, + PRIMARY KEY(`client_id`, `master_encryption_key_version`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `master_encryption_key` ( + `user_id` integer NOT NULL, + `version` integer NOT NULL, + `created_by` integer NOT NULL, + `created_at` integer NOT NULL, + `state` text NOT NULL, + `retired_at` integer, + PRIMARY KEY(`user_id`, `version`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `refresh_token` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `client_id` integer, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `user` ( + `id` integer PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `password` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `client_public_key_unique` ON `client` (`public_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_client_challenge_challenge_unique` ON `user_client_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `refresh_token_user_id_client_id_unique` ON `refresh_token` (`user_id`,`client_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..e01aaba --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,485 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "64e2c1ed-92bf-44d1-9094-7e3610b3224f", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "client": { + "name": "client", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_public_key_unique": { + "name": "client_public_key_unique", + "columns": [ + "public_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_client": { + "name": "user_client", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'challenging'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_client_user_id_user_id_fk": { + "name": "user_client_user_id_user_id_fk", + "tableFrom": "user_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_client_client_id_client_id_fk": { + "name": "user_client_client_id_client_id_fk", + "tableFrom": "user_client", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_client_user_id_client_id_pk": { + "columns": [ + "client_id", + "user_id" + ], + "name": "user_client_user_id_client_id_pk" + } + }, + "uniqueConstraints": {} + }, + "user_client_challenge": { + "name": "user_client_challenge", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_ip": { + "name": "allowed_ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_client_challenge_challenge_unique": { + "name": "user_client_challenge_challenge_unique", + "columns": [ + "challenge" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_client_challenge_user_id_user_id_fk": { + "name": "user_client_challenge_user_id_user_id_fk", + "tableFrom": "user_client_challenge", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_client_challenge_client_id_client_id_fk": { + "name": "user_client_challenge_client_id_client_id_fk", + "tableFrom": "user_client_challenge", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "client_master_encryption_key": { + "name": "client_master_encryption_key", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "master_encryption_key_version": { + "name": "master_encryption_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_master_encryption_key": { + "name": "encrypted_master_encryption_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "client_master_encryption_key_user_id_user_id_fk": { + "name": "client_master_encryption_key_user_id_user_id_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_master_encryption_key_client_id_client_id_fk": { + "name": "client_master_encryption_key_client_id_client_id_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk": { + "columns": [ + "client_id", + "master_encryption_key_version", + "user_id" + ], + "name": "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk" + } + }, + "uniqueConstraints": {} + }, + "master_encryption_key": { + "name": "master_encryption_key", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "master_encryption_key_user_id_user_id_fk": { + "name": "master_encryption_key_user_id_user_id_fk", + "tableFrom": "master_encryption_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "master_encryption_key_created_by_client_id_fk": { + "name": "master_encryption_key_created_by_client_id_fk", + "tableFrom": "master_encryption_key", + "tableTo": "client", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "master_encryption_key_user_id_version_pk": { + "columns": [ + "user_id", + "version" + ], + "name": "master_encryption_key_user_id_version_pk" + } + }, + "uniqueConstraints": {} + }, + "refresh_token": { + "name": "refresh_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "refresh_token_user_id_client_id_unique": { + "name": "refresh_token_user_id_client_id_unique", + "columns": [ + "user_id", + "client_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "refresh_token_client_id_client_id_fk": { + "name": "refresh_token_client_id_client_id_fk", + "tableFrom": "refresh_token", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..70b290a --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1735525637133, + "tag": "0000_spicy_morgan_stark", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 1419eeb..777e492 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,9 +1,12 @@ import { redirect, type ServerInit, type Handle } from "@sveltejs/kit"; import schedule from "node-schedule"; import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; +import { migrateDB } from "$lib/server/db/drizzle"; import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; export const init: ServerInit = () => { + migrateDB(); + schedule.scheduleJob("0 * * * *", () => { cleanupExpiredUserClientChallenges(); cleanupExpiredRefreshTokens(); diff --git a/src/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts index 385ac23..48d029a 100644 --- a/src/lib/server/db/drizzle.ts +++ b/src/lib/server/db/drizzle.ts @@ -1,7 +1,16 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import env from "$lib/server/loadenv"; const client = new Database(env.databaseUrl); +const db = drizzle(client); -export default drizzle(client); +export const migrateDB = () => { + if (process.env.NODE_ENV === "production") { + console.log("test"); + migrate(db, { migrationsFolder: "./drizzle" }); + } +}; + +export default db;