diff --git a/dockerfile b/dockerfile index c72b44d..5e35481 100644 --- a/dockerfile +++ b/dockerfile @@ -12,20 +12,30 @@ ENV PATH="/usr/src/app/node_modules/.bin:${PATH}" ENV DATABASE_URL=postgresql://user:pass@localhost:5432/papo?schema=public ENV PRISMA_IGNORE_ENV_LOAD=true -# Install dependencies (inkl. dev) +# Install backend dependencies COPY package*.json ./ RUN npm ci --include=dev -# Ensure prisma CLI available globally (avoids path issues) -RUN npm install -g prisma@5.4.2 +# Install frontend dependencies +COPY frontend/package*.json ./frontend/ +RUN npm --prefix frontend ci # Copy source COPY . . +# Build frontend +RUN npm run build:web + +# Ensure prisma CLI available globally (avoids path issues) +RUN npm install -g prisma@5.4.2 + # Generate Prisma client (explicit schema path) RUN prisma generate --schema=src/database/schema.prisma +# Build backend +RUN npm run build + # Optional: show versions in build log RUN node -v && npm -v && npx prisma -v -CMD ["npm", "run", "dev"] +CMD ["npm", "start"] diff --git a/src/web/server.ts b/src/web/server.ts index 98f3709..c4d1a20 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -2,6 +2,7 @@ import express from 'express'; import session from 'express-session'; import cookieParser from 'cookie-parser'; import path from 'path'; +import fs from 'fs'; import authRouter from './routes/auth'; import dashboardRouter from './routes/dashboard'; import apiRouter from './routes/api'; @@ -11,7 +12,6 @@ export function createWebServer() { const app = express(); const basePath = env.webBasePath || '/ucp'; const dashboardPath = `${basePath}/dashboard`; - const apiPath = `${basePath}/api`; app.use(express.json({ limit: '5mb' })); app.use(cookieParser()); app.use( @@ -24,12 +24,10 @@ export function createWebServer() { const mount = (suffix: string) => (basePath ? `${basePath}${suffix}` : suffix); app.use(mount('/auth'), authRouter); - app.use(dashboardPath, dashboardRouter); app.use(mount('/api'), apiRouter); // fallback mounts if proxy strips base path if (basePath) { app.use('/api', apiRouter); - app.use('/dashboard', dashboardRouter); } // Redirect bare auth calls to the prefixed path when a base path is set @@ -37,36 +35,66 @@ export function createWebServer() { app.use('/auth', (_req, res) => res.redirect(`${basePath}${_req.originalUrl}`)); } - // Landing pages - app.get('/', (_req, res) => res.redirect(dashboardPath)); - app.get(basePath || '/', (_req, res) => { - res.send(` - - - - - - Papo Dashboard - - - -
-

Papo Dashboard

-

Verwalte Tickets, Module und Automod.

- Zum Dashboard -
- - - `); - }); + // Serve React SPA static assets + const frontendDist = path.join(process.cwd(), 'frontend', 'dist'); + + // If SPA exists, it takes precedence for GET dashboard routes + if (fs.existsSync(path.join(frontendDist, 'index.html'))) { + const spaHtml = fs.readFileSync(path.join(frontendDist, 'index.html'), 'utf-8'); + const configScript = `window.__PAPO__ = ${JSON.stringify({ + baseRoot: basePath, + baseApi: mount('/api'), + baseAuth: mount('/auth'), + baseDashboard: dashboardPath + })}`; + + app.use(basePath || '/', express.static(frontendDist)); + + app.get(`${dashboardPath}(/*)?`, (_req, res) => { + res.type('html').send(spaHtml.replace('__PAPO_CONFIG__', configScript)); + }); + + app.get(mount('/'), (_req, res) => { + res.redirect(dashboardPath); + }); + } else { + // Legacy landing page when SPA is not built + app.get(mount('/'), (_req, res) => { + res.send(` + + + + + + Papo Dashboard + + + +
+

Papo Dashboard

+

Verwalte Tickets, Module und Automod.

+ Zum Dashboard +
+ + + `); + }); + } + + // Old dashboard router – only handles POST/non-GET routes when SPA is active, + // or all routes when SPA is inactive + app.use(dashboardPath, dashboardRouter); + if (basePath) { + app.use('/dashboard', dashboardRouter); + } app.use(mount('/static'), express.static(path.join(process.cwd(), 'static'))); return app;