From c42b8e2904b43845c695c5d2056c74f13ebc92d7 Mon Sep 17 00:00:00 2001 From: Syahdan Date: Mon, 25 May 2026 13:40:09 +0700 Subject: [PATCH 1/3] ci: docker compose for easy access --- .dockerignore | 17 +++++++++ .env.example | 15 ++++++++ README.md | 21 ++++++++++- apps/server/Dockerfile | 33 ++++++++++++++++ apps/web/Dockerfile | 21 +++++++++++ apps/web/nginx.conf | 11 ++++++ docker-compose.yml | 64 ++++++++++++++++++++++++++++++++ docs/run-instructions.md | 21 ++++++++++- packages/db/package.json | 1 + packages/db/src/seed-if-empty.ts | 34 +++++++++++++++++ packages/db/src/seed.ts | 24 ++++++------ 11 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 apps/server/Dockerfile create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/nginx.conf create mode 100644 docker-compose.yml create mode 100644 packages/db/src/seed-if-empty.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e71340 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.jj +node_modules +**/node_modules +.turbo +.cache +coverage +**/dist +**/build +**/*.tsbuildinfo +.env* +**/.env* +!.env.example +!**/.env.example +logs +*.log +docs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2e78d98 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +POSTGRES_DB=minmon +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/minmon + +BETTER_AUTH_SECRET=minmon-compose-secret-change-me-123456 +BETTER_AUTH_URL=http://localhost:3000 +CORS_ORIGIN=http://localhost:3001 +NODE_ENV=development + +SERVER_PORT=3000 +WEB_PORT=3001 +VITE_SERVER_URL=http://localhost:3000 diff --git a/README.md b/README.md index e06f18a..228bcd9 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,25 @@ bun run dev - Web: `http://localhost:5173` - API: `http://localhost:3000` +## Docker Compose Deployment + +Run the full stack with PostgreSQL, database initialization, API, and web UI: + +```bash +cp .env.example .env +docker compose up -d +``` + +Default endpoints: + +- Web: `http://localhost:3001` +- API: `http://localhost:3000` + +On first startup, the database initializer applies the schema and seeds the sample admin only when the users table is empty. + +Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets. + + ## Project Structure ```text @@ -186,5 +205,5 @@ See: ## Notes - DNS checking is limited to A record / IPv4 as required. -- This is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. +- Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation. - Full workspace typecheck is still blocked by a pre-existing desktop typing issue for `three`, but the web app and server app are verified. diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..0c49b3b --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1 + +FROM oven/bun:1.3.12 AS base +WORKDIR /app +ENV CI=1 + +COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./ +COPY apps ./apps +COPY packages ./packages + +FROM base AS deps +RUN bun install --frozen-lockfile + +FROM deps AS tools +ENV NODE_ENV=development +RUN touch apps/server/.env + +FROM deps AS build +ENV NODE_ENV=production +RUN bun run --filter server build + +FROM base AS prod-deps +RUN bun install --frozen-lockfile --production + +FROM oven/bun:1.3.12 AS runtime +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=build /app/apps/server/dist ./apps/server/dist + +EXPOSE 3000 +CMD ["bun", "apps/server/dist/index.mjs"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..f3e637b --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM oven/bun:1.3.12 AS build +WORKDIR /app +ENV CI=1 + +ARG VITE_SERVER_URL=http://localhost:3000 +ENV VITE_SERVER_URL=${VITE_SERVER_URL} + +COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./ +COPY apps ./apps +COPY packages ./packages + +RUN bun install --frozen-lockfile +RUN bun run --filter web build + +FROM nginx:1.27-alpine AS runtime +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/web/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..79fd959 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c3139d9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +name: minmon + +services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB must be set in root .env} + POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER must be set in root .env} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in root .env} + ports: + - "${POSTGRES_PORT:?POSTGRES_PORT must be set in root .env}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + + db-init: + build: + context: . + dockerfile: apps/server/Dockerfile + target: tools + environment: + <<: *server-environment + command: ["sh", "-c", "bun run db:push && bun run --filter @minmon/db db:seed:if-empty"] + depends_on: + postgres: + condition: service_healthy + restart: "no" + + server: + build: + context: . + dockerfile: apps/server/Dockerfile + environment: + <<: *server-environment + ports: + - "${SERVER_PORT:?SERVER_PORT must be set in root .env}:3000" + depends_on: + postgres: + condition: service_healthy + db-init: + condition: service_completed_successfully + restart: unless-stopped + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + args: + VITE_SERVER_URL: ${VITE_SERVER_URL:?VITE_SERVER_URL must be set in root .env} + ports: + - "${WEB_PORT:?WEB_PORT must be set in root .env}:80" + depends_on: + server: + condition: service_started + restart: unless-stopped + +volumes: + postgres_data: diff --git a/docs/run-instructions.md b/docs/run-instructions.md index d59428e..ddbd54a 100644 --- a/docs/run-instructions.md +++ b/docs/run-instructions.md @@ -114,6 +114,25 @@ bun run dev - Web: `http://localhost:5173` - API: `http://localhost:3000` +## Docker Compose Deployment + +Run the deployable stack with one command: + +```bash +cp .env.example .env +docker compose up -d +``` + +Default Docker endpoints: + +- Web: `http://localhost:3001` +- API: `http://localhost:3000` + +The Compose stack starts PostgreSQL, applies the schema, and seeds the sample admin only if the users table is empty. + +Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets. + + ## Current Behavior Notes ### List Ordering @@ -157,4 +176,4 @@ bun x tsc --noEmit -p packages/api/tsconfig.json - Full workspace typecheck may still fail because of a pre-existing `apps/desktop` typing issue for `three`. - The verified implementation path is the web app + server app + database push + database seed. - DNS checking is limited to A record / IPv4. -- This project is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. +- Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation. diff --git a/packages/db/package.json b/packages/db/package.json index 2b29b35..6e42483 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -15,6 +15,7 @@ "db:studio": "drizzle-kit studio", "db:migrate": "drizzle-kit migrate", "db:seed": "bun --env-file=../../apps/server/.env run ./src/seed.ts", + "db:seed:if-empty": "bun --env-file=../../apps/server/.env run ./src/seed-if-empty.ts", "db:start": "docker compose up -d", "db:watch": "docker compose up", "db:stop": "docker compose stop", diff --git a/packages/db/src/seed-if-empty.ts b/packages/db/src/seed-if-empty.ts new file mode 100644 index 0000000..ee6306e --- /dev/null +++ b/packages/db/src/seed-if-empty.ts @@ -0,0 +1,34 @@ +import dotenv from "dotenv"; + +dotenv.config({ + path: new URL("../../../apps/server/.env", import.meta.url).pathname, +}); + +import { db } from "./index"; +import { seed } from "./seed"; + +async function seedIfEmpty() { + const existingUser = await db.query.user.findFirst({ + columns: { + id: true, + }, + }); + + if (existingUser) { + console.info("Database already has users; skipping sample seed."); + return; + } + + await seed(); + console.info("Seeded auth admin and monitoring dashboard sample data."); + console.info("Admin login: admin@minmon.local / admin12345"); +} + +seedIfEmpty() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error("Failed to initialize seed data.", error); + process.exit(1); + }); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index d7aa6d8..86e4063 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -261,18 +261,20 @@ async function seedMonitoringData() { ); } -async function seed() { +export async function seed() { await seedAuthAdmin(); await seedMonitoringData(); } -seed() - .then(() => { - console.info("Seeded auth admin and monitoring dashboard sample data."); - console.info("Admin login: admin@minmon.local / admin12345"); - process.exit(0); - }) - .catch((error) => { - console.error("Failed to seed dashboard sample data.", error); - process.exit(1); - }); +if (import.meta.main) { + seed() + .then(() => { + console.info("Seeded auth admin and monitoring dashboard sample data."); + console.info("Admin login: admin@minmon.local / admin12345"); + process.exit(0); + }) + .catch((error) => { + console.error("Failed to seed dashboard sample data.", error); + process.exit(1); + }); +} From 8ab9a9a497a5302171483ea37a102691d55d013b Mon Sep 17 00:00:00 2001 From: Syahdan Date: Mon, 29 Jun 2026 21:10:22 +0700 Subject: [PATCH 2/3] feat: add role-based access control --- apps/web/src/components/dashboard-shell.tsx | 45 ++++-- apps/web/src/routeTree.gen.ts | 21 +++ apps/web/src/routes/dashboard.tsx | 9 +- apps/web/src/routes/dashboard.users.tsx | 149 ++++++++++++++++++ packages/api/src/context.ts | 25 +++ packages/api/src/index.ts | 31 ++++ packages/api/src/permissions.ts | 1 + packages/api/src/routers/dashboard.ts | 4 +- packages/api/src/routers/domains.ts | 16 +- packages/api/src/routers/index.ts | 2 + packages/api/src/routers/notes.ts | 14 +- packages/api/src/routers/schemas.ts | 5 + packages/api/src/routers/servers.ts | 12 +- packages/api/src/routers/services.ts | 12 +- packages/api/src/routers/users.ts | 90 +++++++++++ packages/db/src/migrations/0001_add_rbac.sql | 40 +++++ packages/db/src/migrations/meta/_journal.json | 9 +- packages/db/src/rbac.ts | 47 ++++++ packages/db/src/schema/auth.ts | 92 ++++++++++- packages/db/src/seed-if-empty.ts | 5 +- packages/db/src/seed.ts | 46 +++++- 21 files changed, 629 insertions(+), 46 deletions(-) create mode 100644 apps/web/src/routes/dashboard.users.tsx create mode 100644 packages/api/src/permissions.ts create mode 100644 packages/api/src/routers/users.ts create mode 100644 packages/db/src/migrations/0001_add_rbac.sql create mode 100644 packages/db/src/rbac.ts diff --git a/apps/web/src/components/dashboard-shell.tsx b/apps/web/src/components/dashboard-shell.tsx index a0147e4..a3ddfee 100644 --- a/apps/web/src/components/dashboard-shell.tsx +++ b/apps/web/src/components/dashboard-shell.tsx @@ -15,6 +15,7 @@ import { LayoutGrid, MenuIcon, Server, + Users, Wrench, } from "lucide-react"; import { useEffect, useState } from "react"; @@ -25,30 +26,42 @@ const dashboardLinks = [ label: "Overview", description: "Health, alerts, and recent checks", icon: LayoutGrid, + permission: "dashboard.read", }, { to: "/dashboard/servers", label: "Servers", description: "Inventory and host details", icon: Server, + permission: "servers.read", }, { to: "/dashboard/services", label: "Services", description: "Processes and uptime targets", icon: Wrench, + permission: "services.read", }, { to: "/dashboard/domains", label: "Domains", description: "DNS, SSL, and expiry status", icon: Globe, + permission: "domains.read", }, { to: "/dashboard/notes", label: "Notes", description: "Runbooks and operational handover", icon: BookText, + permission: "notes.read", + }, + { + to: "/dashboard/users", + label: "Users", + description: "Roles and permission assignments", + icon: Users, + permission: "users.read", }, ] as const; @@ -60,14 +73,18 @@ function isActive(pathname: string, to: string): boolean { function NavItems({ pathname, + permissions, onNavigate, }: { pathname: string; + permissions: string[]; onNavigate?: () => void; }) { + const allowedPermissions = new Set(permissions); + return (