feat: build TeaBoy OS — bilingual Arabic/English internal OS platform

Completed full-stack TeaBoy OS build:

Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
  conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)

Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
  4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies

Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded

All e2e tests pass: login, home screen, services, language toggle, admin, logout
This commit is contained in:
riyadhafraa
2026-04-20 09:20:50 +00:00
parent 593ce6a418
commit 001e9023b2
115 changed files with 14720 additions and 79 deletions
+6
View File
@@ -5,10 +5,16 @@
"type": "module",
"scripts": {
"hello": "tsx ./src/hello.ts",
"seed": "tsx ./src/seed.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@workspace/db": "workspace:*"
},
"devDependencies": {
"@types/bcryptjs": "^3.0.0",
"@types/node": "catalog:",
"bcryptjs": "^3.0.3",
"tsx": "catalog:"
}
}
+326
View File
@@ -0,0 +1,326 @@
import { db } from "@workspace/db";
import {
usersTable,
rolesTable,
userRolesTable,
permissionsTable,
rolePermissionsTable,
appsTable,
serviceCategoriesTable,
servicesTable,
} from "@workspace/db";
import bcrypt from "bcryptjs";
async function main() {
console.log("Seeding database...");
// Create roles
const [adminRole] = await db
.insert(rolesTable)
.values({ name: "admin", descriptionAr: "مدير النظام", descriptionEn: "System Administrator" })
.onConflictDoNothing()
.returning();
const [userRole] = await db
.insert(rolesTable)
.values({ name: "user", descriptionAr: "مستخدم عادي", descriptionEn: "Regular User" })
.onConflictDoNothing()
.returning();
console.log("Roles created");
// Create permissions
const permissions = [
{ name: "apps:manage", descriptionAr: "إدارة التطبيقات", descriptionEn: "Manage Apps" },
{ name: "services:manage", descriptionAr: "إدارة الخدمات", descriptionEn: "Manage Services" },
{ name: "users:manage", descriptionAr: "إدارة المستخدمين", descriptionEn: "Manage Users" },
{ name: "chat:access", descriptionAr: "الوصول للمحادثات", descriptionEn: "Access Chat" },
];
const insertedPerms = await db
.insert(permissionsTable)
.values(permissions)
.onConflictDoNothing()
.returning();
console.log("Permissions created");
// Assign all permissions to admin role
if (adminRole && insertedPerms.length > 0) {
await db
.insert(rolePermissionsTable)
.values(insertedPerms.map((p) => ({ roleId: adminRole.id, permissionId: p.id })))
.onConflictDoNothing();
}
// Assign chat permission to user role
if (userRole && insertedPerms.length > 0) {
const chatPerm = insertedPerms.find((p) => p.name === "chat:access");
if (chatPerm) {
await db
.insert(rolePermissionsTable)
.values({ roleId: userRole.id, permissionId: chatPerm.id })
.onConflictDoNothing();
}
}
// Create admin user
const adminHash = await bcrypt.hash("admin123", 10);
const [adminUser] = await db
.insert(usersTable)
.values({
username: "admin",
email: "admin@teaboy.local",
passwordHash: adminHash,
displayNameAr: "مدير النظام",
displayNameEn: "System Admin",
preferredLanguage: "ar",
isActive: true,
})
.onConflictDoNothing()
.returning();
// Create regular user
const userHash = await bcrypt.hash("user123", 10);
const [regularUser] = await db
.insert(usersTable)
.values({
username: "ahmed",
email: "ahmed@teaboy.local",
passwordHash: userHash,
displayNameAr: "أحمد محمد",
displayNameEn: "Ahmed Mohammed",
preferredLanguage: "ar",
isActive: true,
})
.onConflictDoNothing()
.returning();
console.log("Users created");
// Assign roles
const roles = await db.select().from(rolesTable);
const adminRoleRow = roles.find((r) => r.name === "admin");
const userRoleRow = roles.find((r) => r.name === "user");
if (adminUser && adminRoleRow) {
await db
.insert(userRolesTable)
.values({ userId: adminUser.id, roleId: adminRoleRow.id })
.onConflictDoNothing();
}
if (regularUser && userRoleRow) {
await db
.insert(userRolesTable)
.values({ userId: regularUser.id, roleId: userRoleRow.id })
.onConflictDoNothing();
}
console.log("Roles assigned");
// Create apps
const apps = [
{
slug: "services",
nameAr: "خدماتي",
nameEn: "My Services",
descriptionAr: "اطلب خدماتك الداخلية بسهولة",
descriptionEn: "Order your internal services easily",
iconName: "Coffee",
route: "/services",
color: "#f59e0b",
isActive: true,
isSystem: true,
sortOrder: 1,
},
{
slug: "chat",
nameAr: "المحادثات",
nameEn: "Chat",
descriptionAr: "تواصل مع زملائك",
descriptionEn: "Connect with your colleagues",
iconName: "MessageCircle",
route: "/chat",
color: "#22c55e",
isActive: true,
isSystem: true,
sortOrder: 2,
},
{
slug: "notifications",
nameAr: "الإشعارات",
nameEn: "Notifications",
descriptionAr: "إدارة الإشعارات والتنبيهات",
descriptionEn: "Manage your notifications and alerts",
iconName: "Bell",
route: "/notifications",
color: "#8b5cf6",
isActive: true,
isSystem: true,
sortOrder: 3,
},
{
slug: "admin",
nameAr: "لوحة التحكم",
nameEn: "Admin Panel",
descriptionAr: "إدارة النظام والمستخدمين",
descriptionEn: "System and user management",
iconName: "Settings",
route: "/admin",
color: "#ef4444",
isActive: true,
isSystem: true,
sortOrder: 4,
},
{
slug: "directory",
nameAr: "الدليل الداخلي",
nameEn: "Directory",
descriptionAr: "دليل الموظفين والأقسام",
descriptionEn: "Employees and departments directory",
iconName: "Users",
route: "/directory",
color: "#06b6d4",
isActive: true,
isSystem: false,
sortOrder: 5,
},
{
slug: "calendar",
nameAr: "التقويم",
nameEn: "Calendar",
descriptionAr: "الاجتماعات والمواعيد",
descriptionEn: "Meetings and appointments",
iconName: "Calendar",
route: "/calendar",
color: "#3b82f6",
isActive: true,
isSystem: false,
sortOrder: 6,
},
{
slug: "announcements",
nameAr: "الإعلانات",
nameEn: "Announcements",
descriptionAr: "أخبار وإعلانات الشركة",
descriptionEn: "Company news and announcements",
iconName: "Megaphone",
route: "/announcements",
color: "#ec4899",
isActive: true,
isSystem: false,
sortOrder: 7,
},
{
slug: "documents",
nameAr: "المستندات",
nameEn: "Documents",
descriptionAr: "مستودع المستندات والملفات",
descriptionEn: "Documents and files repository",
iconName: "FileText",
route: "/documents",
color: "#64748b",
isActive: true,
isSystem: false,
sortOrder: 8,
},
];
await db.insert(appsTable).values(apps).onConflictDoNothing();
console.log("Apps created");
// Create service categories
const categories = [
{ nameAr: "مشروبات", nameEn: "Beverages", sortOrder: 1 },
{ nameAr: "أغذية", nameEn: "Food", sortOrder: 2 },
{ nameAr: "أخرى", nameEn: "Other", sortOrder: 3 },
];
const insertedCategories = await db
.insert(serviceCategoriesTable)
.values(categories)
.onConflictDoNothing()
.returning();
const beveragesCat = insertedCategories[0];
// Create services
const services = [
{
categoryId: beveragesCat?.id ?? null,
nameAr: "شاي",
nameEn: "Tea",
descriptionAr: "شاي طازج ومنعش",
descriptionEn: "Fresh and refreshing tea",
price: "0.00",
isAvailable: true,
sortOrder: 1,
},
{
categoryId: beveragesCat?.id ?? null,
nameAr: "قهوة عربية",
nameEn: "Arabic Coffee",
descriptionAr: "قهوة عربية أصيلة بالهيل",
descriptionEn: "Authentic Arabic coffee with cardamom",
price: "0.00",
isAvailable: true,
sortOrder: 2,
},
{
categoryId: beveragesCat?.id ?? null,
nameAr: "ماء",
nameEn: "Water",
descriptionAr: "ماء صافٍ منعش",
descriptionEn: "Cool refreshing water",
price: "0.00",
isAvailable: true,
sortOrder: 3,
},
{
categoryId: beveragesCat?.id ?? null,
nameAr: "قهوة نسكافيه",
nameEn: "Nescafe",
descriptionAr: "قهوة نسكافيه سريعة التحضير",
descriptionEn: "Quick Nescafe coffee",
price: "0.00",
isAvailable: true,
sortOrder: 4,
},
{
categoryId: beveragesCat?.id ?? null,
nameAr: "عصير طازج",
nameEn: "Fresh Juice",
descriptionAr: "عصائر طازجة متنوعة",
descriptionEn: "Various fresh juices",
price: "5.00",
isAvailable: true,
sortOrder: 5,
},
{
categoryId: beveragesCat?.id ?? null,
nameAr: "مشروب غازي",
nameEn: "Soft Drink",
descriptionAr: "مشروبات غازية باردة",
descriptionEn: "Cold carbonated drinks",
price: "3.00",
isAvailable: false,
sortOrder: 6,
},
];
await db.insert(servicesTable).values(services).onConflictDoNothing();
console.log("Services created");
console.log("\n✅ Seeding complete!");
console.log("\nDemo accounts:");
console.log(" Admin: username=admin, password=admin123");
console.log(" User: username=ahmed, password=user123");
process.exit(0);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});