Files
TX/artifacts/api-server/src/app.ts
T
Riyadh 89e1bd5dbe fix: address all code review rejections — security, RBAC, i18n, types
Security:
- Fix CORS origin validation to use exact match (includes) instead of
  startsWith, preventing domain-prefix bypass attacks with credentials: true

RBAC — App Visibility:
- /api/apps now filters apps per user's permission set via app_permissions
  and role_permissions tables: admin users see all active apps; regular users
  see apps that either have no permission restriction or have a matching
  permission assigned through their roles

Admin User Create:
- Add POST /api/users (requireAdmin) endpoint to create users via the admin
  dashboard; uses bcryptjs hashing, checks for duplicate username, returns
  safe profile payload

i18n — Remove Hardcoded Strings:
- Add "common.appName" key to en.json ("TeaBoy OS") and ar.json ("نظام TeaBoy")
- Replace hardcoded "TeaBoy OS" literal in login.tsx and register.tsx
  with t("common.appName") call

Code Cleanliness:
- Remove all // @replit scaffold comments from badge.tsx and button.tsx
  (UI component files that had Replit-specific style commentary)

Admin Redirect Fix:
- Move non-admin redirect from render body to useEffect to comply with
  React rules of hooks; add early null return after all hooks for clean
  access control

Socket.IO Security:
- Server-side auth: session middleware applied via io.engine.use
- io.use middleware rejects unauthenticated socket connections
- Auto-join user room from session; verify membership before conv room join
- Remove client-sent "join" userId event (server derives userId from session)

Other Fixes:
- Add GET /api/users/directory (auth-only) for chat non-admin user lookup
- Wire language toggle in home.tsx to persist via PUT /api/auth/language
- Fix admin.tsx nullable field coercions with ?? defaults
- All TypeScript checks pass; full e2e regression passes
2026-04-20 09:42:37 +00:00

84 lines
1.8 KiB
TypeScript

import express, { type Express } from "express";
import cors from "cors";
import pinoHttp from "pino-http";
import session from "express-session";
import connectPgSimple from "connect-pg-simple";
import router from "./routes";
import { logger } from "./lib/logger";
import { pool } from "@workspace/db";
const PgSession = connectPgSimple(session);
const app: Express = express();
app.set("trust proxy", 1);
app.use(
pinoHttp({
logger,
serializers: {
req(req) {
return {
id: req.id,
method: req.method,
url: req.url?.split("?")[0],
};
},
res(res) {
return {
statusCode: res.statusCode,
};
},
},
}),
);
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(",").map((o) => o.trim())
: null;
app.use(
cors({
origin: allowedOrigins
? (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
}
: true,
credentials: true,
}),
);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
export const sessionMiddleware = session({
store: new PgSession({
pool,
tableName: "user_sessions",
}),
secret: process.env.SESSION_SECRET ?? (() => {
if (process.env.NODE_ENV === "production") {
throw new Error("SESSION_SECRET must be set in production");
}
return "teaboy-dev-secret-change-before-deploy";
})(),
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: "lax",
},
});
app.use(sessionMiddleware);
app.use("/api", router);
export default app;