feat: SPA build + serving im Dockerfile und server.ts
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
This commit is contained in:
18
dockerfile
18
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 DATABASE_URL=postgresql://user:pass@localhost:5432/papo?schema=public
|
||||||
ENV PRISMA_IGNORE_ENV_LOAD=true
|
ENV PRISMA_IGNORE_ENV_LOAD=true
|
||||||
|
|
||||||
# Install dependencies (inkl. dev)
|
# Install backend dependencies
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --include=dev
|
RUN npm ci --include=dev
|
||||||
|
|
||||||
# Ensure prisma CLI available globally (avoids path issues)
|
# Install frontend dependencies
|
||||||
RUN npm install -g prisma@5.4.2
|
COPY frontend/package*.json ./frontend/
|
||||||
|
RUN npm --prefix frontend ci
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
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)
|
# Generate Prisma client (explicit schema path)
|
||||||
RUN prisma generate --schema=src/database/schema.prisma
|
RUN prisma generate --schema=src/database/schema.prisma
|
||||||
|
|
||||||
|
# Build backend
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
# Optional: show versions in build log
|
# Optional: show versions in build log
|
||||||
RUN node -v && npm -v && npx prisma -v
|
RUN node -v && npm -v && npx prisma -v
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import authRouter from './routes/auth';
|
import authRouter from './routes/auth';
|
||||||
import dashboardRouter from './routes/dashboard';
|
import dashboardRouter from './routes/dashboard';
|
||||||
import apiRouter from './routes/api';
|
import apiRouter from './routes/api';
|
||||||
@@ -11,7 +12,6 @@ export function createWebServer() {
|
|||||||
const app = express();
|
const app = express();
|
||||||
const basePath = env.webBasePath || '/ucp';
|
const basePath = env.webBasePath || '/ucp';
|
||||||
const dashboardPath = `${basePath}/dashboard`;
|
const dashboardPath = `${basePath}/dashboard`;
|
||||||
const apiPath = `${basePath}/api`;
|
|
||||||
app.use(express.json({ limit: '5mb' }));
|
app.use(express.json({ limit: '5mb' }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(
|
app.use(
|
||||||
@@ -24,12 +24,10 @@ export function createWebServer() {
|
|||||||
|
|
||||||
const mount = (suffix: string) => (basePath ? `${basePath}${suffix}` : suffix);
|
const mount = (suffix: string) => (basePath ? `${basePath}${suffix}` : suffix);
|
||||||
app.use(mount('/auth'), authRouter);
|
app.use(mount('/auth'), authRouter);
|
||||||
app.use(dashboardPath, dashboardRouter);
|
|
||||||
app.use(mount('/api'), apiRouter);
|
app.use(mount('/api'), apiRouter);
|
||||||
// fallback mounts if proxy strips base path
|
// fallback mounts if proxy strips base path
|
||||||
if (basePath) {
|
if (basePath) {
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
app.use('/dashboard', dashboardRouter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect bare auth calls to the prefixed path when a base path is set
|
// 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}`));
|
app.use('/auth', (_req, res) => res.redirect(`${basePath}${_req.originalUrl}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Landing pages
|
// Serve React SPA static assets
|
||||||
app.get('/', (_req, res) => res.redirect(dashboardPath));
|
const frontendDist = path.join(process.cwd(), 'frontend', 'dist');
|
||||||
app.get(basePath || '/', (_req, res) => {
|
|
||||||
res.send(`
|
// If SPA exists, it takes precedence for GET dashboard routes
|
||||||
<!doctype html>
|
if (fs.existsSync(path.join(frontendDist, 'index.html'))) {
|
||||||
<html lang="de">
|
const spaHtml = fs.readFileSync(path.join(frontendDist, 'index.html'), 'utf-8');
|
||||||
<head>
|
const configScript = `window.__PAPO__ = ${JSON.stringify({
|
||||||
<meta charset="UTF-8" />
|
baseRoot: basePath,
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
baseApi: mount('/api'),
|
||||||
<title>Papo Dashboard</title>
|
baseAuth: mount('/auth'),
|
||||||
<style>
|
baseDashboard: dashboardPath
|
||||||
:root { --bg:#0b0f17; --card:rgba(18,20,30,0.72); --text:#f8fafc; --muted:#a5b4c3; --accent:#f97316; --border:rgba(255,255,255,0.06); }
|
})}`;
|
||||||
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; background:radial-gradient(circle at 18% 20%, rgba(249,115,22,0.16), transparent 32%), radial-gradient(circle at 82% -8%, rgba(255,166,99,0.12), transparent 28%), linear-gradient(140deg, #080c15 0%, #0c1220 48%, #080c15 100%); font-family:'Inter', system-ui, sans-serif; color:var(--text); }
|
|
||||||
.shell { padding:32px 36px; border-radius:18px; background:var(--card); border:1px solid var(--border); box-shadow:0 20px 50px rgba(0,0,0,0.45); backdrop-filter:blur(12px); max-width:520px; width:calc(100% - 32px); text-align:center; }
|
app.use(basePath || '/', express.static(frontendDist));
|
||||||
h1 { margin:0 0 10px; font-size:28px; letter-spacing:0.4px; }
|
|
||||||
p { margin:0 0 18px; color:var(--muted); }
|
app.get(`${dashboardPath}(/*)?`, (_req, res) => {
|
||||||
a { display:inline-flex; align-items:center; gap:10px; padding:12px 18px; border-radius:14px; text-decoration:none; font-weight:800; color:white; background:linear-gradient(130deg, #ff9b3d, #f97316); border:1px solid rgba(249,115,22,0.45); box-shadow:0 14px 34px rgba(249,115,22,0.35); transition:transform 140ms ease, box-shadow 140ms ease; }
|
res.type('html').send(spaHtml.replace('__PAPO_CONFIG__', configScript));
|
||||||
a:hover { transform:translateY(-1px); box-shadow:0 16px 40px rgba(249,115,22,0.4); }
|
});
|
||||||
</style>
|
|
||||||
</head>
|
app.get(mount('/'), (_req, res) => {
|
||||||
<body>
|
res.redirect(dashboardPath);
|
||||||
<div class="shell">
|
});
|
||||||
<h1>Papo Dashboard</h1>
|
} else {
|
||||||
<p>Verwalte Tickets, Module und Automod.</p>
|
// Legacy landing page when SPA is not built
|
||||||
<a href="${dashboardPath}/">Zum Dashboard</a>
|
app.get(mount('/'), (_req, res) => {
|
||||||
</div>
|
res.send(`
|
||||||
</body>
|
<!doctype html>
|
||||||
</html>
|
<html lang="de">
|
||||||
`);
|
<head>
|
||||||
});
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Papo Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg:#0b0f17; --card:rgba(18,20,30,0.72); --text:#f8fafc; --muted:#a5b4c3; --accent:#f97316; --border:rgba(255,255,255,0.06); }
|
||||||
|
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; background:radial-gradient(circle at 18% 20%, rgba(249,115,22,0.16), transparent 32%), radial-gradient(circle at 82% -8%, rgba(255,166,99,0.12), transparent 28%), linear-gradient(140deg, #080c15 0%, #0c1220 48%, #080c15 100%); font-family:'Inter', system-ui, sans-serif; color:var(--text); }
|
||||||
|
.shell { padding:32px 36px; border-radius:18px; background:var(--card); border:1px solid var(--border); box-shadow:0 20px 50px rgba(0,0,0,0.45); backdrop-filter:blur(12px); max-width:520px; width:calc(100% - 32px); text-align:center; }
|
||||||
|
h1 { margin:0 0 10px; font-size:28px; letter-spacing:0.4px; }
|
||||||
|
p { margin:0 0 18px; color:var(--muted); }
|
||||||
|
a { display:inline-flex; align-items:center; gap:10px; padding:12px 18px; border-radius:14px; text-decoration:none; font-weight:800; color:white; background:linear-gradient(130deg, #ff9b3d, #f97316); border:1px solid rgba(249,115,22,0.45); box-shadow:0 14px 34px rgba(249,115,22,0.35); transition:transform 140ms ease, box-shadow 140ms ease; }
|
||||||
|
a:hover { transform:translateY(-1px); box-shadow:0 16px 40px rgba(249,115,22,0.4); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<h1>Papo Dashboard</h1>
|
||||||
|
<p>Verwalte Tickets, Module und Automod.</p>
|
||||||
|
<a href="${dashboardPath}/">Zum Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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')));
|
app.use(mount('/static'), express.static(path.join(process.cwd(), 'static')));
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
Reference in New Issue
Block a user