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:
@@ -360,7 +360,7 @@ export async function customFetch<T = unknown>(
|
||||
|
||||
const requestInfo = { method, url: resolveUrl(input) };
|
||||
|
||||
const response = await fetch(input, { ...init, method, headers });
|
||||
const response = await fetch(input, { ...init, method, headers, credentials: "include" });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await parseErrorBody(response, method);
|
||||
|
||||
@@ -2,9 +2,265 @@
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* TeaBoy OS API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface SuccessResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterBody {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
/** @nullable */
|
||||
displayNameAr?: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
preferredLanguage?: string;
|
||||
}
|
||||
|
||||
export interface LoginBody {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UpdateLanguageBody {
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
/** @nullable */
|
||||
displayNameAr?: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
preferredLanguage: string;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
roles: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
/** @nullable */
|
||||
displayNameAr?: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
preferredLanguage: string;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
roles: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserBody {
|
||||
/** @nullable */
|
||||
displayNameAr?: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
isActive?: boolean;
|
||||
preferredLanguage?: string;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: number;
|
||||
slug: string;
|
||||
nameAr: string;
|
||||
nameEn: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
iconName: string;
|
||||
route: string;
|
||||
color: string;
|
||||
isActive: boolean;
|
||||
isSystem: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateAppBody {
|
||||
slug: string;
|
||||
nameAr: string;
|
||||
nameEn: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
iconName: string;
|
||||
route: string;
|
||||
color: string;
|
||||
isActive?: boolean;
|
||||
isSystem?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface UpdateAppBody {
|
||||
nameAr?: string;
|
||||
nameEn?: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
iconName?: string;
|
||||
route?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: number;
|
||||
/** @nullable */
|
||||
categoryId?: number | null;
|
||||
nameAr: string;
|
||||
nameEn: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
/** @nullable */
|
||||
imageUrl?: string | null;
|
||||
/** @nullable */
|
||||
price?: string | null;
|
||||
isAvailable: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateServiceBody {
|
||||
/** @nullable */
|
||||
categoryId?: number | null;
|
||||
nameAr: string;
|
||||
nameEn: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
/** @nullable */
|
||||
imageUrl?: string | null;
|
||||
/** @nullable */
|
||||
price?: string | null;
|
||||
isAvailable?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface UpdateServiceBody {
|
||||
/** @nullable */
|
||||
categoryId?: number | null;
|
||||
nameAr?: string;
|
||||
nameEn?: string;
|
||||
/** @nullable */
|
||||
descriptionAr?: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn?: string | null;
|
||||
/** @nullable */
|
||||
imageUrl?: string | null;
|
||||
/** @nullable */
|
||||
price?: string | null;
|
||||
isAvailable?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface ServiceCategory {
|
||||
id: number;
|
||||
nameAr: string;
|
||||
nameEn: string;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ParticipantInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
displayNameAr?: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface MessageWithSender {
|
||||
id: number;
|
||||
conversationId: number;
|
||||
senderId: number;
|
||||
content: string;
|
||||
sender: ParticipantInfo;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ConversationWithDetails {
|
||||
id: number;
|
||||
/** @nullable */
|
||||
nameAr?: string | null;
|
||||
/** @nullable */
|
||||
nameEn?: string | null;
|
||||
isGroup: boolean;
|
||||
createdBy: number;
|
||||
participants: ParticipantInfo[];
|
||||
lastMessage?: MessageWithSender | null;
|
||||
unreadCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateConversationBody {
|
||||
participantIds: number[];
|
||||
/** @nullable */
|
||||
nameAr?: string | null;
|
||||
/** @nullable */
|
||||
nameEn?: string | null;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
export interface SendMessageBody {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
userId: number;
|
||||
titleAr: string;
|
||||
titleEn: string;
|
||||
/** @nullable */
|
||||
bodyAr?: string | null;
|
||||
/** @nullable */
|
||||
bodyEn?: string | null;
|
||||
type: string;
|
||||
isRead: boolean;
|
||||
/** @nullable */
|
||||
relatedId?: number | null;
|
||||
/** @nullable */
|
||||
relatedType?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface HomeStats {
|
||||
totalApps: number;
|
||||
totalServices: number;
|
||||
unreadNotifications: number;
|
||||
unreadMessages: number;
|
||||
totalUsers: number;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user