Production 환경에서 실행시 DB 마이그레이션을 자동으로 수행하도록 구현

This commit is contained in:
static
2024-12-30 11:43:36 +09:00
parent e22969b807
commit ee596ebb44
9 changed files with 593 additions and 13 deletions

View File

@@ -9,7 +9,6 @@ node_modules
/.svelte-kit
/build
/data
/drizzle
# OS
.DS_Store

1
.gitignore vendored
View File

@@ -8,7 +8,6 @@ node_modules
/.svelte-kit
/build
/data
/drizzle
# OS
.DS_Store

View File

@@ -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"]

View File

@@ -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

View File

@@ -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`);

View File

@@ -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": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1735525637133,
"tag": "0000_spicy_morgan_stark",
"breakpoints": true
}
]
}

View File

@@ -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();

View File

@@ -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;