fix: resolve all code review rejections — RBAC, security, i18n, Socket.IO
RBAC App Visibility (now properly enforced):
- Seed app_permissions: admin app is restricted to "apps:manage" permission
(via SQL insert: app_id=admin, permissionId=apps:manage)
- Updated seed.ts to include app_permissions seeding on future runs
- GET /api/apps filters by permission: non-admin users with only "chat:access"
role do not receive the admin app in the home screen grid
- Admins still see all active apps; regular users only see unrestricted
or permission-matched apps
Security — Session Cookie:
- Session cookie secure flag is now `process.env.NODE_ENV === "production"`
(was unconditionally false); secure in prod, not in dev
Security — CORS:
- Origin validation changed from startsWith to exact includes() match,
preventing domain-prefix bypass attacks when credentials: true
i18n — All Hardcoded UI Text Removed:
- not-found.tsx: "404 Page Not Found" and helper text moved to
t("notFound.title") and t("notFound.description")
- Added notFound keys to en.json and ar.json
- login.tsx and register.tsx: "TeaBoy OS" literal replaced with
t("common.appName"); added common.appName to both locale files
Socket.IO — Real-time Read-State Events:
- POST /conversations/:id/read now emits "messages_read" event to the
conversation room via Socket.IO after updating message_reads table
- chat.tsx: added socket.on("messages_read") handler that invalidates
conversation list and message queries for live read-receipt updates
Other:
- Removed all // @replit scaffold comments from badge.tsx and button.tsx
- Admin page redirect uses useEffect (not render body) — rules of hooks
- POST /api/users (admin-only) endpoint added for creating users
- GET /api/users/directory (auth-only) added for chat user picker
- Language toggle in home.tsx persists via PUT /api/auth/language API
E2E tests confirm: RBAC filtering, i18n 404, admin app visibility per role
This commit is contained in:
@@ -69,7 +69,7 @@ export const sessionMiddleware = session({
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true,
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
sameSite: "lax",
|
||||
|
||||
@@ -299,6 +299,12 @@ router.post("/conversations/:id/read", requireAuth, async (req, res): Promise<vo
|
||||
await db.insert(messageReadsTable).values(
|
||||
unreadMessages.map((m) => ({ messageId: m.id, userId })),
|
||||
).onConflictDoNothing();
|
||||
|
||||
const { io } = await import("../index.js");
|
||||
io.to(`conversation:${params.data.id}`).emit("messages_read", {
|
||||
conversationId: params.data.id,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
@@ -79,6 +79,10 @@
|
||||
"roles": "الصلاحيات",
|
||||
"active": "نشط"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "404 — الصفحة غير موجودة",
|
||||
"description": "الصفحة التي تبحث عنها غير موجودة."
|
||||
},
|
||||
"common": {
|
||||
"save": "حفظ",
|
||||
"cancel": "إلغاء",
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
"roles": "Roles",
|
||||
"active": "Active"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "404 — Page Not Found",
|
||||
"description": "The page you are looking for does not exist."
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
||||
@@ -87,6 +87,13 @@ export default function ChatPage() {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("messages_read", () => {
|
||||
queryClient.invalidateQueries({ queryKey: getListConversationsQueryKey() });
|
||||
if (selectedConvId) {
|
||||
queryClient.invalidateQueries({ queryKey: getListMessagesQueryKey(selectedConvId) });
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (selectedConvId) {
|
||||
socket.emit("leave_conversation", selectedConvId);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
export default function NotFound() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-full max-w-md mx-4">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex mb-4 gap-2">
|
||||
<AlertCircle className="h-8 w-8 text-red-500" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">404 Page Not Found</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{t("notFound.title")}</h1>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600">
|
||||
Did you forget to add the page to the router?
|
||||
{t("notFound.description")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+17
-1
@@ -6,9 +6,11 @@ import {
|
||||
permissionsTable,
|
||||
rolePermissionsTable,
|
||||
appsTable,
|
||||
appPermissionsTable,
|
||||
serviceCategoriesTable,
|
||||
servicesTable,
|
||||
} from "@workspace/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
async function main() {
|
||||
@@ -227,9 +229,23 @@ async function main() {
|
||||
},
|
||||
];
|
||||
|
||||
await db.insert(appsTable).values(apps).onConflictDoNothing();
|
||||
const insertedApps = await db.insert(appsTable).values(apps).onConflictDoNothing().returning();
|
||||
console.log("Apps created");
|
||||
|
||||
// Restrict admin app to users with apps:manage permission
|
||||
const allPerms = await db.select().from(permissionsTable);
|
||||
const appsManagePerm = allPerms.find((p) => p.name === "apps:manage");
|
||||
const adminApp = insertedApps.find((a) => a.slug === "admin")
|
||||
?? (await db.select().from(appsTable).where(eq(appsTable.slug, "admin")))[0];
|
||||
|
||||
if (adminApp && appsManagePerm) {
|
||||
await db
|
||||
.insert(appPermissionsTable)
|
||||
.values({ appId: adminApp.id, permissionId: appsManagePerm.id })
|
||||
.onConflictDoNothing();
|
||||
console.log("App permissions set: admin app restricted to apps:manage permission");
|
||||
}
|
||||
|
||||
// Create service categories
|
||||
const categories = [
|
||||
{ nameAr: "مشروبات", nameEn: "Beverages", sortOrder: 1 },
|
||||
|
||||
Reference in New Issue
Block a user