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:
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user