From 9254469ef4cdc9670d78ab16bf0e37c70c08278c Mon Sep 17 00:00:00 2001 From: paulji peters Date: Tue, 21 Oct 2025 04:28:13 +0100 Subject: [PATCH] Remove onelga-local-services project from main repo --- projects/README.md | 5 + projects/onelga-local-services/README.md | 148 ------------------ .../backend/.env.example | 9 -- .../backend/.eslintrc.json | 26 --- .../onelga-local-services/backend/.gitignore | 4 - .../backend/package.json | 48 ------ .../backend/prisma/schema.prisma | 94 ----------- .../backend/prisma/seed.ts | 81 ---------- .../backend/scripts/seed-test-staff.ts | 33 ---- .../backend/scripts/seed-test.ts | 97 ------------ .../backend/scripts/smoke-test.ts | 53 ------- .../backend/scripts/unlock-user.ts | 30 ---- .../onelga-local-services/backend/src/app.ts | 24 --- .../backend/src/config/env.ts | 32 ---- .../src/controllers/adminController.ts | 31 ---- .../backend/src/controllers/authController.ts | 90 ----------- .../backend/src/controllers/newsController.ts | 50 ------ .../backend/src/middleware/auth.ts | 42 ----- .../backend/src/routes/adminRoutes.ts | 16 -- .../backend/src/routes/authRoutes.ts | 11 -- .../backend/src/routes/index.ts | 12 -- .../backend/src/routes/newsRoutes.ts | 26 --- .../backend/src/server.ts | 19 --- .../src/services/applicationService.ts | 43 ----- .../backend/src/services/newsService.ts | 60 ------- .../backend/src/services/userService.ts | 77 --------- .../backend/src/utils/prisma.ts | 5 - .../backend/src/utils/token.ts | 15 -- .../backend/tsconfig.json | 28 ---- .../onelga-local-services/docker-compose.yml | 17 -- .../frontend/.env.example | 1 - .../frontend/.eslintrc.json | 22 --- .../onelga-local-services/frontend/.gitignore | 3 - .../onelga-local-services/frontend/index.html | 12 -- .../frontend/package.json | 38 ----- .../frontend/src/App.tsx | 37 ----- .../frontend/src/components/AppLayout.tsx | 49 ------ .../src/components/ProtectedRoute.tsx | 23 --- .../frontend/src/main.tsx | 37 ----- .../frontend/src/pages/HomePage.tsx | 29 ---- .../frontend/src/pages/LoginPage.tsx | 53 ------- .../frontend/src/pages/NewsPage.tsx | 40 ----- .../pages/dashboards/AdminDashboardPage.tsx | 48 ------ .../pages/dashboards/NewsManagementPage.tsx | 42 ----- .../pages/dashboards/ServiceRequestsPage.tsx | 52 ------ .../pages/dashboards/StaffDashboardPage.tsx | 23 --- .../frontend/src/services/api.ts | 52 ------ .../frontend/src/store/hooks.ts | 5 - .../frontend/src/store/index.ts | 11 -- .../frontend/src/store/slices/authSlice.ts | 30 ---- .../frontend/src/types/index.ts | 25 --- .../frontend/tsconfig.json | 21 --- .../frontend/tsconfig.node.json | 9 -- .../frontend/vite.config.ts | 15 -- 54 files changed, 5 insertions(+), 1898 deletions(-) create mode 100644 projects/README.md delete mode 100644 projects/onelga-local-services/README.md delete mode 100644 projects/onelga-local-services/backend/.env.example delete mode 100644 projects/onelga-local-services/backend/.eslintrc.json delete mode 100644 projects/onelga-local-services/backend/.gitignore delete mode 100644 projects/onelga-local-services/backend/package.json delete mode 100644 projects/onelga-local-services/backend/prisma/schema.prisma delete mode 100644 projects/onelga-local-services/backend/prisma/seed.ts delete mode 100644 projects/onelga-local-services/backend/scripts/seed-test-staff.ts delete mode 100644 projects/onelga-local-services/backend/scripts/seed-test.ts delete mode 100644 projects/onelga-local-services/backend/scripts/smoke-test.ts delete mode 100644 projects/onelga-local-services/backend/scripts/unlock-user.ts delete mode 100644 projects/onelga-local-services/backend/src/app.ts delete mode 100644 projects/onelga-local-services/backend/src/config/env.ts delete mode 100644 projects/onelga-local-services/backend/src/controllers/adminController.ts delete mode 100644 projects/onelga-local-services/backend/src/controllers/authController.ts delete mode 100644 projects/onelga-local-services/backend/src/controllers/newsController.ts delete mode 100644 projects/onelga-local-services/backend/src/middleware/auth.ts delete mode 100644 projects/onelga-local-services/backend/src/routes/adminRoutes.ts delete mode 100644 projects/onelga-local-services/backend/src/routes/authRoutes.ts delete mode 100644 projects/onelga-local-services/backend/src/routes/index.ts delete mode 100644 projects/onelga-local-services/backend/src/routes/newsRoutes.ts delete mode 100644 projects/onelga-local-services/backend/src/server.ts delete mode 100644 projects/onelga-local-services/backend/src/services/applicationService.ts delete mode 100644 projects/onelga-local-services/backend/src/services/newsService.ts delete mode 100644 projects/onelga-local-services/backend/src/services/userService.ts delete mode 100644 projects/onelga-local-services/backend/src/utils/prisma.ts delete mode 100644 projects/onelga-local-services/backend/src/utils/token.ts delete mode 100644 projects/onelga-local-services/backend/tsconfig.json delete mode 100644 projects/onelga-local-services/docker-compose.yml delete mode 100644 projects/onelga-local-services/frontend/.env.example delete mode 100644 projects/onelga-local-services/frontend/.eslintrc.json delete mode 100644 projects/onelga-local-services/frontend/.gitignore delete mode 100644 projects/onelga-local-services/frontend/index.html delete mode 100644 projects/onelga-local-services/frontend/package.json delete mode 100644 projects/onelga-local-services/frontend/src/App.tsx delete mode 100644 projects/onelga-local-services/frontend/src/components/AppLayout.tsx delete mode 100644 projects/onelga-local-services/frontend/src/components/ProtectedRoute.tsx delete mode 100644 projects/onelga-local-services/frontend/src/main.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/HomePage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/LoginPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/NewsPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/dashboards/AdminDashboardPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/dashboards/NewsManagementPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/dashboards/ServiceRequestsPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/pages/dashboards/StaffDashboardPage.tsx delete mode 100644 projects/onelga-local-services/frontend/src/services/api.ts delete mode 100644 projects/onelga-local-services/frontend/src/store/hooks.ts delete mode 100644 projects/onelga-local-services/frontend/src/store/index.ts delete mode 100644 projects/onelga-local-services/frontend/src/store/slices/authSlice.ts delete mode 100644 projects/onelga-local-services/frontend/src/types/index.ts delete mode 100644 projects/onelga-local-services/frontend/tsconfig.json delete mode 100644 projects/onelga-local-services/frontend/tsconfig.node.json delete mode 100644 projects/onelga-local-services/frontend/vite.config.ts diff --git a/projects/README.md b/projects/README.md new file mode 100644 index 0000000..f7eee30 --- /dev/null +++ b/projects/README.md @@ -0,0 +1,5 @@ +# Projects + +The `onelga-local-services` project has been moved to a dedicated repository at `../onelga-local-services`. + +Please clone or copy that repository to continue working on the application outside of the main `awesome-copilot` repo. diff --git a/projects/onelga-local-services/README.md b/projects/onelga-local-services/README.md deleted file mode 100644 index 7b5cd37..0000000 --- a/projects/onelga-local-services/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Onelga Local Services - -Onelga Local Services is a full-stack platform that demonstrates a municipal services portal with authentication, admin tooling, and citizen-facing news. - -## Project Structure - -``` -projects/onelga-local-services/ -├── backend/ # Node.js + Express + Prisma API -├── frontend/ # React + TypeScript dashboard and citizen portal -└── docker-compose.yml -``` - -## Getting Started - -### Prerequisites - -- Node.js 18+ -- pnpm, npm, or yarn -- (Optional) Docker Desktop if you prefer running Postgres locally via Docker Compose - -### Backend Setup - -1. Install dependencies: - ```bash - cd projects/onelga-local-services/backend - npm install - ``` -2. Copy environment variables: - ```bash - cp .env.example .env - ``` - Update the values if needed. The default configuration uses SQLite. -3. Generate the Prisma client and apply migrations: - ```bash - npx prisma migrate dev --name init - ``` -4. Seed demo data (admin, staff, and citizen accounts plus sample content): - ```bash - npm run prisma:seed - ``` -5. Start the development server: - ```bash - npm run dev - ``` - The API will be available at `http://localhost:4000`. - -### Frontend Setup - -1. Install dependencies: - ```bash - cd projects/onelga-local-services/frontend - npm install - ``` -2. Copy environment variables: - ```bash - cp .env.example .env - ``` - Adjust `VITE_API_URL` if your backend runs on a different URL. -3. Start the development server: - ```bash - npm run dev - ``` - Open the app at `http://localhost:5173`. - -### Demo Credentials - -Use the seeded accounts to explore the platform locally: - -| Role | Email | Password | -| ------ | -------------------- | ---------- | -| Admin | `admin@onelga.local` | `Passw0rd!` | -| Staff | `staff@onelga.local` | `Passw0rd!` | -| Citizen| `citizen@onelga.local` | `Passw0rd!` | - -### Smoke Test Script - -After starting the backend, run a quick health check: - -```bash -cd projects/onelga-local-services/backend -npx ts-node scripts/smoke-test.ts -``` - -The script signs in with the admin account, fetches dashboard stats, and retrieves the news feed. - -### Unlocking Accounts - -If you lock an account due to repeated failed logins, reset it with: - -```bash -cd projects/onelga-local-services/backend -npm run unlock:user -- user@example.com -``` - -### Docker Compose (Optional) - -A `docker-compose.yml` file is provided to run Postgres locally. Update `DATABASE_URL` in your backend `.env` before starting. - -```bash -cd projects/onelga-local-services -docker-compose up -d -``` - -Run `npx prisma migrate deploy` against the Postgres instance and reseed data. - -## Available Scripts - -### Backend - -- `npm run dev` — Start the Express server with hot reload. -- `npm run build` — Compile TypeScript output. -- `npm run prisma:seed` — Seed baseline data. -- `npm run seed:test` — Seed extended demo data. -- `npm run unlock:user -- ` — Clear failed login attempts for a user. -- `npm run typecheck` / `npm run lint` — Static analysis. - -### Frontend - -- `npm run dev` — Start the Vite dev server. -- `npm run build` — Build production assets. -- `npm run typecheck` / `npm run lint` — Static analysis. - -## API Overview - -Key backend endpoints are namespaced under `/api`: - -- `POST /api/auth/login` — Authenticate a user. -- `GET /api/auth/validate` — Validate a JWT. -- `GET /api/admin/stats` — Admin dashboard metrics. -- `GET /api/admin/applications` — List service applications. -- `POST /api/admin/applications/:id/decide` — Approve or reject an application. -- `GET /api/news` — Public news feed. -- `GET /api/news/admin/articles` — Admin news management (requires admin or staff role). - -Refer to the source code for the full set of routes and controllers. - -## Testing Checklist - -1. Run `npm run typecheck` in both `backend` and `frontend`. -2. Start the backend and frontend dev servers. -3. Sign in with the admin account. -4. Navigate to `/admin`, `/admin/news`, and `/admin/requests` to confirm data loads. -5. Execute the smoke test script. - -## License - -MIT diff --git a/projects/onelga-local-services/backend/.env.example b/projects/onelga-local-services/backend/.env.example deleted file mode 100644 index 0c84954..0000000 --- a/projects/onelga-local-services/backend/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -PORT=4000 -DATABASE_URL="file:./dev.db" -JWT_SECRET="your-jwt-secret" -JWT_EXPIRES_IN="1d" -FRONTEND_URL="http://localhost:5173" -SMTP_HOST="smtp.example.com" -SMTP_PORT=587 -SMTP_USER="username" -SMTP_PASS="password" diff --git a/projects/onelga-local-services/backend/.eslintrc.json b/projects/onelga-local-services/backend/.eslintrc.json deleted file mode 100644 index 12be062..0000000 --- a/projects/onelga-local-services/backend/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "es2021": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:import/recommended", - "plugin:promise/recommended", - "plugin:jsdoc/recommended", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": ["./tsconfig.json"], - "tsconfigRootDir": __dirname, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint", "prettier"], - "rules": { - "prettier/prettier": "error", - "import/no-unresolved": "off", - "jsdoc/require-jsdoc": "off" - }, - "ignorePatterns": ["dist", "node_modules"] -} diff --git a/projects/onelga-local-services/backend/.gitignore b/projects/onelga-local-services/backend/.gitignore deleted file mode 100644 index b098fa4..0000000 --- a/projects/onelga-local-services/backend/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/node_modules -/dist -/.env -/prisma/dev.db* diff --git a/projects/onelga-local-services/backend/package.json b/projects/onelga-local-services/backend/package.json deleted file mode 100644 index 7674925..0000000 --- a/projects/onelga-local-services/backend/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "onelga-local-services-backend", - "version": "1.0.0", - "description": "Backend API for the Onelga Local Services platform", - "main": "dist/server.js", - "scripts": { - "dev": "ts-node-dev --respawn --transpile-only src/server.ts", - "build": "tsc", - "start": "node dist/server.js", - "lint": "eslint 'src/**/*.{ts,tsx}'", - "typecheck": "tsc --noEmit", - "prisma:migrate": "prisma migrate dev", - "prisma:generate": "prisma generate", - "prisma:seed": "ts-node prisma/seed.ts", - "seed:test": "ts-node scripts/seed-test.ts", - "seed:staff": "ts-node scripts/seed-test-staff.ts", - "unlock:user": "ts-node scripts/unlock-user.ts" - }, - "dependencies": { - "@prisma/client": "^5.15.0", - "bcryptjs": "^2.4.3", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "jsonwebtoken": "^9.0.2", - "morgan": "^1.10.0" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.6", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jsonwebtoken": "^9.0.5", - "@types/morgan": "^1.9.7", - "@types/node": "^20.14.9", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^48.2.6", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-promise": "^6.1.1", - "prisma": "^5.15.0", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.4.5", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@typescript-eslint/parser": "^7.13.0" - } -} diff --git a/projects/onelga-local-services/backend/prisma/schema.prisma b/projects/onelga-local-services/backend/prisma/schema.prisma deleted file mode 100644 index 91431df..0000000 --- a/projects/onelga-local-services/backend/prisma/schema.prisma +++ /dev/null @@ -1,94 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -enum Role { - CITIZEN - STAFF - ADMIN -} - -enum ApplicationStatus { - PENDING - UNDER_REVIEW - APPROVED - REJECTED - PENDING_DOCUMENTS - COMPLETED -} - -model User { - id String @id @default(cuid()) - email String @unique - firstName String - lastName String - passwordHash String - role Role @default(CITIZEN) - failedLoginAttempts Int @default(0) - lockoutUntil DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - newsArticles NewsArticle[] - applications Application[] - auditLogs AuditLog[] -} - -model Application { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String - type String - status ApplicationStatus @default(PENDING) - data Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - documents Document[] - assignments ServiceAssignment[] -} - -model Document { - id String @id @default(cuid()) - application Application @relation(fields: [applicationId], references: [id]) - applicationId String - filename String - url String - status String - uploadedAt DateTime @default(now()) -} - -model NewsArticle { - id String @id @default(cuid()) - title String - slug String @unique - content String - published Boolean @default(false) - publishedAt DateTime? - author User? @relation(fields: [authorId], references: [id]) - authorId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model ServiceAssignment { - id String @id @default(cuid()) - application Application @relation(fields: [applicationId], references: [id]) - applicationId String - staff User @relation(fields: [staffId], references: [id]) - staffId String - notes String? - createdAt DateTime @default(now()) -} - -model AuditLog { - id String @id @default(cuid()) - actor User? @relation(fields: [actorId], references: [id]) - actorId String? - action String - details Json? - createdAt DateTime @default(now()) -} diff --git a/projects/onelga-local-services/backend/prisma/seed.ts b/projects/onelga-local-services/backend/prisma/seed.ts deleted file mode 100644 index dd6760b..0000000 --- a/projects/onelga-local-services/backend/prisma/seed.ts +++ /dev/null @@ -1,81 +0,0 @@ -import "dotenv/config"; -import { PrismaClient, Role, ApplicationStatus } from "@prisma/client"; -import bcrypt from "bcryptjs"; - -const prisma = new PrismaClient(); - -const users = [ - { - email: "admin@onelga.local", - role: Role.ADMIN, - firstName: "Admin", - lastName: "User", - }, - { - email: "staff@onelga.local", - role: Role.STAFF, - firstName: "Staff", - lastName: "Member", - }, - { - email: "citizen@onelga.local", - role: Role.CITIZEN, - firstName: "Citizen", - lastName: "Resident", - }, -]; - -async function main() { - const passwordHash = await bcrypt.hash("Passw0rd!", 10); - for (const user of users) { - await prisma.user.upsert({ - where: { email: user.email }, - update: {}, - create: { - ...user, - passwordHash, - }, - }); - } - - const admin = await prisma.user.findUnique({ where: { email: "admin@onelga.local" } }); - const citizen = await prisma.user.findUnique({ where: { email: "citizen@onelga.local" } }); - if (!admin || !citizen) { - throw new Error("Seed users not found"); - } - - await prisma.newsArticle.upsert({ - where: { slug: "welcome-to-onelga" }, - update: {}, - create: { - title: "Welcome to Onelga Services", - slug: "welcome-to-onelga", - content: "Discover services, news, and updates for Onelga citizens.", - published: true, - publishedAt: new Date(), - authorId: admin.id, - }, - }); - - await prisma.application.upsert({ - where: { id: "demo-application" }, - update: {}, - create: { - id: "demo-application", - type: "building-permit", - status: ApplicationStatus.PENDING, - data: { projectName: "Community Center" }, - userId: citizen.id, - }, - }); -} - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (error) => { - console.error(error); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/projects/onelga-local-services/backend/scripts/seed-test-staff.ts b/projects/onelga-local-services/backend/scripts/seed-test-staff.ts deleted file mode 100644 index 7e26f52..0000000 --- a/projects/onelga-local-services/backend/scripts/seed-test-staff.ts +++ /dev/null @@ -1,33 +0,0 @@ -import "dotenv/config"; -import { PrismaClient, Role } from "@prisma/client"; -import bcrypt from "bcryptjs"; - -const prisma = new PrismaClient(); - -async function seedStaff() { - const passwordHash = await bcrypt.hash("Passw0rd!", 10); - - await prisma.user.upsert({ - where: { email: "staff@onelga.local" }, - update: {}, - create: { - email: "staff@onelga.local", - firstName: "Staff", - lastName: "Member", - role: Role.STAFF, - passwordHash, - }, - }); - - console.log("Seeded staff account"); -} - -seedStaff() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (error) => { - console.error(error); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/projects/onelga-local-services/backend/scripts/seed-test.ts b/projects/onelga-local-services/backend/scripts/seed-test.ts deleted file mode 100644 index 0c5f22e..0000000 --- a/projects/onelga-local-services/backend/scripts/seed-test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import "dotenv/config"; -import { PrismaClient, Role, ApplicationStatus } from "@prisma/client"; -import bcrypt from "bcryptjs"; - -const prisma = new PrismaClient(); - -async function seed() { - const passwordHash = await bcrypt.hash("Passw0rd!", 10); - - const admin = await prisma.user.upsert({ - where: { email: "admin@onelga.local" }, - update: {}, - create: { - email: "admin@onelga.local", - firstName: "Admin", - lastName: "User", - role: Role.ADMIN, - passwordHash, - }, - }); - - const staff = await prisma.user.upsert({ - where: { email: "staff@onelga.local" }, - update: {}, - create: { - email: "staff@onelga.local", - firstName: "Staff", - lastName: "Member", - role: Role.STAFF, - passwordHash, - }, - }); - - const citizen = await prisma.user.upsert({ - where: { email: "citizen@onelga.local" }, - update: {}, - create: { - email: "citizen@onelga.local", - firstName: "Citizen", - lastName: "Resident", - role: Role.CITIZEN, - passwordHash, - }, - }); - - await prisma.newsArticle.createMany({ - data: [ - { - title: "City Approves Park Renovation", - slug: "park-renovation", - content: "The city council approved the renovation budget for Central Park.", - published: true, - publishedAt: new Date(), - authorId: admin.id, - }, - { - title: "Seasonal Road Maintenance Schedule", - slug: "road-maintenance", - content: "Road maintenance is scheduled for the coming weeks across the city.", - published: true, - publishedAt: new Date(), - authorId: staff.id, - }, - ], - skipDuplicates: true, - }); - - await prisma.application.createMany({ - data: [ - { - userId: citizen.id, - type: "business-license", - status: ApplicationStatus.UNDER_REVIEW, - data: { businessName: "Citizen Bakery" }, - }, - { - userId: citizen.id, - type: "waste-management", - status: ApplicationStatus.PENDING, - data: { requestedBins: 2 }, - }, - ], - skipDuplicates: true, - }); - - console.log("Seeded test data"); -} - -seed() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (error) => { - console.error(error); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/projects/onelga-local-services/backend/scripts/smoke-test.ts b/projects/onelga-local-services/backend/scripts/smoke-test.ts deleted file mode 100644 index fb38778..0000000 --- a/projects/onelga-local-services/backend/scripts/smoke-test.ts +++ /dev/null @@ -1,53 +0,0 @@ -const BASE_URL = process.env.BASE_URL ?? "http://localhost:4000"; -const ADMIN_EMAIL = process.env.ADMIN_EMAIL ?? "admin@onelga.local"; -const PASSWORD = process.env.ADMIN_PASSWORD ?? "Passw0rd!"; - -type LoginResponse = { - token: string; -}; - -type AdminStats = { - totalUsers: number; - totalApplications: number; - pendingApplications: number; - publishedArticles: number; -}; - -async function main() { - const loginResponse = await fetch(`${BASE_URL}/api/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email: ADMIN_EMAIL, password: PASSWORD }), - }); - - if (!loginResponse.ok) { - throw new Error(`Login failed with status ${loginResponse.status}`); - } - - const loginData = (await loginResponse.json()) as LoginResponse; - console.log("Login successful"); - - const statsResponse = await fetch(`${BASE_URL}/api/admin/stats`, { - headers: { Authorization: `Bearer ${loginData.token}` }, - }); - - if (!statsResponse.ok) { - throw new Error(`Fetching stats failed with status ${statsResponse.status}`); - } - - const stats = (await statsResponse.json()) as AdminStats; - console.log("Admin stats:", stats); - - const newsResponse = await fetch(`${BASE_URL}/api/news`); - if (!newsResponse.ok) { - throw new Error(`Fetching news failed with status ${newsResponse.status}`); - } - - const news = (await newsResponse.json()) as unknown[]; - console.log(`Fetched ${news.length} news articles`); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/projects/onelga-local-services/backend/scripts/unlock-user.ts b/projects/onelga-local-services/backend/scripts/unlock-user.ts deleted file mode 100644 index 3fe430d..0000000 --- a/projects/onelga-local-services/backend/scripts/unlock-user.ts +++ /dev/null @@ -1,30 +0,0 @@ -import "dotenv/config"; -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function unlock(email: string) { - const user = await prisma.user.update({ - where: { email: email.toLowerCase() }, - data: { failedLoginAttempts: 0, lockoutUntil: null }, - }); - - console.log(`Unlocked user ${user.email}`); -} - -const email = process.argv[2]; - -if (!email) { - console.error("Usage: ts-node scripts/unlock-user.ts "); - process.exit(1); -} - -unlock(email) - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (error) => { - console.error(error); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/projects/onelga-local-services/backend/src/app.ts b/projects/onelga-local-services/backend/src/app.ts deleted file mode 100644 index 910fae5..0000000 --- a/projects/onelga-local-services/backend/src/app.ts +++ /dev/null @@ -1,24 +0,0 @@ -import cors from "cors"; -import express from "express"; -import morgan from "morgan"; -import env from "./config/env"; -import routes from "./routes"; - -const app = express(); - -app.use(cors({ origin: env.FRONTEND_URL, credentials: true })); -app.use(express.json()); -app.use(morgan("dev")); - -app.get("/api/health", (_req, res) => { - res.json({ status: "ok" }); -}); - -app.use("/api", routes); - -app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { - console.error(err); - res.status(500).json({ message: "Internal server error" }); -}); - -export default app; diff --git a/projects/onelga-local-services/backend/src/config/env.ts b/projects/onelga-local-services/backend/src/config/env.ts deleted file mode 100644 index f77ba20..0000000 --- a/projects/onelga-local-services/backend/src/config/env.ts +++ /dev/null @@ -1,32 +0,0 @@ -import dotenv from "dotenv"; - -dotenv.config(); - -const requiredEnvVars = ["PORT", "DATABASE_URL", "JWT_SECRET", "JWT_EXPIRES_IN", "FRONTEND_URL"] as const; - -type RequiredEnv = (typeof requiredEnvVars)[number]; - -type EnvConfig = Record & { - smtpHost?: string; - smtpPort?: number; - smtpUser?: string; - smtpPass?: string; -}; - -const env: EnvConfig = requiredEnvVars.reduce((acc, key) => { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing required environment variable: ${key}`); - } - acc[key] = value; - return acc; -}, {} as EnvConfig); - -if (process.env.SMTP_HOST) { - env.smtpHost = process.env.SMTP_HOST; - env.smtpPort = process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined; - env.smtpUser = process.env.SMTP_USER; - env.smtpPass = process.env.SMTP_PASS; -} - -export default env; diff --git a/projects/onelga-local-services/backend/src/controllers/adminController.ts b/projects/onelga-local-services/backend/src/controllers/adminController.ts deleted file mode 100644 index b5cc629..0000000 --- a/projects/onelga-local-services/backend/src/controllers/adminController.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Request, Response } from "express"; -import { ApplicationStatus } from "@prisma/client"; -import { listUsers } from "../services/userService"; -import { getDashboardStats, listApplications, updateApplicationStatus } from "../services/applicationService"; - -export const getStats = async (_req: Request, res: Response) => { - const stats = await getDashboardStats(); - return res.json(stats); -}; - -export const getUsers = async (_req: Request, res: Response) => { - const users = await listUsers(); - return res.json(users); -}; - -export const getApplications = async (req: Request, res: Response) => { - const status = req.query.status as ApplicationStatus | undefined; - const applications = await listApplications(status ? { status } : undefined); - return res.json(applications); -}; - -export const decideApplication = async (req: Request, res: Response) => { - const { id } = req.params; - const { status } = req.body as { status: ApplicationStatus }; - if (!status) { - return res.status(400).json({ message: "Status is required" }); - } - - const updated = await updateApplicationStatus(id, status); - return res.json(updated); -}; diff --git a/projects/onelga-local-services/backend/src/controllers/authController.ts b/projects/onelga-local-services/backend/src/controllers/authController.ts deleted file mode 100644 index 382829d..0000000 --- a/projects/onelga-local-services/backend/src/controllers/authController.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Request, Response } from "express"; -import { Role } from "@prisma/client"; -import { - findUserByEmail, - createUser, - verifyPassword, - recordFailedAttempt, - resetFailedAttempts, - getUserById, -} from "../services/userService"; -import { signToken } from "../utils/token"; - -const normalizeEmail = (email: string) => email.trim().toLowerCase(); - -const sanitizeUser = (user: { id: string; email: string; role: Role; firstName: string; lastName: string }) => ({ - id: user.id, - email: user.email, - role: user.role, - firstName: user.firstName, - lastName: user.lastName, -}); - -export const register = async (req: Request, res: Response) => { - const { email, password, firstName, lastName } = req.body; - if (!email || !password) { - return res.status(400).json({ message: "Email and password are required" }); - } - - if (!firstName || !lastName) { - return res.status(400).json({ message: "First and last name are required" }); - } - - const existing = await findUserByEmail(normalizeEmail(email)); - if (existing) { - return res.status(409).json({ message: "Email already in use" }); - } - - const user = await createUser({ email, password, firstName, lastName }); - const token = signToken({ sub: user.id, role: user.role }); - return res.status(201).json({ - user: sanitizeUser(user), - token, - }); -}; - -export const login = async (req: Request, res: Response) => { - const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ message: "Email and password are required" }); - } - - const user = await findUserByEmail(normalizeEmail(email)); - if (!user) { - return res.status(401).json({ message: "Invalid credentials" }); - } - - if (user.lockoutUntil && user.lockoutUntil > new Date()) { - return res.status(423).json({ message: "Account locked. Try again later." }); - } - - const passwordMatches = await verifyPassword(password, user.passwordHash); - if (!passwordMatches) { - await recordFailedAttempt(user.id); - return res.status(401).json({ message: "Invalid credentials" }); - } - - if (user.failedLoginAttempts > 0 || user.lockoutUntil) { - await resetFailedAttempts(user.id); - } - - const token = signToken({ sub: user.id, role: user.role }); - return res.json({ - token, - user: sanitizeUser(user), - }); -}; - -export const validateToken = async (req: Request, res: Response) => { - const { user } = req as Request & { user?: { id: string; role: Role } }; - if (!user) { - return res.status(401).json({ message: "Invalid token" }); - } - - const freshUser = await getUserById(user.id); - if (!freshUser) { - return res.status(401).json({ message: "Invalid token" }); - } - - return res.json({ user: sanitizeUser(freshUser) }); -}; diff --git a/projects/onelga-local-services/backend/src/controllers/newsController.ts b/projects/onelga-local-services/backend/src/controllers/newsController.ts deleted file mode 100644 index 9558292..0000000 --- a/projects/onelga-local-services/backend/src/controllers/newsController.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Request, Response } from "express"; -import { createArticle, deleteArticle, getAdminStats, getArticleBySlug, listAdminArticles, listPublishedArticles, updateArticle } from "../services/newsService"; - -export const getPublicNews = async (_req: Request, res: Response) => { - const articles = await listPublishedArticles(); - return res.json(articles); -}; - -export const getArticle = async (req: Request, res: Response) => { - const article = await getArticleBySlug(req.params.slug); - if (!article || !article.published) { - return res.status(404).json({ message: "Article not found" }); - } - - return res.json(article); -}; - -export const getAdminArticles = async (_req: Request, res: Response) => { - const articles = await listAdminArticles(); - return res.json(articles); -}; - -export const createAdminArticle = async (req: Request, res: Response) => { - const { title, slug, content, published } = req.body; - const user = (req as Request & { user?: { id: string } }).user; - if (!title || !slug) { - return res.status(400).json({ message: "Title and slug are required" }); - } - - const article = await createArticle({ title, slug, content, published, authorId: user?.id }); - return res.status(201).json(article); -}; - -export const updateAdminArticle = async (req: Request, res: Response) => { - const { id } = req.params; - const { title, slug, content, published } = req.body; - const article = await updateArticle(id, { title, slug, content, published }); - return res.json(article); -}; - -export const removeAdminArticle = async (req: Request, res: Response) => { - const { id } = req.params; - await deleteArticle(id); - return res.status(204).send(); -}; - -export const getNewsStats = async (_req: Request, res: Response) => { - const stats = await getAdminStats(); - return res.json(stats); -}; diff --git a/projects/onelga-local-services/backend/src/middleware/auth.ts b/projects/onelga-local-services/backend/src/middleware/auth.ts deleted file mode 100644 index e21938d..0000000 --- a/projects/onelga-local-services/backend/src/middleware/auth.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Role } from "@prisma/client"; -import { NextFunction, Request, Response } from "express"; -import { verifyToken } from "../utils/token"; -import { getUserById } from "../services/userService"; - -export interface AuthenticatedRequest extends Request { - user?: { - id: string; - role: Role; - }; -} - -export const authenticate = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith("Bearer ")) { - return res.status(401).json({ message: "Authentication token missing" }); - } - - try { - const token = authHeader.substring(7); - const decoded = verifyToken(token); - const user = await getUserById(decoded.sub); - if (!user) { - return res.status(401).json({ message: "Invalid authentication token" }); - } - - req.user = { id: user.id, role: user.role }; - return next(); - } catch (error) { - return res.status(401).json({ message: "Invalid authentication token" }); - } -}; - -export const authorize = (roles: Role[]) => { - return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - if (!req.user || !roles.includes(req.user.role)) { - return res.status(403).json({ message: "You do not have permission to perform this action" }); - } - - return next(); - }; -}; diff --git a/projects/onelga-local-services/backend/src/routes/adminRoutes.ts b/projects/onelga-local-services/backend/src/routes/adminRoutes.ts deleted file mode 100644 index 76b568c..0000000 --- a/projects/onelga-local-services/backend/src/routes/adminRoutes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Role } from "@prisma/client"; -import { Router } from "express"; -import { authenticate, authorize } from "../middleware/auth"; -import { decideApplication, getApplications, getStats, getUsers } from "../controllers/adminController"; - -const router = Router(); - -router.use(authenticate, authorize([Role.ADMIN, Role.STAFF])); - -router.get("/dashboard/stats", getStats); -router.get("/stats", getStats); -router.get("/users", authorize([Role.ADMIN]), getUsers); -router.get("/applications", getApplications); -router.post("/applications/:id/decide", decideApplication); - -export default router; diff --git a/projects/onelga-local-services/backend/src/routes/authRoutes.ts b/projects/onelga-local-services/backend/src/routes/authRoutes.ts deleted file mode 100644 index f054813..0000000 --- a/projects/onelga-local-services/backend/src/routes/authRoutes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Router } from "express"; -import { authenticate } from "../middleware/auth"; -import { login, register, validateToken } from "../controllers/authController"; - -const router = Router(); - -router.post("/login", login); -router.post("/register", register); -router.get("/validate", authenticate, validateToken); - -export default router; diff --git a/projects/onelga-local-services/backend/src/routes/index.ts b/projects/onelga-local-services/backend/src/routes/index.ts deleted file mode 100644 index 95ab800..0000000 --- a/projects/onelga-local-services/backend/src/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from "express"; -import authRoutes from "./authRoutes"; -import adminRoutes from "./adminRoutes"; -import newsRoutes from "./newsRoutes"; - -const router = Router(); - -router.use("/auth", authRoutes); -router.use("/admin", adminRoutes); -router.use("/news", newsRoutes); - -export default router; diff --git a/projects/onelga-local-services/backend/src/routes/newsRoutes.ts b/projects/onelga-local-services/backend/src/routes/newsRoutes.ts deleted file mode 100644 index 057fd54..0000000 --- a/projects/onelga-local-services/backend/src/routes/newsRoutes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Role } from "@prisma/client"; -import { Router } from "express"; -import { authenticate, authorize } from "../middleware/auth"; -import { - createAdminArticle, - getAdminArticles, - getArticle, - getNewsStats, - getPublicNews, - removeAdminArticle, - updateAdminArticle, -} from "../controllers/newsController"; - -const router = Router(); - -router.get("/", getPublicNews); - -router.get("/admin/articles", authenticate, authorize([Role.ADMIN, Role.STAFF]), getAdminArticles); -router.post("/admin/articles", authenticate, authorize([Role.ADMIN, Role.STAFF]), createAdminArticle); -router.put("/admin/articles/:id", authenticate, authorize([Role.ADMIN, Role.STAFF]), updateAdminArticle); -router.delete("/admin/articles/:id", authenticate, authorize([Role.ADMIN, Role.STAFF]), removeAdminArticle); -router.get("/admin/stats", authenticate, authorize([Role.ADMIN, Role.STAFF]), getNewsStats); - -router.get("/:slug", getArticle); - -export default router; diff --git a/projects/onelga-local-services/backend/src/server.ts b/projects/onelga-local-services/backend/src/server.ts deleted file mode 100644 index 99339e3..0000000 --- a/projects/onelga-local-services/backend/src/server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import env from "./config/env"; -import app from "./app"; -import prisma from "./utils/prisma"; - -const port = Number(env.PORT); - -async function start() { - try { - await prisma.$connect(); - app.listen(port, () => { - console.log(`Server listening on port ${port}`); - }); - } catch (error) { - console.error("Failed to start server", error); - process.exit(1); - } -} - -void start(); diff --git a/projects/onelga-local-services/backend/src/services/applicationService.ts b/projects/onelga-local-services/backend/src/services/applicationService.ts deleted file mode 100644 index b66b629..0000000 --- a/projects/onelga-local-services/backend/src/services/applicationService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ApplicationStatus, Prisma } from "@prisma/client"; -import prisma from "../utils/prisma"; - -export const listApplications = async (filters?: { status?: ApplicationStatus }) => { - return prisma.application.findMany({ - where: filters, - include: { - user: { - select: { id: true, email: true, firstName: true, lastName: true, role: true }, - }, - documents: true, - assignments: true, - }, - orderBy: { createdAt: "desc" }, - }); -}; - -export const updateApplicationStatus = (id: string, status: ApplicationStatus) => { - return prisma.application.update({ - where: { id }, - data: { status }, - }); -}; - -export const createApplication = (data: Prisma.ApplicationCreateInput) => { - return prisma.application.create({ data }); -}; - -export const getDashboardStats = async () => { - const [totalUsers, totalApplications, pendingApplications, publishedArticles] = await Promise.all([ - prisma.user.count(), - prisma.application.count(), - prisma.application.count({ where: { status: ApplicationStatus.PENDING } }), - prisma.newsArticle.count({ where: { published: true } }), - ]); - - return { - totalUsers, - totalApplications, - pendingApplications, - publishedArticles, - }; -}; diff --git a/projects/onelga-local-services/backend/src/services/newsService.ts b/projects/onelga-local-services/backend/src/services/newsService.ts deleted file mode 100644 index 5536d61..0000000 --- a/projects/onelga-local-services/backend/src/services/newsService.ts +++ /dev/null @@ -1,60 +0,0 @@ -import prisma from "../utils/prisma"; - -export const listPublishedArticles = () => { - return prisma.newsArticle.findMany({ - where: { published: true }, - orderBy: { publishedAt: "desc" }, - }); -}; - -export const getArticleBySlug = (slug: string) => { - return prisma.newsArticle.findUnique({ where: { slug } }); -}; - -export const listAdminArticles = () => { - return prisma.newsArticle.findMany({ orderBy: { createdAt: "desc" } }); -}; - -export const createArticle = (data: { - title: string; - slug: string; - content: string; - published?: boolean; - authorId?: string; -}) => { - return prisma.newsArticle.create({ - data: { - ...data, - publishedAt: data.published ? new Date() : null, - }, - }); -}; - -export const updateArticle = (id: string, data: { - title?: string; - slug?: string; - content?: string; - published?: boolean; -}) => { - return prisma.newsArticle.update({ - where: { id }, - data: { - ...data, - publishedAt: typeof data.published === "boolean" ? (data.published ? new Date() : null) : undefined, - }, - }); -}; - -export const deleteArticle = (id: string) => { - return prisma.newsArticle.delete({ where: { id } }); -}; - -export const getAdminStats = async () => { - const [totalArticles, publishedArticles, drafts] = await Promise.all([ - prisma.newsArticle.count(), - prisma.newsArticle.count({ where: { published: true } }), - prisma.newsArticle.count({ where: { published: false } }), - ]); - - return { totalArticles, publishedArticles, drafts }; -}; diff --git a/projects/onelga-local-services/backend/src/services/userService.ts b/projects/onelga-local-services/backend/src/services/userService.ts deleted file mode 100644 index 05a9a4f..0000000 --- a/projects/onelga-local-services/backend/src/services/userService.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Role } from "@prisma/client"; -import bcrypt from "bcryptjs"; -import prisma from "../utils/prisma"; - -const MAX_FAILED_ATTEMPTS = 5; -const LOCKOUT_DURATION_MINUTES = 15; - -export const findUserByEmail = (email: string) => { - return prisma.user.findUnique({ where: { email: email.toLowerCase() } }); -}; - -export const getUserById = (id: string) => { - return prisma.user.findUnique({ where: { id } }); -}; - -export const createUser = async (params: { - email: string; - password: string; - firstName: string; - lastName: string; - role?: Role; -}) => { - const hash = await bcrypt.hash(params.password, 10); - return prisma.user.create({ - data: { - email: params.email.toLowerCase(), - passwordHash: hash, - firstName: params.firstName, - lastName: params.lastName, - role: params.role ?? Role.CITIZEN, - }, - }); -}; - -export const verifyPassword = (password: string, hash: string) => bcrypt.compare(password, hash); - -export const recordFailedAttempt = async (userId: string) => { - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return null; - } - - const failedAttempts = user.failedLoginAttempts + 1; - const updateData: { failedLoginAttempts: number; lockoutUntil?: Date } = { - failedLoginAttempts: failedAttempts, - }; - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - updateData.lockoutUntil = new Date(Date.now() + LOCKOUT_DURATION_MINUTES * 60 * 1000); - } - - return prisma.user.update({ - where: { id: userId }, - data: updateData, - }); -}; - -export const resetFailedAttempts = (userId: string) => { - return prisma.user.update({ - where: { id: userId }, - data: { failedLoginAttempts: 0, lockoutUntil: null }, - }); -}; - -export const listUsers = () => { - return prisma.user.findMany({ - orderBy: { createdAt: "desc" }, - select: { - id: true, - email: true, - firstName: true, - lastName: true, - role: true, - createdAt: true, - }, - }); -}; diff --git a/projects/onelga-local-services/backend/src/utils/prisma.ts b/projects/onelga-local-services/backend/src/utils/prisma.ts deleted file mode 100644 index b5bf6ce..0000000 --- a/projects/onelga-local-services/backend/src/utils/prisma.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/projects/onelga-local-services/backend/src/utils/token.ts b/projects/onelga-local-services/backend/src/utils/token.ts deleted file mode 100644 index 1c3f646..0000000 --- a/projects/onelga-local-services/backend/src/utils/token.ts +++ /dev/null @@ -1,15 +0,0 @@ -import jwt from "jsonwebtoken"; -import env from "../config/env"; - -interface TokenPayload { - sub: string; - role: string; -} - -export const signToken = (payload: TokenPayload) => { - return jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN }); -}; - -export const verifyToken = (token: string) => { - return jwt.verify(token, env.JWT_SECRET) as TokenPayload & { iat: number; exp: number }; -}; diff --git a/projects/onelga-local-services/backend/tsconfig.json b/projects/onelga-local-services/backend/tsconfig.json deleted file mode 100644 index 9fdbe53..0000000 --- a/projects/onelga-local-services/backend/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "rootDir": "src", - "outDir": "dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "types": [ - "node" - ], - "moduleResolution": "node", - "lib": [ - "ES2020", - "DOM" - ] - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ] -} diff --git a/projects/onelga-local-services/docker-compose.yml b/projects/onelga-local-services/docker-compose.yml deleted file mode 100644 index cab42bd..0000000 --- a/projects/onelga-local-services/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3.9" - -services: - db: - image: postgres:16 - restart: unless-stopped - environment: - POSTGRES_USER: onelga - POSTGRES_PASSWORD: onelga - POSTGRES_DB: onelga - ports: - - "5432:5432" - volumes: - - postgres-data:/var/lib/postgresql/data - -volumes: - postgres-data: diff --git a/projects/onelga-local-services/frontend/.env.example b/projects/onelga-local-services/frontend/.env.example deleted file mode 100644 index e82d0a0..0000000 --- a/projects/onelga-local-services/frontend/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL="http://localhost:4000" diff --git a/projects/onelga-local-services/frontend/.eslintrc.json b/projects/onelga-local-services/frontend/.eslintrc.json deleted file mode 100644 index f788a87..0000000 --- a/projects/onelga-local-services/frontend/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - "plugin:react-refresh/recommended", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "react-refresh/only-export-components": "off" - } -} diff --git a/projects/onelga-local-services/frontend/.gitignore b/projects/onelga-local-services/frontend/.gitignore deleted file mode 100644 index e5b1704..0000000 --- a/projects/onelga-local-services/frontend/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/node_modules -/dist -/.env diff --git a/projects/onelga-local-services/frontend/index.html b/projects/onelga-local-services/frontend/index.html deleted file mode 100644 index b2bc1f0..0000000 --- a/projects/onelga-local-services/frontend/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Onelga Local Services - - -
- - - diff --git a/projects/onelga-local-services/frontend/package.json b/projects/onelga-local-services/frontend/package.json deleted file mode 100644 index f742e5c..0000000 --- a/projects/onelga-local-services/frontend/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "onelga-local-services-frontend", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint 'src/**/*.{ts,tsx}'", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.21", - "@mui/material": "^5.15.21", - "@reduxjs/toolkit": "^2.2.5", - "@tanstack/react-query": "^5.40.0", - "axios": "^1.7.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-redux": "^9.1.2", - "react-router-dom": "^6.23.1" - }, - "devDependencies": { - "@types/node": "^20.14.9", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@typescript-eslint/parser": "^7.13.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "typescript": "^5.4.5", - "vite": "^5.2.11" - } -} diff --git a/projects/onelga-local-services/frontend/src/App.tsx b/projects/onelga-local-services/frontend/src/App.tsx deleted file mode 100644 index e41d4f3..0000000 --- a/projects/onelga-local-services/frontend/src/App.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Navigate, Route, Routes } from "react-router-dom"; -import AppLayout from "./components/AppLayout"; -import ProtectedRoute from "./components/ProtectedRoute"; -import LoginPage from "./pages/LoginPage"; -import NewsPage from "./pages/NewsPage"; -import AdminDashboardPage from "./pages/dashboards/AdminDashboardPage"; -import NewsManagementPage from "./pages/dashboards/NewsManagementPage"; -import ServiceRequestsPage from "./pages/dashboards/ServiceRequestsPage"; -import StaffDashboardPage from "./pages/dashboards/StaffDashboardPage"; -import HomePage from "./pages/HomePage"; -import { Role } from "./types"; - -const App = () => { - return ( - - }> - } /> - } /> - } /> - - }> - } /> - } /> - } /> - - - }> - } /> - - - } /> - - - ); -}; - -export default App; diff --git a/projects/onelga-local-services/frontend/src/components/AppLayout.tsx b/projects/onelga-local-services/frontend/src/components/AppLayout.tsx deleted file mode 100644 index f1e35c2..0000000 --- a/projects/onelga-local-services/frontend/src/components/AppLayout.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { AppBar, Box, Button, Container, Toolbar, Typography } from "@mui/material"; -import { Link as RouterLink, Outlet, useNavigate } from "react-router-dom"; -import { useAppDispatch, useAppSelector } from "../store/hooks"; -import { clearCredentials } from "../store/slices/authSlice"; - -const AppLayout = () => { - const { user } = useAppSelector((state) => state.auth); - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - - const handleLogout = () => { - dispatch(clearCredentials()); - navigate("/login"); - }; - - return ( - - - - - Onelga Local Services - - {user ? ( - <> - - - - - ) : ( - - )} - - - - - - - ); -}; - -export default AppLayout; diff --git a/projects/onelga-local-services/frontend/src/components/ProtectedRoute.tsx b/projects/onelga-local-services/frontend/src/components/ProtectedRoute.tsx deleted file mode 100644 index 0eb65c9..0000000 --- a/projects/onelga-local-services/frontend/src/components/ProtectedRoute.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Role } from "../types"; -import { Navigate, Outlet } from "react-router-dom"; -import { useAppSelector } from "../store/hooks"; - -interface ProtectedRouteProps { - roles?: Role[]; -} - -const ProtectedRoute = ({ roles }: ProtectedRouteProps) => { - const { token, user } = useAppSelector((state) => state.auth); - - if (!token || !user) { - return ; - } - - if (roles && !roles.includes(user.role)) { - return ; - } - - return ; -}; - -export default ProtectedRoute; diff --git a/projects/onelga-local-services/frontend/src/main.tsx b/projects/onelga-local-services/frontend/src/main.tsx deleted file mode 100644 index 16e4a93..0000000 --- a/projects/onelga-local-services/frontend/src/main.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"; -import { Provider } from "react-redux"; -import { BrowserRouter } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import App from "./App"; -import { store } from "./store"; - -const theme = createTheme({ - palette: { - mode: "light", - primary: { - main: "#0063b2", - }, - secondary: { - main: "#f57c00", - }, - }, -}); - -const queryClient = new QueryClient(); - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - - - - - - - , -); diff --git a/projects/onelga-local-services/frontend/src/pages/HomePage.tsx b/projects/onelga-local-services/frontend/src/pages/HomePage.tsx deleted file mode 100644 index e758ed9..0000000 --- a/projects/onelga-local-services/frontend/src/pages/HomePage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Box, Button, Grid, Typography } from "@mui/material"; -import { Link as RouterLink } from "react-router-dom"; - -const HomePage = () => { - return ( - - - Welcome to Onelga Local Services - - - Access government services, manage applications, and stay informed with the latest community news. - - - - - - - - - - - ); -}; - -export default HomePage; diff --git a/projects/onelga-local-services/frontend/src/pages/LoginPage.tsx b/projects/onelga-local-services/frontend/src/pages/LoginPage.tsx deleted file mode 100644 index a9ea572..0000000 --- a/projects/onelga-local-services/frontend/src/pages/LoginPage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useState } from "react"; -import { Alert, Box, Button, Paper, Stack, TextField, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { login as loginRequest } from "../services/api"; -import { useAppDispatch } from "../store/hooks"; -import { setCredentials } from "../store/slices/authSlice"; - -const LoginPage = () => { - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - const [email, setEmail] = useState("admin@onelga.local"); - const [password, setPassword] = useState("Passw0rd!"); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setLoading(true); - setError(null); - - try { - const response = await loginRequest(email, password); - dispatch(setCredentials(response)); - navigate("/admin"); - } catch (err) { - setError("Login failed. Please check your credentials."); - } finally { - setLoading(false); - } - }; - - return ( - - - - Sign in - - - - setEmail(event.target.value)} /> - setPassword(event.target.value)} /> - {error ? {error} : null} - - - - - - ); -}; - -export default LoginPage; diff --git a/projects/onelga-local-services/frontend/src/pages/NewsPage.tsx b/projects/onelga-local-services/frontend/src/pages/NewsPage.tsx deleted file mode 100644 index f4fe1ea..0000000 --- a/projects/onelga-local-services/frontend/src/pages/NewsPage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Alert, Card, CardContent, CircularProgress, Grid, Typography } from "@mui/material"; -import { fetchPublicNews } from "../services/api"; - -const NewsPage = () => { - const { data, isLoading, error } = useQuery({ queryKey: ["news"], queryFn: fetchPublicNews }); - - if (isLoading) { - return ; - } - - if (error) { - return Failed to load news.; - } - - if (!data?.length) { - return No articles available.; - } - - return ( - - {data.map((article) => ( - - - - - {article.title} - - - {article.content.slice(0, 140)}... - - - - - ))} - - ); -}; - -export default NewsPage; diff --git a/projects/onelga-local-services/frontend/src/pages/dashboards/AdminDashboardPage.tsx b/projects/onelga-local-services/frontend/src/pages/dashboards/AdminDashboardPage.tsx deleted file mode 100644 index 40f2070..0000000 --- a/projects/onelga-local-services/frontend/src/pages/dashboards/AdminDashboardPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Alert, Card, CardContent, CircularProgress, Grid, Typography } from "@mui/material"; -import { fetchAdminStats } from "../../services/api"; -import { useAppSelector } from "../../store/hooks"; - -const AdminDashboardPage = () => { - const { token } = useAppSelector((state) => state.auth); - - const { data, isLoading, error } = useQuery({ - queryKey: ["admin-stats"], - queryFn: () => fetchAdminStats(token ?? ""), - enabled: Boolean(token), - }); - - if (isLoading) { - return ; - } - - if (error || !data) { - return Unable to load dashboard statistics.; - } - - const cards = [ - { label: "Total Users", value: data.totalUsers }, - { label: "Total Applications", value: data.totalApplications }, - { label: "Pending Applications", value: data.pendingApplications }, - { label: "Published Articles", value: data.publishedArticles }, - ]; - - return ( - - {cards.map((card) => ( - - - - {card.label} - - {card.value} - - - - - ))} - - ); -}; - -export default AdminDashboardPage; diff --git a/projects/onelga-local-services/frontend/src/pages/dashboards/NewsManagementPage.tsx b/projects/onelga-local-services/frontend/src/pages/dashboards/NewsManagementPage.tsx deleted file mode 100644 index c495dbf..0000000 --- a/projects/onelga-local-services/frontend/src/pages/dashboards/NewsManagementPage.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Alert, CircularProgress, List, ListItem, ListItemText, Typography } from "@mui/material"; -import { fetchAdminArticles } from "../../services/api"; -import { useAppSelector } from "../../store/hooks"; - -const NewsManagementPage = () => { - const { token } = useAppSelector((state) => state.auth); - - const { data, isLoading, error } = useQuery({ - queryKey: ["admin-articles"], - queryFn: () => fetchAdminArticles(token ?? ""), - enabled: Boolean(token), - }); - - if (isLoading) { - return ; - } - - if (error) { - return Unable to load articles.; - } - - if (!data?.length) { - return No articles found.; - } - - return ( - - {data.map((article) => ( - - - - ))} - - ); -}; - -export default NewsManagementPage; diff --git a/projects/onelga-local-services/frontend/src/pages/dashboards/ServiceRequestsPage.tsx b/projects/onelga-local-services/frontend/src/pages/dashboards/ServiceRequestsPage.tsx deleted file mode 100644 index cee4ebc..0000000 --- a/projects/onelga-local-services/frontend/src/pages/dashboards/ServiceRequestsPage.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { Alert, CircularProgress, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; -import { fetchApplications } from "../../services/api"; -import { useAppSelector } from "../../store/hooks"; -import type { ApplicationSummary } from "../../types"; - -const ServiceRequestsPage = () => { - const { token } = useAppSelector((state) => state.auth); - - const { data, isLoading, error } = useQuery({ - queryKey: ["admin-applications"], - queryFn: () => fetchApplications(token ?? ""), - enabled: Boolean(token), - }); - - if (isLoading) { - return ; - } - - if (error) { - return Unable to load service requests.; - } - - if (!data?.length) { - return No service requests found.; - } - - return ( - - - - - Type - Status - Created - - - - {data.map((application: ApplicationSummary) => ( - - {application.type} - {application.status} - {new Date(application.createdAt).toLocaleDateString()} - - ))} - -
-
- ); -}; - -export default ServiceRequestsPage; diff --git a/projects/onelga-local-services/frontend/src/pages/dashboards/StaffDashboardPage.tsx b/projects/onelga-local-services/frontend/src/pages/dashboards/StaffDashboardPage.tsx deleted file mode 100644 index dd7e063..0000000 --- a/projects/onelga-local-services/frontend/src/pages/dashboards/StaffDashboardPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Alert, Box, Typography } from "@mui/material"; -import { useAppSelector } from "../../store/hooks"; - -const StaffDashboardPage = () => { - const { user } = useAppSelector((state) => state.auth); - - if (!user) { - return You must be signed in.; - } - - return ( - - - Welcome back, {user.firstName} - - - Use the admin dashboard to review and manage service assignments. - - - ); -}; - -export default StaffDashboardPage; diff --git a/projects/onelga-local-services/frontend/src/services/api.ts b/projects/onelga-local-services/frontend/src/services/api.ts deleted file mode 100644 index dd99af5..0000000 --- a/projects/onelga-local-services/frontend/src/services/api.ts +++ /dev/null @@ -1,52 +0,0 @@ -import axios from "axios"; -import type { ApplicationSummary, NewsArticle, User } from "../types"; - -const api = axios.create({ - baseURL: "/api", -}); - -export interface LoginResponse { - token: string; - user: User; -} - -export const login = async (email: string, password: string) => { - const response = await api.post("/auth/login", { email, password }); - return response.data; -}; - -export const fetchAdminStats = async (token: string) => { - const response = await api.get("/admin/stats", { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data as { - totalUsers: number; - totalApplications: number; - pendingApplications: number; - publishedArticles: number; - }; -}; - -export const fetchApplications = async (token: string) => { - const response = await api.get>("/admin/applications", { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data.map((application) => ({ - id: application.id, - type: application.type, - status: application.status, - createdAt: application.createdAt, - })); -}; - -export const fetchAdminArticles = async (token: string) => { - const response = await api.get("/news/admin/articles", { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data; -}; - -export const fetchPublicNews = async () => { - const response = await api.get("/news"); - return response.data; -}; diff --git a/projects/onelga-local-services/frontend/src/store/hooks.ts b/projects/onelga-local-services/frontend/src/store/hooks.ts deleted file mode 100644 index 878ac35..0000000 --- a/projects/onelga-local-services/frontend/src/store/hooks.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; -import type { AppDispatch, RootState } from "./index"; - -export const useAppDispatch = () => useDispatch(); -export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/projects/onelga-local-services/frontend/src/store/index.ts b/projects/onelga-local-services/frontend/src/store/index.ts deleted file mode 100644 index 592be3d..0000000 --- a/projects/onelga-local-services/frontend/src/store/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import authReducer from "./slices/authSlice"; - -export const store = configureStore({ - reducer: { - auth: authReducer, - }, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; diff --git a/projects/onelga-local-services/frontend/src/store/slices/authSlice.ts b/projects/onelga-local-services/frontend/src/store/slices/authSlice.ts deleted file mode 100644 index af07fc3..0000000 --- a/projects/onelga-local-services/frontend/src/store/slices/authSlice.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import type { User } from "../../types"; - -export interface AuthState { - token: string | null; - user: User | null; -} - -const initialState: AuthState = { - token: null, - user: null, -}; - -const authSlice = createSlice({ - name: "auth", - initialState, - reducers: { - setCredentials: (state, action: PayloadAction<{ token: string; user: User }>) => { - state.token = action.payload.token; - state.user = action.payload.user; - }, - clearCredentials: (state) => { - state.token = null; - state.user = null; - }, - }, -}); - -export const { setCredentials, clearCredentials } = authSlice.actions; -export default authSlice.reducer; diff --git a/projects/onelga-local-services/frontend/src/types/index.ts b/projects/onelga-local-services/frontend/src/types/index.ts deleted file mode 100644 index 12f1740..0000000 --- a/projects/onelga-local-services/frontend/src/types/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type Role = "ADMIN" | "STAFF" | "CITIZEN"; - -export interface User { - id: string; - email: string; - firstName: string; - lastName: string; - role: Role; -} - -export interface ApplicationSummary { - id: string; - type: string; - status: string; - createdAt: string; -} - -export interface NewsArticle { - id: string; - title: string; - slug: string; - content: string; - published: boolean; - publishedAt?: string; -} diff --git a/projects/onelga-local-services/frontend/tsconfig.json b/projects/onelga-local-services/frontend/tsconfig.json deleted file mode 100644 index 2f27c13..0000000 --- a/projects/onelga-local-services/frontend/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2020"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/projects/onelga-local-services/frontend/tsconfig.node.json b/projects/onelga-local-services/frontend/tsconfig.node.json deleted file mode 100644 index 9d31e2a..0000000 --- a/projects/onelga-local-services/frontend/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/projects/onelga-local-services/frontend/vite.config.ts b/projects/onelga-local-services/frontend/vite.config.ts deleted file mode 100644 index 3f32e57..0000000 --- a/projects/onelga-local-services/frontend/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - proxy: { - "/api": { - target: process.env.VITE_API_URL ?? "http://localhost:4000", - changeOrigin: true, - }, - }, - }, -});