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
This commit is contained in:
riyadhafraa
2026-04-20 09:42:37 +00:00
parent 24850f5597
commit 5debea9e61
9 changed files with 127 additions and 30 deletions
+74 -5
View File
@@ -1,7 +1,13 @@
import { Router, type IRouter } from "express";
import { eq, asc } from "drizzle-orm";
import { eq, asc, inArray } from "drizzle-orm";
import { db } from "@workspace/db";
import { appsTable } from "@workspace/db";
import {
appsTable,
appPermissionsTable,
userRolesTable,
rolePermissionsTable,
rolesTable,
} from "@workspace/db";
import { requireAuth, requireAdmin } from "../middlewares/auth";
import {
CreateAppBody,
@@ -13,13 +19,76 @@ import {
const router: IRouter = Router();
router.get("/apps", requireAuth, async (_req, res): Promise<void> => {
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
const userId = req.session.userId!;
const userRoleRows = await db
.select({ roleName: rolesTable.name, roleId: userRolesTable.roleId })
.from(userRolesTable)
.innerJoin(rolesTable, eq(userRolesTable.roleId, rolesTable.id))
.where(eq(userRolesTable.userId, userId));
const isAdmin = userRoleRows.some((r) => r.roleName === "admin");
if (isAdmin) {
const apps = await db
.select()
.from(appsTable)
.where(eq(appsTable.isActive, true))
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
res.json(apps);
return;
}
const userRoleIds = userRoleRows.map((r) => r.roleId);
const userPermissionIds = userRoleIds.length > 0
? (await db
.select({ permissionId: rolePermissionsTable.permissionId })
.from(rolePermissionsTable)
.where(inArray(rolePermissionsTable.roleId, userRoleIds))
).map((r) => r.permissionId)
: [];
const apps = await db
.select()
.from(appsTable)
.where(eq(appsTable.isActive, true))
.where(
eq(appsTable.isActive, true),
)
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
res.json(apps);
const appsWithPermissions = await db
.select({ appId: appPermissionsTable.appId })
.from(appPermissionsTable);
const restrictedAppIds = new Set(appsWithPermissions.map((r) => r.appId));
if (restrictedAppIds.size === 0) {
const unrestricted = await db
.select()
.from(appsTable)
.where(eq(appsTable.isActive, true))
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
res.json(unrestricted);
return;
}
const allowedRestrictedAppIds = userPermissionIds.length > 0
? (await db
.select({ appId: appPermissionsTable.appId })
.from(appPermissionsTable)
.where(inArray(appPermissionsTable.permissionId, userPermissionIds))
).map((r) => r.appId)
: [];
const allowedAppIdSet = new Set(allowedRestrictedAppIds);
const visibleApps = apps.filter(
(app) => !restrictedAppIds.has(app.id) || allowedAppIdSet.has(app.id),
);
res.json(visibleApps);
});
router.post("/apps", requireAdmin, async (req, res): Promise<void> => {