Compare commits

..

20 Commits

Author SHA1 Message Date
anys 718e7f9d76 - Add role detection
- Restrict APIs to personnels
- Show 403 for unauthorized access"
2026-01-06 19:05:59 +01:00
anys 7d7cdd1c9a Add 403 error page and Polytech access control. 2026-01-06 18:56:10 +01:00
anys cb89a45743 Check if user is allowed to access 2026-01-06 10:32:52 +01:00
anys 5856eea5f3 Update Dockerfile 2026-01-05 21:37:19 +01:00
Clayzxr d79cd11b41 Init download API (not working) 2025-01-27 16:47:12 +01:00
Clayzxr 793a43ef87 Fixing bug while editing mobility 2025-01-27 16:12:43 +01:00
Clayzxr 8889dc6758 Init download file (not working yet) 2025-01-27 16:04:01 +01:00
Clayzxr 42102c150d Init file manager for Mobility 2025-01-27 16:00:07 +01:00
Clayzxr 1f4ec66a2c Minor fix 2025-01-27 14:57:08 +01:00
Clayzxr c9cb423ae2 Select promotion in EditMobility 2025-01-27 13:41:55 +01:00
Clayzxr a50bfbe975 Select promotion in ConsultMobility 2025-01-27 13:38:06 +01:00
Clayzxr e14efebf1c types.d 2025-01-27 13:25:43 +01:00
Clayzxr ea6b3d1f48 Fixing weeks count 2025-01-27 12:37:40 +01:00
Clayzxr c3d33317b4 Renamed file 2025-01-27 12:05:27 +01:00
Clayzxr 286f84f5a6 Remove console log 2025-01-27 11:16:45 +01:00
Clayzxr 37d2753c56 Working EditMobility 2025-01-27 11:11:44 +01:00
Clayzxr 9d828069a5 Bug fix 2025-01-27 10:56:51 +01:00
Clayzxr 299f820339 Minor fix 2025-01-27 09:49:12 +01:00
Clayzxr 874716c39d Using connect.ts to attach databases 2025-01-27 09:45:50 +01:00
Clayzxr b7e9df71f3 Init PMPR-34 2025-01-27 09:32:22 +01:00
109 changed files with 878 additions and 7893 deletions
-44
View File
@@ -1,44 +0,0 @@
name: "Build and push image"
on:
push:
branches:
- main
jobs:
check-code:
name: "Check Deno code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Check formatting
run: deno fmt --check
- name: Check linting
run: deno lint
deploy:
name: "Build Docker image"
runs-on: ubuntu-latest
needs: check-code
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: registry.docker.polytech.djalim.fr
username: ${{ secrets.registry_login }}
password: ${{ secrets.registry_pass }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: registry.docker.polytech.djalim.fr/polympr:latest
-79
View File
@@ -1,79 +0,0 @@
name: "Tests"
on:
pull_request:
branches:
- main
- develop
push:
branches:
- develop
jobs:
unit:
name: "Unit tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install dependencies
run: deno install
- name: Run unit tests
run: deno task test:unit
integration:
name: "Integration tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Start postgres
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq postgresql > /dev/null
PG_VER=$(ls /etc/postgresql/)
sudo sed -i "s/^#*listen_addresses\s*=.*/listen_addresses = '127.0.0.1'/" /etc/postgresql/$PG_VER/main/postgresql.conf
echo "host all all 127.0.0.1/32 md5" | sudo tee -a /etc/postgresql/$PG_VER/main/pg_hba.conf
sudo pg_ctlcluster $PG_VER main restart
until sudo -u postgres pg_isready -h 127.0.0.1; do sleep 1; done
sudo -u postgres psql -c "CREATE USER test WITH PASSWORD 'test';"
sudo -u postgres psql -c "CREATE DATABASE polympr_test OWNER test;"
sudo -u postgres psql -d polympr_test -c "GRANT ALL ON SCHEMA public TO test;"
- name: Apply migrations
run: |
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
- name: Install dependencies
run: npm install --ignore-scripts && deno install
- name: Run integration tests
env:
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task test:integration
- name: Run e2e tests
env:
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task test:e2e
@@ -4,10 +4,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- develop
push:
branches:
- develop
permissions: permissions:
contents: read contents: read
-79
View File
@@ -1,79 +0,0 @@
name: "Tests"
on:
pull_request:
branches:
- main
- develop
push:
branches:
- develop
jobs:
unit:
name: "Unit tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install dependencies
run: deno install
- name: Run unit tests
run: deno task test:unit
integration:
name: "Integration tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Start postgres
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq postgresql > /dev/null
PG_VER=$(ls /etc/postgresql/)
sudo sed -i "s/^#*listen_addresses\s*=.*/listen_addresses = '127.0.0.1'/" /etc/postgresql/$PG_VER/main/postgresql.conf
echo "host all all 127.0.0.1/32 md5" | sudo tee -a /etc/postgresql/$PG_VER/main/pg_hba.conf
sudo pg_ctlcluster $PG_VER main restart
until sudo -u postgres pg_isready -h 127.0.0.1; do sleep 1; done
sudo -u postgres psql -c "CREATE USER test WITH PASSWORD 'test';"
sudo -u postgres psql -c "CREATE DATABASE polympr_test OWNER test;"
sudo -u postgres psql -d polympr_test -c "GRANT ALL ON SCHEMA public TO test;"
- name: Apply migrations
run: |
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
- name: Install dependencies
run: npm install --ignore-scripts && deno install
- name: Run integration tests
env:
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task test:integration
- name: Run e2e tests
env:
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task test:e2e
-338
View File
@@ -1,338 +0,0 @@
# PolyMPR - Claude Code Context
## 📋 Project Overview
**PolyMPR** (Poly Management Platform for Resources) is a modular HR management
system built with **Deno + Fresh** framework. It's designed to help
organizations manage HR, student records, notes, mobility programs, and
role-based administration.
### Stack
- **Runtime**: Deno
- **Framework**: Fresh (edge-ready web framework)
- **Database**: PostgreSQL with Drizzle ORM
- **Frontend**: Preact with signals
- **Authentication**: JWT-based via cookies
- **Testing**: Deno test framework with HappyDOM for DOM testing
### Current Status
🚧 **In Progress** - Application is far from complete. The schema below is the
**final/definitive schema** that should guide all development.
---
## 🏗️ Architecture
### Module Structure
The application uses a **modulith architecture** with the following modules:
```
routes/(apps)/
├── students/ - Student management & promotions
├── notes/ - Grade management & academic records
├── mobility/ - Mobility programs & exchanges
└── admin/ - Role & permission management
```
### Key Directories
- `/routes` - Fresh routes and components
- `/databases` - Database connection, schema, and migrations
- `/defaults` - Interfaces and shared types
- `/tests` - Unit, integration, and E2E tests
- `/static` - Public assets
### Authentication Flow
1. User authenticates via CAS (Polytech)
2. JWT token stored in `sessionToken` cookie
3. Middleware validates token on each request
4. Public routes: `/`, `/login`, `/logout`, `/about`, `/contact`
5. All other routes require authentication
---
## 📊 Database Schema (Final/Definitive)
```mermaid
erDiagram
USER {
string id PK
string nom
string prenom
int idRole FK
}
ROLE {
int id PK
string nom
}
PERMISSION {
int id PK
string nom
}
ROLE_PERMISSION {
int idRole PK,FK
int idPermission PK,FK
}
STUDENT {
int numEtud PK
string nom
string prenom
string idPromo FK
}
PROMOTION {
string idPromo PK
string annee
}
MODULE {
string id PK
string nom
}
ENSEIGNEMENT {
string idProf PK,FK
string idModule PK,FK
string idPromo PK,FK
}
UE {
int id PK
string nom
}
UE_MODULE {
string idModule PK,FK
int idUE PK,FK
string idPromo PK,FK
float coeff
}
NOTE {
int numEtud PK,FK
string idModule PK,FK
float note
}
AJUSTEMENT {
int numEtud PK,FK
int idUE PK,FK
float valeur
}
USER }o--|| ROLE : "a"
ROLE_PERMISSION }o--|| ROLE : "accorde"
ROLE_PERMISSION }o--|| PERMISSION : "inclut"
ENSEIGNEMENT }o--|| USER : "réalisé par"
ENSEIGNEMENT }o--|| MODULE : "porte sur"
ENSEIGNEMENT }o--|| PROMOTION : "concerne"
STUDENT }o--|| PROMOTION : "appartient à"
UE_MODULE }o--|| MODULE : "associe"
UE_MODULE }o--|| UE : "appartient à"
UE_MODULE }o--|| PROMOTION : "pour"
NOTE }o--|| STUDENT : "reçoit"
NOTE }o--|| MODULE : "dans"
AJUSTEMENT }o--|| STUDENT : "concerne"
AJUSTEMENT }o--|| UE : "dans"
```
### Current Schema (Incomplete)
The current Drizzle ORM schema in `/databases/schema.ts` only implements:
- `promotions`
- `students`
- `mobility`
**Migration needed**: Update schema to match the final ER diagram above.
---
## 🎯 Open Issues (69 total)
### UI Pages
**Catalog**
- 📋 UI - Page Catalogue d'applications (#71)
**Components**
- 🎨 UI (composant) - Popup Résultats d'import (#75)
**Students**
- 📋 UI - Admin Liste des élèves (#79)
- 📋 UI - Admin Gestion des promotions (#80)
- 📋 UI - Admin Import xlsx élèves (#81)
- 📋 UI - Admin Édition d'un élève (#82)
**Notes**
- 📋 UI - Page Élève Mes Notes (#72)
- 📋 UI - Admin Consulter les notes (#73)
- 📋 UI - Admin Importer des notes (.xlsx) (#74)
- 📋 UI - Admin Édition notes d'un élève (#76)
- 📋 UI - Admin Récap notes élève / semestre (#77)
- 📋 UI - Admin Gestion des UEs (#78)
**Administration**
- 📋 UI - Gestion des utilisateurs (#83)
- 📋 UI - Gestion des rôles (#84)
- 📋 UI - Permissions d'un rôle (#85)
- 📋 UI - Vue des permissions (#86)
- 📋 UI - Gestion des modules (#87)
- 📋 UI - Enseignements (Assignations) (#88)
---
### API Endpoints
**Students API**
- 📋 GET `/students` (#7)
- 📋 POST `/students` (#8)
- 📋 POST `/students/import-csv` (#9)
- 📋 GET `/students/{numEtud}` (#10)
- 📋 PUT `/students/{numEtud}` (#11)
- 📋 DELETE `/students/{numEtud}` (#12)
- 📋 GET `/promotions` (#13)
- 📋 POST `/promotions` (#14)
- 📋 GET `/promotions/{idPromo}` (#15)
- 📋 PUT `/promotions/{idPromo}` (#16)
- 📋 DELETE `/promotions/{idPromo}` (#17)
**Administration API - Modules & Enseignements**
- 📋 GET `/modules` (#23)
- 📋 POST `/modules` (#24)
- 📋 GET `/modules/{idModule}` (#25)
- 📋 PUT `/modules/{idModule}` (#26)
- 📋 DELETE `/modules/{idModule}` (#27)
- 📋 POST `/enseignements` (#29)
- 📋 GET `/enseignements/{idProf}/{idModule}/{idPromo}` (#30)
- 📋 DELETE `/enseignements/{idProf}/{idModule}/{idPromo}` (#31)
**Notes API - UEs & UE-Modules**
- 📋 GET `/ues` (#32)
- 📋 POST `/ues` (#33)
- 📋 GET `/ues/{idUE}` (#34)
- 📋 PUT `/ues/{idUE}` (#35)
- 📋 DELETE `/ues/{idUE}` (#36)
- 📋 GET `/ue-modules` (#37)
- 📋 POST `/ue-modules` (#38)
- 📋 GET `/ue-modules/{idModule}/{idUE}/{idPromo}` (#39)
- 📋 PUT `/ue-modules/{idModule}/{idUE}/{idPromo}` (#40)
- 📋 DELETE `/ue-modules/{idModule}/{idUE}/{idPromo}` (#41)
**Notes API - Notes & Ajustements**
- 📋 GET `/notes` (#42)
- 📋 POST `/notes` (#43)
- 📋 POST `/notes/import-xlsx` (#44)
- 📋 GET `/notes/{numEtud}/{idModule}` (#45)
- 📋 PUT `/notes/{numEtud}/{idModule}` (#46)
- 📋 DELETE `/notes/{numEtud}/{idModule}` (#47)
- 📋 GET `/ajustements` (#48)
- 📋 POST `/ajustements` (#49)
- 📋 GET `/ajustements/{numEtud}/{idUE}` (#50)
- 📋 PUT `/ajustements/{numEtud}/{idUE}` (#51)
- 📋 DELETE `/ajustements/{numEtud}/{idUE}` (#52)
**Administration API - Users, Roles & Permissions**
- 📋 GET `/users` (#60)
- 📋 POST `/users` (#61)
- 📋 GET `/users/{id}` (#62)
- 📋 PUT `/users/{id}` (#63)
- 📋 DELETE `/users/{id}` (#64)
- 📋 GET `/roles` (#65)
- 📋 POST `/roles` (#66)
- 📋 GET `/roles/{idRole}` (#67)
- 📋 PUT `/roles/{idRole}` (#68)
- 📋 DELETE `/roles/{idRole}` (#69)
- 📋 GET `/permissions` (#70)
---
## 🎨 Design Reference
**Figma Prototype**:
https://www.figma.com/design/La79bsUsWnJCtMsrrt2zGd/Prototype?node-id=0-1
This is the **final design specification** for the UI. All UI implementations
should follow this design.
---
## 🚀 Development Guidelines
### Getting Started
```bash
# Run tests
deno task test
# Start development server
deno task start
# Build for production
deno task build
# Format & lint
deno task check
```
### Git Workflow
1. Create branch: `git checkout -b PMPR-{ISSUE_ID}`
2. Implement changes
3. Run tests and linting
4. Submit PR
### Code Style
- Format: Follow Deno defaults (enforced via `deno fmt`)
- Linting: Fresh recommended rules
- TypeScript strict mode enabled
- Use Drizzle ORM for database operations
### Testing
- Write unit tests for business logic
- Integration tests for API endpoints
- E2E tests with HappyDOM for UI interactions
- Mock database with provided helpers
---
## 📦 Key Dependencies
- **fresh@1.7.3** - Web framework
- **drizzle-orm@0.45.2** - ORM
- **pg@8.20.0** - PostgreSQL driver
- **@popov/jwt@1.0.1** - JWT utilities
- **preact@10.22.0** - UI library
- **happy-dom@16.0.0** - DOM testing
---
## 🔗 Related Resources
- **Repository**: https://git.polytech.djalim.fr/djalim/PolyMPR
- **Issue Tracker**: Gitea (via `tea` CLI)
- **Wiki**: Check CONTRIBUTING.md for dev setup
- **Database**: PostgreSQL (configured in `.env`)
---
## 💡 Important Notes
1. **Current Limitation**: The database schema in `/databases/schema.ts` does
NOT match the final ER diagram. This is a priority migration task.
2. **Design System**: Follow the Figma prototype for all UI work.
3. **Module Pattern**: Each module should follow the same pattern: routes, API
endpoints, components, and tests.
4. **Permissions**: All admin operations should respect the ROLE_PERMISSION
system.
5. **Fresh Conventions**: Routes use Fresh's file-based routing convention
(e.g., `routes/path/index.tsx`).
+1 -1
View File
@@ -3,7 +3,7 @@ FROM denoland/deno:alpine
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN deno cache main.ts --allow-import RUN deno cache --allow-import main.ts
RUN deno task build RUN deno task build
USER deno USER deno
-158
View File
@@ -1,158 +0,0 @@
# Bug Report — PolyMPR
> Généré le 2026-04-23
---
## 🔴 Critique
### #1 — Schema mismatch : module mobility entièrement cassé
**Fichier** : `routes/(apps)/mobility/api/insert_mobility.ts`
Références à des colonnes inexistantes dans le schéma Drizzle :
| Utilisé dans le code | Colonne réelle |
| ---------------------- | ------------------ |
| `students.userId` | `students.numEtud` |
| `students.firstName` | `students.nom` |
| `students.lastName` | `students.prenom` |
| `students.promotionId` | `students.idPromo` |
| `promotions.endyear` | `promotions.annee` |
| `promotions.current` | _(n'existe pas)_ |
Le module crashe à l'exécution. À corriger en alignant les noms de colonnes avec
le schéma.
---
### #2 — Auth manquante sur de nombreux endpoints
Les endpoints suivants n'ont aucune vérification `eduPersonPrimaryAffiliation` :
- `routes/(apps)/notes/api/notes.ts` (GET, POST)
- `routes/(apps)/notes/api/ue-modules.ts` (GET, POST)
- `routes/(apps)/notes/api/ues.ts` (GET, POST)
- `routes/(apps)/notes/api/ues/[idUE].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/users.ts` (GET, POST)
- `routes/(apps)/admin/api/users/[id].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/modules/[idModule].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/roles.ts` (GET, POST)
- `routes/(apps)/admin/api/roles/[idRole].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/permissions.ts` (GET)
- `routes/(apps)/mobility/api/insert_mobility.ts`
Tous ces endpoints exposent des données sensibles sans vérifier les permissions.
---
## 🟠 Haut
### #3 — Bug Drizzle ORM : `.where()` avec plusieurs `eq()` sans `and()`
**Fichier** : `routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts` — lignes
34, 72, 100
`.where()` n'accepte qu'un seul argument. Passer plusieurs `eq()` séparés par
des virgules ne génère pas le SQL attendu (seule la première condition est prise
en compte).
```ts
// ❌ Incorrect
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
// ✅ Correct
.where(and(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)))
```
---
### #4 — Bug Drizzle ORM : `.where()` à 3 conditions sans `and()`
**Fichier** :
`routes/(apps)/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts` — handler
GET (~ligne 41)
Même problème que #3, mais avec 3 conditions. Les handlers PUT et DELETE ont
déjà `and()`, seul le GET est affecté.
```ts
// ❌ Incorrect
.where(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
)
// ✅ Correct
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
```
---
## 🟡 Moyen
### #5 — `and()` passé avec des valeurs `undefined`
**Fichier** : `routes/(apps)/notes/api/ue-modules.ts`
```ts
and(
idPromo ? eq(ueModules.idPromo, idPromo) : undefined,
idUE ? eq(ueModules.idUE, idUE) : undefined,
);
```
Drizzle tolère les `undefined` dans `and()` dans certaines versions, mais ce
n'est pas garanti. Mieux vaut construire les conditions dynamiquement avant de
les passer.
---
### #6 — Validation `!numEtud` rejette faussement `0`
**Fichier** : `routes/(apps)/notes/api/notes.ts` — handler POST
```ts
// ❌ Rejette numEtud = 0
if (note === undefined || !numEtud || !idModule)
// ✅ Correct
if (note === undefined || numEtud === undefined || numEtud === null || !idModule)
```
---
### #7 — `Number(idRole)` sans vérification `isNaN`
**Fichier** : `routes/(apps)/admin/api/users.ts`
Si `idRole` est une chaîne non numérique, `Number()` retourne `NaN` ce qui
provoque une erreur SQL.
```ts
// ❌ Pas de vérification
const rows = idRole
? await db.select().from(users).where(eq(users.idRole, Number(idRole)))
: await db.select().from(users);
// ✅ Valider avant usage
const role = Number(idRole);
if (isNaN(role)) return new Response(..., { status: 400 });
```
---
### #8 — Réponses d'erreur en texte brut au lieu de JSON
**Fichier** : `routes/(apps)/notes/api/notes.ts`
Certaines réponses d'erreur retournent une string sans
`content-type: application/json`, incohérent avec le reste de l'API qui retourne
`{ error: "..." }`.
-233
View File
@@ -1,233 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"dotenv": "^17.4.0",
"drizzle-orm": "^0.45.2",
"pg": "^8.20.0",
},
"devDependencies": {
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"tsx": "^4.21.0",
},
},
},
"packages": {
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"dotenv": ["dotenv@17.4.0", "", {}, "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ=="],
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
}
}
+5 -19
View File
@@ -1,24 +1,10 @@
services: services:
app: app:
image: registry.docker.polytech.djalim.fr/polympr:latest container_name: deno_fresh_app
build: .
ports: ports:
- "8008:80" - "80:80"
- "4430:443" - "443:443"
volumes: volumes:
- /home/kevin/PolyMPR/:/app - .:/app
command: deno run -A main.ts command: deno run -A main.ts
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
db:
image: postgres
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASS}
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
-20
View File
@@ -1,20 +0,0 @@
# Contributing
Thank you for your interest in contributing to our project! We appreciate your
help in making this project better. To get started with contributing, please
refer to our
[Contributing Guide](https://github.com/fedyna-k/PolyMPR/wiki/Contributing) on
the project's wiki.
The Contributing Guide provides detailed information on how to:
- Set up your development environment
- Submit issues and feature requests
- Fork the repository and create pull requests
- Follow our coding standards and guidelines
- Report bugs and suggest improvements
If you have any questions or need further assistance, feel free to reach out to
us by opening an issue or contacting the maintainers directly.
Happy coding! 💻✨
-14
View File
@@ -1,14 +0,0 @@
import { drizzle } from "npm:drizzle-orm@0.45.2/node-postgres";
import pg from "npm:pg@8.20.0";
const { Pool } = pg;
const pool = new Pool({
host: Deno.env.get("POSTGRES_HOST"),
port: Number(Deno.env.get("POSTGRES_PORT") ?? 5432),
user: Deno.env.get("POSTGRES_USER"),
password: Deno.env.get("POSTGRES_PASS"),
database: Deno.env.get("POSTGRES_DB"),
});
export const db = drizzle(pool);
+1 -1
View File
@@ -7,5 +7,5 @@ CREATE TABLE mobility (
destinationCountry text, destinationCountry text,
destinationName text, destinationName text,
mobilityStatus text default 'N/A', mobilityStatus text default 'N/A',
foreign key (studentId) references students(userId) attestationFile blob
); );
+1
View File
@@ -1,5 +1,6 @@
create table promotions ( create table promotions (
id integer primary key autoincrement, id integer primary key autoincrement,
name text,
endyear integer, endyear integer,
current integer current integer
); );
@@ -1,100 +0,0 @@
CREATE TABLE "ajustements" (
"numEtud" integer NOT NULL,
"idUE" integer NOT NULL,
"valeur" double precision NOT NULL,
CONSTRAINT "ajustements_numEtud_idUE_pk" PRIMARY KEY("numEtud","idUE")
);
--> statement-breakpoint
CREATE TABLE "enseignements" (
"idProf" text NOT NULL,
"idModule" text NOT NULL,
"idPromo" text NOT NULL,
CONSTRAINT "enseignements_idProf_idModule_idPromo_pk" PRIMARY KEY("idProf","idModule","idPromo")
);
--> statement-breakpoint
CREATE TABLE "mobility" (
"id" serial PRIMARY KEY NOT NULL,
"studentId" integer,
"startDate" date,
"endDate" date,
"weeksCount" integer,
"destinationCountry" text,
"destinationName" text,
"mobilityStatus" text DEFAULT 'N/A'
);
--> statement-breakpoint
CREATE TABLE "modules" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "notes" (
"numEtud" integer NOT NULL,
"idModule" text NOT NULL,
"note" double precision NOT NULL,
CONSTRAINT "notes_numEtud_idModule_pk" PRIMARY KEY("numEtud","idModule")
);
--> statement-breakpoint
CREATE TABLE "permissions" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "promotions" (
"idPromo" text PRIMARY KEY NOT NULL,
"annee" text
);
--> statement-breakpoint
CREATE TABLE "role_permissions" (
"idRole" integer NOT NULL,
"idPermission" text NOT NULL,
CONSTRAINT "role_permissions_idRole_idPermission_pk" PRIMARY KEY("idRole","idPermission")
);
--> statement-breakpoint
CREATE TABLE "roles" (
"id" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "students" (
"numEtud" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL,
"prenom" text NOT NULL,
"idPromo" text
);
--> statement-breakpoint
CREATE TABLE "ue_modules" (
"idModule" text NOT NULL,
"idUE" integer NOT NULL,
"idPromo" text NOT NULL,
"coeff" double precision NOT NULL,
CONSTRAINT "ue_modules_idModule_idUE_idPromo_pk" PRIMARY KEY("idModule","idUE","idPromo")
);
--> statement-breakpoint
CREATE TABLE "ues" (
"id" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL,
"prenom" text NOT NULL,
"idRole" integer
);
--> statement-breakpoint
ALTER TABLE "ajustements" ADD CONSTRAINT "ajustements_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ajustements" ADD CONSTRAINT "ajustements_idUE_ues_id_fk" FOREIGN KEY ("idUE") REFERENCES "public"."ues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idProf_users_id_fk" FOREIGN KEY ("idProf") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mobility" ADD CONSTRAINT "mobility_studentId_students_numEtud_fk" FOREIGN KEY ("studentId") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notes" ADD CONSTRAINT "notes_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notes" ADD CONSTRAINT "notes_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_idRole_roles_id_fk" FOREIGN KEY ("idRole") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_idPermission_permissions_id_fk" FOREIGN KEY ("idPermission") REFERENCES "public"."permissions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "students" ADD CONSTRAINT "students_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idUE_ues_id_fk" FOREIGN KEY ("idUE") REFERENCES "public"."ues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_idRole_roles_id_fk" FOREIGN KEY ("idRole") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;
@@ -1,680 +0,0 @@
{
"id": "bd317b68-1c46-4e83-b4d3-a14f68751afb",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.ajustements": {
"name": "ajustements",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idUE": {
"name": "idUE",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"valeur": {
"name": "valeur",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"ajustements_numEtud_students_numEtud_fk": {
"name": "ajustements_numEtud_students_numEtud_fk",
"tableFrom": "ajustements",
"tableTo": "students",
"columnsFrom": [
"numEtud"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ajustements_idUE_ues_id_fk": {
"name": "ajustements_idUE_ues_id_fk",
"tableFrom": "ajustements",
"tableTo": "ues",
"columnsFrom": [
"idUE"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"ajustements_numEtud_idUE_pk": {
"name": "ajustements_numEtud_idUE_pk",
"columns": [
"numEtud",
"idUE"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.enseignements": {
"name": "enseignements",
"schema": "",
"columns": {
"idProf": {
"name": "idProf",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"enseignements_idProf_users_id_fk": {
"name": "enseignements_idProf_users_id_fk",
"tableFrom": "enseignements",
"tableTo": "users",
"columnsFrom": [
"idProf"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"enseignements_idModule_modules_id_fk": {
"name": "enseignements_idModule_modules_id_fk",
"tableFrom": "enseignements",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"enseignements_idPromo_promotions_idPromo_fk": {
"name": "enseignements_idPromo_promotions_idPromo_fk",
"tableFrom": "enseignements",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"enseignements_idProf_idModule_idPromo_pk": {
"name": "enseignements_idProf_idModule_idPromo_pk",
"columns": [
"idProf",
"idModule",
"idPromo"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.mobility": {
"name": "mobility",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"studentId": {
"name": "studentId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"startDate": {
"name": "startDate",
"type": "date",
"primaryKey": false,
"notNull": false
},
"endDate": {
"name": "endDate",
"type": "date",
"primaryKey": false,
"notNull": false
},
"weeksCount": {
"name": "weeksCount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"destinationCountry": {
"name": "destinationCountry",
"type": "text",
"primaryKey": false,
"notNull": false
},
"destinationName": {
"name": "destinationName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"mobilityStatus": {
"name": "mobilityStatus",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'N/A'"
}
},
"indexes": {},
"foreignKeys": {
"mobility_studentId_students_numEtud_fk": {
"name": "mobility_studentId_students_numEtud_fk",
"tableFrom": "mobility",
"tableTo": "students",
"columnsFrom": [
"studentId"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.modules": {
"name": "modules",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"note": {
"name": "note",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"notes_numEtud_students_numEtud_fk": {
"name": "notes_numEtud_students_numEtud_fk",
"tableFrom": "notes",
"tableTo": "students",
"columnsFrom": [
"numEtud"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"notes_idModule_modules_id_fk": {
"name": "notes_idModule_modules_id_fk",
"tableFrom": "notes",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"notes_numEtud_idModule_pk": {
"name": "notes_numEtud_idModule_pk",
"columns": [
"numEtud",
"idModule"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.permissions": {
"name": "permissions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.promotions": {
"name": "promotions",
"schema": "",
"columns": {
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": true,
"notNull": true
},
"annee": {
"name": "annee",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.role_permissions": {
"name": "role_permissions",
"schema": "",
"columns": {
"idRole": {
"name": "idRole",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idPermission": {
"name": "idPermission",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"role_permissions_idRole_roles_id_fk": {
"name": "role_permissions_idRole_roles_id_fk",
"tableFrom": "role_permissions",
"tableTo": "roles",
"columnsFrom": [
"idRole"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"role_permissions_idPermission_permissions_id_fk": {
"name": "role_permissions_idPermission_permissions_id_fk",
"tableFrom": "role_permissions",
"tableTo": "permissions",
"columnsFrom": [
"idPermission"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"role_permissions_idRole_idPermission_pk": {
"name": "role_permissions_idRole_idPermission_pk",
"columns": [
"idRole",
"idPermission"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.roles": {
"name": "roles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.students": {
"name": "students",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"prenom": {
"name": "prenom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"students_idPromo_promotions_idPromo_fk": {
"name": "students_idPromo_promotions_idPromo_fk",
"tableFrom": "students",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ue_modules": {
"name": "ue_modules",
"schema": "",
"columns": {
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idUE": {
"name": "idUE",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": true
},
"coeff": {
"name": "coeff",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"ue_modules_idModule_modules_id_fk": {
"name": "ue_modules_idModule_modules_id_fk",
"tableFrom": "ue_modules",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ue_modules_idUE_ues_id_fk": {
"name": "ue_modules_idUE_ues_id_fk",
"tableFrom": "ue_modules",
"tableTo": "ues",
"columnsFrom": [
"idUE"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ue_modules_idPromo_promotions_idPromo_fk": {
"name": "ue_modules_idPromo_promotions_idPromo_fk",
"tableFrom": "ue_modules",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"ue_modules_idModule_idUE_idPromo_pk": {
"name": "ue_modules_idModule_idUE_idPromo_pk",
"columns": [
"idModule",
"idUE",
"idPromo"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ues": {
"name": "ues",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"prenom": {
"name": "prenom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idRole": {
"name": "idRole",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"users_idRole_roles_id_fk": {
"name": "users_idRole_roles_id_fk",
"tableFrom": "users",
"tableTo": "roles",
"columnsFrom": [
"idRole"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1777155028708,
"tag": "0000_square_jetstream",
"breakpoints": true
}
]
}
-99
View File
@@ -1,99 +0,0 @@
import {
date,
doublePrecision,
integer,
pgTable,
primaryKey,
serial,
text,
} from "drizzle-orm/pg-core";
export const roles = pgTable("roles", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const permissions = pgTable("permissions", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const rolePermissions = pgTable("role_permissions", {
idRole: integer("idRole").notNull().references(() => roles.id),
idPermission: text("idPermission").notNull().references(() => permissions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idRole, t.idPermission] }),
}));
export const users = pgTable("users", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idRole: integer("idRole").references(() => roles.id),
});
export const promotions = pgTable("promotions", {
id: text("idPromo").primaryKey(),
annee: text("annee"),
});
export const students = pgTable("students", {
numEtud: serial("numEtud").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idPromo: text("idPromo").references(() => promotions.id),
});
export const modules = pgTable("modules", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const enseignements = pgTable("enseignements", {
idProf: text("idProf").notNull().references(() => users.id),
idModule: text("idModule").notNull().references(() => modules.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idProf, t.idModule, t.idPromo] }),
}));
export const ues = pgTable("ues", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const ueModules = pgTable("ue_modules", {
idModule: text("idModule").notNull().references(() => modules.id),
idUE: integer("idUE").notNull().references(() => ues.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
coeff: doublePrecision("coeff").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.idModule, t.idUE, t.idPromo] }),
}));
export const notes = pgTable("notes", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idModule: text("idModule").notNull().references(() => modules.id),
note: doublePrecision("note").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idModule] }),
}));
export const ajustements = pgTable("ajustements", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idUE: integer("idUE").notNull().references(() => ues.id),
valeur: doublePrecision("valeur").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idUE] }),
}));
export const mobility = pgTable("mobility", {
id: serial("id").primaryKey(),
studentId: integer("studentId").references(() => students.numEtud),
startDate: date("startDate"),
endDate: date("endDate"),
weeksCount: integer("weeksCount"),
destinationCountry: text("destinationCountry"),
destinationName: text("destinationName"),
mobilityStatus: text("mobilityStatus").default("N/A"),
});
-99
View File
@@ -1,99 +0,0 @@
import {
date,
doublePrecision,
integer,
pgTable,
primaryKey,
serial,
text,
} from "npm:drizzle-orm@0.45.2/pg-core";
export const roles = pgTable("roles", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const permissions = pgTable("permissions", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const rolePermissions = pgTable("role_permissions", {
idRole: integer("idRole").notNull().references(() => roles.id),
idPermission: text("idPermission").notNull().references(() => permissions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idRole, t.idPermission] }),
}));
export const users = pgTable("users", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idRole: integer("idRole").references(() => roles.id),
});
export const promotions = pgTable("promotions", {
id: text("idPromo").primaryKey(),
annee: text("annee"),
});
export const students = pgTable("students", {
numEtud: serial("numEtud").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idPromo: text("idPromo").references(() => promotions.id),
});
export const modules = pgTable("modules", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const enseignements = pgTable("enseignements", {
idProf: text("idProf").notNull().references(() => users.id),
idModule: text("idModule").notNull().references(() => modules.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idProf, t.idModule, t.idPromo] }),
}));
export const ues = pgTable("ues", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const ueModules = pgTable("ue_modules", {
idModule: text("idModule").notNull().references(() => modules.id),
idUE: integer("idUE").notNull().references(() => ues.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
coeff: doublePrecision("coeff").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.idModule, t.idUE, t.idPromo] }),
}));
export const notes = pgTable("notes", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idModule: text("idModule").notNull().references(() => modules.id),
note: doublePrecision("note").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idModule] }),
}));
export const ajustements = pgTable("ajustements", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idUE: integer("idUE").notNull().references(() => ues.id),
valeur: doublePrecision("valeur").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idUE] }),
}));
export const mobility = pgTable("mobility", {
id: serial("id").primaryKey(),
studentId: integer("studentId").references(() => students.numEtud),
startDate: date("startDate"),
endDate: date("endDate"),
weeksCount: integer("weeksCount"),
destinationCountry: text("destinationCountry"),
destinationName: text("destinationName"),
mobilityStatus: text("mobilityStatus").default("N/A"),
});
+4 -2
View File
@@ -1,14 +1,16 @@
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser"; import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
import { AsyncRoute } from "$fresh/src/server/types.ts"; import { AsyncRoute } from "$fresh/src/server/types.ts";
export interface AuthenticatedState { interface AuthenticatedState {
isAuthenticated: true; isAuthenticated: true;
isFromPolytech: boolean;
role: "etudiants" | "personnels" | "autres";
session: CasContent; session: CasContent;
availablePages: Record<string, string>;
} }
interface UnauthenticatedState { interface UnauthenticatedState {
isAuthenticated: false; isAuthenticated: false;
isFromPolytech: false;
session: undefined; session: undefined;
} }
+2 -12
View File
@@ -9,12 +9,7 @@
"start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts", "start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts",
"build": "deno run -A --unstable-ffi dev.ts build", "build": "deno run -A --unstable-ffi dev.ts build",
"preview": "deno run -A --unstable-ffi main.ts", "preview": "deno run -A --unstable-ffi main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update .", "update": "deno run -A -r https://fresh.deno.dev/update ."
"test": "deno test -A --no-check tests/",
"test:unit": "deno test -A --no-check tests/unit/",
"test:integration": "deno test -A --no-check tests/integration/",
"test:e2e": "deno test -A --no-check tests/e2e/",
"migrate": "node_modules/.bin/drizzle-kit migrate"
}, },
"lint": { "lint": {
"rules": { "rules": {
@@ -34,17 +29,12 @@
"@popov/jwt": "jsr:@popov/jwt@^1.0.1", "@popov/jwt": "jsr:@popov/jwt@^1.0.1",
"@psych/sheet": "jsr:@psych/sheet@^1.0.6", "@psych/sheet": "jsr:@psych/sheet@^1.0.6",
"@std/cli": "jsr:@std/cli@^1.0.10", "@std/cli": "jsr:@std/cli@^1.0.10",
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"preact": "https://esm.sh/preact@10.22.0", "preact": "https://esm.sh/preact@10.22.0",
"preact/": "https://esm.sh/preact@10.22.0/", "preact/": "https://esm.sh/preact@10.22.0/",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/", "$std/": "https://deno.land/std@0.216.0/",
"@std/assert": "jsr:@std/assert@^1.0.0", "$root/": "./"
"@std/testing": "jsr:@std/testing@^1.0.0",
"happy-dom": "npm:happy-dom@^16.0.0",
"$root/": "./",
"$apps/": "./routes/(apps)/"
}, },
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
-17
View File
@@ -1,17 +0,0 @@
import { defineConfig } from "drizzle-kit";
import process from "node:process";
const url = process.env.DATABASE_URL ??
`postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASS}@${
process.env.POSTGRES_HOST ?? "localhost"
}:${process.env.POSTGRES_PORT ?? 5432}/${process.env.POSTGRES_DB}`;
export default defineConfig({
dialect: "postgresql",
schema: "./databases/schema.kit.ts",
out: "./databases/migrations",
dbCredentials: {
url,
ssl: false,
},
});
-8
View File
@@ -1,8 +0,0 @@
#Local mode, set to true to access admin pages with any users
LOCAL=false
POSTGRES_HOST = db
POSTGRES_PORT = 5432
POSTGRES_PASS = astrongpass
POSTGRES_USER = postgres
POSTGRES_DB = polympr
Generated
-61
View File
@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-62
View File
@@ -1,62 +0,0 @@
{
description = "PolyMPR CLI - A tool for managing PolyMPR modules";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.pmpr = pkgs.stdenv.mkDerivation {
pname = "pmpr";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
pkgs.deno
pkgs.autoPatchelfHook
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
buildPhase = ''
export HOME=$TMPDIR
deno cache toolbox/cli.ts
deno compile -A --output pmpr toolbox/cli.ts
'';
installPhase = ''
mkdir -p $out/bin
cp pmpr $out/bin/pmpr
'';
};
packages.default = self.packages.${system}.pmpr;
devShells.default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.deno
pkgs.patchelf
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
export NIX_LD_INTERPRETER=$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)
echo "Welcome to PolyMPR development shell!"
echo "Use 'deno task compile' to build the CLI."
'';
};
}
);
}
-2
View File
@@ -1,8 +1,6 @@
import { defineConfig } from "$fresh/server.ts"; import { defineConfig } from "$fresh/server.ts";
import ensureDatabases from "$root/databases/ensure.ts"; import ensureDatabases from "$root/databases/ensure.ts";
import { load } from "@std/dotenv";
await load({ envPath: "./.env", export: true });
await ensureDatabases(); await ensureDatabases();
export default defineConfig({ export default defineConfig({
server: { server: {
+14 -11
View File
@@ -3,22 +3,24 @@
// This file is automatically updated during development when running `dev.ts`. // This file is automatically updated during development when running `dev.ts`.
import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; import * as $_apps_layout from "./routes/(apps)/_layout.tsx";
import * as $_apps_middleware from "./routes/(apps)/_middleware.ts"; import * as $_apps_mobility_api_download from "./routes/(apps)/mobility/api/download.ts";
import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.ts"; import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert-mobility.ts";
import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx"; import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx";
import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx"; import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx";
import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx"; import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx";
import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx"; import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx";
import * as $_apps_mobility_types_d from "./routes/(apps)/mobility/types.d.ts";
import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx"; import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx";
import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx"; import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx";
import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx"; import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx"; import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
import * as $_apps_students_api_students from "./routes/(apps)/students/api/students.ts"; import * as $_apps_students_api_insert_students from "./routes/(apps)/students/api/insert_students.ts";
import * as $_apps_students_index from "./routes/(apps)/students/index.tsx"; import * as $_apps_students_index from "./routes/(apps)/students/index.tsx";
import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx"; import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx";
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx"; import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx"; import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
import * as $_apps_students_types_d from "./routes/(apps)/students/types.d.ts"; import * as $_apps_students_partials_overview from "./routes/(apps)/students/partials/overview.tsx";
import * as $_403 from "./routes/_403.tsx";
import * as $_404 from "./routes/_404.tsx"; import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx"; import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts"; import * as $_middleware from "./routes/_middleware.ts";
@@ -31,7 +33,6 @@ import * as $_islands_AppNavigator from "./routes/(_islands)/AppNavigator.tsx";
import * as $_islands_Navbar from "./routes/(_islands)/Navbar.tsx"; import * as $_islands_Navbar from "./routes/(_islands)/Navbar.tsx";
import * as $_apps_mobility_islands_ConsultMobility from "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx"; import * as $_apps_mobility_islands_ConsultMobility from "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx";
import * as $_apps_mobility_islands_EditMobility from "./routes/(apps)/mobility/(_islands)/EditMobility.tsx"; import * as $_apps_mobility_islands_EditMobility from "./routes/(apps)/mobility/(_islands)/EditMobility.tsx";
import * as $_apps_mobility_islands_ImportFile from "./routes/(apps)/mobility/(_islands)/ImportFile.tsx";
import * as $_apps_students_islands_ConsultStudents from "./routes/(apps)/students/(_islands)/ConsultStudents.tsx"; import * as $_apps_students_islands_ConsultStudents from "./routes/(apps)/students/(_islands)/ConsultStudents.tsx";
import * as $_apps_students_islands_EditStudents from "./routes/(apps)/students/(_islands)/EditStudents.tsx"; import * as $_apps_students_islands_EditStudents from "./routes/(apps)/students/(_islands)/EditStudents.tsx";
import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx"; import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx";
@@ -40,8 +41,8 @@ import type { Manifest } from "$fresh/server.ts";
const manifest = { const manifest = {
routes: { routes: {
"./routes/(apps)/_layout.tsx": $_apps_layout, "./routes/(apps)/_layout.tsx": $_apps_layout,
"./routes/(apps)/_middleware.ts": $_apps_middleware, "./routes/(apps)/mobility/api/download.ts": $_apps_mobility_api_download,
"./routes/(apps)/mobility/api/insert_mobility.ts": "./routes/(apps)/mobility/api/insert-mobility.ts":
$_apps_mobility_api_insert_mobility, $_apps_mobility_api_insert_mobility,
"./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index,
"./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx": "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx":
@@ -50,12 +51,14 @@ const manifest = {
$_apps_mobility_partials_index, $_apps_mobility_partials_index,
"./routes/(apps)/mobility/partials/overview.tsx": "./routes/(apps)/mobility/partials/overview.tsx":
$_apps_mobility_partials_overview, $_apps_mobility_partials_overview,
"./routes/(apps)/mobility/types.d.ts": $_apps_mobility_types_d,
"./routes/(apps)/notes/index.tsx": $_apps_notes_index, "./routes/(apps)/notes/index.tsx": $_apps_notes_index,
"./routes/(apps)/notes/partials/(admin)/courses.tsx": "./routes/(apps)/notes/partials/(admin)/courses.tsx":
$_apps_notes_partials_admin_courses, $_apps_notes_partials_admin_courses,
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index, "./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes, "./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
"./routes/(apps)/students/api/students.ts": $_apps_students_api_students, "./routes/(apps)/students/api/insert_students.ts":
$_apps_students_api_insert_students,
"./routes/(apps)/students/index.tsx": $_apps_students_index, "./routes/(apps)/students/index.tsx": $_apps_students_index,
"./routes/(apps)/students/partials/(admin)/consult.tsx": "./routes/(apps)/students/partials/(admin)/consult.tsx":
$_apps_students_partials_admin_consult, $_apps_students_partials_admin_consult,
@@ -63,7 +66,9 @@ const manifest = {
$_apps_students_partials_admin_upload, $_apps_students_partials_admin_upload,
"./routes/(apps)/students/partials/index.tsx": "./routes/(apps)/students/partials/index.tsx":
$_apps_students_partials_index, $_apps_students_partials_index,
"./routes/(apps)/students/types.d.ts": $_apps_students_types_d, "./routes/(apps)/students/partials/overview.tsx":
$_apps_students_partials_overview,
"./routes/_403.tsx": $_403,
"./routes/_404.tsx": $_404, "./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app, "./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware, "./routes/_middleware.ts": $_middleware,
@@ -80,8 +85,6 @@ const manifest = {
$_apps_mobility_islands_ConsultMobility, $_apps_mobility_islands_ConsultMobility,
"./routes/(apps)/mobility/(_islands)/EditMobility.tsx": "./routes/(apps)/mobility/(_islands)/EditMobility.tsx":
$_apps_mobility_islands_EditMobility, $_apps_mobility_islands_EditMobility,
"./routes/(apps)/mobility/(_islands)/ImportFile.tsx":
$_apps_mobility_islands_ImportFile,
"./routes/(apps)/students/(_islands)/ConsultStudents.tsx": "./routes/(apps)/students/(_islands)/ConsultStudents.tsx":
$_apps_students_islands_ConsultStudents, $_apps_students_islands_ConsultStudents,
"./routes/(apps)/students/(_islands)/EditStudents.tsx": "./routes/(apps)/students/(_islands)/EditStudents.tsx":
-18
View File
@@ -1,18 +0,0 @@
Copyright 2025 - PolyMPR team @ Polytech Marseille
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-12
View File
@@ -1,12 +0,0 @@
{
"dependencies": {
"dotenv": "^17.4.0",
"drizzle-orm": "^0.45.2",
"pg": "^8.20.0"
},
"devDependencies": {
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"tsx": "^4.21.0"
}
}
+2 -87
View File
@@ -1,89 +1,4 @@
# ✨ PolyMPR ✨ # ✨ PolyMPR ✨
**PolyMPR** (Poly Management Platform for Resources) is a modern, modular The ✨ Poly Module de Pilotage des Ressources is the ultimate tool to handle
framework built on **Deno** and **Fresh**, designed to help organizations various HR task in the INFO department.
transition their HR systems to the cloud. With its **modulith architecture**,
PolyMPR simplifies the development, deployment, and maintenance of HR
applications, making it the perfect choice for teams looking to modernize their
workflows. 🌐
## Features ✨
- **Modular Design**: Easily add, remove, or update features without disrupting
the entire system. 🧩
- **Cloud-Native**: Built for the cloud, enabling seamless integration with
cloud services (amU DataCenter). ☁️
- **Deno-Powered**: Utilizes Deno's secure runtime for TypeScript. 🦕
- **Fresh Framework**: Delivers fast, edge-ready web applications with minimal
overhead. ⚡
- **HR-Focused**: Tailored to meet the unique needs of INFO's HR. 👩‍💼👨‍💼
## Getting Started 🛠️
### Prerequisites
- **Deno**: Install Deno by following the
[official guide](https://deno.land/#installation).
- **Docker** (optional): Install Docker for containerized deployments. Follow
the [Docker installation guide](https://docs.docker.com/get-docker/).
### Installation
1. Clone the PolyMPR repository:
```bash
git clone https://github.com/fedyna-k/PolyMPR.git
cd PolyMPR
```
2. Start the application:
```bash
deno task start
```
3. Access the application at `https://localhost`.
For detailed installation instructions, check out the
[Installation Guide](./wiki/installation).
## Modules Overview 🧩
PolyMPR comes with a variety of modules to streamline HR processes.
To learn how to create a module, visit the [Module Overview](./wiki/modules).
## CLI Documentation 📄
The **PolyMPR CLI** simplifies development tasks. Here are some common commands:
- Create a new module:
```bash
pmpr module create <module-name-kebab-case>
```
For detailed CLI usage, check out the [CLI Documentation](./wiki/cli).
## Contributing 🤝
We welcome contributions from the community! Whether you're fixing bugs, adding
features, or improving documentation, your help is appreciated. Heres how to
get started:
1. Create a new issue.
2. Create a new branch for your changes:
```bash
git checkout -b PMPR-:ISSUE_ID:
```
3. Commit your changes and push them to your branch.
4. Submit a pull request.
For more details, read the [Contributing Guide](./contributing).
## Community and Support 🌟
Join the PolyMPR community to connect with other users and developers:
- **GitHub Discussions**: Ask questions and share ideas. 💬
- **Issue Tracker**: Report bugs or request features. 🐛
## License 📜
PolyMPR is open-source and released under the **MIT License**. Feel free to use,
modify, and distribute it as per the license terms.
+7 -4
View File
@@ -1,19 +1,22 @@
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts"; import { Partial } from "$fresh/runtime.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { State } from "$root/defaults/interfaces.ts";
import { AppProperties } from "$root/defaults/interfaces.ts";
import Navbar from "$root/routes/(_islands)/Navbar.tsx"; import Navbar from "$root/routes/(_islands)/Navbar.tsx";
// deno-lint-ignore require-await
export default async function AppLayout( export default async function AppLayout(
request: Request, request: Request,
context: FreshContext<AuthenticatedState>, context: FreshContext<State>,
) { ) {
const pathname = new URL(request.url).pathname; const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1]; const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
return ( return (
<section id="app"> <section id="app">
<Navbar currentApp={currentApp} pages={context.state.availablePages} /> <Navbar currentApp={currentApp} pages={properties.pages} />
<section id="app-body"> <section id="app-body">
<Partial name="body"> <Partial name="body">
<context.Component /> <context.Component />
-36
View File
@@ -1,36 +0,0 @@
import { FreshContext, MiddlewareHandler } from "$fresh/server.ts";
import {
AppProperties,
AuthenticatedState,
} from "$root/defaults/interfaces.ts";
export const handler: MiddlewareHandler<AuthenticatedState>[] = [
/**
* Get all available pages for current user.
* @param request The HTTP incomming request.
* @param context The Fresh context object with custom `AuthenticatedState`.
* @returns The response from the next middleware.
*/
async function getAllAvailablePages(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
context.state.availablePages = properties.pages;
if (
context.state.session.eduPersonPrimaryAffiliation == "student" &&
Deno.env.get("LOCAL") != "true"
) {
properties.adminOnly.forEach((page) =>
delete context.state.availablePages[page]
);
}
return await context.next();
},
];
-13
View File
@@ -1,13 +0,0 @@
import { AppProperties } from "$root/defaults/interfaces.ts";
const properties: AppProperties = {
name: "Admin",
icon: "school",
pages: {
index: "Homepage",
},
adminOnly: [],
hint: "PolyMPR module",
};
export default properties;
-70
View File
@@ -1,70 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { enseignements } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
const _NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
const CONFLICT = new Response(
JSON.stringify({ error: "Cet enseignement existe déjà." }),
{ status: 409, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #29 POST /enseignements
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const body: {
idProf: string;
idModule: string;
idPromo: string;
} = await request.json();
if (!body.idProf || !body.idModule || !body.idPromo) {
return new Response(null, { status: 400 });
}
// Check if enseignement already exists
const existing = await db
.select()
.from(enseignements)
.where(
and(
eq(enseignements.idProf, body.idProf),
eq(enseignements.idModule, body.idModule),
eq(enseignements.idPromo, body.idPromo),
),
)
.then((rows) => rows[0] ?? null);
if (existing) {
return CONFLICT;
}
const [created] = await db
.insert(enseignements)
.values({
idProf: body.idProf,
idModule: body.idModule,
idPromo: body.idPromo,
})
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -1,75 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { enseignements } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #30 GET /enseignements/{idProf}/{idModule}/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idProf = context.params.idProf;
const idModule = context.params.idModule;
const idPromo = context.params.idPromo;
const enseignement = await db
.select()
.from(enseignements)
.where(
and(
eq(enseignements.idProf, idProf),
eq(enseignements.idModule, idModule),
eq(enseignements.idPromo, idPromo),
),
)
.then((rows) => rows[0] ?? null);
if (!enseignement) return NOT_FOUND;
return new Response(JSON.stringify(enseignement), {
headers: { "content-type": "application/json" },
});
},
// #31 DELETE /enseignements/{idProf}/{idModule}/{idPromo}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idProf = context.params.idProf;
const idModule = context.params.idModule;
const idPromo = context.params.idPromo;
const [deleted] = await db
.delete(enseignements)
.where(
and(
eq(enseignements.idProf, idProf),
eq(enseignements.idModule, idModule),
eq(enseignements.idPromo, idPromo),
),
)
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-22
View File
@@ -1,22 +0,0 @@
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async POST(request, context) {
if (request.headers.get("content-type") != "application/json") {
return new Response(null, {
status: 400,
});
}
const responseBody = {
requestBody: await request.json(),
context,
};
return new Response(JSON.stringify(responseBody), {
headers: {
"content-type": "application/json",
},
});
},
};
-63
View File
@@ -1,63 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { modules } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers<null, AuthenticatedState> = {
// #23 GET /modules
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
const rows = await db.select().from(modules);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #24 POST /modules
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const body: { id: string; nom: string } = await request.json();
if (!body.id || !body.nom) {
return new Response(null, { status: 400 });
}
const existing = await db
.select()
.from(modules)
.where(eq(modules.id, body.id))
.then((rows) => rows[0] ?? null);
if (existing) {
return new Response(
JSON.stringify({ error: "Un module avec cet identifiant existe déjà" }),
{ status: 409, headers: { "content-type": "application/json" } },
);
}
const [created] = await db
.insert(modules)
.values({ id: body.id, nom: body.nom })
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -1,65 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { modules } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #25 GET /modules/{idModule}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const module = await db
.select()
.from(modules)
.where(eq(modules.id, context.params.idModule))
.then((rows) => rows[0] ?? null);
if (!module) return NOT_FOUND;
return new Response(JSON.stringify(module), {
headers: { "content-type": "application/json" },
});
},
// #26 PUT /modules/{idModule}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string } = await request.json();
const [updated] = await db
.update(modules)
.set({ nom: body.nom })
.where(eq(modules.id, context.params.idModule))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #27 DELETE /modules/{idModule}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const [deleted] = await db
.delete(modules)
.where(eq(modules.id, context.params.idModule))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-22
View File
@@ -1,22 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
const PERMISSIONS = [
{ id: "student_read", nom: "Consulter les élèves" },
{ id: "student_write", nom: "Gérer les élèves" },
{ id: "note_read", nom: "Consulter les notes" },
{ id: "note_write", nom: "Gérer les notes" },
{ id: "module_read", nom: "Consulter les modules" },
{ id: "module_write", nom: "Gérer les modules" },
{ id: "user_read", nom: "Consulter les utilisateurs" },
{ id: "user_write", nom: "Gérer les utilisateurs" },
{ id: "role_write", nom: "Gérer les rôles" },
] as const;
export const handler: Handlers<null, AuthenticatedState> = {
GET(_request, _context): Response {
return new Response(JSON.stringify(PERMISSIONS), {
headers: { "content-type": "application/json" },
});
},
};
-68
View File
@@ -1,68 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { rolePermissions, roles } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
async function getRoleWithPermissions(
id: number,
): Promise<{ id: number; nom: string; permissions: string[] } | null> {
const role = await db
.select()
.from(roles)
.where(eq(roles.id, id))
.then((rows) => rows[0] ?? null);
if (!role) return null;
const perms = await db
.select({ idPermission: rolePermissions.idPermission })
.from(rolePermissions)
.where(eq(rolePermissions.idRole, id));
return {
id: role.id,
nom: role.nom,
permissions: perms.map((p) => p.idPermission),
};
}
export const handler: Handlers<null, AuthenticatedState> = {
// #65 GET /roles
async GET(
_request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const allRoles = await db.select().from(roles);
const result = await Promise.all(
allRoles.map((r) => getRoleWithPermissions(r.id)),
);
return new Response(JSON.stringify(result), {
headers: { "content-type": "application/json" },
});
},
// #66 POST /roles
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string } = await request.json();
if (!body.nom) {
return new Response(null, { status: 400 });
}
const [created] = await db
.insert(roles)
.values({ nom: body.nom })
.returning();
return new Response(
JSON.stringify({ id: created.id, nom: created.nom, permissions: [] }),
{ status: 201, headers: { "content-type": "application/json" } },
);
},
};
-101
View File
@@ -1,101 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { rolePermissions, roles } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
async function getRoleWithPermissions(
id: number,
): Promise<{ id: number; nom: string; permissions: string[] } | null> {
const role = await db
.select()
.from(roles)
.where(eq(roles.id, id))
.then((rows) => rows[0] ?? null);
if (!role) return null;
const perms = await db
.select({ idPermission: rolePermissions.idPermission })
.from(rolePermissions)
.where(eq(rolePermissions.idRole, id));
return {
id: role.id,
nom: role.nom,
permissions: perms.map((p) => p.idPermission),
};
}
export const handler: Handlers<null, AuthenticatedState> = {
// #67 GET /roles/{idRole}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
const role = await getRoleWithPermissions(id);
if (!role) return NOT_FOUND;
return new Response(JSON.stringify(role), {
headers: { "content-type": "application/json" },
});
},
// #68 PUT /roles/{idRole}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
const body: { nom: string; permissions: string[] } = await request.json();
const [updated] = await db
.update(roles)
.set({ nom: body.nom })
.where(eq(roles.id, id))
.returning();
if (!updated) return NOT_FOUND;
// Reset permissions
await db.delete(rolePermissions).where(eq(rolePermissions.idRole, id));
if (body.permissions?.length) {
await db.insert(rolePermissions).values(
body.permissions.map((p) => ({ idRole: id, idPermission: p })),
);
}
const role = await getRoleWithPermissions(id);
return new Response(JSON.stringify(role), {
headers: { "content-type": "application/json" },
});
},
// #69 DELETE /roles/{idRole}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
// Cascade delete role_permissions first
await db.delete(rolePermissions).where(eq(rolePermissions.idRole, id));
const [deleted] = await db
.delete(roles)
.where(eq(roles.id, id))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-67
View File
@@ -1,67 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { users } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers<null, AuthenticatedState> = {
// #60 GET /users
async GET(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const url = new URL(request.url);
const idRole = url.searchParams.get("idRole");
const rows = idRole
? await db.select().from(users).where(eq(users.idRole, Number(idRole)))
: await db.select().from(users);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #61 POST /users
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { id: string; nom: string; prenom: string; idRole: number } =
await request.json();
if (!body.id || !body.nom || !body.prenom) {
return new Response(null, { status: 400 });
}
const existing = await db
.select()
.from(users)
.where(eq(users.id, body.id))
.then((rows) => rows[0] ?? null);
if (existing) {
return new Response(
JSON.stringify({
error: "Un utilisateur avec cet identifiant existe déjà",
}),
{ status: 409, headers: { "content-type": "application/json" } },
);
}
const [created] = await db
.insert(users)
.values({
id: body.id,
nom: body.nom,
prenom: body.prenom,
idRole: body.idRole,
})
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
-66
View File
@@ -1,66 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { users } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #62 GET /users/{id}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const user = await db
.select()
.from(users)
.where(eq(users.id, context.params.id))
.then((rows) => rows[0] ?? null);
if (!user) return NOT_FOUND;
return new Response(JSON.stringify(user), {
headers: { "content-type": "application/json" },
});
},
// #63 PUT /users/{id}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string; prenom: string; idRole: number } = await request
.json();
const [updated] = await db
.update(users)
.set({ nom: body.nom, prenom: body.prenom, idRole: body.idRole })
.where(eq(users.id, context.params.id))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #64 DELETE /users/{id}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const [deleted] = await db
.delete(users)
.where(eq(users.id, context.params.id))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-2
View File
@@ -1,2 +0,0 @@
import makeIndex from "$root/defaults/makeIndex.ts";
export default makeIndex(import.meta.dirname!);
-13
View File
@@ -1,13 +0,0 @@
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts";
export function Index(_request: Request, _context: FreshContext<State>) {
return <h2>Welcome to Admin.</h2>;
}
export const config = getPartialsConfig();
export default makePartials(Index);
@@ -1,75 +1,100 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
interface Promotion {
id: number;
name: string;
}
interface Student {
id: string;
firstName: string;
lastName: string;
promotionId: number;
}
interface Mobility {
id: number;
studentId: string;
startDate: string | null;
endDate: string | null;
weeksCount: number | null;
destinationCountry: string | null;
destinationName: string | null;
mobilityStatus: string;
}
export default function ConsultMobility() { export default function ConsultMobility() {
const [data, setData] = useState< const [mobilityData, setMobilityData] = useState<MobilityData[]>([]);
| { const [promotions, setPromotions] = useState<Promotion[]>([]);
promotions?: Promotion[]; const [selectedPromotion, setSelectedPromotion] = useState<number | "all">("all");
students?: Student[];
mobilities?: Mobility[];
}
| null
>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
console.log("ConsultMobility: Fetching data from API...");
try { try {
const response = await fetch("/mobility/api/insert_mobility"); console.log("ConsultMobility: Fetching data from API...");
console.log("ConsultMobility: API response status:", response.status); const response = await fetch("/mobility/api/insert-mobility");
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching data: ${response.statusText}`); throw new Error(`Error fetching data: ${response.statusText}`);
} }
const result = await response.json(); const result = await response.json();
console.log("ConsultMobility: Data fetched successfully:", result); console.log("ConsultMobility: Data fetched successfully:", result);
setData(result);
setPromotions(result.promotions);
const mergedData = result.students.map((student: any) => {
const existingMobility = result.mobilities.find(
(mobility: any) => mobility.studentId === student.id
);
return {
id: existingMobility ? existingMobility.id : null,
studentId: student.id,
firstName: student.firstName,
lastName: student.lastName,
startDate: existingMobility?.startDate || null,
endDate: existingMobility?.endDate || null,
weeksCount: existingMobility?.weeksCount || null,
destinationCountry: existingMobility?.destinationCountry || null,
destinationName: existingMobility?.destinationName || null,
mobilityStatus: existingMobility?.mobilityStatus || "N/A",
promotionId: student.promotionId,
promotionName: student.promotionName,
attestationFile: existingMobility?.attestationFile || null,
};
});
setMobilityData(mergedData);
} catch (err) { } catch (err) {
console.error("ConsultMobility: Error fetching data:", err); console.error("ConsultMobility: Error fetching data:", err);
setError("Failed to load mobility data. Please try again later."); setError("Failed to load data. Please try again later.");
} }
}; };
fetchData(); fetchData();
}, []); }, []);
if (error) { const filteredData =
return <p className="error">{error}</p>; selectedPromotion === "all"
? mobilityData
: mobilityData.filter((entry) => entry.promotionId === selectedPromotion);
const downloadFile = (id: number | null) => {
if (!id) {
alert("No file available for download.");
return;
} }
if (!data?.promotions) { const downloadUrl = `/mobility/api/download/${id}`;
return <p>No promotions found.</p>; window.open(downloadUrl, "_blank");
} };
return ( return (
<section> <section>
<h2>Consult Mobility</h2> <h2>Consult Mobility</h2>
{data.promotions.map((promo) => ( {error && <p className="error">{error}</p>}
<div>
<label htmlFor="promotionSelect">Select Promotion: </label>
<select
id="promotionSelect"
value={selectedPromotion}
onChange={(e) =>
setSelectedPromotion(
e.target.value === "all" ? "all" : Number(e.target.value)
)
}
>
<option value="all">All Promotions</option>
{promotions.map((promo) => (
<option key={promo.id} value={promo.id}>
{promo.name}
</option>
))}
</select>
</div>
{promotions.map((promo) => (
<div key={promo.id}> <div key={promo.id}>
{selectedPromotion === "all" || selectedPromotion === promo.id ? (
<>
<h3>Promotion: {promo.name}</h3> <h3>Promotion: {promo.name}</h3>
<table> <table>
<thead> <thead>
@@ -83,31 +108,40 @@ export default function ConsultMobility() {
<th>Destination Country</th> <th>Destination Country</th>
<th>Destination Name</th> <th>Destination Name</th>
<th>Status</th> <th>Status</th>
<th>Attestation File</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.students {filteredData
?.filter((student) => student.promotionId === promo.id) .filter((entry) => entry.promotionId === promo.id)
.map((student) => { .map((entry) => (
const mobility = data.mobilities?.find((mob) => <tr key={entry.studentId}>
mob.studentId === student.id <td>{entry.studentId}</td>
); <td>{entry.firstName}</td>
return ( <td>{entry.lastName}</td>
<tr key={student.id}> <td>{entry.startDate || "N/A"}</td>
<td>{student.id}</td> <td>{entry.endDate || "N/A"}</td>
<td>{student.firstName}</td> <td>{entry.weeksCount || "0"}</td>
<td>{student.lastName}</td> <td>{entry.destinationCountry || "N/A"}</td>
<td>{mobility?.startDate || "N/A"}</td> <td>{entry.destinationName || "N/A"}</td>
<td>{mobility?.endDate || "N/A"}</td> <td>{entry.mobilityStatus}</td>
<td>{mobility?.weeksCount ?? "N/A"}</td> <td>
<td>{mobility?.destinationCountry || "N/A"}</td> {entry.attestationFile ? (
<td>{mobility?.destinationName || "N/A"}</td> <button
<td>{mobility?.mobilityStatus || "N/A"}</td> onClick={() => downloadFile(entry.id)}
>
Download
</button>
) : (
"No file"
)}
</td>
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</>
) : null}
</div> </div>
))} ))}
</section> </section>
@@ -1,75 +0,0 @@
import { useEffect, useState } from "preact/hooks";
interface Promotion {
id: number;
name: string;
}
interface Student {
id: number;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
promotionName: string;
}
export default function ConsultStudents_test() {
const [data, setData] = useState<
{ promotions: Promotion[]; students: Student[] } | null
>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("/students/api/insert_students");
if (!response.ok) {
throw new Error(`Error fetching data: ${response.statusText}`);
}
const result = await response.json();
setData(result);
} catch (err) {
console.error("Error fetching data:", err);
setError("Failed to load data. Please try again later.");
}
};
fetchData();
}, []);
return (
<section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>}
{data?.promotions.map((promo) => (
<div key={promo.id}>
<h3>Promotion: {promo.id}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{data.students
.filter((student) => student.promotionId === promo.id)
.map((student) => (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.mail}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</section>
);
}
+197 -149
View File
@@ -1,117 +1,97 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
interface Student {
id: string;
firstName: string;
lastName: string;
promotionId: number;
}
interface Promotion {
id: number;
name: string;
}
interface Mobility {
id: number | null;
studentId: string;
startDate: string | null;
endDate: string | null;
weeksCount: number | null;
destinationCountry: string | null;
destinationName: string | null;
mobilityStatus: string;
}
export default function EditMobility() { export default function EditMobility() {
const [data, setData] = useState< const [mobilityData, setMobilityData] = useState<MobilityData[]>([]);
| { const [promotions, setPromotions] = useState<Promotion[]>([]);
promotions?: Promotion[]; const [selectedPromotion, setSelectedPromotion] = useState<number | "all">("all");
students?: Student[];
mobilities?: Mobility[];
}
| null
>(null);
const [error, setError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
useEffect(() => { useEffect(() => {
const fetchData = async () => { async function fetchMobilityData() {
console.log("EditMobility: Fetching data from API..."); console.log("EditMobility: Fetching data from API...");
try { const response = await fetch("/mobility/api/insert-mobility");
const response = await fetch("/mobility/api/insert_mobility"); const data = await response.json();
console.log("EditMobility: API response status:", response.status); console.log("EditMobility: Data fetched successfully:", data);
if (!response.ok) { setPromotions(data.promotions);
throw new Error(`Error fetching data: ${response.statusText}`);
}
const result = await response.json(); const initializedData = data.students.map((student: any) => {
console.log("EditMobility: Data fetched successfully:", result); const existingMobility = data.mobilities.find(
setData(result); (mobility: any) => mobility.studentId === student.id
} catch (err) { );
console.error("EditMobility: Error fetching data:", err); return {
setError("Failed to load mobility data. Please try again later."); id: existingMobility ? existingMobility.id : null,
} studentId: student.id,
firstName: student.firstName,
lastName: student.lastName,
startDate: existingMobility?.startDate || null,
endDate: existingMobility?.endDate || null,
weeksCount: existingMobility?.weeksCount || null,
destinationCountry: existingMobility?.destinationCountry || null,
destinationName: existingMobility?.destinationName || null,
mobilityStatus: existingMobility?.mobilityStatus || "N/A",
attestationFile: existingMobility?.attestationFile || null,
promotionId: student.promotionId,
promotionName: student.promotionName,
}; };
});
setMobilityData(initializedData);
}
fetchData(); fetchMobilityData();
}, []); }, []);
const handleChange = ( const handleFileChange = (studentId: string, file: File | null) => {
studentId: string, if (file && file.type !== "application/pdf") {
field: keyof Mobility, alert("Only PDF files are allowed.");
value: string | number | null, return;
) => { }
if (!data) return;
setData((prevData) => { setMobilityData((prev) =>
if (!prevData) return null; prev.map((entry) =>
entry.studentId === studentId ? { ...entry, attestationFile: file } : entry
const updatedMobilities = prevData.mobilities?.map((mobility) => { )
if (mobility.studentId === studentId) {
const updatedMobility = { ...mobility, [field]: value };
if (field === "startDate" || field === "endDate") {
const startDate = new Date(updatedMobility.startDate || "");
const endDate = new Date(updatedMobility.endDate || "");
if (startDate && endDate && startDate <= endDate) {
const weeks = Math.ceil(
(endDate.getTime() - startDate.getTime()) /
(7 * 24 * 60 * 60 * 1000),
); );
updatedMobility.weeksCount = weeks;
} else {
updatedMobility.weeksCount = null;
}
}
return updatedMobility;
}
return mobility;
}) || [];
return { ...prevData, mobilities: updatedMobilities };
});
}; };
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
try { try {
const response = await fetch("/mobility/api/insert_mobility", { console.log("EditMobility: Sending data to API...");
method: "POST",
headers: { "Content-Type": "application/json" }, const formData = new FormData();
body: JSON.stringify({ data: data?.mobilities }),
mobilityData.forEach((entry) => {
formData.append(
"data",
JSON.stringify({
id: entry.id,
studentId: entry.studentId,
startDate: entry.startDate,
endDate: entry.endDate,
destinationCountry: entry.destinationCountry,
destinationName: entry.destinationName,
mobilityStatus: entry.mobilityStatus,
})
);
if (entry.attestationFile instanceof File) {
formData.append(`file_${entry.studentId}`, entry.attestationFile);
}
}); });
console.log("EditMobility: Save response status:", response.status); const response = await fetch("/mobility/api/insert-mobility", {
method: "POST",
body: formData,
});
if (response.ok) { if (response.ok) {
alert("Data saved successfully!"); alert("Data saved successfully!");
globalThis.location.reload(); console.log("EditMobility: Save response status:", response.status);
} else { } else {
throw new Error(`Failed to save data: ${response.statusText}`); alert("Failed to save data.");
console.error("EditMobility: Save response status:", response.status);
} }
} catch (error) { } catch (error) {
console.error("EditMobility: Error saving data:", error); console.error("EditMobility: Error saving data:", error);
@@ -121,20 +101,49 @@ export default function EditMobility() {
} }
}; };
if (error) { const filteredData =
return <p className="error">{error}</p>; selectedPromotion === "all"
} ? mobilityData
: mobilityData.filter((entry) => entry.promotionId === selectedPromotion);
if (!data?.promotions) { const groupedData = promotions.map((promo) => ({
return <p>Loading data...</p>; promotion: promo.name,
} students: filteredData.filter((entry) => entry.promotionId === promo.id),
}));
const handleDownload = (id: number) => {
window.open(`/mobility/api/download/${id}`, "_blank");
};
return ( return (
<section> <div>
<h2>Edit Mobility</h2> <h2>Edit Mobility</h2>
{data.promotions.map((promo) => (
<div key={promo.id}> <div>
<h3>Promotion: {promo.name}</h3> <label htmlFor="promotionSelect">Select Promotion: </label>
<select
id="promotionSelect"
value={selectedPromotion}
onChange={(e) =>
setSelectedPromotion(
e.target.value === "all" ? "all" : Number(e.target.value)
)
}
>
<option value="all">All Promotions</option>
{promotions.map((promo) => (
<option key={promo.id} value={promo.id}>
{promo.name}
</option>
))}
</select>
</div>
{groupedData.map((group) => (
<div key={group.promotion}>
{group.students.length > 0 && (
<>
<h3>Promotion: {group.promotion}</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -147,84 +156,88 @@ export default function EditMobility() {
<th>Destination Country</th> <th>Destination Country</th>
<th>Destination Name</th> <th>Destination Name</th>
<th>Status</th> <th>Status</th>
<th>Attestation File</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.students {group.students.map((entry) => (
?.filter((student) => student.promotionId === promo.id) <tr key={entry.studentId}>
.map((student) => { <td>{entry.studentId}</td>
const mobility = data.mobilities?.find((mob) => <td>{entry.firstName}</td>
mob.studentId === student.id <td>{entry.lastName}</td>
) || {
id: null,
studentId: student.id,
startDate: null,
endDate: null,
weeksCount: null,
destinationCountry: null,
destinationName: null,
mobilityStatus: "N/A",
};
return (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td> <td>
<input <input
type="date" type="date"
value={mobility.startDate || ""} value={entry.startDate || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"startDate", data.studentId === entry.studentId
e.target.value, ? { ...data, startDate: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<input <input
type="date" type="date"
value={mobility.endDate || ""} value={entry.endDate || ""}
onChange={(e) => onChange={(e) =>
handleChange(student.id, "endDate", e.target.value)} setMobilityData((prev) =>
prev.map((data) =>
data.studentId === entry.studentId
? { ...data, endDate: e.target.value }
: data
)
)
}
/> />
</td> </td>
<td>{mobility.weeksCount ?? "N/A"}</td> <td>{entry.weeksCount || "0"}</td>
<td> <td>
<input <input
type="text" type="text"
value={mobility.destinationCountry || ""} value={entry.destinationCountry || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"destinationCountry", data.studentId === entry.studentId
e.target.value, ? { ...data, destinationCountry: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<input <input
type="text" type="text"
value={mobility.destinationName || ""} value={entry.destinationName || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"destinationName", data.studentId === entry.studentId
e.target.value, ? { ...data, destinationName: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<select <select
value={mobility.mobilityStatus} value={entry.mobilityStatus}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"mobilityStatus", data.studentId === entry.studentId
e.target.value, ? { ...data, mobilityStatus: e.target.value }
)} : data
)
)
}
> >
<option value="N/A">N/A</option> <option value="N/A">N/A</option>
<option value="Planned">Planned</option> <option value="Planned">Planned</option>
@@ -233,16 +246,51 @@ export default function EditMobility() {
<option value="Validated">Validated</option> <option value="Validated">Validated</option>
</select> </select>
</td> </td>
<td>
{entry.attestationFile ? (
<>
<button onClick={() => handleDownload(entry.id!)}>
Download
</button>
<button
onClick={() =>
setMobilityData((prev) =>
prev.map((data) =>
data.studentId === entry.studentId
? { ...data, attestationFile: null }
: data
)
)
}
>
Delete
</button>
</>
) : (
<input
type="file"
accept=".pdf"
onChange={(e) =>
handleFileChange(
entry.studentId,
e.target.files?.[0] || null
)
}
/>
)}
</td>
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</>
)}
</div> </div>
))} ))}
<button type="button" onClick={handleSave} disabled={isSaving}>
<button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Confirm"} {isSaving ? "Saving..." : "Confirm"}
</button> </button>
</section> </div>
); );
} }
+1 -2
View File
@@ -8,9 +8,8 @@ const properties: AppProperties = {
index: "Homepage", index: "Homepage",
overview: "Mobility overview", overview: "Mobility overview",
edit_mobility: "Mobility management", edit_mobility: "Mobility management",
consult_students_test: "Test consult students",
}, },
adminOnly: ["edit_mobility", "consult_students_test"], adminOnly: ["edit_mobility"],
}; };
export default properties; export default properties;
+43
View File
@@ -0,0 +1,43 @@
import { Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
async GET(request) {
try {
console.log("API /mobility/api/download/:id GET called");
const url = new URL(request.url);
const id = url.pathname.split("/").pop();
if (!id) {
return new Response("Invalid request: Missing ID", { status: 400 });
}
console.log("Connecting to mobility database...");
using connection = connect("mobility");
console.log("Connected to databases.");
const query = connection.database.prepare(
`SELECT attestationFile FROM mobility WHERE id = ?`
);
const result = query.get(id);
if (!result || !result.attestationFile) {
return new Response("No file found for the given ID", { status: 404 });
}
const fileBuffer = result.attestationFile;
return new Response(fileBuffer, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="attestation_${id}.pdf"`,
},
});
} catch (error) {
console.error("Error fetching file:", error);
return new Response("Failed to fetch file", { status: 500 });
}
},
};
@@ -0,0 +1,131 @@
import { Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() {
try {
using connection = connect("mobility");
const mobilities = connection.database.prepare(
`SELECT
mobility.id,
mobility.studentId,
mobility.startDate,
mobility.endDate,
mobility.weeksCount,
mobility.destinationCountry,
mobility.destinationName,
mobility.mobilityStatus,
mobility.attestationFile -- Inclure le fichier
FROM mobility`
).all();
const students = connection.database.prepare(
`SELECT
students.userId AS id,
students.firstName,
students.lastName,
students.promotionId AS promotionId,
promotions.name AS promotionName
FROM students.students
LEFT JOIN students.promotions ON students.promotionId = promotions.id`
).all();
const promotions = connection.database.prepare(
`SELECT id, name FROM students.promotions`
).all();
return new Response(
JSON.stringify({ mobilities, students, promotions }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
console.error("Error fetching mobility data:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
async POST(request) {
console.log("API /mobility/api/insert-mobility POST called");
try {
const formData = await request.formData();
const dataEntries = formData.getAll("data").map((item) => JSON.parse(item as string));
console.log("Parsed data entries:", dataEntries);
const fileMap: Record<string, Uint8Array> = {};
for (const [key, value] of formData.entries()) {
if (key.startsWith("file_") && value instanceof File) {
const studentId = key.split("_")[1];
const file = value as File;
fileMap[studentId] = new Uint8Array(await file.arrayBuffer());
console.log(`File processed for studentId ${studentId}`);
}
}
using connection = connect("mobility");
const insertQuery = connection.database.prepare(
`INSERT INTO mobility (
id, studentId, startDate, endDate, weeksCount, destinationCountry, destinationName, mobilityStatus, attestationFile
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
startDate = excluded.startDate,
endDate = excluded.endDate,
weeksCount = excluded.weeksCount,
destinationCountry = excluded.destinationCountry,
destinationName = excluded.destinationName,
mobilityStatus = excluded.mobilityStatus,
attestationFile = excluded.attestationFile`
);
for (const mobility of dataEntries) {
const {
id = null,
studentId,
startDate,
endDate,
destinationCountry,
destinationName,
mobilityStatus = "N/A",
} = mobility;
let calculatedWeeksCount = null;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (start <= end) {
const differenceInDays = Math.ceil(
(end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)
);
calculatedWeeksCount = Math.floor(differenceInDays / 7);
}
}
const attestationFile = fileMap[studentId] || null;
console.log(`Inserting/Updating mobility for studentId: ${studentId}`);
insertQuery.run(
id,
studentId,
startDate,
endDate,
calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
attestationFile
);
}
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", { status: 200 });
} catch (error) {
console.error("Error inserting mobility data:", error);
return new Response("Failed to insert/update data", { status: 500 });
}
},
};
@@ -1,122 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { mobility, promotions, students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
async GET() {
try {
const studentRows = await db
.select({
id: students.userId,
firstName: students.firstName,
lastName: students.lastName,
promotionId: students.promotionId,
endyear: promotions.endyear,
current: promotions.current,
})
.from(students)
.leftJoin(promotions, eq(students.promotionId, promotions.id));
const mobilityRows = await db.select().from(mobility);
const promotionRows = await db
.select({
id: promotions.id,
endyear: promotions.endyear,
current: promotions.current,
})
.from(promotions);
return new Response(
JSON.stringify({
mobilities: mobilityRows,
students: studentRows,
promotions: promotionRows,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
} catch (error) {
console.error("Error fetching mobility data:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
async POST(request) {
try {
const body = await request.json();
const { data } = body;
if (!Array.isArray(data)) {
throw new Error("Invalid request body");
}
for (const entry of data) {
const {
id,
studentId,
startDate,
endDate,
weeksCount,
destinationCountry,
destinationName,
mobilityStatus = "N/A",
} = entry;
const studentExists = await db
.select({ userId: students.userId })
.from(students)
.where(eq(students.userId, studentId))
.limit(1)
.then((rows) => rows.length > 0);
if (!studentExists) {
console.warn(`Skipping mobility for unknown studentId: ${studentId}`);
continue;
}
let calculatedWeeksCount = weeksCount;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
calculatedWeeksCount = start <= end
? Math.ceil(
(end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000),
)
: null;
}
await db
.insert(mobility)
.values({
id,
studentId,
startDate,
endDate,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
})
.onConflictDoUpdate({
target: mobility.id,
set: {
startDate,
endDate,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
},
});
}
return new Response("Data inserted/updated successfully", {
status: 200,
});
} catch (error) {
console.error("Error inserting mobility data:", error);
return new Response("Failed to insert/update data", { status: 500 });
}
},
};
@@ -1,21 +0,0 @@
import ConsultStudents_test from "$root/routes/(apps)/mobility/(_islands)/ConsultStudents_test.tsx";
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await
async function Mobility(_request: Request, _context: FreshContext<State>) {
return (
<>
<h1>Test consult students</h1>
<ConsultStudents_test />
</>
);
}
export const config = getPartialsConfig();
export default makePartials(Mobility);
+1 -1
View File
@@ -10,7 +10,7 @@ import { State } from "$root/routes/_middleware.ts";
async function Mobility(_request: Request, _context: FreshContext<State>) { async function Mobility(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h1>Edit mobility</h1> <h1>Mobility overview</h1>
<ConsultMobility /> <ConsultMobility />
</> </>
); );
+21
View File
@@ -0,0 +1,21 @@
interface Promotion {
id: number;
name: string;
}
interface MobilityData {
id: number | null;
studentId: string;
firstName: string;
lastName: string;
startDate: string | null;
endDate: string | null;
weeksCount: number | null;
destinationCountry: string | null;
destinationName: string | null;
mobilityStatus: string;
promotionId: number;
promotionName: string;
//attestationFile: File | null;
}
-83
View File
@@ -1,83 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { ajustements } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers<null, AuthenticatedState> = {
// #48 GET /ajustements
async GET(request) {
try {
const url = new URL(request.url);
const numEtudParam = url.searchParams.get("numEtud");
const idUEParam = url.searchParams.get("idUE");
let query = db.select().from(ajustements).$dynamic();
if (numEtudParam) {
const numEtud = parseInt(numEtudParam);
if (isNaN(numEtud)) {
return new Response("Paramètre numEtud invalide", { status: 400 });
}
query = query.where(eq(ajustements.numEtud, numEtud));
}
if (idUEParam) {
const idUE = parseInt(idUEParam);
if (isNaN(idUE)) {
return new Response("Paramètre idUE invalide", { status: 400 });
}
query = query.where(eq(ajustements.idUE, idUE));
}
const result = await query;
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching ajustements:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #49 POST /ajustements
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
try {
const body: { numEtud: number; idUE: number; valeur: number } =
await request.json();
if (!body.numEtud || !body.idUE || body.valeur === undefined) {
return new Response(
JSON.stringify({ error: "Champs requis: numEtud, idUE, valeur" }),
{ status: 400, headers: { "content-type": "application/json" } },
);
}
const [created] = await db
.insert(ajustements)
.values({
numEtud: body.numEtud,
idUE: body.idUE,
valeur: body.valeur,
})
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
} catch (error) {
console.error("Error creating ajustement:", error);
return new Response("Failed to create ajustement", { status: 500 });
}
},
};
@@ -1,107 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { ajustements } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ajustement introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #50 GET /ajustements/{numEtud}/{idUE}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const idUE = Number(context.params.idUE);
if (isNaN(numEtud) || isNaN(idUE)) {
return new Response("Paramètres invalides", { status: 400 });
}
const ajustement = await db
.select()
.from(ajustements)
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
.then((rows) => rows[0] ?? null);
if (!ajustement) return NOT_FOUND;
return new Response(JSON.stringify(ajustement), {
headers: { "content-type": "application/json" },
});
},
// #51 PUT /ajustements/{numEtud}/{idUE}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const idUE = Number(context.params.idUE);
if (isNaN(numEtud) || isNaN(idUE)) {
return new Response("Paramètres invalides", { status: 400 });
}
const body: { valeur: number } = await request.json();
if (body.valeur === undefined) {
return new Response(JSON.stringify({ error: "Champ requis: valeur" }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
const [updated] = await db
.update(ajustements)
.set({ valeur: body.valeur })
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #52 DELETE /ajustements/{numEtud}/{idUE}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const idUE = Number(context.params.idUE);
if (isNaN(numEtud) || isNaN(idUE)) {
return new Response("Paramètres invalides", { status: 400 });
}
const [deleted] = await db
.delete(ajustements)
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-64
View File
@@ -1,64 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts";
import { notes } from "../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// #42 GET /notes
async GET(request) {
try {
const url = new URL(request.url);
const numEtudParam = url.searchParams.get("numEtud");
const idModule = url.searchParams.get("idModule");
let query = db.select().from(notes).$dynamic();
if (numEtudParam) {
const numEtud = parseInt(numEtudParam);
if (isNaN(numEtud)) {
return new Response("Paramètre numEtud invalide", { status: 400 });
}
query = query.where(eq(notes.numEtud, numEtud));
}
if (idModule) {
query = query.where(eq(notes.idModule, idModule));
}
const result = await query;
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching notes:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #43 POST /notes
async POST(request) {
try {
const body = await request.json();
const { note, numEtud, idModule } = body;
if (note === undefined || !numEtud || !idModule) {
return new Response("Champs 'note', 'numEtud' et 'idModule' requis", {
status: 400,
});
}
const result = await db.insert(notes).values({ note, numEtud, idModule })
.returning();
return new Response(JSON.stringify(result[0]), {
status: 201,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error creating note:", error);
return new Response("Failed to create note", { status: 500 });
}
},
};
@@ -1,139 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../../databases/db.ts";
import { notes } from "../../../../../../databases/schema.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// #45 GET /notes/:numEtud/:idModule
async GET(_request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.select().from(notes).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
);
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify(result[0]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching note:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #46 PUT /notes/:numEtud/:idModule
async PUT(request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const body = await request.json();
const { note } = body;
if (note === undefined) {
return new Response("Champ 'note' manquant", { status: 400 });
}
const result = await db.update(notes).set({ note }).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
).returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify(result[0]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error updating note:", error);
return new Response("Failed to update note", { status: 500 });
}
},
// #47 DELETE /notes/:numEtud/:idModule
async DELETE(_request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.delete(notes).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
).returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(null, { status: 204 });
} catch (error) {
console.error("Error deleting note:", error);
return new Response("Failed to delete note", { status: 500 });
}
},
};
-66
View File
@@ -1,66 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts";
import { ueModules } from "../../../../databases/schema.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// #37 GET /ue-modules
async GET(request) {
try {
const url = new URL(request.url);
const idPromo = url.searchParams.get("idPromo");
const idUEParam = url.searchParams.get("idUE");
const idUE = idUEParam ? parseInt(idUEParam) : null;
if (idUEParam && isNaN(idUE!)) {
return new Response("Paramètre idUE invalide", { status: 400 });
}
const result = await db.select().from(ueModules).where(
and(
idPromo ? eq(ueModules.idPromo, idPromo) : undefined,
idUE ? eq(ueModules.idUE, idUE) : undefined,
),
);
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching UE-modules:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #38 POST /ue-modules
async POST(request) {
try {
const body = await request.json();
const { idModule, idUE, idPromo, coeff } = body;
if (!idModule || !idUE || !idPromo || coeff === undefined) {
return new Response(
"Champs 'idModule', 'idUE', 'idPromo' et 'coeff' requis",
{ status: 400 },
);
}
const result = await db.insert(ueModules).values({
idModule,
idUE,
idPromo,
coeff,
}).returning();
return new Response(JSON.stringify(result[0]), {
status: 201,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error creating UE-module:", error);
return new Response("Failed to create UE-module", { status: 500 });
}
},
};
@@ -1,139 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { ueModules } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Association UE-Module introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
const BAD_REQUEST = new Response(
JSON.stringify({ error: "Paramètres invalides" }),
{ status: 400, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #39 GET /ue-modules/{idModule}/{idUE}/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const ueModuleAssociation = await db
.select()
.from(ueModules)
.where(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
)
.then((rows) => rows[0] ?? null);
if (!ueModuleAssociation) return NOT_FOUND;
return new Response(JSON.stringify(ueModuleAssociation), {
headers: { "content-type": "application/json" },
});
},
// #40 PUT /ue-modules/{idModule}/{idUE}/{idPromo}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const body: { coeff: number } = await request.json();
if (typeof body.coeff !== "number") {
return new Response(
JSON.stringify({ error: "Le champ 'coeff' doit être un nombre" }),
{ status: 400, headers: { "content-type": "application/json" } },
);
}
const [updated] = await db
.update(ueModules)
.set({ coeff: body.coeff })
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
.returning();
if (!updated) return NOT_FOUND;
return new Response(
JSON.stringify({
idModule: updated.idModule,
idUE: updated.idUE,
idPromo: updated.idPromo,
coeff: updated.coeff,
}),
{
headers: { "content-type": "application/json" },
},
);
},
// #41 DELETE /ue-modules/{idModule}/{idUE}/{idPromo}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const [deleted] = await db
.delete(ueModules)
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-42
View File
@@ -1,42 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts";
import { ues } from "../../../../databases/schema.ts";
export const handler: Handlers = {
// #32 GET /ues
async GET() {
try {
const result = await db.select().from(ues);
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching UEs:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #33 POST /ues
async POST(request) {
try {
const body = await request.json();
const { nom } = body;
if (!nom) {
return new Response("Champ 'nom' manquant", { status: 400 });
}
const result = await db.insert(ues).values({ nom }).returning();
return new Response(JSON.stringify(result[0]), {
status: 201,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error creating UE:", error);
return new Response("Failed to create UE", { status: 500 });
}
},
};
-122
View File
@@ -1,122 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../databases/db.ts";
import { ues } from "../../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// # 34 GET /ues/:idUE
async GET(_request, context) {
try {
const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) {
return new Response(
JSON.stringify({ error: "Paramètre idUE invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.select().from(ues).where(eq(ues.id, idUE));
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify(result[0]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching UE:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #35 PUT /ues/:idUE
async PUT(request, context) {
try {
const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) {
return new Response(
JSON.stringify({ error: "Paramètre idUE invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const body = await request.json();
const { nom } = body;
if (!nom) {
return new Response("Champ 'nom' manquant", { status: 400 });
}
const result = await db.update(ues).set({ nom }).where(eq(ues.id, idUE))
.returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify(result[0]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error updating UE:", error);
return new Response("Failed to update UE", { status: 500 });
}
},
// #36 DELETE /ues/:idUE
async DELETE(_request, context) {
try {
const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) {
return new Response(
JSON.stringify({ error: "Paramètre idUE invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.delete(ues).where(eq(ues.id, idUE)).returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(null, { status: 204 });
} catch (error) {
console.error("Error deleting UE:", error);
return new Response("Failed to delete UE", { status: 500 });
}
},
};
@@ -1,30 +0,0 @@
import Student from "$root/routes/(apps)/students/(_components)/Student.tsx";
type PromotionProps = { students: Student[]; promo: Promotion };
export default function Promotion(props: PromotionProps) {
if (!props.promo) {
return <p>Unable to find user in database.</p>;
}
return (
<div key={props.promo.id}>
<h3>Promotion {props.promo.endyear}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{props.students
.filter((student) => student.promotionId === props.promo.id)
.map((student) => <Student key={student.id} student={student} />)}
</tbody>
</table>
</div>
);
}
@@ -1,31 +0,0 @@
import { CasContent } from "$root/defaults/interfaces.ts";
type SelfPortraitProps = { self: CasContent };
const regex =
/^(?<year>\d{4})(?<month>\d{2})(?<date>\d{2})(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})Z$/;
export default function SelfPortrait(props: SelfPortraitProps) {
const { year, month, date, hours, minutes, seconds } = props.self
.amuDateValidation.match(regex)!.groups!;
const validationIsoDate =
`${year}-${month}-${date}T${hours}:${minutes}:${seconds}Z`;
const validationDate = new Date(validationIsoDate);
return (
<div id="self-portrait">
<div>Identity</div>
<div>{props.self.supannCivilite} {props.self.displayName}</div>
<div>Student number</div>
<div>{props.self.uid}</div>
<div>amU mail</div>
<div>{props.self.mail}</div>
<div>First amU registration</div>
<div>{validationDate.toLocaleString()}</div>
<div>amU class code</div>
<div>{props.self.supannEtuEtape}</div>
</div>
);
}
@@ -1,13 +0,0 @@
type StudentProps = { student: Student; promo?: number };
export default function Student(props: StudentProps) {
return (
<tr key={props.student.userId}>
<td>{props.student.userId}</td>
<td>{props.student.firstName}</td>
<td>{props.student.lastName}</td>
<td>{props.student.mail}</td>
{props.promo && <td>{props.promo}</td>}
</tr>
);
}
@@ -1,45 +1,75 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import Promotion from "$root/routes/(apps)/students/(_components)/Promotion.tsx";
type SingleUserResponse = { promo: Promotion; student: Student }; interface Promotion {
type ManyUsersResponse = { promos: Promotion[]; students: Student[] }; id: number;
name: string;
}
type APIResponse = SingleUserResponse | ManyUsersResponse; interface Student {
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
export default function ConsultStudents() { export default function ConsultStudents() {
const [data, setData] = useState<APIResponse | null>(null); const [data, setData] = useState<
{ promotions: Promotion[]; students: Student[] } | null
>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const response = await fetch("/students/api/students"); try {
const response = await fetch("/students/api/insert_students");
if (!response.ok) { if (!response.ok) {
setError("Failed to load data. Please try again later."); throw new Error(`Error fetching data: ${response.statusText}`);
} }
const result: APIResponse = await response.json(); const result = await response.json();
console.log("Fetched data:", result);
setData(result); setData(result);
} catch (err) {
console.error("Error fetching data:", err);
setError("Failed to load data. Please try again later.");
}
}; };
fetchData(); fetchData();
}, []); }, []);
return ( return (
<> <section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
{data && ((Object.hasOwn(data, "student")) {data?.promotions.map((promo) => (
? ( <div key={promo.id}>
<Promotion <h3>Promotion: {promo.name}</h3>
students={[(data as SingleUserResponse).student]} <table>
promo={(data as SingleUserResponse).promo} <thead>
/> <tr>
) <th>ID</th>
: (data as ManyUsersResponse).promos.map((promo) => ( <th>First Name</th>
<Promotion <th>Last Name</th>
students={(data as ManyUsersResponse).students} <th>Email</th>
promo={promo} </tr>
/> </thead>
)))} <tbody>
</> {data.students
.filter((student) => student.promotionId === promo.id)
.map((student) => (
<tr key={student.userId}>
<td>{student.userId}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.mail}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</section>
); );
} }
@@ -1,111 +1,75 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts" // @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts"
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs"; import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { Signal, useSignal } from "@preact/signals"; import { useSignal } from "@preact/signals";
/** export default function UploadStudents() {
* Create a new handler for file change that displays const statusMessage = useSignal<string>("");
* messages in statusMessage and gets file data in fileData. const fileData = useSignal<File | null>(null);
* @param statusMessage The status message signal.
* @param fileData The file data signal. const handleFileChange = (event: Event) => {
* @returns The file change handler.
*/
function getFileChangeHandler(
statusMessage: Signal<string>,
fileData: Signal<File | null>,
): (event: Event) => void {
/**
* Handle file change.
* @param event The file change event.
*/
return (event: Event) => {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) { if (input.files && input.files.length > 0) {
fileData.value = input.files[0]; fileData.value = input.files[0];
statusMessage.value = `File selected: ${input.files[0].name}`; statusMessage.value = "File selected: " + input.files[0].name;
} else { } else {
fileData.value = null; fileData.value = null;
statusMessage.value = "No file selected"; statusMessage.value = "No file selected";
} }
}; };
}
/** const confirmUpload = () => {
* Create a new handler that sends data file to server.
* @param statusMessage The status message signal.
* @param fileData The file data signal.
* @returns The file confirmation handler.
*/
function getUploadConfirmationFunction(
statusMessage: Signal<string>,
fileData: Signal<File | null>,
): () => void {
/**
* Add students to database.
* @returns Confirm upload of students.
*/
return () => {
if (!fileData.value) { if (!fileData.value) {
statusMessage.value = "Please select a file before confirming upload."; statusMessage.value = "Please select a file before confirming upload.";
return; return;
} }
try {
const reader = new FileReader(); const reader = new FileReader();
/** reader.onload = async (e) => {
* Send all data to the server. const arrayBuffer = e.target?.result as ArrayBuffer;
* @param event The finished progress event.
*/
reader.onload = async (event: ProgressEvent<FileReader>) => {
const arrayBuffer = event.target!.result as ArrayBuffer;
const workbook = XLSX.read(arrayBuffer, { type: "array" }); const workbook = XLSX.read(arrayBuffer, { type: "array" });
let allOK = true;
for (const sheetName of workbook.SheetNames) { for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName]; const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { const data = XLSX.utils.sheet_to_json(sheet, {
header: ["userId", "lastName", "firstName", "mail"], header: ["Identifiant", "Nom", "Prénom", "Mail"],
range: 1, range: 1, // Ignorer les en-têtes
}); });
const response = await fetch("/students/api/students", { console.log(`Data from sheet ${sheetName}:`, data);
const response = await fetch("/students/api/insert_students", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ promoName: sheetName, data }), body: JSON.stringify({ promoName: sheetName, data }),
}); });
if (!response.ok) { if (!response.ok) {
allOK = false; throw new Error(`Failed to insert data for promotion ${sheetName}`);
} }
} }
statusMessage.value = allOK statusMessage.value = "Data uploaded and inserted successfully!";
? "Failed to insert all data."
: "Data uploaded and inserted successfully!";
}; };
/**
* Display error message if any.
*/
reader.onerror = () => { reader.onerror = () => {
statusMessage.value = "Error reading the file."; statusMessage.value = "Error reading the file.";
}; };
reader.readAsArrayBuffer(fileData.value); reader.readAsArrayBuffer(fileData.value);
}; } catch (error) {
console.error("Error uploading file:", error);
statusMessage.value = "An unexpected error occurred during upload.";
} }
};
export default function UploadStudents() {
const statusMessage = useSignal<string>("");
const fileData = useSignal<File | null>(null);
const handleFileChange = getFileChangeHandler(statusMessage, fileData);
const confirmUpload = getUploadConfirmationFunction(statusMessage, fileData);
return ( return (
<> <div>
<h2>Upload Students</h2>
<input type="file" accept=".xlsx, .xls" onChange={handleFileChange} /> <input type="file" accept=".xlsx, .xls" onChange={handleFileChange} />
<button type="button" onClick={confirmUpload}>Confirm Upload</button> <button onClick={confirmUpload}>Confirm Upload</button>
<p>{statusMessage.value}</p> <p>{statusMessage.value}</p>
</> </div>
); );
} }
+1
View File
@@ -5,6 +5,7 @@ const properties: AppProperties = {
icon: "badge", icon: "badge",
pages: { pages: {
index: "Homepage", index: "Homepage",
overview: "Students overview",
upload: "Upload students", upload: "Upload students",
consult: "Consult students", consult: "Consult students",
}, },
@@ -0,0 +1,83 @@
import { Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() {
try {
using connection = connect("students");
const promotions = connection.database.prepare(
"select id, name from promotions",
).all();
const students = connection.database
.prepare(
`select userId, firstName, lastName, mail, promotionId from students`,
)
.all();
return new Response(
JSON.stringify({ promotions, students }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
console.error("Error fetching data:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
async POST(request) {
console.log("API /students/api/insert_students called");
try {
const body = await request.json();
const { data, promoName } = body;
console.log("Received data:", { promoName, data });
if (!promoName || !Array.isArray(data)) {
throw new Error("Invalid request body");
}
using connection = connect("students");
connection.database.prepare(
"INSERT OR IGNORE INTO promotions (name) VALUES (?)",
).run(promoName);
const promoIdRow: { id: number } = connection.database
.prepare("SELECT id FROM promotions WHERE name = ?")
.get(promoName)!;
const promoId = promoIdRow.id;
console.log(`Promotion ID for "${promoName}":`, promoId);
const insertQuery = connection.database.prepare(
`INSERT INTO students
(userId, firstName, lastName, mail, promotionId)
VALUES (?, ?, ?, ?, ?)`,
);
for (const student of data) {
console.log("Inserting student:", student);
insertQuery.run(
student.Identifiant,
student.Nom,
student["Prénom"],
student.Mail,
promoId,
);
}
console.log("All data inserted successfully");
return new Response("Data inserted successfully", { status: 201 });
} catch (error) {
console.error("Error inserting data:", error);
return new Response("Failed to insert data", { status: 500 });
}
},
};
-49
View File
@@ -1,49 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { promotions } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
export const handler: Handlers<null, AuthenticatedState> = {
// #13 GET /promotions
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
const rows = await db.select().from(promotions);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #14 POST /promotions
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const body: { idPromo: string; annee: string } = await request.json();
if (!body.idPromo || !body.annee) {
return new Response(null, { status: 400 });
}
const [created] = await db
.insert(promotions)
.values({ id: body.idPromo, annee: body.annee })
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -1,79 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { promotions } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #15 GET /promotions/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const promo = await db
.select()
.from(promotions)
.where(eq(promotions.id, context.params.idPromo))
.then((rows) => rows[0] ?? null);
if (!promo) return NOT_FOUND;
return new Response(JSON.stringify(promo), {
headers: { "content-type": "application/json" },
});
},
// #16 PUT /promotions/{idPromo}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const body: { annee: string } = await request.json();
const [updated] = await db
.update(promotions)
.set({ annee: body.annee })
.where(eq(promotions.id, context.params.idPromo))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #17 DELETE /promotions/{idPromo}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const [deleted] = await db
.delete(promotions)
.where(eq(promotions.id, context.params.idPromo))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
-61
View File
@@ -1,61 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers<null, AuthenticatedState> = {
// #7 GET /students
async GET(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
const url = new URL(request.url);
const idPromo = url.searchParams.get("idPromo");
const rows = idPromo
? await db.select().from(students).where(eq(students.idPromo, idPromo))
: await db.select().from(students);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #8 POST /students
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const body: {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
} = await request.json();
if (!body.nom || !body.prenom || !body.idPromo) {
return new Response(null, { status: 400 });
}
const [created] = await db
.insert(students)
.values({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -1,83 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #10 GET /students/{numEtud}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const student = await db
.select()
.from(students)
.where(eq(students.numEtud, numEtud))
.then((rows) => rows[0] ?? null);
if (!student) return NOT_FOUND;
return new Response(JSON.stringify(student), {
headers: { "content-type": "application/json" },
});
},
// #11 PUT /students/{numEtud}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const body: { nom: string; prenom: string; idPromo: string } = await request
.json();
const [updated] = await db
.update(students)
.set({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.where(eq(students.numEtud, numEtud))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #12 DELETE /students/{numEtud}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const [deleted] = await db
.delete(students)
.where(eq(students.numEtud, numEtud))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
@@ -1,64 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
// #9 POST /students/import-csv
export const handler: Handlers<null, AuthenticatedState> = {
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const formData = await request.formData();
const file = formData.get("file") as File | null;
const idPromo = formData.get("idPromo") as string | null;
if (!file || !idPromo) {
return new Response(null, { status: 400 });
}
const text = await file.text();
const lines = text.trim().split("\n");
let imported = 0;
const errors: { line: number; message: string }[] = [];
for (let i = 0; i < lines.length; i++) {
const lineNum = i + 1;
const cols = lines[i].split(",").map((c) => c.trim());
const [numEtudStr, nom, prenom] = cols;
if (!numEtudStr) {
errors.push({ line: lineNum, message: "Numéro étudiant manquant" });
continue;
}
const numEtud = Number(numEtudStr);
if (isNaN(numEtud)) {
errors.push({ line: lineNum, message: "Numéro étudiant invalide" });
continue;
}
if (!nom || !prenom) {
errors.push({ line: lineNum, message: "Nom ou prénom manquant" });
continue;
}
await db
.insert(students)
.values({ nom, prenom, idPromo })
.onConflictDoNothing();
imported++;
}
return new Response(JSON.stringify({ imported, errors }), {
headers: { "content-type": "application/json" },
});
},
};
@@ -1,16 +1,17 @@
import ConsultStudents from "../../(_islands)/ConsultStudents.tsx"; import ConsultStudents from "$root/routes/(apps)/students/(_islands)/ConsultStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h2>Consult students</h2> <h1>Manage Promotions</h1>
<ConsultStudents /> <ConsultStudents />
</> </>
); );
@@ -1,16 +1,17 @@
import UploadStudents from "../../(_islands)/UploadStudents.tsx"; import UploadStudents from "$root/routes/(apps)/students/(_islands)/UploadStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h2>Upload Students</h2> <h1>Manage Promotions</h1>
<UploadStudents /> <UploadStudents />
</> </>
); );
+2 -9
View File
@@ -3,18 +3,11 @@ import {
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrait.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) { export async function Index(_request: Request, context: FreshContext<State>) {
return ( return <h2>Welcome to {context.state.session?.displayName}.</h2>;
<>
<h2>Welcome {context.state.session?.givenName}!</h2>
<h3>Your amU identity</h3>
<SelfPortrait self={context.state.session!} />
</>
);
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
@@ -0,0 +1,17 @@
import { Partial } from "$fresh/runtime.ts";
import { RouteConfig } from "$fresh/server.ts";
type ModulesProps = Record<string | number | symbol, never>;
export const config: RouteConfig = {
skipAppWrapper: true,
skipInheritedLayouts: true,
};
export default function Modules(_props: ModulesProps) {
return (
<Partial name="body">
<a href="students" f-partial={"notes/partials"}>students</a>
</Partial>
);
}
-13
View File
@@ -1,13 +0,0 @@
interface Student {
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
interface Promotion {
id: number;
endyear: number;
current: number;
}
+12
View File
@@ -0,0 +1,12 @@
import { Head } from "$fresh/runtime.ts";
export default function Error403() {
return (
<>
<Head>
<title>403 - Forbidden</title>
</Head>
<p>403</p>
</>
);
}
-1
View File
@@ -27,7 +27,6 @@ export default async function App(
<link rel="stylesheet" href="/styles/main.css" /> <link rel="stylesheet" href="/styles/main.css" />
<link rel="stylesheet" href="/styles/app.css" /> <link rel="stylesheet" href="/styles/app.css" />
<link rel="stylesheet" href="styles/app-cards.css" /> <link rel="stylesheet" href="styles/app-cards.css" />
<link rel="stylesheet" href="styles/students.css" />
</head> </head>
<body f-client-nav> <body f-client-nav>
<Header link={link} /> <Header link={link} />
+56 -5
View File
@@ -21,10 +21,19 @@ const deleteKey = (user: string) => delete jwtKeyCache[user];
* @returns `true` if the route is public, `false` otherwise. * @returns `true` if the route is public, `false` otherwise.
*/ */
function isRoutePublic(route: string): boolean { function isRoutePublic(route: string): boolean {
return PUBLIC_ROUTES.includes(route) || return (
!!(route.match(/\..+$/)?.[0] ?? false); PUBLIC_ROUTES.includes(route) || !!(route.match(/\..+$/)?.[0] ?? false)
);
} }
/**
* Checks if the given route is an API route.
* @param route The route to check.
* @returns `true` if the route is an API route, `false` otherwise.
*/
function isRouteAnAPI(route: string): boolean {
return route.includes("/api/");
}
/** /**
* Get the given user's key, creating it if not already existing. * Get the given user's key, creating it if not already existing.
* @param user The key's user. * @param user The key's user.
@@ -44,6 +53,7 @@ export function getKey(user: string): string {
export const handler: MiddlewareHandler<State>[] = [ export const handler: MiddlewareHandler<State>[] = [
/** /**
* Check if user is authenticated and add session to context accordingly. * Check if user is authenticated and add session to context accordingly.
* Only authenticated users who are members of Polytech are allowed.
* @param request The HTTP incomming request. * @param request The HTTP incomming request.
* @param context The Fresh context object with custom `State`. * @param context The Fresh context object with custom `State`.
* @returns The response from the next middleware. * @returns The response from the next middleware.
@@ -55,6 +65,7 @@ export const handler: MiddlewareHandler<State>[] = [
const cookies = getCookies(request.headers); const cookies = getCookies(request.headers);
if (!cookies["sessionToken"]) { if (!cookies["sessionToken"]) {
context.state.isAuthenticated = false; context.state.isAuthenticated = false;
context.state.isFromPolytech = false;
return await context.next(); return await context.next();
} }
@@ -67,9 +78,32 @@ export const handler: MiddlewareHandler<State>[] = [
); );
if (context.state.isAuthenticated) { if (context.state.isAuthenticated) {
const session: CasContent = const session: CasContent = (
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user; getJwtPayload(cookies["sessionToken"]) as LoginJWT
).user;
const isFromPolytech = Object.values(session.memberOf).some(
(value) =>
typeof value === "string" && value.includes("cn=amu:ufr:polytech"),
);
context.state.isFromPolytech = isFromPolytech;
if (isFromPolytech) {
context.state.session = session; context.state.session = session;
if (Object.values(session.memberOf).some(
(value) => typeof value === "string" && value.includes("cn=amu:ufr:polytech:personnels")
)) {
context.state.role = "personnels";
} else if (Object.values(session.memberOf).some(
(value) => typeof value === "string" && value.includes("cn=amu:ufr:polytech:etudiants")
)) {
context.state.role = "etudiants";
} else {
context.state.role = "autres";
}
}
} }
return await context.next(); return await context.next();
@@ -87,7 +121,8 @@ export const handler: MiddlewareHandler<State>[] = [
): Promise<Response> { ): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) { if (!isRoutePublic(url.pathname)) {
if (!context.state.isAuthenticated) {
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
@@ -96,6 +131,22 @@ export const handler: MiddlewareHandler<State>[] = [
}); });
} }
if (!context.state.isFromPolytech) {
return new Response(null, {
status: 403,
headers: {
Location: "/403",
},
});
}
if (isRouteAnAPI(url.pathname) && !(context.state.role == "personnels")) {
return new Response(null, {
status: 403,
});
}
}
return await context.next(); return await context.next();
}, },
]; ];
-23
View File
@@ -1,23 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "polympr-dev";
nativeBuildInputs = [
pkgs.deno
pkgs.patchelf
pkgs.tea
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
# Find the dynamic linker
export NIX_LD_INTERPRETER=$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)
echo "Welcome to PolyMPR development shell!"
echo "Use 'deno task compile' to build the CLI."
echo "If on NixOS, it will be automatically patched."
'';
}
+2 -7
View File
@@ -3,7 +3,7 @@
padding: 1em 0; padding: 1em 0;
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 4em; gap: 1em;
} }
#app > #app-body { #app > #app-body {
@@ -16,7 +16,7 @@
} }
#app > nav > a { #app > nav > a {
padding: 0.5em 4em 0.5em 1em; padding: 0.25em 0.5em;
color: light-dark(var(--light-foreground), var(--dark-foreground)); color: light-dark(var(--light-foreground), var(--dark-foreground));
} }
@@ -57,10 +57,5 @@
#app { #app {
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: none; grid-template-columns: none;
gap: 1em;
}
#app > nav > a {
padding: 0.5em 1em;
} }
} }
-15
View File
@@ -1,15 +0,0 @@
#self-portrait {
display: grid;
gap: 1em;
grid-template-columns: auto 1fr;
}
#self-portrait > div:nth-child(2n+1) {
font-weight: var(--font-weight-bold);
}
@media screen and (max-width: 1024px) {
#self-portrait {
grid-template-columns: 1fr;
}
}
View File
-210
View File
@@ -1,210 +0,0 @@
// #113 - E2E tests for /modules endpoints
import { assertEquals } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedModules, truncateAll } from "../helpers/db_integration.ts";
import { handler as modulesHandler } from "$apps/admin/api/modules.ts";
import { handler as moduleHandler } from "$apps/admin/api/modules/[idModule].ts";
// --- GET /modules ---
Deno.test({
name: "e2e modules: GET /modules returns all as employee",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }, {
id: "INFO101",
nom: "Informatique",
}]);
const res = await modulesHandler.GET!(
makeGetRequest("/modules"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: GET /modules returns empty for non-employee",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
const res = await modulesHandler.GET!(
makeGetRequest("/modules"),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /modules ---
Deno.test({
name: "e2e modules: POST /modules creates module (201)",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "PHYS101", nom: "Physique" }),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertEquals(body.id, "PHYS101");
assertEquals(body.nom, "Physique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: POST /modules 409 on duplicate id",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "MATH101", nom: "Doublon" }),
makeEmployeeContext(),
);
assertEquals(res.status, 409);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: POST /modules 400 on missing fields",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "X" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: POST /modules 403 for non-employee",
async fn() {
await truncateAll();
const res = await modulesHandler.POST!(
makeJsonRequest("/modules", "POST", { id: "X", nom: "Y" }),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /modules/:id ---
Deno.test({
name: "e2e modules: GET /modules/:id returns module",
async fn() {
await truncateAll();
await seedModules([{ id: "ELEC201", nom: "Électronique" }]);
const res = await moduleHandler.GET!(
makeGetRequest("/modules/ELEC201"),
makeEmployeeContext({ idModule: "ELEC201" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Électronique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: GET /modules/:id 404 when not found",
async fn() {
await truncateAll();
const res = await moduleHandler.GET!(
makeGetRequest("/modules/GHOST"),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /modules/:id ---
Deno.test({
name: "e2e modules: PUT /modules/:id updates nom",
async fn() {
await truncateAll();
await seedModules([{ id: "CHIM101", nom: "Chimie" }]);
const res = await moduleHandler.PUT!(
makeJsonRequest("/modules/CHIM101", "PUT", { nom: "Chimie organique" }),
makeEmployeeContext({ idModule: "CHIM101" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Chimie organique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: PUT /modules/:id 404 when not found",
async fn() {
await truncateAll();
const res = await moduleHandler.PUT!(
makeJsonRequest("/modules/GHOST", "PUT", { nom: "X" }),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /modules/:id ---
Deno.test({
name: "e2e modules: DELETE /modules/:id returns 204",
async fn() {
await truncateAll();
await seedModules([{ id: "BIO101", nom: "Biologie" }]);
const res = await moduleHandler.DELETE!(
makeGetRequest("/modules/BIO101"),
makeEmployeeContext({ idModule: "BIO101" }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e modules: DELETE /modules/:id 404 when not found",
async fn() {
await truncateAll();
const res = await moduleHandler.DELETE!(
makeGetRequest("/modules/GHOST"),
makeEmployeeContext({ idModule: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-212
View File
@@ -1,212 +0,0 @@
// #110 - E2E tests for /promotions endpoints
import { assertEquals } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedPromotions, truncateAll } from "../helpers/db_integration.ts";
import { handler as promotionsHandler } from "$apps/students/api/promotions.ts";
import { handler as promotionHandler } from "$apps/students/api/promotions/[idPromo].ts";
// --- GET /promotions ---
Deno.test({
name: "e2e promotions: GET /promotions returns all as employee",
async fn() {
await truncateAll();
await seedPromotions([
{ id: "PEIP1-2024", annee: "2024" },
{ id: "PEIP2-2024", annee: "2024" },
]);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions returns empty for non-employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024", annee: "2024" }]);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /promotions ---
Deno.test({
name: "e2e promotions: POST /promotions creates promotion (201)",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", {
idPromo: "INFO3-2025",
annee: "2025",
}),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertEquals(body.id, "INFO3-2025");
assertEquals(body.annee, "2025");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: POST /promotions 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X", annee: "2025" }),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: POST /promotions 400 on missing fields",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: GET /promotions/:id returns promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024", annee: "2024" }]);
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeEmployeeContext({ idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.id, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeContextWithAffiliation("student", { idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: PUT /promotions/:id updates annee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2023", annee: "2023" }]);
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/INFO3-2023", "PUT", { annee: "2024" }),
makeEmployeeContext({ idPromo: "INFO3-2023" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.annee, "2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: PUT /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/GHOST", "PUT", { annee: "2025" }),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: DELETE /promotions/:id returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]);
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/INFO3-2022"),
makeEmployeeContext({ idPromo: "INFO3-2022" }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: DELETE /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-175
View File
@@ -1,175 +0,0 @@
// #112 - E2E tests for /roles endpoints
import { assertEquals, assertExists } from "@std/assert";
import {
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedRoles, testDb, truncateAll } from "../helpers/db_integration.ts";
import { permissions } from "$root/databases/schema.ts";
import { handler as rolesHandler } from "$apps/admin/api/roles.ts";
import { handler as roleHandler } from "$apps/admin/api/roles/[idRole].ts";
// --- GET /roles ---
Deno.test({
name: "e2e roles: GET /roles returns all with permissions",
async fn() {
await truncateAll();
await seedRoles([{ nom: "admin" }, { nom: "employee" }]);
const res = await rolesHandler.GET!(
makeGetRequest("/roles"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertExists(body[0].permissions);
assertEquals(Array.isArray(body[0].permissions), true);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /roles ---
Deno.test({
name: "e2e roles: POST /roles creates role (201)",
async fn() {
await truncateAll();
const res = await rolesHandler.POST!(
makeJsonRequest("/roles", "POST", { nom: "viewer" }),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertExists(body.id);
assertEquals(body.nom, "viewer");
assertEquals(body.permissions, []);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e roles: POST /roles 400 on missing nom",
async fn() {
await truncateAll();
const res = await rolesHandler.POST!(
makeJsonRequest("/roles", "POST", {}),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /roles/:id ---
Deno.test({
name: "e2e roles: GET /roles/:id returns role with permissions",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "admin" }]);
await testDb.insert(permissions).values([
{ id: "student_read", nom: "Consulter les élèves" },
]);
const res = await roleHandler.GET!(
makeGetRequest(`/roles/${role.id}`),
makeEmployeeContext({ idRole: String(role.id) }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "admin");
assertEquals(Array.isArray(body.permissions), true);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e roles: GET /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.GET!(
makeGetRequest("/roles/9999"),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /roles/:id ---
Deno.test({
name: "e2e roles: PUT /roles/:id updates nom and permissions",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "employee" }]);
await testDb.insert(permissions).values([
{ id: "note_read", nom: "Consulter les notes" },
]);
const res = await roleHandler.PUT!(
makeJsonRequest(`/roles/${role.id}`, "PUT", {
nom: "teacher",
permissions: ["note_read"],
}),
makeEmployeeContext({ idRole: String(role.id) }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "teacher");
assertEquals(body.permissions, ["note_read"]);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e roles: PUT /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.PUT!(
makeJsonRequest("/roles/9999", "PUT", { nom: "ghost", permissions: [] }),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /roles/:id ---
Deno.test({
name: "e2e roles: DELETE /roles/:id returns 204",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "moderator" }]);
const res = await roleHandler.DELETE!(
makeGetRequest(`/roles/${role.id}`),
makeEmployeeContext({ idRole: String(role.id) }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e roles: DELETE /roles/:id 404 when not found",
async fn() {
await truncateAll();
const res = await roleHandler.DELETE!(
makeGetRequest("/roles/9999"),
makeEmployeeContext({ idRole: "9999" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-288
View File
@@ -1,288 +0,0 @@
// #109 - E2E tests for /students endpoints
// Appelle les handlers Fresh directement avec un vrai contexte + vraie DB
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import {
seedPromotions,
seedStudents,
truncateAll,
} from "../helpers/db_integration.ts";
import { handler as studentsHandler } from "$apps/students/api/students.ts";
import { handler as studentHandler } from "$apps/students/api/students/[numEtud].ts";
// --- GET /students ---
Deno.test({
name: "e2e students: GET /students returns all students as employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
]);
const req = makeGetRequest("/students");
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertExists(body.find((s: { nom: string }) => s.nom === "Dupont"));
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students returns empty array for non-employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
]);
const req = makeGetRequest("/students");
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students?idPromo filters by promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }, { id: "PEIP2-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
{ nom: "Durand", prenom: "Claire", idPromo: "PEIP2-2024" },
]);
const req = makeGetRequest("/students", { idPromo: "PEIP1-2024" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertEquals(
body.every((s: { idPromo: string }) => s.idPromo === "PEIP1-2024"),
true,
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /students ---
Deno.test({
name: "e2e students: POST /students creates a student (201)",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const req = makeJsonRequest("/students", "POST", {
nom: "Leroy",
prenom: "Paul",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 201);
const body = await res.json();
assertExists(body.numEtud);
assertEquals(body.nom, "Leroy");
assertEquals(body.idPromo, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: POST /students 403 for non-employee",
async fn() {
await truncateAll();
const req = makeJsonRequest("/students", "POST", {
nom: "Test",
prenom: "User",
idPromo: "PEIP1-2024",
});
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: POST /students 400 when missing required fields",
async fn() {
await truncateAll();
const req = makeJsonRequest("/students", "POST", { nom: "Leroy" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /students/:numEtud ---
Deno.test({
name: "e2e students: GET /students/:numEtud returns student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Bernard", prenom: "Lucie", idPromo: "INFO3-2024" },
]);
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.numEtud, s.numEtud);
assertEquals(body.nom, "Bernard");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 403 for non-employee",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/12345");
const ctx = makeContextWithAffiliation("student", { numEtud: "12345" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /students/:numEtud ---
Deno.test({
name: "e2e students: PUT /students/:numEtud updates student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]);
const [s] = await seedStudents([
{ nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" },
]);
const req = makeJsonRequest(`/students/${s.numEtud}`, "PUT", {
nom: "Grand",
prenom: "Hugo",
idPromo: "INFO4-2024",
});
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Grand");
assertEquals(body.idPromo, "INFO4-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: PUT /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const req = makeJsonRequest("/students/999999", "PUT", {
nom: "Ghost",
prenom: "Ghost",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /students/:numEtud ---
Deno.test({
name: "e2e students: DELETE /students/:numEtud returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" },
]);
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.DELETE!(req, ctx);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: DELETE /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.DELETE!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-123
View File
@@ -1,123 +0,0 @@
// Mock de fetch() pour les tests — supporte méthodes HTTP et status codes
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
export interface MockRoute {
method?: HttpMethod;
status?: number;
body?: unknown;
headers?: Record<string, string>;
}
// deno-lint-ignore no-explicit-any
let _originalFetch: ((input: any, init?: any) => Promise<Response>) | null =
null;
let _calls: { url: string; method: string; body?: unknown }[] = [];
/**
* Remplace globalThis.fetch par un mock configurable.
*
* Usage simple (GET 200 par défaut) :
* mockFetch({ "/students": studentsData })
*
* Usage avancé (méthode + status) :
* mockFetch({ "/students": { method: "POST", status: 201, body: newStudent } })
*/
export function mockFetch(
routes: Record<string, unknown | MockRoute>,
): void {
_originalFetch = globalThis.fetch;
_calls = [];
globalThis.fetch = (
input: string | URL | Request,
init?: RequestInit,
): Promise<Response> => {
const url = typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
const method = (init?.method ?? "GET").toUpperCase();
// Parse le body si présent
let reqBody: unknown = undefined;
if (init?.body) {
try {
reqBody = JSON.parse(init.body as string);
} catch {
reqBody = init.body;
}
}
_calls.push({ url, method, body: reqBody });
for (const [pattern, config] of Object.entries(routes)) {
if (!url.includes(pattern)) continue;
// Config simple : la valeur est directement le body de réponse (GET 200)
if (!isRouteConfig(config)) {
return new Response(JSON.stringify(config), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
// Config avancée : vérifier la méthode si spécifiée
if (config.method && config.method !== method) continue;
const status = config.status ?? 200;
// 204 : pas de body
if (status === 204) {
return new Response(null, { status: 204 });
}
return new Response(
config.body !== undefined ? JSON.stringify(config.body) : null,
{
status,
headers: {
"Content-Type": "application/json",
...config.headers,
},
},
);
}
return new Response(JSON.stringify({ error: "Not Found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
};
}
/**
* Restaure le fetch original.
*/
export function restoreFetch(): void {
if (_originalFetch) {
globalThis.fetch = _originalFetch;
_originalFetch = null;
}
_calls = [];
}
/**
* Retourne la liste des appels fetch interceptés.
*/
export function getFetchCalls(): {
url: string;
method: string;
body?: unknown;
}[] {
return [..._calls];
}
function isRouteConfig(value: unknown): value is MockRoute {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const v = value as Record<string, unknown>;
return "status" in v || "method" in v || "body" in v;
}
-89
View File
@@ -1,89 +0,0 @@
// Helper pour les tests d'intégration avec PostgreSQL
// Nécessite les variables d'environnement POSTGRES_* (ou TEST_DATABASE_URL)
import { drizzle } from "npm:drizzle-orm@0.45.2/node-postgres";
import pg from "npm:pg@8.20.0";
import * as schema from "$root/databases/schema.ts";
const { Pool } = pg;
function createTestPool(): pg.Pool {
const url = Deno.env.get("TEST_DATABASE_URL");
if (url) {
return new Pool({ connectionString: url });
}
return new Pool({
host: Deno.env.get("POSTGRES_HOST") ?? "localhost",
port: Number(Deno.env.get("POSTGRES_PORT") ?? 5432),
user: Deno.env.get("POSTGRES_USER") ?? "test",
password: Deno.env.get("POSTGRES_PASS") ?? "test",
database: Deno.env.get("POSTGRES_DB") ?? "polympr_test",
ssl: false,
});
}
export const testPool = createTestPool();
export const testDb = drizzle(testPool, { schema });
const ALL_TABLES =
'"mobility","ajustements","notes","ue_modules","enseignements","role_permissions","students","users","modules","ues","promotions","permissions","roles"';
/**
* Vide toutes les tables dans le bon ordre.
* À appeler dans beforeEach de chaque test d'intégration.
*/
export async function truncateAll(): Promise<void> {
const client = await testPool.connect();
try {
await client.query(
`TRUNCATE TABLE ${ALL_TABLES} RESTART IDENTITY CASCADE`,
);
} finally {
client.release();
}
}
/**
* Ferme le pool à la fin de la suite de tests.
*/
export async function closeTestPool(): Promise<void> {
await testPool.end();
}
// --- Helpers d'insertion de fixtures ---
export async function seedRoles(
rows: { nom: string }[],
): Promise<typeof schema.roles.$inferSelect[]> {
return await testDb.insert(schema.roles).values(rows).returning();
}
export async function seedPromotions(
rows: { id: string; annee?: string }[],
): Promise<typeof schema.promotions.$inferSelect[]> {
return await testDb.insert(schema.promotions).values(rows).returning();
}
export async function seedStudents(
rows: { nom: string; prenom: string; idPromo?: string }[],
): Promise<typeof schema.students.$inferSelect[]> {
return await testDb.insert(schema.students).values(rows).returning();
}
export async function seedModules(
rows: { id: string; nom: string }[],
): Promise<typeof schema.modules.$inferSelect[]> {
return await testDb.insert(schema.modules).values(rows).returning();
}
export async function seedUes(
rows: { nom: string }[],
): Promise<typeof schema.ues.$inferSelect[]> {
return await testDb.insert(schema.ues).values(rows).returning();
}
export async function seedUsers(
rows: { id: string; nom: string; prenom: string; idRole?: number }[],
): Promise<typeof schema.users.$inferSelect[]> {
return await testDb.insert(schema.users).values(rows).returning();
}
-122
View File
@@ -1,122 +0,0 @@
// Mock de la couche Drizzle pour les tests unitaires/intégration
// Permet de tester les handlers sans connexion PostgreSQL
export interface MockQueryResult<T> {
rows: T[];
}
export interface MockDbConfig {
// Table name → array of rows
// deno-lint-ignore no-explicit-any
tables: Record<string, Record<string, any>[]>;
}
/**
* Crée un mock de la DB Drizzle.
* Simule select/insert/update/delete avec un store en mémoire.
*
* Usage :
* ```ts
* const db = createMockDb({
* tables: {
* students: [{ numEtud: 21212006, nom: "Dupont", ... }],
* notes: [],
* }
* });
*
* // Lire toutes les lignes d'une table
* const rows = db.getTable("students");
*
* // Insérer
* db.insert("students", { numEtud: 21212009, nom: "Test", ... });
*
* // Trouver par clé
* const student = db.findOne("students", (r) => r.numEtud === 21212006);
*
* // Supprimer
* db.deleteWhere("students", (r) => r.numEtud === 21212006);
* ```
*/
export function createMockDb(config: MockDbConfig) {
// Deep clone pour éviter les mutations entre tests
// deno-lint-ignore no-explicit-any
const tables: Record<string, Record<string, any>[]> = {};
for (const [name, rows] of Object.entries(config.tables)) {
tables[name] = rows.map((r) => ({ ...r }));
}
return {
/** Retourne toutes les lignes d'une table */
getTable<T = Record<string, unknown>>(name: string): T[] {
return (tables[name] ?? []) as T[];
},
/** Retourne les lignes qui matchent le filtre */
findMany<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): T[] {
return (this.getTable<T>(name)).filter(predicate);
},
/** Retourne la première ligne qui matche, ou undefined */
findOne<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): T | undefined {
return (this.getTable<T>(name)).find(predicate);
},
/** Insère une ligne dans la table */
insert<T = Record<string, unknown>>(name: string, row: T): T {
if (!tables[name]) tables[name] = [];
const copy = { ...row } as T;
// deno-lint-ignore no-explicit-any
tables[name].push(copy as any);
return copy;
},
/** Met à jour les lignes qui matchent le prédicat */
updateWhere<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
updates: Partial<T>,
): number {
const rows = this.getTable<T>(name);
let count = 0;
for (const row of rows) {
if (predicate(row)) {
Object.assign(row as Record<string, unknown>, updates);
count++;
}
}
return count;
},
/** Supprime les lignes qui matchent le prédicat */
deleteWhere<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): number {
const before = (tables[name] ?? []).length;
tables[name] = (tables[name] ?? []).filter(
(r) => !predicate(r as unknown as T),
);
return before - tables[name].length;
},
/** Vide une table */
clear(name: string): void {
tables[name] = [];
},
/** Vide toutes les tables */
reset(): void {
for (const name of Object.keys(tables)) {
tables[name] = [];
}
},
};
}
export type MockDb = ReturnType<typeof createMockDb>;
-137
View File
@@ -1,137 +0,0 @@
// Types et données de test alignés sur l'API REST PolyMPR
// --- Types ---
export interface Student {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
}
export interface Promotion {
idPromo: string;
annee: string;
}
export interface Prof {
id: number;
nom: string;
prenom: string;
}
export interface Module {
id: string;
nom: string;
}
export interface Note {
note: number;
numEtud: number;
idModule: string;
}
export interface UE {
id: number;
nom: string;
}
export interface UeModule {
idModule: string;
idUE: number;
idPromo: string;
coeff: number;
}
export interface Enseignement {
idProf: number;
idModule: string;
idPromo: string;
}
export interface Ajustement {
numEtud: number;
idUE: number;
valeur: number;
}
export interface ImportResult {
imported: number;
errors: { line: number; message: string }[];
}
export interface ApiError {
error: string;
}
// --- Fixtures ---
export const students: Student[] = [
{ numEtud: 21212006, nom: "Dupont", prenom: "Jean", idPromo: "4AFISE25/26" },
{
numEtud: 21212007,
nom: "Martin",
prenom: "Alice",
idPromo: "4AFISE25/26",
},
{
numEtud: 21212008,
nom: "Durand",
prenom: "Claire",
idPromo: "3AFISE25/26",
},
];
export const promotions: Promotion[] = [
{ idPromo: "4AFISE25/26", annee: "2025" },
{ idPromo: "3AFISE25/26", annee: "2025" },
{ idPromo: "JIA4A2526", annee: "2025" },
];
export const profs: Prof[] = [
{ id: 1, nom: "Leclerc", prenom: "Jean" },
{ id: 2, nom: "Moreau", prenom: "Sophie" },
];
export const modules: Module[] = [
{ id: "JIN702C", nom: "Optimisation" },
{ id: "JIN703C", nom: "Informatique" },
{ id: "JIN704C", nom: "Physique" },
];
export const notes: Note[] = [
{ note: 15.5, numEtud: 21212006, idModule: "JIN702C" },
{ note: 12.0, numEtud: 21212006, idModule: "JIN703C" },
{ note: 18.0, numEtud: 21212007, idModule: "JIN702C" },
{ note: 9.0, numEtud: 21212008, idModule: "JIN704C" },
];
export const ues: UE[] = [
{ id: 1, nom: "UE Informatique" },
{ id: 2, nom: "UE Mathématiques" },
];
export const ueModules: UeModule[] = [
{ idModule: "JIN702C", idUE: 1, idPromo: "4AFISE25/26", coeff: 3.0 },
{ idModule: "JIN703C", idUE: 2, idPromo: "4AFISE25/26", coeff: 4.0 },
{ idModule: "JIN704C", idUE: 1, idPromo: "3AFISE25/26", coeff: 2.0 },
];
export const enseignements: Enseignement[] = [
{ idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" },
{ idProf: 2, idModule: "JIN703C", idPromo: "4AFISE25/26" },
{ idProf: 1, idModule: "JIN704C", idPromo: "3AFISE25/26" },
];
export const ajustements: Ajustement[] = [
{ numEtud: 21212006, idUE: 1, valeur: 13.25 },
{ numEtud: 21212008, idUE: 1, valeur: 11.0 },
];
// --- Réponses d'erreur standard ---
export const ERROR_NOT_FOUND: ApiError = { error: "Ressource introuvable" };
export const ERROR_CONFLICT: ApiError = { error: "Ressource déjà existante" };
export const ERROR_BAD_REQUEST: ApiError = { error: "Requête invalide" };
export const ERROR_UNAUTHORIZED: ApiError = { error: "Non authentifié" };
export const ERROR_FORBIDDEN: ApiError = { error: "Accès interdit" };
-88
View File
@@ -1,88 +0,0 @@
// Helper pour les tests E2E — appel direct des handlers Fresh
// sans lancer de serveur HTTP
import { FreshContext } from "$fresh/server.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { CasContent } from "$root/defaults/interfaces.ts";
const BASE_EMPLOYEE_SESSION: CasContent = {
amuCampus: "",
amuComposante: "",
amuDateValidation: "",
coGroup: "",
eduPersonPrimaryAffiliation: "employee",
eduPersonPrincipalName: "test.user@polytech.fr",
mail: "test.user@polytech.fr",
displayName: "Test User",
givenName: "Test",
memberOf: [],
sn: "User",
supannCivilite: "M.",
supannEntiteAffectation: "",
supannEtuAnneeInscription: "",
supannEtuEtape: "",
uid: "test.user",
};
/**
* Crée un FreshContext mock authentifié en tant qu'employee.
*/
export function makeEmployeeContext(
params: Record<string, string> = {},
): FreshContext<AuthenticatedState> {
return {
params,
state: {
isAuthenticated: true,
session: { ...BASE_EMPLOYEE_SESSION },
availablePages: {},
},
render: () => Promise.resolve(new Response()),
renderNotFound: () => Promise.resolve(new Response(null, { status: 404 })),
next: () => Promise.resolve(new Response()),
} as unknown as FreshContext<AuthenticatedState>;
}
/**
* Crée un FreshContext mock avec un affiliation personnalisée.
*/
export function makeContextWithAffiliation(
affiliation: string,
params: Record<string, string> = {},
): FreshContext<AuthenticatedState> {
const ctx = makeEmployeeContext(params);
(ctx.state as AuthenticatedState).session.eduPersonPrimaryAffiliation =
affiliation;
return ctx;
}
/**
* Crée une Request GET simple.
*/
export function makeGetRequest(
path: string,
searchParams?: Record<string, string>,
): Request {
const url = new URL(`http://localhost${path}`);
if (searchParams) {
for (const [k, v] of Object.entries(searchParams)) {
url.searchParams.set(k, v);
}
}
return new Request(url.toString());
}
/**
* Crée une Request POST/PUT avec un corps JSON.
*/
export function makeJsonRequest(
path: string,
method: string,
body: unknown,
): Request {
return new Request(`http://localhost${path}`, {
method,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
-55
View File
@@ -1,55 +0,0 @@
// Setup happy-dom + wrapper render pour les tests de composants Preact
import { Window } from "happy-dom";
let _window: Window | null = null;
/**
* Initialise un environnement DOM virtuel via happy-dom.
* À appeler avant de rendre des composants Preact dans les tests.
*/
export function setupDOM(): void {
_window = new Window({ url: "http://localhost" });
// Expose les globals DOM nécessaires à Preact
const globals = _window as unknown as Record<string, unknown>;
const target = globalThis as unknown as Record<string, unknown>;
for (
const key of [
"document",
"navigator",
"location",
"HTMLElement",
"HTMLInputElement",
"HTMLTextAreaElement",
"HTMLSelectElement",
"Event",
"CustomEvent",
"KeyboardEvent",
"MouseEvent",
"InputEvent",
"MutationObserver",
"requestAnimationFrame",
"cancelAnimationFrame",
]
) {
target[key] = globals[key];
}
target["window"] = _window;
}
/**
* Nettoie l'environnement DOM.
* À appeler dans un afterEach ou à la fin d'un test.
*/
export function cleanupDOM(): void {
if (_window) {
const doc = _window.document;
doc.body.innerHTML = "";
doc.head.innerHTML = "";
_window.close();
_window = null;
}
}
View File
-104
View File
@@ -1,104 +0,0 @@
// #113 - Integration tests for /modules endpoints
import { assertEquals, assertExists, assertRejects } from "@std/assert";
import { seedModules, testDb, truncateAll } from "../helpers/db_integration.ts";
import { modules } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
Deno.test({
name: "integration modules: list all modules",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }, {
id: "INFO101",
nom: "Informatique",
}]);
const rows = await testDb.select().from(modules);
assertEquals(rows.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: create and retrieve by id",
async fn() {
await truncateAll();
const [created] = await testDb.insert(modules).values({
id: "PHYS101",
nom: "Physique",
}).returning();
assertExists(created);
assertEquals(created.id, "PHYS101");
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "PHYS101"))
.then((r) => r[0] ?? null);
assertExists(row);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: get by id returns null when not found",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "NONEXISTENT"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: duplicate id insert fails",
async fn() {
await truncateAll();
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
await assertRejects(() =>
testDb.insert(modules).values({ id: "MATH101", nom: "Doublon" })
);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: update nom",
async fn() {
await truncateAll();
await seedModules([{ id: "ELEC201", nom: "Électronique" }]);
const [updated] = await testDb
.update(modules)
.set({ nom: "Électronique numérique" })
.where(eq(modules.id, "ELEC201"))
.returning();
assertEquals(updated.nom, "Électronique numérique");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration modules: delete removes the module",
async fn() {
await truncateAll();
await seedModules([{ id: "BIO101", nom: "Biologie" }]);
await testDb.delete(modules).where(eq(modules.id, "BIO101"));
const row = await testDb
.select()
.from(modules)
.where(eq(modules.id, "BIO101"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
-112
View File
@@ -1,112 +0,0 @@
// #110 - Integration tests for /promotions endpoints
import { assertEquals, assertExists } from "@std/assert";
import {
seedPromotions,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { promotions } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
Deno.test({
name: "integration promotions: list all",
async fn() {
await truncateAll();
await seedPromotions([
{ id: "PEIP1-2024", annee: "2024" },
{ id: "PEIP2-2024", annee: "2024" },
]);
const rows = await testDb.select().from(promotions);
assertEquals(rows.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: create and retrieve by id",
async fn() {
await truncateAll();
const [created] = await testDb
.insert(promotions)
.values({ id: "INFO3-2025", annee: "2025" })
.returning();
assertExists(created);
assertEquals(created.id, "INFO3-2025");
assertEquals(created.annee, "2025");
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2025"))
.then((r) => r[0] ?? null);
assertExists(row);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: get by id returns null when not found",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "NONEXISTENT"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update annee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2023", annee: "2023" }]);
const [updated] = await testDb
.update(promotions)
.set({ annee: "2024" })
.where(eq(promotions.id, "INFO3-2023"))
.returning();
assertExists(updated);
assertEquals(updated.annee, "2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: delete removes the row",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]);
await testDb.delete(promotions).where(eq(promotions.id, "INFO3-2022"));
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2022"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update non-existent returns empty",
async fn() {
await truncateAll();
const result = await testDb
.update(promotions)
.set({ annee: "2099" })
.where(eq(promotions.id, "GHOST"))
.returning();
assertEquals(result.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});

Some files were not shown because too many files have changed in this diff Show More