Compare commits

..

4 Commits

Author SHA1 Message Date
anys dc0af96470 Refactor AuthenticatedState to store displayName and uid
The AuthenticatedState interface was updated to directly store the
`displayName` and `uid` properties. Previously, it stored the entire
`CasContent` object, which contained these properties along with others
that were not consistently used. This change simplifies the interface
and reduces redundancy.
2026-01-08 20:04:17 +01:00
anys bda47fd88b Update role enum and access control Remove isRouteAnAPI(route: string):
boolean

Refactor role determination logic to use `eduPersonPrimaryAffiliation`
and `amuComposante`. This simplifies checking for Polytech affiliation
and identifies roles like professor, administration, and student more
accurately. The API access control is updated to reflect the new role
names.
2026-01-08 19:34:29 +01:00
anys c0a335d33f - Add role detection
- Restrict APIs to personnels
- Show 403 for unauthorized access"
2026-01-08 19:33:20 +01:00
anys e818051621 Add 403 error page and Polytech access control. 2026-01-08 19:33:03 +01:00
59 changed files with 317 additions and 2926 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
@@ -4,10 +4,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- develop
push:
branches:
- develop
permissions: permissions:
contents: read contents: read
@@ -28,6 +24,3 @@ jobs:
- name: Check linting - name: Check linting
run: deno lint run: deno lint
- name: Run tests
run: deno test -A --no-check tests/
+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 main.ts --allow-import flag
RUN deno task build RUN deno task build
USER deno USER deno
-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 -21
View File
@@ -1,26 +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]
-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);
-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"),
});
+5 -1
View File
@@ -3,12 +3,16 @@ import { AsyncRoute } from "$fresh/src/server/types.ts";
export interface AuthenticatedState { export interface AuthenticatedState {
isAuthenticated: true; isAuthenticated: true;
session: CasContent; isFromPolytech: boolean;
role: "etudiant" | "professeur" | "administration" | "autre";
displayName: string;
uid: string;
availablePages: Record<string, string>; availablePages: Record<string, string>;
} }
interface UnauthenticatedState { interface UnauthenticatedState {
isAuthenticated: false; isAuthenticated: false;
isFromPolytech: false;
session: undefined; session: undefined;
} }
+1 -5
View File
@@ -9,8 +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/"
}, },
"lint": { "lint": {
"rules": { "rules": {
@@ -36,9 +35,6 @@
"@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",
"@std/testing": "jsr:@std/testing@^1.0.0",
"happy-dom": "npm:happy-dom@^16.0.0",
"$root/": "./", "$root/": "./",
"$apps/": "./routes/(apps)/" "$apps/": "./routes/(apps)/"
}, },
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from "drizzle-kit";
import process from "node:process";
export default defineConfig({
dialect: "postgresql",
schema: "./databases/schema.ts",
out: "./databases/migrations",
dbCredentials: {
host: process.env.POSTGRES_HOST!,
port: Number(process.env.POSTGRES_PORT ?? 5432),
user: process.env.POSTGRES_USER!,
password: process.env.POSTGRES_PASS!,
database: process.env.POSTGRES_DB!,
},
});
-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
+1 -1
View File
@@ -2,7 +2,7 @@ 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"; import { load } from "@std/dotenv";
await load({ envPath: "./.env", export: true }); await load({ envPath: "./.env.development.local", export: true });
await ensureDatabases(); await ensureDatabases();
export default defineConfig({ export default defineConfig({
server: { server: {
+2
View File
@@ -19,6 +19,7 @@ import * as $_apps_students_partials_admin_consult from "./routes/(apps)/student
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_types_d from "./routes/(apps)/students/types.d.ts";
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";
@@ -64,6 +65,7 @@ const manifest = {
"./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/types.d.ts": $_apps_students_types_d,
"./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,
-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"
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ export const handler: MiddlewareHandler<AuthenticatedState>[] = [
context.state.availablePages = properties.pages; context.state.availablePages = properties.pages;
if ( if (
context.state.session.eduPersonPrimaryAffiliation == "student" && context.state.role == "etudiant" &&
Deno.env.get("LOCAL") != "true" Deno.env.get("LOCAL") != "true"
) { ) {
properties.adminOnly.forEach((page) => properties.adminOnly.forEach((page) =>
-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";
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";
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);
@@ -240,7 +240,7 @@ export default function EditMobility() {
</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> </section>
+110 -64
View File
@@ -1,40 +1,55 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import { Database } from "@db/sqlite";
import { mobility, promotions, students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() { async GET() {
try { try {
const studentRows = await db console.log("Connecting to mobility database...");
.select({ const connection = new Database("databases/data/mobility.db", {
id: students.userId, create: false,
firstName: students.firstName, });
lastName: students.lastName, connection.run(
promotionId: students.promotionId, "ATTACH DATABASE 'databases/data/students.db' AS students",
endyear: promotions.endyear, );
current: promotions.current, console.log("Connected to databases.");
})
.from(students)
.leftJoin(promotions, eq(students.promotionId, promotions.id));
const mobilityRows = await db.select().from(mobility); const students = connection.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 promotionRows = await db const mobilities = connection.prepare(
.select({ `SELECT
id: promotions.id, mobility.id,
endyear: promotions.endyear, mobility.studentId,
current: promotions.current, mobility.startDate,
}) mobility.endDate,
.from(promotions); mobility.weeksCount,
mobility.destinationCountry,
mobility.destinationName,
mobility.mobilityStatus
FROM mobility`,
).all();
const promotions = connection.prepare(
`SELECT id, name FROM students.promotions`,
).all();
connection.close();
return new Response( return new Response(
JSON.stringify({ JSON.stringify({ mobilities, students, promotions }),
mobilities: mobilityRows, {
students: studentRows, status: 200,
promotions: promotionRows, headers: { "Content-Type": "application/json" },
}), },
{ status: 200, headers: { "Content-Type": "application/json" } },
); );
} catch (error) { } catch (error) {
console.error("Error fetching mobility data:", error); console.error("Error fetching mobility data:", error);
@@ -43,6 +58,8 @@ export const handler: Handlers = {
}, },
async POST(request) { async POST(request) {
console.log("API /mobility/api/insert_mobility POST called");
try { try {
const body = await request.json(); const body = await request.json();
const { data } = body; const { data } = body;
@@ -50,8 +67,32 @@ export const handler: Handlers = {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error("Invalid request body"); throw new Error("Invalid request body");
} }
console.log("Connecting to mobility database...");
const connection = new Database("databases/data/mobility.db", {
create: false,
});
for (const entry of data) { console.log("Attaching students database...");
connection.run(
"ATTACH DATABASE 'databases/data/students.db' AS students",
);
console.log("Students database attached successfully.");
const insertQuery = connection.prepare(
`INSERT INTO mobility (
id, studentId, startDate, endDate, weeksCount, destinationCountry, destinationName, mobilityStatus
)
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`,
);
for (const mobility of data) {
const { const {
id, id,
studentId, studentId,
@@ -61,16 +102,19 @@ export const handler: Handlers = {
destinationCountry, destinationCountry,
destinationName, destinationName,
mobilityStatus = "N/A", mobilityStatus = "N/A",
} = entry; } = mobility;
const studentExists = await db console.log("Processing mobility data:", mobility);
.select({ userId: students.userId })
.from(students)
.where(eq(students.userId, studentId))
.limit(1)
.then((rows) => rows.length > 0);
if (!studentExists) { const studentExists = connection
.prepare(
`SELECT COUNT(*) AS count FROM students.students WHERE userId = ?`,
)
.get(studentId);
console.log(`Student ${studentId} exists:`, studentExists.count > 0);
if (studentExists.count === 0) {
console.warn(`Skipping mobility for unknown studentId: ${studentId}`); console.warn(`Skipping mobility for unknown studentId: ${studentId}`);
continue; continue;
} }
@@ -79,38 +123,40 @@ export const handler: Handlers = {
if (startDate && endDate) { if (startDate && endDate) {
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
calculatedWeeksCount = start <= end if (start <= end) {
? Math.ceil( calculatedWeeksCount = Math.ceil(
(end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000), (end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000),
) );
: null; } else {
calculatedWeeksCount = null;
}
} }
await db console.log("Executing SQL insert/update query for:", {
.insert(mobility) id,
.values({ studentId,
id, startDate,
studentId, endDate,
startDate, calculatedWeeksCount,
endDate, destinationCountry,
weeksCount: calculatedWeeksCount, destinationName,
destinationCountry, mobilityStatus,
destinationName, });
mobilityStatus,
}) insertQuery.run(
.onConflictDoUpdate({ id,
target: mobility.id, studentId,
set: { startDate,
startDate, endDate,
endDate, calculatedWeeksCount,
weeksCount: calculatedWeeksCount, destinationCountry,
destinationCountry, destinationName,
destinationName, mobilityStatus,
mobilityStatus, );
},
});
} }
connection.close();
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", { return new Response("Data inserted/updated successfully", {
status: 200, status: 200,
}); });
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// 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 <h2>Welcome to {context.state.session?.displayName}.</h2>; return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
-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 });
},
};
-61
View File
@@ -1,61 +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";
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,121 +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";
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 });
}
},
};
-58
View File
@@ -1,58 +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";
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,136 +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 });
}
},
};
-103
View File
@@ -1,103 +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";
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 });
}
},
};
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Courses(_request: Request, context: FreshContext<State>) { async function Courses(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.session?.displayName}.</h2>; return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// 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 <h2>Welcome to {context.state.session?.displayName}.</h2>; return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Notes(_request: Request, context: FreshContext<State>) { async function Notes(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.session?.displayName}.</h2>; return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
@@ -22,7 +22,7 @@ export default function Promotion(props: PromotionProps) {
<tbody> <tbody>
{props.students {props.students
.filter((student) => student.promotionId === props.promo.id) .filter((student) => student.promotionId === props.promo.id)
.map((student) => <Student key={student.id} student={student} />)} .map((student) => <Student student={student} />)}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -104,7 +104,7 @@ export default function UploadStudents() {
return ( return (
<> <>
<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>
</> </>
); );
-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 });
},
};
+131 -41
View File
@@ -1,61 +1,151 @@
import { FreshContext, Handlers } from "$fresh/server.ts"; import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import connect from "$root/databases/connect.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2"; import { Database } from "@db/sqlite";
/**
* Gets itself from the database.
* @param database The database connection
* @param userId The user ID.
* @returns Itself from the database.
*/
function getItself(
database: Database,
userId: string,
): { student: Student | null; promo: Promotion | null } {
const studentQuery = "select * from students where userId = ?";
const student: Student | undefined = database.prepare(studentQuery).get(
userId,
);
if (!student) {
return { student: null, promo: null };
}
const promoQuery = "select * from promotions where id = ?";
const promo: Promotion | undefined = database.prepare(promoQuery).get(
student.promotionId,
);
return { student, promo: promo ?? null };
}
/**
* Gets itself from the database.
* @param database The database connexion
* @param userId The user ID.
* @returns Itself from the database.
*/
function getAll(
database: Database,
): { students: Student[]; promos: Promotion[] } {
const studentsQuery = `
select userId, firstName, lastName, mail, promotionId
from students inner join promotions
on students.promotionId = promotions.id
where promotions.current < 6`;
const students: Student[] = database.prepare(studentsQuery).all();
const promosQuery = "select * from promotions where promotions.current < 6";
const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
return { students, promos };
}
/**
* Add users to the database.
* @param database The database connexion
* @param students The students to add
* @param promoId The promotion id.
*/
function addStudents(database: Database, students: Student[], promoId: string) {
const query = `
INSERT INTO students
(userId, firstName, lastName, mail, promotionId)
VALUES (?, ?, ?, ?, ?)`;
const statement = database.prepare(query);
for (const student of students) {
statement.run(
student.userId,
student.firstName,
student.lastName,
student.mail,
promoId,
);
}
}
export const handler: Handlers<null, AuthenticatedState> = { export const handler: Handlers<null, AuthenticatedState> = {
// #7 GET /students /**
* The students the user can see.
* @param _request The HTTP request.
* @param _context The context with authenticated state.
* @returns All students our user can see.
*/
// deno-lint-ignore require-await
async GET( async GET(
request: Request, _request: Request,
context: FreshContext<AuthenticatedState>, context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { using connection = connect("students");
return new Response(JSON.stringify([]), { const database = connection.database;
headers: { "content-type": "application/json" },
}); if (context.state.role == "etudiant") {
return new Response(
JSON.stringify(getItself(database, context.state.uid)),
{
headers: {
"content-type": "application/json",
},
},
);
} }
const url = new URL(request.url); return new Response(
const idPromo = url.searchParams.get("idPromo"); JSON.stringify(getAll(database)),
{
const rows = idPromo headers: {
? await db.select().from(students).where(eq(students.idPromo, idPromo)) "content-type": "application/json",
: await db.select().from(students); },
},
return new Response(JSON.stringify(rows), { );
headers: { "content-type": "application/json" },
});
}, },
/**
// #8 POST /students * Add students in the database.
* @param request The HTTP request.
* @param _context The Fresh context.
* @returns HTTP 201 on successful insert.
*/
async POST( async POST(
request: Request, request: Request,
context: FreshContext<AuthenticatedState>, _context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { const { students, promo }: { students: Student[]; promo: string } =
return new Response(null, { status: 403 }); await request.json();
}
const body: { if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
} = await request.json();
if (!body.nom || !body.prenom || !body.idPromo) {
return new Response(null, { status: 400 }); return new Response(null, { status: 400 });
} }
const [created] = await db using connection = connect("students");
.insert(students) const database = connection.database;
.values({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.returning();
return new Response(JSON.stringify(created), { const { endyear, current } = promo.match(
status: 201, /^(?<endyear>\d{4})-(?<current>\d)A$/,
headers: { "content-type": "application/json" }, )?.groups!;
});
database.prepare(
"insert or ignore into promotions (endyear, current) values (?, ?)",
).run(endyear, current);
const { id: promoId }: { id: string } = database
.prepare("select id from promotions where endyear = ? and current = ?")
.get(endyear, current)!;
addStudents(database, students, promoId);
return new Response(null, { status: 201 });
}, },
}; };
@@ -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 -7
View File
@@ -8,13 +8,7 @@ import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrai
// 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 {context.state.displayName || 'Guest'}!</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();
+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>
</>
);
}
+39 -8
View File
@@ -44,6 +44,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 +56,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 +69,27 @@ 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;
context.state.session = session;
const isFromPolytech = session.amuComposante.includes("polytech");
context.state.isFromPolytech = isFromPolytech;
if (isFromPolytech) {
context.state.displayName = session.displayName;
context.state.uid = session.uid;
if (session.eduPersonPrimaryAffiliation == "faculty") {
context.state.role = "professeur"
} else if (session.eduPersonPrimaryAffiliation == "employee") {
context.state.role = "administration"
} else if (session.eduPersonPrimaryAffiliation == "student") {
context.state.role = "etudiant";
} else {
context.state.role = "autre";
}
}
} }
return await context.next(); return await context.next();
@@ -87,13 +107,24 @@ 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)) {
return new Response(null, { if (!context.state.isAuthenticated) {
status: 302, return new Response(null, {
headers: { status: 302,
Location: "/login", headers: {
}, Location: "/login",
}); },
});
}
if (!context.state.isFromPolytech) {
return new Response(null, {
status: 403,
headers: {
Location: "/403",
},
});
}
} }
return await context.next(); return await context.next();
View File
-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;
}
-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" };
-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
-266
View File
@@ -1,266 +0,0 @@
import { assertEquals, assertExists } from "@std/assert";
import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts";
import { createMockDb } from "../helpers/db_mock.ts";
import {
ERROR_CONFLICT,
ERROR_NOT_FOUND,
modules,
notes,
type Student,
students,
} from "../helpers/fixtures.ts";
import { cleanupDOM, setupDOM } from "../helpers/render.ts";
// --- Fixtures ---
Deno.test("fixtures - students match API shape", () => {
assertEquals(students.length, 3);
assertEquals(students[0].numEtud, 21212006);
assertEquals(students[0].idPromo, "4AFISE25/26");
assertEquals(typeof students[0].idPromo, "string");
});
Deno.test("fixtures - modules have string ids", () => {
assertEquals(modules[0].id, "JIN702C");
assertEquals(typeof modules[0].id, "string");
});
Deno.test("fixtures - notes use decimal values", () => {
assertEquals(notes[0].note, 15.5);
assertEquals(notes[0].idModule, "JIN702C");
});
// --- Mock fetch simple (GET 200) ---
Deno.test("mockFetch - GET returns mocked data", async () => {
mockFetch({ "/students": students });
try {
const res = await fetch("http://localhost/api/students");
assertEquals(res.status, 200);
const data = await res.json();
assertEquals(data.length, 3);
assertEquals(data[0].nom, "Dupont");
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - returns 404 for unknown routes", async () => {
mockFetch({});
try {
const res = await fetch("http://localhost/api/unknown");
assertEquals(res.status, 404);
} finally {
restoreFetch();
}
});
// --- Mock fetch avancé (méthodes + status codes) ---
Deno.test("mockFetch - POST 201 created", async () => {
const newStudent = students[0];
mockFetch({
"/students": { method: "POST", status: 201, body: newStudent },
});
try {
const res = await fetch("http://localhost/api/students", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newStudent),
});
assertEquals(res.status, 201);
const data = await res.json();
assertEquals(data.numEtud, 21212006);
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - DELETE 204 no content", async () => {
mockFetch({
"/students/21212006": { method: "DELETE", status: 204 },
});
try {
const res = await fetch("http://localhost/api/students/21212006", {
method: "DELETE",
});
assertEquals(res.status, 204);
assertEquals(res.body, null);
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - 404 error response", async () => {
mockFetch({
"/students/99999": { status: 404, body: ERROR_NOT_FOUND },
});
try {
const res = await fetch("http://localhost/api/students/99999");
assertEquals(res.status, 404);
const data = await res.json();
assertEquals(data.error, "Ressource introuvable");
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - 409 conflict", async () => {
mockFetch({
"/enseignements": { method: "POST", status: 409, body: ERROR_CONFLICT },
});
try {
const res = await fetch("http://localhost/api/enseignements", {
method: "POST",
body: JSON.stringify({
idProf: 1,
idModule: "JIN702C",
idPromo: "4AFISE25/26",
}),
});
assertEquals(res.status, 409);
} finally {
restoreFetch();
}
});
// --- getFetchCalls ---
Deno.test("getFetchCalls - tracks all intercepted calls", async () => {
mockFetch({ "/notes": notes });
try {
await fetch("http://localhost/api/notes");
await fetch("http://localhost/api/notes?numEtud=21212006");
const calls = getFetchCalls();
assertEquals(calls.length, 2);
assertEquals(calls[0].method, "GET");
assertEquals(calls[1].url, "http://localhost/api/notes?numEtud=21212006");
} finally {
restoreFetch();
}
});
Deno.test("getFetchCalls - captures POST body", async () => {
mockFetch({ "/notes": { method: "POST", status: 201, body: notes[0] } });
try {
await fetch("http://localhost/api/notes", {
method: "POST",
body: JSON.stringify(notes[0]),
});
const calls = getFetchCalls();
assertEquals(calls.length, 1);
assertEquals(calls[0].method, "POST");
assertEquals((calls[0].body as { note: number }).note, 15.5);
} finally {
restoreFetch();
}
});
// --- Mock DB ---
Deno.test("mockDb - getTable returns seeded rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
assertEquals(db.getTable("students").length, 3);
});
Deno.test("mockDb - findOne by key", () => {
const db = createMockDb({ tables: { students: [...students] } });
const found = db.findOne<Student>("students", (r) => r.numEtud === 21212006);
assertExists(found);
assertEquals(found.nom, "Dupont");
});
Deno.test("mockDb - findOne returns undefined for missing", () => {
const db = createMockDb({ tables: { students: [...students] } });
const found = db.findOne<Student>("students", (r) => r.numEtud === 99999);
assertEquals(found, undefined);
});
Deno.test("mockDb - insert adds a row", () => {
const db = createMockDb({ tables: { students: [] } });
const newStudent: Student = {
numEtud: 21212099,
nom: "Test",
prenom: "User",
idPromo: "4AFISE25/26",
};
db.insert("students", newStudent);
assertEquals(db.getTable("students").length, 1);
assertEquals(
db.findOne<Student>("students", (r) => r.numEtud === 21212099)?.nom,
"Test",
);
});
Deno.test("mockDb - updateWhere modifies matching rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
const updated = db.updateWhere<Student>(
"students",
(r) => r.numEtud === 21212006,
{ prenom: "Marie" },
);
assertEquals(updated, 1);
assertEquals(
db.findOne<Student>("students", (r) => r.numEtud === 21212006)?.prenom,
"Marie",
);
});
Deno.test("mockDb - deleteWhere removes matching rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
const deleted = db.deleteWhere<Student>(
"students",
(r) => r.numEtud === 21212006,
);
assertEquals(deleted, 1);
assertEquals(db.getTable("students").length, 2);
});
Deno.test("mockDb - findMany with filter", () => {
const db = createMockDb({ tables: { students: [...students] } });
const promo4 = db.findMany<Student>(
"students",
(r) => r.idPromo === "4AFISE25/26",
);
assertEquals(promo4.length, 2);
});
Deno.test("mockDb - reset clears all tables", () => {
const db = createMockDb({
tables: { students: [...students], notes: [...notes] },
});
db.reset();
assertEquals(db.getTable("students").length, 0);
assertEquals(db.getTable("notes").length, 0);
});
Deno.test("mockDb - isolated between instances", () => {
const db1 = createMockDb({ tables: { students: [...students] } });
const db2 = createMockDb({ tables: { students: [...students] } });
db1.deleteWhere<Student>("students", () => true);
assertEquals(db1.getTable("students").length, 0);
assertEquals(db2.getTable("students").length, 3);
});
// --- happy-dom ---
Deno.test({
name: "happy-dom - document is available after setup",
sanitizeResources: false,
sanitizeOps: false,
fn() {
setupDOM();
try {
const doc = globalThis.document;
assertExists(doc);
const div = doc.createElement("div");
div.textContent = "hello";
doc.body.appendChild(div);
assertEquals(doc.body.textContent, "hello");
} finally {
cleanupDOM();
}
},
});