feat: project scaffold — Docker, NestJS, Nuxt 3, Prisma config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-12 17:05:37 -04:00
parent 99f3c5caab
commit 88dbb99f9d
15 changed files with 308 additions and 0 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
DB_PASSWORD=changeme
JWT_SECRET=generate-a-64-char-hex-string
ENCRYPTION_KEY=generate-a-64-char-hex-string

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
dist/
.output/
.nuxt/
.env
*.log
.DS_Store
backend/prisma/*.db

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# Stage 1: Frontend build
FROM node:20-alpine AS frontend
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npx nuxi generate
# Stage 2: Backend build
FROM node:20-alpine AS backend
WORKDIR /app/backend
COPY backend/package*.json ./
RUN npm ci
COPY backend/ ./
RUN npx prisma generate
RUN npm run build
# Stage 3: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=backend /app/backend/dist ./dist
COPY --from=backend /app/backend/node_modules ./node_modules
COPY --from=backend /app/backend/package.json ./
COPY --from=backend /app/backend/prisma ./prisma
COPY --from=frontend /app/frontend/.output/public ./public
EXPOSE 3000
CMD ["node", "dist/main.js"]

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# Wraith
Self-hosted MobaXterm replacement — SSH + SFTP + RDP in a browser.
## Stack
- **Backend:** NestJS 10, Prisma 6, PostgreSQL 16, ssh2, guacd
- **Frontend:** Nuxt 3 (SPA), PrimeVue 4, Tailwind CSS, xterm.js 5
## Quick Start
```bash
cp .env.example .env
# Edit .env with real secrets
docker compose up -d
```
Default credentials: `admin@wraith.local` / `wraith`
## Development
```bash
# Backend
cd backend && npm install && npm run dev
# Frontend
cd frontend && npm install && npm run dev
```

8
backend/nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

63
backend/package.json Normal file
View File

@ -0,0 +1,63 @@
{
"name": "wraith-backend",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
"dev": "nest start --watch",
"test": "jest",
"test:watch": "jest --watch",
"prisma:migrate": "prisma migrate dev",
"prisma:generate": "prisma generate",
"prisma:seed": "ts-node prisma/seed.ts"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.0.0",
"@nestjs/mapped-types": "^2.0.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-ws": "^10.0.0",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/websockets": "^10.0.0",
"@prisma/client": "^6.0.0",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0",
"ssh2": "^1.15.0",
"ws": "^8.16.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.0",
"@types/node": "^20.0.0",
"@types/passport-jwt": "^4.0.0",
"@types/ssh2": "^1.15.0",
"@types/ws": "^8.5.0",
"jest": "^29.0.0",
"prisma": "^6.0.0",
"ts-jest": "^29.0.0",
"ts-node": "^10.9.0",
"typescript": "^5.3.0"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.ts$": "ts-jest" },
"testEnvironment": "node",
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
}
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*.spec.ts"]
}

22
backend/tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"paths": { "@/*": ["src/*"] }
}
}

36
docker-compose.yml Normal file
View File

@ -0,0 +1,36 @@
services:
app:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgresql://wraith:${DB_PASSWORD}@postgres:5432/wraith
JWT_SECRET: ${JWT_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
GUACD_HOST: guacd
GUACD_PORT: "4822"
depends_on:
postgres:
condition: service_healthy
guacd:
condition: service_started
restart: unless-stopped
guacd:
image: guacamole/guacd
restart: always
postgres:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
environment:
POSTGRES_DB: wraith
POSTGRES_USER: wraith
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wraith"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:

5
frontend/app.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

View File

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body, #__nuxt {
@apply h-full bg-gray-900 text-gray-100;
}

View File

@ -0,0 +1,5 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-950">
<slot />
</div>
</template>

29
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,29 @@
export default defineNuxtConfig({
ssr: false,
devtools: { enabled: false },
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
'@primevue/nuxt-module',
],
css: ['~/assets/css/main.css'],
primevue: {
options: {
theme: 'none',
},
},
runtimeConfig: {
public: {
apiBase: process.env.API_BASE || 'http://localhost:3000',
},
},
devServer: {
port: 3001,
},
nitro: {
devProxy: {
'/api': { target: 'http://localhost:3000/api', ws: true },
'/ws': { target: 'ws://localhost:3000/ws', ws: true },
},
},
})

30
frontend/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "wraith-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "nuxi dev",
"build": "nuxi generate",
"preview": "nuxi preview"
},
"dependencies": {
"@pinia/nuxt": "^0.5.0",
"@primevue/themes": "^4.0.0",
"guacamole-common-js": "^1.5.0",
"lucide-vue-next": "^0.300.0",
"monaco-editor": "^0.45.0",
"pinia": "^2.1.0",
"primevue": "^4.0.0",
"@xterm/xterm": "^5.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.0.0",
"@primevue/nuxt-module": "^4.0.0",
"nuxt": "^3.10.0",
"typescript": "^5.3.0"
}
}

View File

@ -0,0 +1,32 @@
import type { Config } from 'tailwindcss'
export default {
content: [
'./components/**/*.vue',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./composables/**/*.ts',
'./app.vue',
],
darkMode: 'class',
theme: {
extend: {
colors: {
wraith: {
50: '#f0f4ff',
100: '#dbe4ff',
200: '#bac8ff',
300: '#91a7ff',
400: '#748ffc',
500: '#5c7cfa',
600: '#4c6ef5',
700: '#4263eb',
800: '#3b5bdb',
900: '#364fc7',
950: '#1e3a8a',
},
},
},
},
plugins: [],
} satisfies Config