diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cba0585 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DB_PASSWORD=changeme +JWT_SECRET=generate-a-64-char-hex-string +ENCRYPTION_KEY=generate-a-64-char-hex-string diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0237449 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.output/ +.nuxt/ +.env +*.log +.DS_Store +backend/prisma/*.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ec2111 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5020a56 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..045d98d --- /dev/null +++ b/backend/package.json @@ -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": { + "^@/(.*)$": "/src/$1" + } + } +} diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 0000000..cc01185 --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*.spec.ts"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..c01dd18 --- /dev/null +++ b/backend/tsconfig.json @@ -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/*"] } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c7c3d52 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/frontend/app.vue b/frontend/app.vue new file mode 100644 index 0000000..f8eacfa --- /dev/null +++ b/frontend/app.vue @@ -0,0 +1,5 @@ + diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css new file mode 100644 index 0000000..c7303f9 --- /dev/null +++ b/frontend/assets/css/main.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body, #__nuxt { + @apply h-full bg-gray-900 text-gray-100; +} diff --git a/frontend/layouts/auth.vue b/frontend/layouts/auth.vue new file mode 100644 index 0000000..8f4529b --- /dev/null +++ b/frontend/layouts/auth.vue @@ -0,0 +1,5 @@ + diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts new file mode 100644 index 0000000..4ad4390 --- /dev/null +++ b/frontend/nuxt.config.ts @@ -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 }, + }, + }, +}) diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..bb717b7 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..131a394 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -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