From f0af54533a1fe93bf7c403cd4591d27834c91cd5 Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Thu, 25 Apr 2024 22:32:45 +0100 Subject: [PATCH 1/6] =?UTF-8?q?cooking:=20this=20docker=20config=20be=20st?= =?UTF-8?q?raight=20=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5?= =?UTF-8?q?=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5?= =?UTF-8?q?=F0=9F=94=A5=F0=9F=94=A5=F0=9F=94=A5!!!!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.tpl | 8 ++ .gitignore | 1 + apps/anvil/.dev.env | 47 ------------ apps/anvil/.env | 46 ------------ apps/anvil/Dockerfile | 14 +++- apps/anvil/package.json | 8 +- apps/forge/.env | 3 - apps/forge/.env.development | 3 + apps/forge/.env.production | 3 + apps/mine/.gitignore | 1 + apps/mine/Dockerfile | 20 ++++- apps/mine/package.json | 2 +- config/README.md | 11 ++- config/anvil/.env.development.tpl | 51 +++++++++++++ config/anvil/.env.production.tpl | 49 ++++++++++++ .../.env => config/mine/.env.development.tpl | 0 config/mine/.env.production.tpl | 3 + docker-compose.yml | 74 ++++++++++++++++--- pnpm-lock.yaml | 46 +----------- scripts/gen-cert.sh | 2 + 20 files changed, 231 insertions(+), 161 deletions(-) create mode 100644 .env.tpl delete mode 100644 apps/anvil/.dev.env delete mode 100644 apps/anvil/.env delete mode 100644 apps/forge/.env create mode 100644 apps/forge/.env.development create mode 100644 apps/forge/.env.production create mode 100644 config/anvil/.env.development.tpl create mode 100644 config/anvil/.env.production.tpl rename apps/mine/.env => config/mine/.env.development.tpl (100%) create mode 100644 config/mine/.env.production.tpl create mode 100755 scripts/gen-cert.sh diff --git a/.env.tpl b/.env.tpl new file mode 100644 index 0000000..841db13 --- /dev/null +++ b/.env.tpl @@ -0,0 +1,8 @@ +OP_CONNECT_TOKEN="op://IT/jcjmzwjh6sjrr2uybko6rrizwu/type" +OP_CONNECT_HOST=http://op-api:8080 +EDGEDB_SERVER_ADMIN_UI=enabled +EDGEDB_SERVER_TLS_CERT_MODE=require_file +EDGEDB_SERVER_TLS_KEY=/ignis_certs/key.pem +EDGEDB_SERVER_TLS_CERT=/ignis_certs/cert.pem +EDGEDB_SERVER_USER="op://IT/Ignis EdgeDB Docker Prod/username" +EDGEDB_SERVER_PASSWORD="op://IT/Ignis EdgeDB Docker Prod/password" \ No newline at end of file diff --git a/.gitignore b/.gitignore index ca931a1..8abff67 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Thumbs.db **/.turbo/** deploy/container-images/** **/secret/** +.env \ No newline at end of file diff --git a/apps/anvil/.dev.env b/apps/anvil/.dev.env deleted file mode 100644 index 1b85c9c..0000000 --- a/apps/anvil/.dev.env +++ /dev/null @@ -1,47 +0,0 @@ -# LDAP -LDAP_HOST = "ldap://auth.shef.ac.uk" -LDAP_PORT = 389 -LDAP_BASE = "ou=Users,dc=sheffield,dc=ac,dc=uk" - -# Google -GOOGLE_CLIENT_ID = "op://IT/Anvil OAuth2 Google/client id" -GOOGLE_CLIENT_SECRET = "op://IT/Anvil OAuth2 Google/client secret" -GOOGLE_CLIENT_CALLBACK_URL = "http://127.0.0.1:3000/v1/authentication/google/callback" -GOOGLE_SERVICE_ACCOUNT_EMAIL = "op://IT/Google Cloud/email" -GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY = "op://IT/Google Cloud/private key" - -# Discord -DISCORD_CLIENT_ID = "op://IT/Anvil OAuth2 Discord/client id" -DISCORD_CLIENT_SECRET = "op://IT/Anvil OAuth2 Discord/client secret" -DISCORD_CLIENT_CALLBACK_URL = "http://127.0.0.1:3000/v1/authentication/discord/callback" - -# AUTH -JWT_SECRET = "op://Private/JWT Secret Key/password" -ACCESS_TOKEN_EXPIRES_IN = "1h" -REFRESH_TOKEN_EXPIRES_IN = "7d" - -# Email -EMAIL_HOST = "op://Private/Mailtrap Email Account/SMTP/SMTP server" -EMAIL_PORT = "op://Private/Mailtrap Email Account/SMTP/port number" -EMAIL_USER = "op://Private/Mailtrap Email Account/SMTP/username" -EMAIL_PASS = "op://Private/Mailtrap Email Account/SMTP/password" -EMAIL_FROM = "iforge@sheffield.ac.uk" -EMAIL_SMTP_REQUIRE_TLS = true -EMAIL_RATE_MAX = 50 # Max number of emails per processor per EMAIL_RATE_DURATION -EMAIL_RATE_DURATION = 1000 # Milliseconds - -# Redis -REDIS_HOST = "127.0.0.1" -REDIS_PORT = 6379 -REDIS_DB = "0" - -# Training -TRAINING_URL = "https://training.iforge.sheffield.ac.uk" -TRAINING_SITE_USERNAME = "op://IT/Sheffield Login/username" -TRAINING_SITE_PASSWORD = "op://IT/Sheffield Login/password" - -# CDN -CDN_URL = "http://[::]:8080" - -# Front End -FRONT_END_URL = "http://127.0.0.1:8000" diff --git a/apps/anvil/.env b/apps/anvil/.env deleted file mode 100644 index 0cca0c9..0000000 --- a/apps/anvil/.env +++ /dev/null @@ -1,46 +0,0 @@ -# LDAP -LDAP_HOST="ldap://" -LDAP_PORT="" -LDAP_BASE="" - -# Google -GOOGLE_CLIENT_ID = "op://IT/Anvil OAuth2 Google/client id" -GOOGLE_CLIENT_SECRET = "op://IT/Anvil OAuth2 Google/client secret" -GOOGLE_CLIENT_CALLBACK_URL = "https://iforge.sheffield.ac.uk/api/v1/authentication/google/callback" -GOOGLE_SERVICE_ACCOUNT_EMAIL = "op://IT/Google Cloud/email" -GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY = "op://IT/Google Cloud/private key" - -# Discord -DISCORD_CLIENT_ID = "op://IT/Anvil OAuth2 Discord/client id" -DISCORD_CLIENT_SECRET = "op://IT/Anvil OAuth2 Discord/client secret" -DISCORD_CLIENT_CALLBACK_URL = "http://127.0.0.1:3000/api/v1/authentication/discord/redirect" - -# AUTH -JWT_SECRET = "" -ACCESS_TOKEN_EXPIRES_IN = "1h" -REFRESH_TOKEN_EXPIRES_IN = "7d" - -# Email -EMAIL_HOST = "op://IT/Anvil MailServer Credentials/server" -EMAIL_PORT = "op://IT/Anvil MailServer Credentials/port number" -EMAIL_FROM = "iforge@sheffield.ac.uk" -EMAIL_SMTP_REQUIRE_TLS = true -EMAIL_RATE_MAX = "50" # Max number of emails per processor per EMAIL_RATE_DURATION -EMAIL_RATE_DURATION = "1000" # Milliseconds - -# Redis -REDIS_HOST = "" -REDIS_PORT = "" -REDIS_DB = "" - -# Training -TRAINING_URL="https://training.iforge.sheffield.ac.uk" -TRAINING_SITE_USERNAME="op://IT/Sheffield Login/username" -TRAINING_SITE_PASSWORD="op://IT/Sheffield Login/password" - -# CDN -CDN_URL = "https://cdn.iforge.sheffield.ac.uk" -ANVIL_PORT=3000 - -# Front End -FRONT_END_URL = "https://iforge.sheffield.ac.uk" diff --git a/apps/anvil/Dockerfile b/apps/anvil/Dockerfile index bcc368b..5157df5 100644 --- a/apps/anvil/Dockerfile +++ b/apps/anvil/Dockerfile @@ -1,3 +1,4 @@ +FROM 1password/op:2 as op FROM node:20-alpine AS deps WORKDIR /app @@ -29,6 +30,17 @@ FROM node:20-slim AS anvil WORKDIR /app COPY --from=build /prod/forge ./ COPY --from=build /app/apps/anvil/dist ./dist + +# Create iforge user and set ownership +RUN useradd -m iforge +RUN chown -R iforge:iforge /app + +# Copy 1Password home directory from the op image +COPY --from=op --chown=iforge:iforge /home/opuser/ /home/iforge/ +COPY --from=op /usr/local/bin/op /usr/local/bin/op + +USER iforge + EXPOSE 3000 ENV NODE_PATH=/app/node_modules -CMD node dist/src/main.js +CMD ["/usr/local/bin/op", "run", "--env-file=/config/.env.production.tpl", "--", "node", "dist/src/main.js"] diff --git a/apps/anvil/package.json b/apps/anvil/package.json index c61f6c6..8164d3f 100644 --- a/apps/anvil/package.json +++ b/apps/anvil/package.json @@ -7,10 +7,10 @@ "private": true, "scripts": { "build": "nest build", - "start": "OP_ACCOUNT=iforge.1password.com op run --env-file=.dev.env -- nest start", - "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=.dev.env -- nest start --watch --preserveWatchOutput", - "start:debug": "OP_ACCOUNT=iforge.1password.com op run --env-file=.dev.env -- nest start --debug --watch --preserveWatchOutput", - "start:prod": "OP_ACCOUNT=iforge.1password.com op run --env-file=.env -- node dist/src/main", + "start": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start", + "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start --watch --preserveWatchOutput", + "start:debug": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start --debug --watch --preserveWatchOutput", + "start:prod": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.production.tpl -- node dist/src/main", "start:prod:docker": "node dist/src/main.js", "start:email": "op run --env-file=.dev.env -- email preview src/email/templates", "test": "jest", diff --git a/apps/forge/.env b/apps/forge/.env deleted file mode 100644 index 9c3f0ff..0000000 --- a/apps/forge/.env +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_URL = "http://127.0.0.1:3000/v1" -VITE_DISCORD_URL = "https://discord.gg/AkTDMga" -VITE_CDN_URL = "http://localhost:4000" diff --git a/apps/forge/.env.development b/apps/forge/.env.development new file mode 100644 index 0000000..73396f4 --- /dev/null +++ b/apps/forge/.env.development @@ -0,0 +1,3 @@ +VITE_API_URL="http://127.0.0.1:3000/v1" +VITE_DISCORD_URL="https://discord.gg/AkTDMga" +VITE_CDN_URL="http://localhost:4000" diff --git a/apps/forge/.env.production b/apps/forge/.env.production new file mode 100644 index 0000000..96d7e37 --- /dev/null +++ b/apps/forge/.env.production @@ -0,0 +1,3 @@ +VITE_API_URL="https://iforge.sheffield.ac.uk/api/v1" +VITE_DISCORD_URL="https://discord.gg/AkTDMga" +VITE_CDN_URL="https://cdn.iforge.sheffield.ac.uk" diff --git a/apps/mine/.gitignore b/apps/mine/.gitignore index 74e2402..b64679a 100644 --- a/apps/mine/.gitignore +++ b/apps/mine/.gitignore @@ -18,3 +18,4 @@ Thumbs.db !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +*.env \ No newline at end of file diff --git a/apps/mine/Dockerfile b/apps/mine/Dockerfile index da3829b..1443616 100644 --- a/apps/mine/Dockerfile +++ b/apps/mine/Dockerfile @@ -1,11 +1,23 @@ -FROM rust:1.77.2-alpine AS build +FROM 1password/op:2 as op +FROM rust:1-alpine AS build RUN apk add musl-dev COPY apps/mine /app/apps/mine WORKDIR /app/apps/mine RUN cargo build --release -FROM scratch as mine +FROM alpine:latest as mine WORKDIR /app -COPY --from=build /app/apps/mine/target/release/mine ./ +COPY --from=build /app/apps/mine/target/release/mine ./mine + +# Create iforge user and set ownership +RUN adduser -D iforge +RUN chown -R iforge:iforge /app + +# Copy 1Password home directory from the op image +COPY --from=op --chown=iforge:iforge /home/opuser/ /home/iforge/ +COPY --from=op /usr/local/bin/op /usr/local/bin/op + +USER iforge + EXPOSE 4000 -CMD ["./mine"] +CMD ["/usr/local/bin/op", "run", "--env-file=/config/.env.production.tpl", "--", "/app/mine"] diff --git a/apps/mine/package.json b/apps/mine/package.json index 3b2d1bc..3d0295c 100644 --- a/apps/mine/package.json +++ b/apps/mine/package.json @@ -9,7 +9,7 @@ "build": "cargo build", "format": "rustfmt src/*.rs --edition=2021", "start": "cargo run", - "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=.env -- cargo watch -x run", + "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/mine/.env.development.tpl -- cargo watch -x run", "lint": "cargo clippy", "lint:fix": "cargo clippy --fix" } diff --git a/config/README.md b/config/README.md index e567848..7190790 100644 --- a/config/README.md +++ b/config/README.md @@ -2,6 +2,11 @@ ## WIP Docker development config -` -config/secret/1password-credentials.json needed -` \ No newline at end of file +``` +# Gret this from 1Password +config/secret/op/1password-credentials.json needed + +# For DB cert use ../scripts/gen-cert.sh +config/secret/db/cert.pem needed +config/secret/db/key.pem needed +``` \ No newline at end of file diff --git a/config/anvil/.env.development.tpl b/config/anvil/.env.development.tpl new file mode 100644 index 0000000..fab9cd0 --- /dev/null +++ b/config/anvil/.env.development.tpl @@ -0,0 +1,51 @@ +# LDAP +LDAP_HOST="ldap://auth.shef.ac.uk" +LDAP_PORT=389 +LDAP_BASE="ou=Users,dc=sheffield,dc=ac,dc=uk" + +# Google +GOOGLE_CLIENT_ID="op://IT/Anvil OAuth2 Google/client id" +GOOGLE_CLIENT_SECRET="op://IT/Anvil OAuth2 Google/client secret" +GOOGLE_CLIENT_CALLBACK_URL="http://127.0.0.1:3000/v1/authentication/google/callback" +GOOGLE_SERVICE_ACCOUNT_EMAIL="op://IT/Google Cloud/email" +GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY="op://IT/Google Cloud/private key" + +# Discord +DISCORD_CLIENT_ID="op://IT/Anvil OAuth2 Discord/client id" +DISCORD_CLIENT_SECRET="op://IT/Anvil OAuth2 Discord/client secret" +DISCORD_CLIENT_CALLBACK_URL="http://127.0.0.1:3000/v1/authentication/discord/callback" + +# AUTH +JWT_SECRET="op://Private/JWT Secret Key/password" +ACCESS_TOKEN_EXPIRES_IN="1h" +REFRESH_TOKEN_EXPIRES_IN="7d" + +# Email +EMAIL_HOST="op://Private/Mailtrap Email Account/SMTP/SMTP server" +EMAIL_PORT="op://Private/Mailtrap Email Account/SMTP/port number" +EMAIL_USER="op://Private/Mailtrap Email Account/SMTP/username" +EMAIL_PASS="op://Private/Mailtrap Email Account/SMTP/password" +EMAIL_FROM="iforge@sheffield.ac.uk" +EMAIL_SMTP_REQUIRE_TLS=true +EMAIL_RATE_MAX=50 # Max number of emails per processor per EMAIL_RATE_DURATION +EMAIL_RATE_DURATION=1000 # Milliseconds + +# Redis +REDIS_HOST="127.0.0.1" +REDIS_PORT=6379 +REDIS_DB="0" + +# Training +TRAINING_URL="https://training.iforge.sheffield.ac.uk" +TRAINING_SITE_USERNAME="op://IT/Sheffield Login/username" +TRAINING_SITE_PASSWORD="op://IT/Sheffield Login/password" + +# CDN +CDN_URL="http://[::]:4000" + +# Front End +FRONT_END_URL="http://127.0.0.1:8000" + +# DB +EDGEDB_DSN="op://IT/Ignis EdgeDB Docker Prod/EDGEDB_DSN" +NODE_EXTRA_CA_CERTS="/ignis_certs/ignis_cert.pem" \ No newline at end of file diff --git a/config/anvil/.env.production.tpl b/config/anvil/.env.production.tpl new file mode 100644 index 0000000..0c6c4a1 --- /dev/null +++ b/config/anvil/.env.production.tpl @@ -0,0 +1,49 @@ +# LDAP +LDAP_HOST="op://IT/Active LDAP/Host/LDAP_HOST" +LDAP_PORT="op://IT/Active LDAP/Host/LDAP_PORT" +LDAP_BASE="ou=Users,dc=sheffield,dc=ac,dc=uk" +LDAP_USER="op://IT/Active LDAP/username" +LDAP_PASS="op://IT/Active LDAP/password" + +# Google +GOOGLE_CLIENT_ID="op://IT/Anvil OAuth2 Google/client id" +GOOGLE_CLIENT_SECRET="op://IT/Anvil OAuth2 Google/client secret" +GOOGLE_CLIENT_CALLBACK_URL="https://iforge.sheffield.ac.uk/api/v1/authentication/google/callback" +GOOGLE_SERVICE_ACCOUNT_EMAIL="op://IT/Google Cloud/email" +GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY="op://IT/Google Cloud/private key" + +# Discord +DISCORD_CLIENT_ID="op://IT/Anvil OAuth2 Discord/client id" +DISCORD_CLIENT_SECRET="op://IT/Anvil OAuth2 Discord/client secret" +DISCORD_CLIENT_CALLBACK_URL="http://127.0.0.1:3000/api/v1/authentication/discord/redirect" + +# AUTH +JWT_SECRET="op://IT/Anvil JWT Signing Key/credential" +ACCESS_TOKEN_EXPIRES_IN="1h" +REFRESH_TOKEN_EXPIRES_IN="7d" + +# Email +EMAIL_HOST="op://IT/Anvil MailServer Credentials/server" +EMAIL_PORT="op://IT/Anvil MailServer Credentials/port number" +EMAIL_USER="op://IT/Sheffield Login/email" +EMAIL_PASS="op://IT/Sheffield Login/App Password" +EMAIL_FROM="iforge@sheffield.ac.uk" +EMAIL_SMTP_REQUIRE_TLS=true +EMAIL_RATE_MAX="50" # Max number of emails per processor per EMAIL_RATE_DURATION +EMAIL_RATE_DURATION="1000" # Milliseconds + +# Redis +REDIS_HOST="127.0.0.1" +REDIS_PORT="6379" +REDIS_DB="0" + +# CDN +CDN_URL="https://cdn.iforge.sheffield.ac.uk" +ANVIL_PORT=3000 + +# Front End +FRONT_END_URL="https://iforge.sheffield.ac.uk" + +# DB +EDGEDB_DSN="op://IT/Ignis EdgeDB Docker Prod/EDGEDB_DSN" +NODE_EXTRA_CA_CERTS="/ignis_certs/ignis_cert.pem" \ No newline at end of file diff --git a/apps/mine/.env b/config/mine/.env.development.tpl similarity index 100% rename from apps/mine/.env rename to config/mine/.env.development.tpl diff --git a/config/mine/.env.production.tpl b/config/mine/.env.production.tpl new file mode 100644 index 0000000..44323b3 --- /dev/null +++ b/config/mine/.env.production.tpl @@ -0,0 +1,3 @@ +JWT_SECRET="op://IT/Anvil JWT Signing Key/credential" +ALLOWED_TO_UPLOAD="791df7d2-af1d-11ee-b204-671fc9e18c19,791e9ab6-af1d-11ee-b204-079c1aebb8e8" +MINE_PORT="4000" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 667b3c8..7386781 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,34 @@ version: "3.8" services: + proxy: + image: traefik:latest + container_name: proxy + ports: + - "8000:80" + networks: + - internal + - external + db: - image: edgedb/edgedb:5.0-beta.3 + image: edgedb/edgedb:5.1 env_file: - .env - environment: - EDGEDB_SERVER_SECURITY: insecure_dev_mode - EDGEDB_SERVER_ADMIN_UI: enabled restart: unless-stopped container_name: edgedb volumes: - db_data:/var/lib/edgedb/data - ./apps/anvil/dbschema:/dbschema + - ./config/secret/db:/ignis_certs:ro ports: - "5656:5656" networks: - internal + healthcheck: + test: curl --fail http://db:5656/server/status/ready || exit 1 + interval: 8s + retries: 10 + start_period: 6s + timeout: 10s op-connect-api: image: 1password/connect-api:latest @@ -24,7 +37,7 @@ services: ports: - "8080:8080" volumes: - - "./config/secret/1password-credentials.json:/home/opuser/.op/1password-credentials.json" + - "./config/secret/op/1password-credentials.json:/home/opuser/.op/1password-credentials.json" - "1p_data:/home/opuser/.op/data" networks: - internal @@ -34,48 +47,89 @@ services: container_name: op-sync restart: always volumes: - - "./config/secret/1password-credentials.json:/home/opuser/.op/1password-credentials.json" + - "./config/secret/op/1password-credentials.json:/home/opuser/.op/1password-credentials.json" - "1p_data:/home/opuser/.op/data" networks: - internal cache: - image: redis:latest - container_name: valkey + image: valkey/valkey:unstable + container_name: cache restart: unless-stopped ports: - "6379:6379" networks: - internal + volumes: + - cache_data:/data anvil: build: dockerfile: ./apps/anvil/Dockerfile context: . container_name: anvil + restart: unless-stopped + user: iforge + env_file: + - .env ports: - "3000:3000" - + volumes: + - "./config/anvil:/config" + - ./config/secret/db:/ignis_certs:ro + networks: + - internal + depends_on: + db: + condition: service_healthy + cache: + condition: service_started + op-connect-api: + condition: service_started + op-connect-sync: + condition: service_started + mine: build: dockerfile: ./apps/mine/Dockerfile context: . container_name: mine + restart: unless-stopped + user: iforge + env_file: + - .env ports: - "4000:4000" + volumes: + - "./config/mine:/config" + networks: + - internal + depends_on: + db: + condition: service_healthy + cache: + condition: service_started + op-connect-api: + condition: service_started + op-connect-sync: + condition: service_started forge: build: dockerfile: ./apps/forge/Dockerfile context: . container_name: forge + restart: unless-stopped ports: - "80:80" + networks: + - internal networks: internal: + external: volumes: db_data: - redis_data: + cache_data: 1p_data: \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb136b8..f08a263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,7 +111,7 @@ importers: specifier: ^9.0.2 version: 9.0.2 jsx-email: - specifier: ^1.10.12 + specifier: 1.10.12 version: 1.10.12(@jsx-email/app-preview@1.2.5(@types/node@20.12.7)(@types/react-dom@18.2.25)(@types/react@18.2.79)(react@18.2.0)(rollup@4.14.3)(terser@5.30.3)(ts-node@10.9.2(@swc/core@1.4.16(@swc/helpers@0.5.5))(@types/node@20.12.7)(typescript@5.4.5)))(@types/node@20.12.7)(@types/react@18.2.79)(react@18.2.0)(rollup@4.14.3)(terser@5.30.3) ldapjs: specifier: ^3.0.7 @@ -3977,9 +3977,6 @@ packages: resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} engines: {node: '>= 6.0.0'} - call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} - call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -4530,10 +4527,6 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -5212,9 +5205,6 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -7576,10 +7566,6 @@ packages: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} - set-function-length@1.2.0: - resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} - engines: {node: '>= 0.4'} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -12771,12 +12757,6 @@ snapshots: mime-types: 2.1.35 ylru: 1.4.0 - call-bind@1.0.5: - dependencies: - function-bind: 1.1.2 - get-intrinsic: 1.2.2 - set-function-length: 1.2.0 - call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -13343,12 +13323,6 @@ snapshots: dependencies: clone: 1.0.4 - define-data-property@1.1.1: - dependencies: - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -13359,8 +13333,8 @@ snapshots: define-properties@1.2.1: dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 delaunator@5.0.1: @@ -14175,10 +14149,6 @@ snapshots: has-own-prop@2.0.0: {} - has-property-descriptors@1.0.1: - dependencies: - get-intrinsic: 1.2.2 - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.0 @@ -16291,7 +16261,7 @@ snapshots: object.assign@4.1.5: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -17367,14 +17337,6 @@ snapshots: transitivePeerDependencies: - supports-color - set-function-length@1.2.0: - dependencies: - define-data-property: 1.1.1 - function-bind: 1.1.2 - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 diff --git a/scripts/gen-cert.sh b/scripts/gen-cert.sh new file mode 100755 index 0000000..ea02c09 --- /dev/null +++ b/scripts/gen-cert.sh @@ -0,0 +1,2 @@ +openssl req -x509 -newkey rsa:4096 -keyout ./config/secret/db/ignis_key.pem -out ./config/secret/db/ignis_cert.pem -days 3650 -nodes \ +-subj "/C=GB/ST=South Yorkshire/L=Sheffield/O=iForge Makerspace/OU=IT Team/CN=db" From 41968656028a68a1d89b821186389eeb758dddac Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Thu, 25 Apr 2024 23:04:25 +0100 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20now=20we're=20cooking=20=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.tpl | 6 +++--- README.md | 18 ++++++++++++++++++ docker-compose.yml | 2 +- package.json | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.env.tpl b/.env.tpl index 841db13..477f07f 100644 --- a/.env.tpl +++ b/.env.tpl @@ -1,8 +1,8 @@ -OP_CONNECT_TOKEN="op://IT/jcjmzwjh6sjrr2uybko6rrizwu/type" +OP_CONNECT_TOKEN="op://IT/jcjmzwjh6sjrr2uybko6rrizwu/credential" OP_CONNECT_HOST=http://op-api:8080 EDGEDB_SERVER_ADMIN_UI=enabled EDGEDB_SERVER_TLS_CERT_MODE=require_file -EDGEDB_SERVER_TLS_KEY=/ignis_certs/key.pem -EDGEDB_SERVER_TLS_CERT=/ignis_certs/cert.pem +EDGEDB_SERVER_TLS_KEY_FILE=/ignis_certs/ignis_key.pem +EDGEDB_SERVER_TLS_CERT_FILE=/ignis_certs/ignis_cert.pem EDGEDB_SERVER_USER="op://IT/Ignis EdgeDB Docker Prod/username" EDGEDB_SERVER_PASSWORD="op://IT/Ignis EdgeDB Docker Prod/password" \ No newline at end of file diff --git a/README.md b/README.md index 698fcfc..86beb4b 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,21 @@ DESCRIPTION COMING SOON ## Manual Install Each [app](/apps) has installation instructions in its README. + +## Docker Compose + +``` +# First create the secret dirs +mkdir -p ./config/secret/{db,op} + +# Then create the cert +./scripts/gen-cert.sh + +# Get the 1password-credentials.json and place it in ./config/secret/op + +# Next gen the docker compose env from the template +pnpm env:gen + +# Docker compose up +docker compose up -d +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7386781..6b2acd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - external db: - image: edgedb/edgedb:5.1 + image: edgedb/edgedb:5.2 env_file: - .env restart: unless-stopped diff --git a/package.json b/package.json index 7802a31..b933cdb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "workspaces": ["packages/*", "apps/*", "tooling/*"], "scripts": { "dev": "turbo run dev", + "env:gen": "OP_ACCOUNT=iforge.1password.com op inject -f -i .env.tpl -o .env", "ui:add": "pnpm --filter ui ui:add", "ui:dev": "pnpm --filter ui ui:dev", "lint": "turbo lint --continue", From f68a4202dd973005ec43c9bd082be7c3b9283c50 Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Fri, 26 Apr 2024 16:20:45 +0100 Subject: [PATCH 3/6] feat: AD LDAP (Timeouts in responses tho) --- .devcontainer/Dockerfile | 33 --- apps/anvil/package.json | 8 +- apps/anvil/src/app.module.ts | 8 +- .../auth/interfaces/ldap-user.interface.ts | 15 +- apps/anvil/src/ldap/ldap.class.ts | 269 ++++++++++++++++++ apps/anvil/src/ldap/ldap.module.ts | 32 ++- apps/anvil/src/ldap/ldap.service.ts | 204 +++---------- apps/anvil/src/sign-in/sign-in.service.ts | 13 +- apps/anvil/src/users/users.service.ts | 25 +- apps/mine/package.json | 2 +- config/anvil/.env.development.tpl | 14 +- config/proxy/traefik.yml | 29 ++ docker-compose.yml | 47 +-- 13 files changed, 428 insertions(+), 271 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 apps/anvil/src/ldap/ldap.class.ts create mode 100644 config/proxy/traefik.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 97e9005..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Use the official Node.js 20 slim image from Docker Hub as the base image -FROM node:20-slim AS base - -# Set the environment variable for PNPM home and add it to the PATH -ENV PNPM_HOME="/pnpm" \ - PATH="$PNPM_HOME:$PATH" - -# Install corepack to manage Node.js package managers and install Rust and other essential tools -RUN corepack enable && \ - apt-get update && \ - apt-get install -y curl git build-essential && \ - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - . $HOME/.cargo/env && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Add Rust to the system PATH -ENV PATH="/root/.cargo/bin:${PATH}" - -# Copy the local code to the container -COPY . /code - -# Set the working directory to the app directory -WORKDIR /code - -# Install dependencies using pnpm -RUN pnpm install - -# Expose ports (3000, 8000, 4000) used by your application -EXPOSE 3000 8000 4000 - -# Default command to run when starting the container -CMD ["pnpm", "run", "dev"] diff --git a/apps/anvil/package.json b/apps/anvil/package.json index 8164d3f..c3d67c8 100644 --- a/apps/anvil/package.json +++ b/apps/anvil/package.json @@ -7,10 +7,10 @@ "private": true, "scripts": { "build": "nest build", - "start": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start", - "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start --watch --preserveWatchOutput", - "start:debug": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.development.tpl -- nest start --debug --watch --preserveWatchOutput", - "start:prod": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/anvil/.env.production.tpl -- node dist/src/main", + "start": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/anvil/.env.development.tpl -- nest start", + "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/anvil/.env.development.tpl -- nest start --watch --preserveWatchOutput", + "start:debug": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/anvil/.env.development.tpl -- nest start --debug --watch --preserveWatchOutput", + "start:prod": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/anvil/.env.production.tpl -- node dist/src/main", "start:prod:docker": "node dist/src/main.js", "start:email": "op run --env-file=.dev.env -- email preview src/email/templates", "test": "jest", diff --git a/apps/anvil/src/app.module.ts b/apps/anvil/src/app.module.ts index 3a39f06..fdd8143 100644 --- a/apps/anvil/src/app.module.ts +++ b/apps/anvil/src/app.module.ts @@ -26,14 +26,13 @@ import { UsersModule } from "./users/users.module"; @Module({ imports: [ + ConfigModule.forRoot({ + envFilePath: ".env.production", + }), EdgeDBModule, UsersModule, AuthenticationModule, - LdapModule, ScheduleModule.forRoot(), - ConfigModule.forRoot({ - envFilePath: ".env.production", - }), ThrottlerModule.forRoot([ { name: "short", @@ -47,6 +46,7 @@ import { UsersModule } from "./users/users.module"; SignInModule, BullModule.registerQueue({ name: "email" }), RootModule, + LdapModule, ], providers: [ { diff --git a/apps/anvil/src/auth/interfaces/ldap-user.interface.ts b/apps/anvil/src/auth/interfaces/ldap-user.interface.ts index 6d7d135..d73ff42 100644 --- a/apps/anvil/src/auth/interfaces/ldap-user.interface.ts +++ b/apps/anvil/src/auth/interfaces/ldap-user.interface.ts @@ -1,18 +1,15 @@ export interface LdapUser { dn: string; - /** User's id */ - uid: string; cn: string; + /** User's organisational unit */ + ou: string; /** User's surname */ sn: string; + /** User's first name */ + givenName: string; initials: string; - /** User's organisational unit */ - ou: string; /** User's email */ mail: string; - /** User's first name */ - givenName: string; - shefReportingFaculty: string; - userPrincipalName: string; - "mS-DS-ConsistencyGuid"?: string; + uid: string; + shefLibraryNumber: string; } diff --git a/apps/anvil/src/ldap/ldap.class.ts b/apps/anvil/src/ldap/ldap.class.ts new file mode 100644 index 0000000..a905016 --- /dev/null +++ b/apps/anvil/src/ldap/ldap.class.ts @@ -0,0 +1,269 @@ +import * as ldap from "ldapjs"; +import { EventEmitter } from "events"; +import { Logger, OnModuleInit } from "@nestjs/common"; + +export interface LdapWrapperOptions { + hostName: string; + port?: number; + connectTimeout?: number; + receiveTimeout?: number; + user?: string; + password?: string; + useSSL?: boolean; + searchBase?: string; + defaultAttributes?: string[]; +} + +export class LdapClass extends EventEmitter implements OnModuleInit { + private readonly logger: Logger; + private client: ldap.Client | null = null; + private connected = false; + private retryCount = 0; + private readonly maxRetries = 3; + private readonly retryDelay = 2000; // 2 seconds + private readonly hostName: string; + private readonly port: number; + private readonly connectTimeout: number; + private readonly receiveTimeout: number; + private readonly user: string; + private readonly password: string; + private readonly useSSL: boolean; + private readonly searchBase: string; + private readonly defaultAttributes: string[]; + + constructor(options: LdapWrapperOptions) { + super(); + this.logger = new Logger(LdapClass.name); + this.hostName = options.hostName; + this.port = options.port!; + this.connectTimeout = options.connectTimeout!; + this.receiveTimeout = options.receiveTimeout!; + this.user = options.user!; + this.password = options.password!; + this.useSSL = !!options.useSSL; + this.searchBase = options.searchBase!; + this.defaultAttributes = options.defaultAttributes!; + this.connect(); + } + + async onModuleInit() { + this.connect(); + } + + private connect(): void { + if (this.connected || this.retryCount >= this.maxRetries) { + return; + } + + const url = `${this.useSSL ? "ldaps" : "ldap"}://${this.hostName}:${this.port}`; + this.logger.log(`Connecting to LDAP server: ${url}`, LdapClass.name); + + this.client = ldap.createClient({ + url, + connectTimeout: this.connectTimeout, + timeout: this.receiveTimeout, + bindDN: this.user, + bindCredentials: this.password, + tlsOptions: { + rejectUnauthorized: true, + followRedirects: true, + }, + reconnect: true, + }); + + this.client.on("connect", () => { + this.connected = true; + this.retryCount = 0; + this.logger.log("Connected to LDAP server", LdapClass.name); + this.emit("connect"); + }); + + this.client.on("error", (err) => { + this.connected = false; + this.logger.error(`LDAP connection error: ${err.message}`, LdapClass.name); + this.emit("error", err); + this.reconnect(); + }); + + this.client.on("end", () => { + this.connected = false; + this.logger.log("LDAP connection ended", LdapClass.name); + this.emit("end"); + this.reconnect(); + }); + } + + private reconnect(): void { + if (this.retryCount < this.maxRetries) { + this.retryCount++; + this.logger.log(`Attempting to reconnect to LDAP server (retry ${this.retryCount})`, LdapClass.name); + setTimeout(() => this.connect(), this.retryDelay); + } else { + this.logger.error("Maximum retry count reached. Unable to connect to LDAP server.", LdapClass.name); + this.emit("error", new Error("Maximum retry count reached. Unable to connect to LDAP server.")); + } + } + + async bind(dn: string, password: string): Promise { + if (!this.client) { + throw new Error("LDAP client is not initialized."); + } + + this.logger.log(`Binding to LDAP server with DN: ${dn}`, LdapClass.name); + + return new Promise((resolve, reject) => { + this.client!.bind(dn, password, (err) => { + if (err) { + this.logger.error(`LDAP bind error: ${err.message}`, LdapClass.name); + reject(err); + } else { + this.logger.log("LDAP bind successful", LdapClass.name); + resolve(); + } + }); + }); + } + + async search(base: string, options: ldap.SearchOptions): Promise { + if (!this.client) { + throw new Error("LDAP client is not initialized."); + } + + this.logger.log(`Searching LDAP with base: ${base} and filter: ${options.filter}`, LdapClass.name); + + return new Promise((resolve, reject) => { + const results: ldap.SearchEntry[] = []; + + const searchOptions: ldap.SearchOptions = { + ...options, + }; + + const searchCallback = async (err: ldap.Error | null, res: ldap.SearchCallbackResponse): Promise => { + if (err) { + this.logger.error(`LDAP search error: ${err.message}`, LdapClass.name); + reject(err); + return; + } + + res.on("searchEntry", (entry) => { + results.push(entry); + }); + + res.on("error", async (err: ldap.Error) => { + this.logger.error(`LDAP search error: ${err.message}`, LdapClass.name); + reject(err); + }); + + res.on("end", (result) => { + if (result?.status === 0) { + this.logger.log(`LDAP search completed. Found ${results.length} entries.`, LdapClass.name); + resolve(results); + } else { + this.logger.error(`LDAP search ended with status: ${result?.status}`, LdapClass.name); + reject(new Error(`LDAP search ended with status: ${result?.status}`)); + } + }); + }; + + this.client!.search(base, searchOptions, searchCallback); + }); + } + + async getDN(searchFilter: string, attributes: string[]): Promise { + this.logger.log(`Getting DN with filter: ${searchFilter}`, LdapClass.name); + + const searchOptions: ldap.SearchOptions = { + scope: "sub", + filter: searchFilter, + attributes: attributes, + }; + + const results = await this.search(this.searchBase, searchOptions); + if (results.length === 0) { + this.logger.warn("No matching entries found", LdapClass.name); + throw new Error("No matching entries found."); + } + + const dn = results[0].dn.toString(); + this.logger.log(`Found DN: ${dn}`, LdapClass.name); + return dn; + } + + async authenticate(username: string, password: string): Promise { + this.logger.log(`Authenticating user: ${username}`, LdapClass.name); + + const searchFilter = `(uid=${username})`; + const dn = await this.getDN(searchFilter, ["dn"]); + + try { + await this.bind(dn, password); + this.logger.log(`Authentication successful for user: ${username}`, LdapClass.name); + return true; + } catch (err) { + this.logger.error(`Authentication failed for user: ${username}`, LdapClass.name); + return false; + } + } + + async lookup( + searchFilter: string, + attributes: string[] = this.defaultAttributes, + returnAsString = false, + ): Promise | null> { + this.logger.log(`Looking up with filter: ${searchFilter}`, LdapClass.name); + + const searchOptions: ldap.SearchOptions = { + scope: "sub", + filter: searchFilter, + attributes: attributes, + }; + + const results = await this.search(this.searchBase, searchOptions); + if (results.length === 0) { + this.logger.warn("No matching entries found", LdapClass.name); + return null; + } + + const result = results[0]; + const attributesDict: Record = {}; + + for (const attribute of attributes) { + const values = result.attributes.filter((attr) => attr.type === attribute).map((attr) => attr.values[0]); + if (values.length > 0) { + attributesDict[attribute] = returnAsString ? values.join(",") : values; + } + } + + this.logger.log(`Lookup completed. Found attributes: ${JSON.stringify(attributesDict)}`, LdapClass.name); + return attributesDict; + } + async lookupByUsername( + username: string, + attributes: string[] = this.defaultAttributes, + ): Promise | null> { + this.logger.log(`Looking up user by username: ${username}`, LdapClass.name); + + const searchFilter = `(&(objectClass=person)(uid=${username}))`; + return this.lookup(searchFilter, attributes, true); + } + + async lookupByEmail( + email: string, + attributes: string[] = this.defaultAttributes, + ): Promise | null> { + this.logger.log(`Looking up user by email: ${email}`, LdapClass.name); + + const searchFilter = `(&(objectClass=person)(mail=${email}))`; + return this.lookup(searchFilter, attributes, true); + } + + async lookupByUcardNumber( + ucardNumber: string, + attributes: string[] = this.defaultAttributes, + ): Promise | null> { + this.logger.log(`Looking up user by ucard number: ${ucardNumber}`, LdapClass.name); + + const searchFilter = `(&(objectClass=person)(sheflibrarynumber=${ucardNumber}))`; + return this.lookup(searchFilter, attributes, true); + } +} diff --git a/apps/anvil/src/ldap/ldap.module.ts b/apps/anvil/src/ldap/ldap.module.ts index bb50a59..4aff7cd 100644 --- a/apps/anvil/src/ldap/ldap.module.ts +++ b/apps/anvil/src/ldap/ldap.module.ts @@ -1,9 +1,35 @@ import { Module } from "@nestjs/common"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import { LdapService } from "./ldap.service"; +import { LdapClass } from "./ldap.class"; @Module({ - imports: [], - providers: [LdapService], - exports: [LdapService], + imports: [ConfigModule], + providers: [ + LdapService, + { + provide: LdapClass, + useFactory: (configService: ConfigService) => { + const defaultAttributes = configService + .get("LDAP_DEFAULT_ATTRIBUTES", "givenName,sn,mail,uid,shefLibraryNumber") + .split(","); + + return new LdapClass({ + hostName: configService.get("LDAP_HOST")!, + port: configService.get("LDAP_PORT"), + user: configService.get("LDAP_USER"), + password: configService.get("LDAP_PASS"), + searchBase: configService.get("LDAP_BASE"), + connectTimeout: 5_000, + receiveTimeout: 10_000, + useSSL: configService.get("LDAP_SSL", true), + defaultAttributes: defaultAttributes, + }); + }, + + inject: [ConfigService], + }, + ], + exports: [LdapService, LdapClass], }) export class LdapModule {} diff --git a/apps/anvil/src/ldap/ldap.service.ts b/apps/anvil/src/ldap/ldap.service.ts index abd1259..a2a296e 100644 --- a/apps/anvil/src/ldap/ldap.service.ts +++ b/apps/anvil/src/ldap/ldap.service.ts @@ -1,179 +1,69 @@ import { LdapUser } from "@/auth/interfaces/ldap-user.interface"; import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import * as ldap from "ldapjs"; +import { LdapClass } from "@/ldap/ldap.class"; @Injectable() -export class LdapService implements OnModuleInit { +export class LdapService { private readonly logger = new Logger(LdapService.name); - private client: ldap.Client | null; - constructor() { - this.client = null; - } - - async onModuleInit() { - await this.connect(); - } - - async connect() { - if (!this.client) { - this.logger.debug("Attempting to create an LDAP client..."); - - this.client = ldap.createClient({ - url: `${process.env.LDAP_HOST}:${process.env.LDAP_PORT}`, - connectTimeout: 2_000, - timeout: 5_000, - }); - - this.client.on("connect", () => { - this.logger.log("Successfully connected to LDAP server."); - }); + constructor(private readonly ldapClass: LdapClass) {} - this.client.on("error", (err) => { - this.logger.error(`LDAP connection error: ${err.message}`); - this.client = null; // Reset the client to reconnect later. - }); - } else { - this.logger.warn("LDAP client already exists. Reusing existing connection."); - } - } - - private async bind(dn: string, password: string): Promise { - await this.ensureConnected(); - - this.logger.debug(`Binding to DN: ${dn}`); - return new Promise((resolve, reject) => { - this.client!.bind(dn, password, (err: any) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + async authenticate(username: string, password: string): Promise { + this.logger.log(`Authenticating user: ${username}`); + const result = await this.ldapClass.authenticate(username, password); + this.logger.log(`Authentication result for user ${username}: ${result}`); + return result; } - async ensureConnected() { - if (!this.client?.connected) { - this.logger.debug("Re-establishing LDAP client connection..."); - await this.connect(); + async findUserByUsername(username: string): Promise { + this.logger.log(`Looking up user by username: ${username}`); + const result = await this.ldapClass.lookupByUsername(username); + if (result) { + const user = this.formatLdapUser(result); + this.logger.log(`Found user by username ${username}:`, user); + return user; } + this.logger.log(`User not found by username: ${username}`); + return null; } - async resetConnection() { - if (this.client) { - this.client.unbind((err) => { - if (err) { - this.logger.error(`Error unbinding LDAP client: ${err.message}`); - } - this.client = null; - this.logger.debug("LDAP client unbound and reset."); - }); + async findUserByEmail(email: string): Promise { + this.logger.log(`Looking up user by email: ${email}`); + const result = await this.ldapClass.lookupByEmail(email); + if (result) { + const user = this.formatLdapUser(result); + this.logger.log(`Found user by email ${email}:`, user); + return user; } - await this.connect(); - } - - private async search(base: string, options: ldap.SearchOptions): Promise { - this.logger.debug(`Performing search with base: ${base} and filter: ${options.filter}`); - await this.resetConnection(); // Reset connection before each search - return new Promise((resolve, reject) => { - this.client!.search(base, options, (err, res) => { - if (err) { - this.logger.error(`Search initiation error: ${err.message}`); - reject(err); - return; - } - - const results: ldap.SearchEntry[] = []; - res.on("searchEntry", (entry) => { - this.logger.debug(`Received entry: ${entry.dn.toString()}`); - results.push(entry); - }); - res.on("end", () => { - this.logger.debug(`Search completed, found ${results.length} entries.`); - resolve(results); - }); - res.on("error", (error) => { - this.logger.error(`Search stream error: ${error.message}`); - reject(error); - }); - // Timeout handler to avoid hanging - res.on("timeout", () => { - this.logger.error("Search request timed out."); - reject(new Error("LDAP search request timed out")); - }); - }); - }); + this.logger.log(`User not found by email: ${email}`); + return null; } - async authenticate(uid: string, password: string): Promise { - this.logger.debug(`Authenticating user with UID: ${uid}`); - await this.ensureConnected(); - const searchBase = process.env.LDAP_BASE!; - const options: ldap.SearchOptions = { - filter: `(&(objectclass=person)(uid=${uid}))`, - scope: "sub", - attributes: ["dn"], - }; - - try { - const results = await this.search(searchBase, options); - if (results.length === 0) { - return false; - } - - const userDn = results[0].dn.toString(); - await this.bind(userDn, password); - return true; - } catch (error) { - throw error; // or handle it more gracefully + async findUserByUcardNumber(ucardNumber: string): Promise { + this.logger.log(`Looking up user by ucard number: ${ucardNumber}`); + const result = await this.ldapClass.lookupByUcardNumber(ucardNumber); + if (result) { + const user = this.formatLdapUser(result); + this.logger.log(`Found user by ucard number ${ucardNumber}:`, user); + return user; } + this.logger.log(`User not found by ucard number: ${ucardNumber}`); + return null; } - // query params can go by mail, uid - async lookup(searchFilter: string, attributes: string[] | undefined = undefined): Promise { - this.logger.debug(`Looking up entries with filter: ${searchFilter}`); - const searchBase = process.env.LDAP_BASE!; - const options: ldap.SearchOptions = { - filter: searchFilter, - scope: "sub", - attributes: attributes, - timeLimit: 10, + private formatLdapUser(result: Record): LdapUser { + this.logger.log(`Formatting response ${JSON.stringify(result)}`); + return { + dn: result.dn as string, + uid: result.uid as string, + cn: result.cn as string, + sn: result.sn as string, + initials: result.initials as string, + ou: result.ou as string, + mail: result.mail as string, + givenName: result.givenName as string, + shefLibraryNumber: result.shefLibraryNumber as string, }; - - try { - const results = await this.search(searchBase, options); - return results.length > 0 ? results : null; - } catch (error) { - throw error; - } - } - - async lookupUsername(username: string): Promise { - await this.ensureConnected(); - this.logger.debug(`Starting LDAP search for username: ${username}`); - const users = await this.lookup(`(&(objectclass=person)(uid=${username}))`); - this.logger.debug(`LDAP search completed for username: ${username}`); - if (!users) { - return null; - } - return Object.fromEntries( - users[0].attributes.map((attr) => { - return [attr.type, attr.values[0]]; - }), - ) as any; - } - - async lookupEmail(email: string): Promise { - await this.ensureConnected(); - const users = await this.lookup(`(&(objectclass=person)(mail=${email}))`); - if (!users) { - return null; - } - return Object.fromEntries( - users[0].attributes.map((attr) => { - return [attr.type, attr.values[0]]; - }), - ) as any; } } diff --git a/apps/anvil/src/sign-in/sign-in.service.ts b/apps/anvil/src/sign-in/sign-in.service.ts index 752eb70..680a7c3 100644 --- a/apps/anvil/src/sign-in/sign-in.service.ts +++ b/apps/anvil/src/sign-in/sign-in.service.ts @@ -159,7 +159,7 @@ export class SignInService implements OnModuleInit { } } else { // no user registered, fetch from ldap - const ldapUser = await this.ldapService.lookupUsername(register_user.username); + const ldapUser = await this.ldapService.findUserByUsername(register_user.username); if (!ldapUser) { throw new NotFoundException({ message: `User with username ${register_user.username} couldn't be found. Perhaps you made a typo? (it should look like fe6if)`, @@ -169,17 +169,6 @@ export class SignInService implements OnModuleInit { user = await this.userService.insertLdapUser(ldapUser); } - await this.dbService.query( - e.update(e.users.User, () => ({ - filter_single: { - username: register_user.username, - }, - set: { - ucard_number: register_user.ucard_number, - }, - })), - ); - await this.dbService.query( e.insert(e.sign_in.UserRegistration, { location: castLocation(location), diff --git a/apps/anvil/src/users/users.service.ts b/apps/anvil/src/users/users.service.ts index 5138aa2..c198e6d 100644 --- a/apps/anvil/src/users/users.service.ts +++ b/apps/anvil/src/users/users.service.ts @@ -207,7 +207,7 @@ export class UsersService { async createOrFindUser(googleUser: GoogleUser): Promise { let user = await this.findByEmail(removeDomain(googleUser.email)); if (!user) { - const ldapUser = await this.ldapService.lookupEmail(googleUser.email); + const ldapUser = await this.ldapService.findUserByEmail(googleUser.email); if (!ldapUser) { throw new Error("Failed to fetch a matching user on LDAP"); } @@ -230,21 +230,6 @@ export class UsersService { } async insertLdapUser(ldapUser: LdapUser, profile_picture: string | undefined = undefined): Promise { - const minUcardNumber: any = e.min( - // TODO move back into create call when edgedb/edgedb-js#835 is resolved - e.set( - e.assert_single( - e.select(e.users.User, (user) => ({ - order_by: { - expression: user.ucard_number, - direction: e.ASC, - }, - limit: 1, - })), - ).ucard_number, - -1, - ), - ); return await this.create({ username: ldapUser.uid, email: removeDomain(ldapUser.mail), @@ -252,13 +237,7 @@ export class UsersService { last_name: ldapUser.sn, organisational_unit: ldapUser.ou, roles: e.select(e.auth.Role, () => ({ filter_single: { name: "User" } })), - ucard_number: e.op( - // atomically decrement this field for new inserts where we don't have it - // to preserves the uniqueness. - minUcardNumber, - "-", - 1, - ), + ucard_number: ldapUser.shefLibraryNumber.slice(3), profile_picture, }); } diff --git a/apps/mine/package.json b/apps/mine/package.json index 3d0295c..157cdb0 100644 --- a/apps/mine/package.json +++ b/apps/mine/package.json @@ -9,7 +9,7 @@ "build": "cargo build", "format": "rustfmt src/*.rs --edition=2021", "start": "cargo run", - "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/env/mine/.env.development.tpl -- cargo watch -x run", + "dev": "OP_ACCOUNT=iforge.1password.com op run --env-file=../../config/mine/.env.development.tpl -- cargo watch -x run", "lint": "cargo clippy", "lint:fix": "cargo clippy --fix" } diff --git a/config/anvil/.env.development.tpl b/config/anvil/.env.development.tpl index fab9cd0..0068299 100644 --- a/config/anvil/.env.development.tpl +++ b/config/anvil/.env.development.tpl @@ -1,7 +1,11 @@ # LDAP -LDAP_HOST="ldap://auth.shef.ac.uk" -LDAP_PORT=389 -LDAP_BASE="ou=Users,dc=sheffield,dc=ac,dc=uk" +LDAP_HOST="op://IT/Active LDAP/Host/LDAP_HOST" +LDAP_PORT="op://IT/Active LDAP/Host/LDAP_PORT" +LDAP_BASE="dc=shefuniad,dc=shef,dc=ac,dc=uk" +LDAP_USER="op://IT/Active LDAP/username" +LDAP_PASS="op://IT/Active LDAP/password" +LDAP_DEFAULT_ATTRIBUTES="givenName,sn,mail,uid,shefLibraryNumber,ou" +LDAP_SSL=true # Google GOOGLE_CLIENT_ID="op://IT/Anvil OAuth2 Google/client id" @@ -45,7 +49,3 @@ CDN_URL="http://[::]:4000" # Front End FRONT_END_URL="http://127.0.0.1:8000" - -# DB -EDGEDB_DSN="op://IT/Ignis EdgeDB Docker Prod/EDGEDB_DSN" -NODE_EXTRA_CA_CERTS="/ignis_certs/ignis_cert.pem" \ No newline at end of file diff --git a/config/proxy/traefik.yml b/config/proxy/traefik.yml new file mode 100644 index 0000000..667ed25 --- /dev/null +++ b/config/proxy/traefik.yml @@ -0,0 +1,29 @@ +api: + dashboard: true + +entryPoints: + web: + address: :80 + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: :443 + +certificatesResolvers: + mycert: + acme: + email: + storage: acme.json + httpChallenge: + entryPoint: web + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + +log: + level: INFO \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6b2acd6..bae4e2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,23 +4,25 @@ services: image: traefik:latest container_name: proxy ports: - - "8000:80" + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./config/proxy/traefik.yml:/traefik.yml:ro networks: + - web - internal - - external db: image: edgedb/edgedb:5.2 env_file: - .env restart: unless-stopped - container_name: edgedb + container_name: db volumes: - db_data:/var/lib/edgedb/data - - ./apps/anvil/dbschema:/dbschema + - ./apps/anvil/dbschema:/dbschema:ro - ./config/secret/db:/ignis_certs:ro - ports: - - "5656:5656" networks: - internal healthcheck: @@ -56,8 +58,6 @@ services: image: valkey/valkey:unstable container_name: cache restart: unless-stopped - ports: - - "6379:6379" networks: - internal volumes: @@ -72,13 +72,12 @@ services: user: iforge env_file: - .env - ports: - - "3000:3000" volumes: - - "./config/anvil:/config" + - "./config/anvil:/config:ro" - ./config/secret/db:/ignis_certs:ro networks: - internal + - data depends_on: db: condition: service_healthy @@ -88,7 +87,12 @@ services: condition: service_started op-connect-sync: condition: service_started - + labels: + - "traefik.enable=true" + - "traefik.http.routers.anvil.rule=Host(`api.iforge.sheffield.ac.uk`) || Host(`anvil.localhost`) || Host(`anvil.local`)" + - "traefik.http.routers.anvil.entrypoints=https" + - "traefik.http.routers.anvil.tls=true" + mine: build: dockerfile: ./apps/mine/Dockerfile @@ -98,10 +102,8 @@ services: user: iforge env_file: - .env - ports: - - "4000:4000" volumes: - - "./config/mine:/config" + - "./config/mine:/config:ro" networks: - internal depends_on: @@ -113,6 +115,11 @@ services: condition: service_started op-connect-sync: condition: service_started + labels: + - "traefik.enable=true" + - "traefik.http.routers.anvil.rule=Host(`cdn.iforge.sheffield.ac.uk`) || Host(`mine.localhost`) || Host(`mine.local`)" + - "traefik.http.routers.anvil.entrypoints=https" + - "traefik.http.routers.anvil.tls=true" forge: build: @@ -120,14 +127,18 @@ services: context: . container_name: forge restart: unless-stopped - ports: - - "80:80" networks: - internal + labels: + - "traefik.enable=true" + - "traefik.http.routers.anvil.rule=Host(`iforge.sheffield.ac.uk`) || Host(`forge.localhost`) || Host(`forge.local`)" + - "traefik.http.routers.anvil.entrypoints=https" + - "traefik.http.routers.anvil.tls=true" networks: internal: - external: + web: + data: volumes: db_data: From c301baaa3c5ffbf0765ce8a2daa8f4f3ddf05e83 Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Fri, 26 Apr 2024 17:36:18 +0100 Subject: [PATCH 4/6] feat: AD LDAP Working - refactor: Rename several signIn files to simplify naming structure - fix: Correct props assignment for `PostSignIn` and `PostSignOut` services - refactor: Change signIn attribute `ucard_number` from number to string. Adjust getter and setter methods accordingly. - refactor: Remove username attribute assignment in `PostRegister` service --- .../auth/interfaces/ldap-user.interface.ts | 3 - apps/anvil/src/ldap/ldap.class.ts | 2 +- apps/anvil/src/ldap/ldap.service.ts | 3 - apps/anvil/src/sign-in/dto/sigs-in-dto.ts | 11 +- apps/anvil/src/sign-in/sign-in.controller.ts | 2 +- apps/anvil/src/sign-in/sign-in.service.ts | 8 +- apps/anvil/src/users/users.service.ts | 11 +- .../index.tsx => QueueDispatcher.tsx} | 5 +- .../index.tsx => RegisterDispatcher.tsx} | 5 +- .../SelectedTrainingPipDisplay.tsx | 4 +- .../index.tsx => SignInDispatcher.tsx} | 7 +- .../index.tsx => SignInFlowProgress.tsx} | 9 +- .../index.tsx => SignInManager.tsx} | 24 ++-- .../signin/actions/SignInManager/types.ts | 64 ---------- .../index.tsx => SignInReasonInput.tsx} | 18 +-- .../actions/SignInRegisterForm/index.tsx | 81 ------------ .../index.tsx => SignOutDispatcher.tsx} | 5 +- .../index.tsx => ToolSelectionInput.tsx} | 11 +- .../TrainingDisplay.tsx | 2 +- .../TrainingSelectionList.tsx | 6 +- .../{UCardInput/index.tsx => UCardInput.tsx} | 7 +- .../useDoubleTapEscape.ts | 0 apps/forge/src/lib/utils.ts | 4 + apps/forge/src/redux/signin.slice.ts | 3 +- .../_reponly/signin/actions/enqueue.tsx | 4 +- .../_reponly/signin/actions/in.tsx | 4 +- .../_reponly/signin/actions/out.tsx | 4 +- .../_reponly/signin/actions/register.tsx | 4 +- .../routes/_authenticated/signin/index.tsx | 2 +- .../src/services/signin/signInService.ts | 118 +++++++++--------- apps/forge/src/types/signInActions.ts | 60 +++++++++ apps/forge/src/types/signin.ts | 2 +- 32 files changed, 201 insertions(+), 292 deletions(-) rename apps/forge/src/components/signin/actions/{QueueDispatcher/index.tsx => QueueDispatcher.tsx} (94%) rename apps/forge/src/components/signin/actions/{RegisterDispatcher/index.tsx => RegisterDispatcher.tsx} (95%) rename apps/forge/src/components/signin/actions/{ToolSelectionInput => }/SelectedTrainingPipDisplay.tsx (81%) rename apps/forge/src/components/signin/actions/{SignInDispatcher/index.tsx => SignInDispatcher.tsx} (92%) rename apps/forge/src/components/signin/actions/{SignInFlowProgress/index.tsx => SignInFlowProgress.tsx} (94%) rename apps/forge/src/components/signin/actions/{SignInManager/index.tsx => SignInManager.tsx} (94%) delete mode 100644 apps/forge/src/components/signin/actions/SignInManager/types.ts rename apps/forge/src/components/signin/actions/{SignInReasonInput/index.tsx => SignInReasonInput.tsx} (94%) delete mode 100644 apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx rename apps/forge/src/components/signin/actions/{SignOutDispatcher/index.tsx => SignOutDispatcher.tsx} (94%) rename apps/forge/src/components/signin/actions/{ToolSelectionInput/index.tsx => ToolSelectionInput.tsx} (96%) rename apps/forge/src/components/signin/actions/{ToolSelectionInput => }/TrainingDisplay.tsx (97%) rename apps/forge/src/components/signin/actions/{ToolSelectionInput => }/TrainingSelectionList.tsx (94%) rename apps/forge/src/components/signin/actions/{UCardInput/index.tsx => UCardInput.tsx} (93%) rename apps/forge/src/{components/signin/actions/SignInManager => hooks}/useDoubleTapEscape.ts (100%) create mode 100644 apps/forge/src/types/signInActions.ts diff --git a/apps/anvil/src/auth/interfaces/ldap-user.interface.ts b/apps/anvil/src/auth/interfaces/ldap-user.interface.ts index d73ff42..a3f5148 100644 --- a/apps/anvil/src/auth/interfaces/ldap-user.interface.ts +++ b/apps/anvil/src/auth/interfaces/ldap-user.interface.ts @@ -1,13 +1,10 @@ export interface LdapUser { - dn: string; - cn: string; /** User's organisational unit */ ou: string; /** User's surname */ sn: string; /** User's first name */ givenName: string; - initials: string; /** User's email */ mail: string; uid: string; diff --git a/apps/anvil/src/ldap/ldap.class.ts b/apps/anvil/src/ldap/ldap.class.ts index a905016..44511ae 100644 --- a/apps/anvil/src/ldap/ldap.class.ts +++ b/apps/anvil/src/ldap/ldap.class.ts @@ -263,7 +263,7 @@ export class LdapClass extends EventEmitter implements OnModuleInit { ): Promise | null> { this.logger.log(`Looking up user by ucard number: ${ucardNumber}`, LdapClass.name); - const searchFilter = `(&(objectClass=person)(sheflibrarynumber=${ucardNumber}))`; + const searchFilter = `(&(objectClass=person)(shefLibraryNumber=${ucardNumber}))`; return this.lookup(searchFilter, attributes, true); } } diff --git a/apps/anvil/src/ldap/ldap.service.ts b/apps/anvil/src/ldap/ldap.service.ts index a2a296e..31e5bf9 100644 --- a/apps/anvil/src/ldap/ldap.service.ts +++ b/apps/anvil/src/ldap/ldap.service.ts @@ -55,11 +55,8 @@ export class LdapService { private formatLdapUser(result: Record): LdapUser { this.logger.log(`Formatting response ${JSON.stringify(result)}`); return { - dn: result.dn as string, uid: result.uid as string, - cn: result.cn as string, sn: result.sn as string, - initials: result.initials as string, ou: result.ou as string, mail: result.mail as string, givenName: result.givenName as string, diff --git a/apps/anvil/src/sign-in/dto/sigs-in-dto.ts b/apps/anvil/src/sign-in/dto/sigs-in-dto.ts index 45bb559..5b379fa 100644 --- a/apps/anvil/src/sign-in/dto/sigs-in-dto.ts +++ b/apps/anvil/src/sign-in/dto/sigs-in-dto.ts @@ -12,15 +12,10 @@ const UpdateSignInSchema = CreateSignInSchema.partial({ }); const RegisterUser = z.object({ - username: z.string(), - ucard_number: z.number(), + ucard_number: z.string(), }); export class CreateSignInDto extends createZodDto(SignInSchema) {} -export class FinaliseSignInDto extends createZodDto( - CreateSignInSchema.omit({ location: true }), -) {} -export class UpdateSignInDto extends createZodDto( - UpdateSignInSchema.omit({ location: true }), -) {} +export class FinaliseSignInDto extends createZodDto(CreateSignInSchema.omit({ location: true })) {} +export class UpdateSignInDto extends createZodDto(UpdateSignInSchema.omit({ location: true })) {} export class RegisterUserDto extends createZodDto(RegisterUser) {} diff --git a/apps/anvil/src/sign-in/sign-in.controller.ts b/apps/anvil/src/sign-in/sign-in.controller.ts index 05f62d7..c700bcf 100644 --- a/apps/anvil/src/sign-in/sign-in.controller.ts +++ b/apps/anvil/src/sign-in/sign-in.controller.ts @@ -35,7 +35,7 @@ export class SignInController { @IsRep() @Post("register-user") async registerUser(@Param("location") location: Location, @Body() registerUser: RegisterUserDto) { - this.logger.log(`Registering user at location: ${location}`, SignInController.name); + this.logger.log(`Registering user: ${registerUser.ucard_number} at location: ${location}`, SignInController.name); return this.signInService.registerUser(location, registerUser); } diff --git a/apps/anvil/src/sign-in/sign-in.service.ts b/apps/anvil/src/sign-in/sign-in.service.ts index 680a7c3..b5eeb5b 100644 --- a/apps/anvil/src/sign-in/sign-in.service.ts +++ b/apps/anvil/src/sign-in/sign-in.service.ts @@ -148,7 +148,7 @@ export class SignInService implements OnModuleInit { async registerUser(location: Location, register_user: RegisterUserDto) { // There may be a way to do this in fewer, more atomic steps, just haven't figured out how - let user = await this.userService.findByUsername(register_user.username); + let user = await this.userService.findByUcardNumber(parseInt(register_user.ucard_number.slice(3))); if (user) { if (user.ucard_number > 0) { @@ -159,10 +159,10 @@ export class SignInService implements OnModuleInit { } } else { // no user registered, fetch from ldap - const ldapUser = await this.ldapService.findUserByUsername(register_user.username); + const ldapUser = await this.ldapService.findUserByUcardNumber(register_user.ucard_number); if (!ldapUser) { throw new NotFoundException({ - message: `User with username ${register_user.username} couldn't be found. Perhaps you made a typo? (it should look like fe6if)`, + message: `User with ucard no ${register_user.ucard_number} couldn't be found. Perhaps you made a typo? (it should look like 001739897)`, code: ErrorCodes.ldap_not_found, }); } @@ -173,7 +173,7 @@ export class SignInService implements OnModuleInit { e.insert(e.sign_in.UserRegistration, { location: castLocation(location), user: e.select(e.users.User, () => ({ - filter_single: { username: register_user.username }, + filter_single: { ucard_number: e.int64(parseInt(register_user.ucard_number.toString().slice(3))) }, })), }), ); diff --git a/apps/anvil/src/users/users.service.ts b/apps/anvil/src/users/users.service.ts index c198e6d..6faef75 100644 --- a/apps/anvil/src/users/users.service.ts +++ b/apps/anvil/src/users/users.service.ts @@ -89,6 +89,10 @@ function removeDomain(email: string): string { return email.slice(0, email.length - "@sheffield.ac.uk".length); } +function ldapLibraryToUcardNumber(shefLibraryNumber: string): number { + return parseInt(shefLibraryNumber.slice(3)); +} + @Injectable() export class UsersService { constructor( @@ -237,13 +241,16 @@ export class UsersService { last_name: ldapUser.sn, organisational_unit: ldapUser.ou, roles: e.select(e.auth.Role, () => ({ filter_single: { name: "User" } })), - ucard_number: ldapUser.shefLibraryNumber.slice(3), + ucard_number: ldapLibraryToUcardNumber(ldapUser.shefLibraryNumber), profile_picture, }); } async createOrFindLdapUser(ldapUser: LdapUser): Promise { - return (await this.findByUsername(ldapUser.uid)) ?? (await this.insertLdapUser(ldapUser)); + return ( + (await this.findByUcardNumber(ldapLibraryToUcardNumber(ldapUser.shefLibraryNumber))) ?? + (await this.insertLdapUser(ldapUser)) + ); } async idToUsername(id: string) { diff --git a/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx b/apps/forge/src/components/signin/actions/QueueDispatcher.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx rename to apps/forge/src/components/signin/actions/QueueDispatcher.tsx index 28161e3..575f9c6 100644 --- a/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/QueueDispatcher.tsx @@ -7,11 +7,12 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { useState } from "react"; import { signinActions } from "@/redux/signin.slice.ts"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; import { PostQueueInPerson, PostQueueProps } from "@/services/signin/queueService.ts"; import { errorDisplay } from "@/components/errors/ErrorDisplay"; +import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const QueueDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const dispatch: AppDispatch = useDispatch(); @@ -24,7 +25,7 @@ const QueueDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queueProps: PostQueueProps = { locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, + uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx b/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx similarity index 95% rename from apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx rename to apps/forge/src/components/signin/actions/RegisterDispatcher.tsx index e772ddb..9580f20 100644 --- a/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx @@ -8,7 +8,7 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { useState } from "react"; import { signinActions } from "@/redux/signin.slice.ts"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; import { errorDisplay } from "@/components/errors/ErrorDisplay"; @@ -24,9 +24,8 @@ const RegisterDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const registerProps: PostRegisterProps = { locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, + uCardNumber: signInSession?.ucard_number ?? "0", signal: abortController.signal, - username: signInSession?.username ?? "", }; const { isPending, error, mutate } = useMutation({ diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput/SelectedTrainingPipDisplay.tsx b/apps/forge/src/components/signin/actions/SelectedTrainingPipDisplay.tsx similarity index 81% rename from apps/forge/src/components/signin/actions/ToolSelectionInput/SelectedTrainingPipDisplay.tsx rename to apps/forge/src/components/signin/actions/SelectedTrainingPipDisplay.tsx index c127bac..9673086 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput/SelectedTrainingPipDisplay.tsx +++ b/apps/forge/src/components/signin/actions/SelectedTrainingPipDisplay.tsx @@ -1,5 +1,5 @@ -import { Training } from "@ignis/types/sign_in"; -import { Badge } from "@ui/components/ui/badge"; +import { Training } from "@ignis/types/sign_in.ts"; +import { Badge } from "@ui/components/ui/badge.tsx"; import React from "react"; interface SelectedTrainingPipDisplayProps { diff --git a/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx b/apps/forge/src/components/signin/actions/SignInDispatcher.tsx similarity index 92% rename from apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx rename to apps/forge/src/components/signin/actions/SignInDispatcher.tsx index a9deab0..3bab350 100644 --- a/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInDispatcher.tsx @@ -8,10 +8,11 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { useEffect, useState } from "react"; import { signinActions } from "@/redux/signin.slice.ts"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; import { errorDisplay } from "@/components/errors/ErrorDisplay"; +import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const SignInDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queryClient = useQueryClient(); @@ -25,10 +26,10 @@ const SignInDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signInProps: PostSignInProps = { locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, + uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), signal: abortController.signal, postBody: { - ucard_number: signInSession?.ucard_number ?? 0, + ucard_number: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), location: activeLocation, reason_id: signInSession?.sign_in_reason?.id ?? "", tools: signInSession?.training?.map((training) => training.name) ?? [], diff --git a/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx b/apps/forge/src/components/signin/actions/SignInFlowProgress.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx rename to apps/forge/src/components/signin/actions/SignInFlowProgress.tsx index 5eb0325..fa6944c 100644 --- a/apps/forge/src/components/signin/actions/SignInFlowProgress/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInFlowProgress.tsx @@ -1,11 +1,4 @@ -import { - AnyStep, - EnqueueSteps, - FlowType, - RegisterSteps, - SignInSteps, - SignOutSteps, -} from "@/components/signin/actions/SignInManager/types.ts"; +import { AnyStep, EnqueueSteps, FlowType, RegisterSteps, SignInSteps, SignOutSteps } from "@/types/signInActions.ts"; import { Card, CardContent, CardHeader } from "@ui/components/ui/card.tsx"; import { Timeline, TimelineDot, TimelineHeading, TimelineItem, TimelineLine } from "@ui/components/ui/timeline.tsx"; import React, { useEffect, useState } from "react"; diff --git a/apps/forge/src/components/signin/actions/SignInManager/index.tsx b/apps/forge/src/components/signin/actions/SignInManager.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/SignInManager/index.tsx rename to apps/forge/src/components/signin/actions/SignInManager.tsx index f06624c..9bea9ba 100644 --- a/apps/forge/src/components/signin/actions/SignInManager/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInManager.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useEffect, useLayoutEffect, useState } from "react"; -import UCardInput from "@/components/signin/actions/UCardInput"; -import SignInReasonInput from "@/components/signin/actions/SignInReasonInput"; +import UCardInput from "@/components/signin/actions/UCardInput.tsx"; +import SignInReasonInput from "@/components/signin/actions/SignInReasonInput.tsx"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { signinActions } from "@/redux/signin.slice.ts"; @@ -15,16 +15,15 @@ import { RegisterSteps, SignInSteps, SignOutSteps, -} from "@/components/signin/actions/SignInManager/types"; -import ToolSelectionInput from "@/components/signin/actions/ToolSelectionInput"; -import SignInDispatcher from "@/components/signin/actions/SignInDispatcher"; -import SignOutDispatcher from "@/components/signin/actions/SignOutDispatcher"; -import SignInFlowProgress from "@/components/signin/actions/SignInFlowProgress"; +} from "@/types/signInActions.ts"; +import ToolSelectionInput from "@/components/signin/actions/ToolSelectionInput.tsx"; +import SignInDispatcher from "@/components/signin/actions/SignInDispatcher.tsx"; +import SignOutDispatcher from "@/components/signin/actions/SignOutDispatcher.tsx"; +import SignInFlowProgress from "@/components/signin/actions/SignInFlowProgress.tsx"; import { Button } from "@ui/components/ui/button.tsx"; -import useDoubleTapEscape from "@/components/signin/actions/SignInManager/useDoubleTapEscape.ts"; -import QueueDispatcher from "@/components/signin/actions/QueueDispatcher"; -import RegisterDispatcher from "@/components/signin/actions/RegisterDispatcher"; -import SignInRegisterForm from "@/components/signin/actions/SignInRegisterForm"; +import useDoubleTapEscape from "@/hooks/useDoubleTapEscape.ts"; +import QueueDispatcher from "@/components/signin/actions/QueueDispatcher.tsx"; +import RegisterDispatcher from "@/components/signin/actions/RegisterDispatcher.tsx"; const flowConfig: FlowConfiguration = { [FlowType.SignIn]: { @@ -39,8 +38,7 @@ const flowConfig: FlowConfiguration = { }, [FlowType.Register]: { [RegisterSteps.Step1]: UCardInput, - [RegisterSteps.Step2]: SignInRegisterForm, - [RegisterSteps.Step3]: RegisterDispatcher, + [RegisterSteps.Step2]: RegisterDispatcher, }, [FlowType.Enqueue]: { [EnqueueSteps.Step1]: UCardInput, diff --git a/apps/forge/src/components/signin/actions/SignInManager/types.ts b/apps/forge/src/components/signin/actions/SignInManager/types.ts deleted file mode 100644 index 940fd38..0000000 --- a/apps/forge/src/components/signin/actions/SignInManager/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; - -export enum SignInSteps { - Step1 = 'UCard Input', - Step2 = 'Fetching Training', - Step3 = 'Reason Input', - Step4 = 'Sign In', -} - -export enum SignOutSteps { - Step1 = 'UCard Input', - Step2 = 'Sign Out', -} - -export enum RegisterSteps { - Step1 = 'UCard Input', - Step2 = 'Username Input', - Step3 = 'Register', -} - -export enum EnqueueSteps { - Step1 = 'UCard Input', - Step2 = 'Enqueue', -} - -export enum FlowType { - SignIn = 'SIGN_IN', - SignOut = 'SIGN_OUT', - Register = 'REGISTER', - Enqueue = 'ENQUEUE', -} - -export interface StepComponentProps { - onPrimary: () => void; - onSecondary: () => void; -} - -export interface FlowStepComponent extends React.FC { -} - - -export interface FlowConfiguration { - [FlowType.SignIn]: Record; - [FlowType.SignOut]: Record; - [FlowType.Register]: Record; - [FlowType.Enqueue]: Record; -} - - -// Define a type that can be either SignInSteps or SignOutSteps. -export type AnyStep = SignInSteps | SignOutSteps | RegisterSteps | EnqueueSteps; - -export const flowTypeToPrintTable = (flowType: FlowType) => { - switch (flowType) { - case FlowType.SignIn: - return "Sign In"; - case FlowType.SignOut: - return "Sign Out"; - case FlowType.Register: - return "Register"; - case FlowType.Enqueue: - return "Enqueue"; - } -} \ No newline at end of file diff --git a/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx b/apps/forge/src/components/signin/actions/SignInReasonInput.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx rename to apps/forge/src/components/signin/actions/SignInReasonInput.tsx index f2f604c..016dd0d 100644 --- a/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInReasonInput.tsx @@ -1,14 +1,14 @@ import { ErrorDisplayProps, errorDisplay } from "@/components/errors/ErrorDisplay"; -import { Category } from "@/components/icons/SignInReason"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types"; +import { Category } from "@/components/icons/SignInReason.tsx"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { signinActions } from "@/redux/signin.slice.ts"; -import { AppDispatch, AppRootState } from "@/redux/store"; -import { useSignInReasons } from "@/services/signin/signInReasonService"; -import type { Reason } from "@ignis/types/sign_in"; -import { Button } from "@ui/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card"; -import { Input } from "@ui/components/ui/input"; -import { Loader } from "@ui/components/ui/loader"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { useSignInReasons } from "@/services/signin/signInReasonService.ts"; +import type { Reason } from "@ignis/types/sign_in.ts"; +import { Button } from "@ui/components/ui/button.tsx"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { Input } from "@ui/components/ui/input.tsx"; +import { Loader } from "@ui/components/ui/loader.tsx"; import Fuse from "fuse.js"; import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; diff --git a/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx b/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx deleted file mode 100644 index 3cf6be7..0000000 --- a/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; -import { Button } from "@ui/components/ui/button.tsx"; -import { Input } from "@ui/components/ui/input.tsx"; -import React, { useState } from "react"; -import { useDispatch } from "react-redux"; -import { AppDispatch } from "@/redux/store.ts"; -import { signinActions } from "@/redux/signin.slice.ts"; - -const SignInRegisterForm: FlowStepComponent = ({ onSecondary, onPrimary }) => { - const dispatch: AppDispatch = useDispatch(); - const [userInput, setUserInput] = useState(""); - const [validationError, setValidationError] = useState(null); - - const validateInput = (input: string) => { - if (input.length < 6) { - setValidationError("Username must be at least 6 characters long."); - return false; - } - - setValidationError(null); - return true; - }; - - const handleInputChange = (event: React.ChangeEvent) => { - setUserInput(event.target.value); - validateInput(event.target.value); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - // Example: Submit on Enter key - if (event.key === "Enter" && validateInput(userInput)) { - handlePrimaryClick(); - } - }; - - const handlePrimaryClick = () => { - if (validateInput(userInput)) { - dispatch(signinActions.updateSignInSessionField("username", userInput)); - onPrimary?.(); - } - }; - - const handleSecondaryClick = () => { - onSecondary?.(); - }; - - return ( - <> - - - Registering User - - -
- - {validationError &&

{validationError}

} -
-
- - - - -
- - ); -}; - -export default SignInRegisterForm; diff --git a/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx b/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx rename to apps/forge/src/components/signin/actions/SignOutDispatcher.tsx index 9b8a51f..7c9319c 100644 --- a/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx @@ -8,10 +8,11 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { useEffect, useState } from "react"; import { signinActions } from "@/redux/signin.slice.ts"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; import { errorDisplay } from "@/components/errors/ErrorDisplay"; +import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const SignOutDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queryClient = useQueryClient(); @@ -25,7 +26,7 @@ const SignOutDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signOutProps: PostSignOutProps = { locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, + uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx b/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx similarity index 96% rename from apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx rename to apps/forge/src/components/signin/actions/ToolSelectionInput.tsx index cb5a04b..e9fb8db 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx +++ b/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx @@ -1,14 +1,14 @@ import { errorDisplay } from "@/components/errors/ErrorDisplay"; -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; -import { SelectedTrainingPipDisplay } from "@/components/signin/actions/ToolSelectionInput/SelectedTrainingPipDisplay.tsx"; -import ToolSelectionList from "@/components/signin/actions/ToolSelectionInput/TrainingSelectionList.tsx"; +import { FlowStepComponent } from "@/types/signInActions.ts"; +import { SelectedTrainingPipDisplay } from "@/components/signin/actions/SelectedTrainingPipDisplay.tsx"; +import ToolSelectionList from "@/components/signin/actions/TrainingSelectionList.tsx"; import { signinActions } from "@/redux/signin.slice.ts"; import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { GetSignIn, GetSignInProps } from "@/services/signin/signInService.ts"; import { Training, User } from "@ignis/types/sign_in.ts"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; -import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@ui/components/ui/collapsible.tsx"; @@ -16,6 +16,7 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { ChevronsDownUp, ChevronsUpDown } from "lucide-react"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; /* three categories of tools that can be selected: @@ -51,7 +52,7 @@ const ToolSelectionInput: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signInProps: GetSignInProps = { locationName: activeLocation, - uCardNumber: ucardNumber ?? 0, + uCardNumber: fullUCardToDBRepresentation(ucardNumber ?? "0"), signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingDisplay.tsx b/apps/forge/src/components/signin/actions/TrainingDisplay.tsx similarity index 97% rename from apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingDisplay.tsx rename to apps/forge/src/components/signin/actions/TrainingDisplay.tsx index 3ec9a5f..bc1ad41 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingDisplay.tsx +++ b/apps/forge/src/components/signin/actions/TrainingDisplay.tsx @@ -1,4 +1,4 @@ -import { Training } from "@ignis/types/sign_in"; +import { Training } from "@ignis/types/sign_in.ts"; import { Button } from "@ui/components/ui/button.tsx"; import React from "react"; diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingSelectionList.tsx b/apps/forge/src/components/signin/actions/TrainingSelectionList.tsx similarity index 94% rename from apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingSelectionList.tsx rename to apps/forge/src/components/signin/actions/TrainingSelectionList.tsx index 4d83afa..16011ae 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput/TrainingSelectionList.tsx +++ b/apps/forge/src/components/signin/actions/TrainingSelectionList.tsx @@ -1,10 +1,10 @@ -import TrainingDisplay from "@/components/signin/actions/ToolSelectionInput/TrainingDisplay.tsx"; +import TrainingDisplay from "@/components/signin/actions/TrainingDisplay.tsx"; import { cn } from "@/lib/utils.ts"; -import { Training } from "@ignis/types/sign_in"; +import { Training } from "@ignis/types/sign_in.ts"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert.tsx"; import { ScrollArea } from "@ui/components/ui/scroll-area.tsx"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@ui/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@ui/components/ui/tooltip.tsx"; import { Info } from "lucide-react"; import React, { useState } from "react"; diff --git a/apps/forge/src/components/signin/actions/UCardInput/index.tsx b/apps/forge/src/components/signin/actions/UCardInput.tsx similarity index 93% rename from apps/forge/src/components/signin/actions/UCardInput/index.tsx rename to apps/forge/src/components/signin/actions/UCardInput.tsx index 6555fba..eafcc7e 100644 --- a/apps/forge/src/components/signin/actions/UCardInput/index.tsx +++ b/apps/forge/src/components/signin/actions/UCardInput.tsx @@ -1,9 +1,9 @@ -import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { signinActions, useSignInSessionField } from "@/redux/signin.slice.ts"; import { AppDispatch } from "@/redux/store.ts"; import { Button } from "@ui/components/ui/button.tsx"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; -import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@ui/components/ui/input-otp"; +import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@ui/components/ui/input-otp.tsx"; import { useEffect, useRef, useState } from "react"; import { useDispatch } from "react-redux"; @@ -26,8 +26,7 @@ const UCardInput: FlowStepComponent = ({ onPrimary }) => { const handleOnSubmit = () => { if (isOtpValid) { - const parsedOtp = parseInt(otp.slice(-6), 10); - dispatch(signinActions.updateSignInSessionField("ucard_number", parsedOtp)); + dispatch(signinActions.updateSignInSessionField("ucard_number", otp)); onPrimary?.(); } }; diff --git a/apps/forge/src/components/signin/actions/SignInManager/useDoubleTapEscape.ts b/apps/forge/src/hooks/useDoubleTapEscape.ts similarity index 100% rename from apps/forge/src/components/signin/actions/SignInManager/useDoubleTapEscape.ts rename to apps/forge/src/hooks/useDoubleTapEscape.ts diff --git a/apps/forge/src/lib/utils.ts b/apps/forge/src/lib/utils.ts index cb11068..7d4b939 100644 --- a/apps/forge/src/lib/utils.ts +++ b/apps/forge/src/lib/utils.ts @@ -45,3 +45,7 @@ export function extractError(error: Error): string { } return error?.message || "Unknown Error. Contact the IT Team"; } + +export function fullUCardToDBRepresentation(ucard_number: string): number { + return parseInt(ucard_number.slice(3), 10); +} diff --git a/apps/forge/src/redux/signin.slice.ts b/apps/forge/src/redux/signin.slice.ts index 54345e6..b0947a2 100644 --- a/apps/forge/src/redux/signin.slice.ts +++ b/apps/forge/src/redux/signin.slice.ts @@ -1,8 +1,7 @@ import { RESET_APP } from "@/types/common.ts"; import { SignInSession, SignInState } from "@/types/signin.ts"; -import { Location, LocationStatus, Reason, Training } from "@ignis/types/sign_in.ts"; +import { Location, LocationStatus } from "@ignis/types/sign_in.ts"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { useState } from "react"; import { useSelector } from "react-redux"; import { RootState } from "./store"; diff --git a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/enqueue.tsx b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/enqueue.tsx index fda695f..ac841ee 100644 --- a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/enqueue.tsx +++ b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/enqueue.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import ActiveLocationSelector from "@/components/signin/ActiveLocationSelector"; -import SignInActionsManager from "@/components/signin/actions/SignInManager"; -import { FlowType } from "@/components/signin/actions/SignInManager/types.ts"; +import SignInActionsManager from "@/components/signin/actions/SignInManager.tsx"; +import { FlowType } from "@/types/signInActions.ts"; import Title from "@/components/title"; const EnqueueComponent = () => { diff --git a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/in.tsx b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/in.tsx index 4f1a3dd..a8f871a 100644 --- a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/in.tsx +++ b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/in.tsx @@ -1,6 +1,6 @@ import ActiveLocationSelector from "@/components/signin/ActiveLocationSelector"; -import SignInActionsManager from "@/components/signin/actions/SignInManager"; -import { FlowType } from "@/components/signin/actions/SignInManager/types.ts"; +import SignInActionsManager from "@/components/signin/actions/SignInManager.tsx"; +import { FlowType } from "@/types/signInActions.ts"; import Title from "@/components/title"; import { createFileRoute } from "@tanstack/react-router"; diff --git a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/out.tsx b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/out.tsx index fe35adf..f791ebf 100644 --- a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/out.tsx +++ b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/out.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import ActiveLocationSelector from "@/components/signin/ActiveLocationSelector"; -import SignInActionsManager from "@/components/signin/actions/SignInManager"; -import { FlowType } from "@/components/signin/actions/SignInManager/types.ts"; +import SignInActionsManager from "@/components/signin/actions/SignInManager.tsx"; +import { FlowType } from "@/types/signInActions.ts"; import Title from "@/components/title"; const OutComponent = () => { diff --git a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/register.tsx b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/register.tsx index 992e2a4..817c39c 100644 --- a/apps/forge/src/routes/_authenticated/_reponly/signin/actions/register.tsx +++ b/apps/forge/src/routes/_authenticated/_reponly/signin/actions/register.tsx @@ -1,6 +1,6 @@ import ActiveLocationSelector from "@/components/signin/ActiveLocationSelector"; -import SignInActionsManager from "@/components/signin/actions/SignInManager"; -import { FlowType } from "@/components/signin/actions/SignInManager/types.ts"; +import SignInActionsManager from "@/components/signin/actions/SignInManager.tsx"; +import { FlowType } from "@/types/signInActions.ts"; import Title from "@/components/title"; import { createFileRoute } from "@tanstack/react-router"; diff --git a/apps/forge/src/routes/_authenticated/signin/index.tsx b/apps/forge/src/routes/_authenticated/signin/index.tsx index 1d4acdc..5d69269 100644 --- a/apps/forge/src/routes/_authenticated/signin/index.tsx +++ b/apps/forge/src/routes/_authenticated/signin/index.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import ActiveLocationSelector from "@/components/signin/ActiveLocationSelector"; -import SignInActionsManager from "@/components/signin/actions/SignInManager"; +import SignInActionsManager from "@/components/signin/actions/SignInManager.tsx"; import Title from "@/components/title"; const SignInAppIndexComponent = () => { diff --git a/apps/forge/src/services/signin/signInService.ts b/apps/forge/src/services/signin/signInService.ts index e28c06b..8d97cab 100644 --- a/apps/forge/src/services/signin/signInService.ts +++ b/apps/forge/src/services/signin/signInService.ts @@ -1,77 +1,79 @@ import axiosInstance from "@/api/axiosInstance.ts"; -import {FinaliseSignInDto, User} from "@ignis/types/sign_in.ts"; +import { FinaliseSignInDto, User } from "@ignis/types/sign_in.ts"; export interface GetSignInProps { - locationName: string; - uCardNumber: number; - signal: AbortSignal + locationName: string; + uCardNumber: number; + signal: AbortSignal; } -export const GetSignIn = async ({locationName, uCardNumber, signal}: GetSignInProps): Promise => { - try { - const {data} = await axiosInstance.get(`/location/${locationName}/sign-in/${uCardNumber}`, {signal: signal}); - return data; - } catch (error) { - console.error("An error occurred while Getting Sign In:", error); - throw error; - } +export const GetSignIn = async ({ locationName, uCardNumber, signal }: GetSignInProps): Promise => { + try { + const { data } = await axiosInstance.get(`/location/${locationName}/sign-in/${uCardNumber}`, { signal: signal }); + return data; + } catch (error) { + console.error("An error occurred while Getting Sign In:", error); + throw error; + } }; export interface PostSignInProps { - signal: AbortSignal; - locationName: string; - uCardNumber: number; - postBody: FinaliseSignInDto; + signal: AbortSignal; + locationName: string; + uCardNumber: number; + postBody: FinaliseSignInDto; } -export const PostSignIn = async ({locationName, uCardNumber, signal, postBody}: PostSignInProps): Promise => { - try { - const {data} = await axiosInstance.post(`/location/${locationName}/sign-in/${uCardNumber}`, postBody, {signal: signal}); - return data; - } catch (error) { - console.error("An error occurred while Posting to Sign In:", error); - throw error; - } +export const PostSignIn = async ({ locationName, uCardNumber, signal, postBody }: PostSignInProps): Promise => { + try { + const { data } = await axiosInstance.post(`/location/${locationName}/sign-in/${uCardNumber}`, postBody, { + signal: signal, + }); + return data; + } catch (error) { + console.error("An error occurred while Posting to Sign In:", error); + throw error; + } }; export interface PostSignOutProps { - signal: AbortSignal; - locationName: string; - uCardNumber: number; -} - -export const PostSignOut = async ({locationName, uCardNumber, signal}: PostSignOutProps): Promise => { - try { - const {data} = await axiosInstance.post(`/location/${locationName}/sign-out/${uCardNumber}`,{} ,{signal: signal}); - return data; - } catch (error) { - console.error("An error occurred while Posting to Sign Out:", error); - throw error; - } + signal: AbortSignal; + locationName: string; + uCardNumber: number; } +export const PostSignOut = async ({ locationName, uCardNumber, signal }: PostSignOutProps): Promise => { + try { + const { data } = await axiosInstance.post( + `/location/${locationName}/sign-out/${uCardNumber}`, + {}, + { signal: signal }, + ); + return data; + } catch (error) { + console.error("An error occurred while Posting to Sign Out:", error); + throw error; + } +}; export interface PostRegisterProps { - signal: AbortSignal; - locationName: string; - uCardNumber: number; - username: string; + signal: AbortSignal; + locationName: string; + uCardNumber: string; } -export const PostRegister = async ({ - locationName, - uCardNumber, - username, - signal - }: PostRegisterProps): Promise => { - try { - const {data} = await axiosInstance.post(`/location/${locationName}/register-user`, { - username: username, - ucard_number: uCardNumber - }, { signal: signal }); - return data; - } catch (error) { - console.error("An error occurred while Posting to Register Endpoint:", error); - throw error; - } -}; \ No newline at end of file +export const PostRegister = async ({ locationName, uCardNumber, signal }: PostRegisterProps): Promise => { + try { + const { data } = await axiosInstance.post( + `/location/${locationName}/register-user`, + { + ucard_number: uCardNumber, + }, + { signal: signal }, + ); + return data; + } catch (error) { + console.error("An error occurred while Posting to Register Endpoint:", error); + throw error; + } +}; diff --git a/apps/forge/src/types/signInActions.ts b/apps/forge/src/types/signInActions.ts new file mode 100644 index 0000000..86e877e --- /dev/null +++ b/apps/forge/src/types/signInActions.ts @@ -0,0 +1,60 @@ +import React from "react"; + +export enum SignInSteps { + Step1 = "UCard Input", + Step2 = "Fetching Training", + Step3 = "Reason Input", + Step4 = "Sign In", +} + +export enum SignOutSteps { + Step1 = "UCard Input", + Step2 = "Sign Out", +} + +export enum RegisterSteps { + Step1 = "UCard Input", + Step2 = "Register", +} + +export enum EnqueueSteps { + Step1 = "UCard Input", + Step2 = "Enqueue", +} + +export enum FlowType { + SignIn = "SIGN_IN", + SignOut = "SIGN_OUT", + Register = "REGISTER", + Enqueue = "ENQUEUE", +} + +export interface StepComponentProps { + onPrimary: () => void; + onSecondary: () => void; +} + +export interface FlowStepComponent extends React.FC {} + +export interface FlowConfiguration { + [FlowType.SignIn]: Record; + [FlowType.SignOut]: Record; + [FlowType.Register]: Record; + [FlowType.Enqueue]: Record; +} + +// Define a type that can be either SignInSteps or SignOutSteps. +export type AnyStep = SignInSteps | SignOutSteps | RegisterSteps | EnqueueSteps; + +export const flowTypeToPrintTable = (flowType: FlowType) => { + switch (flowType) { + case FlowType.SignIn: + return "Sign In"; + case FlowType.SignOut: + return "Sign Out"; + case FlowType.Register: + return "Register"; + case FlowType.Enqueue: + return "Enqueue"; + } +}; diff --git a/apps/forge/src/types/signin.ts b/apps/forge/src/types/signin.ts index 45e717f..6406138 100644 --- a/apps/forge/src/types/signin.ts +++ b/apps/forge/src/types/signin.ts @@ -10,7 +10,7 @@ export interface SignInState { // TODO IDEALLY THIS WOULD BE A SESSION PER FLOW TYPE BUT I DON'T WANT TO REFACTOR THE WHOLE THING RN export interface SignInSession { - ucard_number: number | null; + ucard_number: string; is_rep: boolean; sign_in_reason: Reason | null; training: Training[] | null; From 2c52ff8b456e6b34fa79592a9cfaa1f924ddef8a Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Sat, 27 Apr 2024 02:45:42 +0100 Subject: [PATCH 5/6] feat: remove manual reg'ing more to come for the UX for this part of #1 --- .../authentication/authentication.module.ts | 29 +++----- .../strategies/ldap.strategy.ts | 43 ------------ apps/anvil/src/sign-in/dto/sigs-in-dto.ts | 5 -- apps/anvil/src/sign-in/sign-in.controller.ts | 55 ++++++--------- apps/anvil/src/sign-in/sign-in.service.ts | 68 +++++++++++-------- apps/anvil/src/users/users.service.ts | 25 ++++--- .../signin/actions/QueueDispatcher.tsx | 17 +++-- .../signin/actions/RegisterDispatcher.tsx | 2 +- .../signin/actions/SignInDispatcher.tsx | 17 +++-- .../signin/actions/SignInManager.tsx | 28 ++++---- .../signin/actions/SignInReasonInput.tsx | 14 ++-- .../signin/actions/SignOutDispatcher.tsx | 15 ++-- .../signin/actions/ToolSelectionInput.tsx | 9 ++- .../components/signin/actions/UCardInput.tsx | 8 +-- .../components/SignedInUserCard/index.tsx | 3 +- apps/forge/src/lib/constants.ts | 1 + apps/forge/src/lib/utils.ts | 5 +- .../src/services/signin/signInService.ts | 6 +- packages/types/sign_in.ts | 5 +- packages/types/users.ts | 2 +- 20 files changed, 143 insertions(+), 214 deletions(-) delete mode 100644 apps/anvil/src/auth/authentication/strategies/ldap.strategy.ts diff --git a/apps/anvil/src/auth/authentication/authentication.module.ts b/apps/anvil/src/auth/authentication/authentication.module.ts index e0c4957..caf74d8 100644 --- a/apps/anvil/src/auth/authentication/authentication.module.ts +++ b/apps/anvil/src/auth/authentication/authentication.module.ts @@ -1,17 +1,16 @@ -import { Logger, Module } from "@nestjs/common"; -import { AuthenticationService } from "./authentication.service"; -import { AuthenticationController } from "./authentication.controller"; -import { UsersModule } from "@/users/users.module"; -import { PassportModule } from "@nestjs/passport"; +import { EdgeDBModule } from "@/edgedb/edgedb.module"; import { LdapModule } from "@/ldap/ldap.module"; -import { DiscordStrategy } from "./strategies/discord.strategy"; -import { LdapAuthStrategy } from "./strategies/ldap.strategy"; -import { JwtModule } from "@nestjs/jwt"; import { IntegrationsModule } from "@/users/integrations/integrations.module"; +import { UsersModule } from "@/users/users.module"; +import { Logger, Module } from "@nestjs/common"; +import { JwtModule } from "@nestjs/jwt"; +import { PassportModule } from "@nestjs/passport"; +import { AuthenticationController } from "./authentication.controller"; +import { AuthenticationService } from "./authentication.service"; import { BlacklistService } from "./blacklist/blacklist.service"; -import { JwtStrategy } from "./strategies/jwt.strategy"; -import { EdgeDBModule } from "@/edgedb/edgedb.module"; +import { DiscordStrategy } from "./strategies/discord.strategy"; import { GoogleStrategy } from "./strategies/google.strategy"; +import { JwtStrategy } from "./strategies/jwt.strategy"; @Module({ imports: [ @@ -29,15 +28,7 @@ import { GoogleStrategy } from "./strategies/google.strategy"; }), }), ], - providers: [ - AuthenticationService, - DiscordStrategy, - GoogleStrategy, - LdapAuthStrategy, - BlacklistService, - JwtStrategy, - Logger, - ], + providers: [AuthenticationService, DiscordStrategy, GoogleStrategy, BlacklistService, JwtStrategy, Logger], controllers: [AuthenticationController], }) export class AuthenticationModule {} diff --git a/apps/anvil/src/auth/authentication/strategies/ldap.strategy.ts b/apps/anvil/src/auth/authentication/strategies/ldap.strategy.ts deleted file mode 100644 index f035932..0000000 --- a/apps/anvil/src/auth/authentication/strategies/ldap.strategy.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; -import { PassportStrategy } from "@nestjs/passport"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const LdapStrategy = require("passport-ldapauth").Strategy; -import { LdapService } from "@/ldap/ldap.service"; -import { UsersService } from "@/users/users.service"; -import { LdapUser } from "../../interfaces/ldap-user.interface"; -import type { User } from "@ignis/types/users"; - -@Injectable() -export class LdapAuthStrategy extends PassportStrategy(LdapStrategy, "ldap") { - constructor( - private readonly ldapService: LdapService, - private readonly usersService: UsersService, - ) { - super({ - server: { - url: process.env.LDAP_HOST + ":" + process.env.LDAP_PORT, - searchBase: process.env.LDAP_BASE, - searchFilter: "(uid={{username}})", - bindFunction: async (req: any, callback: any) => { - try { - const isAuthenticated = await this.ldapService.authenticate( - req.body.username, - req.body.password, - ); - if (isAuthenticated) { - callback(null, { username: req.body.username }); - } else { - callback(new UnauthorizedException()); - } - } catch (error) { - callback(error); - } - }, - }, - }); - } - - async validate(ldapUser: LdapUser): Promise { - return await this.usersService.createOrFindLdapUser(ldapUser); - } -} diff --git a/apps/anvil/src/sign-in/dto/sigs-in-dto.ts b/apps/anvil/src/sign-in/dto/sigs-in-dto.ts index 5b379fa..e7f1dbe 100644 --- a/apps/anvil/src/sign-in/dto/sigs-in-dto.ts +++ b/apps/anvil/src/sign-in/dto/sigs-in-dto.ts @@ -11,11 +11,6 @@ const UpdateSignInSchema = CreateSignInSchema.partial({ tools: true, }); -const RegisterUser = z.object({ - ucard_number: z.string(), -}); - export class CreateSignInDto extends createZodDto(SignInSchema) {} export class FinaliseSignInDto extends createZodDto(CreateSignInSchema.omit({ location: true })) {} export class UpdateSignInDto extends createZodDto(UpdateSignInSchema.omit({ location: true })) {} -export class RegisterUserDto extends createZodDto(RegisterUser) {} diff --git a/apps/anvil/src/sign-in/sign-in.controller.ts b/apps/anvil/src/sign-in/sign-in.controller.ts index c700bcf..0a9190c 100644 --- a/apps/anvil/src/sign-in/sign-in.controller.ts +++ b/apps/anvil/src/sign-in/sign-in.controller.ts @@ -1,19 +1,16 @@ import { CheckAbilities } from "@/auth/authorization/decorators/check-abilities-decorator"; import { IsRep } from "@/auth/authorization/decorators/check-roles-decorator"; import { CaslAbilityGuard } from "@/auth/authorization/guards/casl-ability.guard"; -import { User } from "@/shared/decorators/user.decorator"; import { TrainingService } from "@/training/training.service"; -import { UsersService } from "@/users/users.service"; -import { ErrorCodes } from "@/shared/constants/ErrorCodes"; -import { sign_in as sign_in_, users } from "@ignis/types"; -import type { List, Location } from "@ignis/types/sign_in"; +import { UsersService, ldapLibraryToUcardNumber } from "@/users/users.service"; +import { sign_in as sign_in_ } from "@ignis/types"; +import type { List, Location, LocationStatus } from "@ignis/types/sign_in"; import type { User as User_ } from "@ignis/types/users"; -import { Body, Controller, Get, NotFoundException, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; +import { Body, Controller, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; +import { Logger } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; -import { Throttle } from "@nestjs/throttler"; -import { FinaliseSignInDto, RegisterUserDto, UpdateSignInDto } from "./dto/sigs-in-dto"; +import { FinaliseSignInDto, UpdateSignInDto } from "./dto/sigs-in-dto"; import { SignInService } from "./sign-in.service"; -import { Logger } from "@nestjs/common"; @Controller("location/:location") @UseGuards(AuthGuard("jwt"), CaslAbilityGuard) @@ -27,38 +24,23 @@ export class SignInController { @Get() @IsRep() - async getList(@Param("location") location: Location) { + async getList(@Param("location") location: Location): Promise { return this.signInService.getList(location); } - @Throttle({ default: { limit: 1, ttl: 1000 } }) - @IsRep() - @Post("register-user") - async registerUser(@Param("location") location: Location, @Body() registerUser: RegisterUserDto) { - this.logger.log(`Registering user: ${registerUser.ucard_number} at location: ${location}`, SignInController.name); - return this.signInService.registerUser(location, registerUser); - } - @Get("sign-in/:ucard_number") @IsRep() async signInOptions( @Param("location") location: Location, - @Param("ucard_number", ParseIntPipe) ucard_number: number, + @Param("ucard_number") ucard_number: string, ): Promise { this.logger.log( `Retrieving sign-in options for UCard number: ${ucard_number} at location: ${location}`, SignInController.name, ); - const user = await this.userService.findByUcardNumber(ucard_number); - if (!user) { - this.logger.warn(`User with UCard number ${ucard_number} is not registered`, SignInController.name); - throw new NotFoundException({ - message: `User with UCard number ${ucard_number} is not registered`, - code: ErrorCodes.not_registered, - }); - } + const user = await this.signInService.getUserForSignIn(location, ucard_number); - if (await this.signInService.isRep(ucard_number)) { + if (user?.is_rep) { return { // reasons, training: await this.signInService.getTrainings(user.id, location), @@ -67,7 +49,7 @@ export class SignInController { }; } - const extras = await this.signInService.preSignInChecks(location, ucard_number); + const extras = await this.signInService.preSignInChecks(location, user.ucard_number); // const [trainings, reasons] = await Promise.all([ // this.trainingService.getUserxxTrainingForLocation(user.username, location), @@ -85,17 +67,18 @@ export class SignInController { @IsRep() async signIn( @Param("location") location: Location, - @Param("ucard_number", ParseIntPipe) ucard_number: number, + @Param("ucard_number") ucard_number: string, @Body() finaliseSignInDto: FinaliseSignInDto, ) { this.logger.log(`Signing in UCard number: ${ucard_number} at location: ${location}`, SignInController.name); - if (await this.signInService.isRep(ucard_number)) { - return await this.signInService.repSignIn(location, ucard_number, finaliseSignInDto.reason_id); + const ucard_number_ = ldapLibraryToUcardNumber(ucard_number); + if (await this.signInService.isRep(ucard_number_)) { + return await this.signInService.repSignIn(location, ucard_number_, finaliseSignInDto.reason_id); } return await this.signInService.signIn( location, - ucard_number, + ucard_number_, finaliseSignInDto.tools, finaliseSignInDto.reason_id, ); @@ -122,13 +105,13 @@ export class SignInController { @Post("sign-out/:ucard_number") @IsRep() - async signOut(@Param("location") location: Location, @Param("ucard_number", ParseIntPipe) ucard_number: number) { + async signOut(@Param("location") location: Location, @Param("ucard_number") ucard_number: string) { this.logger.log(`Signing out UCard number: ${ucard_number} at location: ${location}`, SignInController.name); - return await this.signInService.signOut(location, ucard_number); + return await this.signInService.signOut(location, ldapLibraryToUcardNumber(ucard_number)); } @Get("status") - async getSignInStatus(@Param("location") location: Location) { + async getSignInStatus(@Param("location") location: Location): Promise { this.logger.log(`Retrieving sign-in status for location: ${location}`, SignInController.name); return await this.signInService.getStatusForLocation(location); } diff --git a/apps/anvil/src/sign-in/sign-in.service.ts b/apps/anvil/src/sign-in/sign-in.service.ts index b5eeb5b..996867d 100644 --- a/apps/anvil/src/sign-in/sign-in.service.ts +++ b/apps/anvil/src/sign-in/sign-in.service.ts @@ -10,7 +10,7 @@ import e from "@dbschema/edgeql-js"; import { std } from "@dbschema/interfaces"; import { getUserTrainingForSignIn } from "@dbschema/queries/getUserTrainingForSignIn.query"; import type { Location, LocationStatus, Training } from "@ignis/types/sign_in"; -import type { PartialUser } from "@ignis/types/users"; +import type { PartialUser, User } from "@ignis/types/users"; import { BadRequestException, HttpException, @@ -19,9 +19,9 @@ import { NotFoundException, OnModuleInit, } from "@nestjs/common"; +import { Logger } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; -import { CardinalityViolationError, InvalidValueError, QueryAssertionError } from "edgedb"; -import { RegisterUserDto } from "./dto/sigs-in-dto"; +import { CardinalityViolationError, InvalidValueError } from "edgedb"; export const REP_ON_SHIFT = "Rep On Shift"; export const REP_OFF_SHIFT = "Rep Off Shift"; @@ -39,6 +39,7 @@ function castLocation(location: Location) { @Injectable() export class SignInService implements OnModuleInit { private readonly disabledQueue: Set; + private readonly logger: Logger; constructor( private readonly dbService: EdgeDBService, @@ -47,6 +48,7 @@ export class SignInService implements OnModuleInit { private readonly emailService: EmailService, ) { this.disabledQueue = new Set(); + this.logger = new Logger(SignInService.name); } async onModuleInit() { @@ -146,39 +148,45 @@ export class SignInService implements OnModuleInit { await this.removeFromQueue(location, user.id); } - async registerUser(location: Location, register_user: RegisterUserDto) { - // There may be a way to do this in fewer, more atomic steps, just haven't figured out how - let user = await this.userService.findByUcardNumber(parseInt(register_user.ucard_number.slice(3))); - + async getUserForSignIn( + location: Location, + ucard_number: string, + ): Promise { + let user = await this.dbService.query( + e.select(e.users.User, (user) => ({ + filter_single: { ucard_number: ldapLibraryToUcardNumber(ucard_number) }, + ...UserProps(user), + is_rep: e.select(e.op(user.__type__.name, "=", "users::Rep")), + registered: e.select(true as boolean), + })), + ); if (user) { - if (user.ucard_number > 0) { - throw new BadRequestException({ - message: `User ${register_user.ucard_number} is already registered`, - code: ErrorCodes.already_registered, - }); - } - } else { - // no user registered, fetch from ldap - const ldapUser = await this.ldapService.findUserByUcardNumber(register_user.ucard_number); - if (!ldapUser) { - throw new NotFoundException({ - message: `User with ucard no ${register_user.ucard_number} couldn't be found. Perhaps you made a typo? (it should look like 001739897)`, - code: ErrorCodes.ldap_not_found, - }); - } - user = await this.userService.insertLdapUser(ldapUser); + return user; } - await this.dbService.query( - e.insert(e.sign_in.UserRegistration, { - location: castLocation(location), - user: e.select(e.users.User, () => ({ - filter_single: { ucard_number: e.int64(parseInt(register_user.ucard_number.toString().slice(3))) }, - })), - }), + this.logger.log(`Registering user: ${ucard_number} at location: ${location}`, SignInService.name); + + // no user registered, fetch from ldap + const ldapUser = await this.ldapService.findUserByUcardNumber(ucard_number); + if (!ldapUser) { + throw new NotFoundException({ + message: `User with ucard no ${ucard_number} couldn't be found. Perhaps you made a typo? (it should look like 001739897)`, + code: ErrorCodes.ldap_not_found, + }); + } + + user = await this.dbService.query( + e.select( + e.insert(e.sign_in.UserRegistration, { + location: castLocation(location), + user: e.insert(e.users.User, this.userService.ldapUserProps(ldapUser)), + }).user, + (user) => ({ ...UserProps(user), is_rep: e.select(false as boolean), registered: e.select(false as boolean) }), + ), ); await this.emailService.sendWelcomeEmail(user); + return user; } async getTrainings(id: string, location: Location): Promise { diff --git a/apps/anvil/src/users/users.service.ts b/apps/anvil/src/users/users.service.ts index 6faef75..2290b4e 100644 --- a/apps/anvil/src/users/users.service.ts +++ b/apps/anvil/src/users/users.service.ts @@ -2,6 +2,7 @@ import { GoogleUser } from "@/auth/interfaces/google-user.interface"; import { LdapUser } from "@/auth/interfaces/ldap-user.interface"; import { EdgeDBService } from "@/edgedb/edgedb.service"; import { LdapService } from "@/ldap/ldap.service"; +import { ErrorCodes } from "@/shared/constants/ErrorCodes"; import e from "@dbschema/edgeql-js"; import { addInPersonTraining } from "@dbschema/queries/addInPersonTraining.query"; import { users } from "@ignis/types"; @@ -89,7 +90,7 @@ function removeDomain(email: string): string { return email.slice(0, email.length - "@sheffield.ac.uk".length); } -function ldapLibraryToUcardNumber(shefLibraryNumber: string): number { +export function ldapLibraryToUcardNumber(shefLibraryNumber: string): number { return parseInt(shefLibraryNumber.slice(3)); } @@ -213,9 +214,14 @@ export class UsersService { if (!user) { const ldapUser = await this.ldapService.findUserByEmail(googleUser.email); if (!ldapUser) { - throw new Error("Failed to fetch a matching user on LDAP"); + throw new NotFoundException({ + message: `User with email ${googleUser.email} couldn't be found`, + code: ErrorCodes.ldap_not_found, + }); } - user = await this.insertLdapUser(ldapUser, googleUser.picture); + user = await this.dbService.query( + e.assert_single(e.select(e.insert(e.users.User, this.ldapUserProps(ldapUser, googleUser.picture)), UserProps)), + ); } if (user.profile_picture !== googleUser.picture) { @@ -233,8 +239,8 @@ export class UsersService { return user; } - async insertLdapUser(ldapUser: LdapUser, profile_picture: string | undefined = undefined): Promise { - return await this.create({ + ldapUserProps(ldapUser: LdapUser, profile_picture: string | undefined = undefined) { + return { username: ldapUser.uid, email: removeDomain(ldapUser.mail), first_name: ldapUser.givenName, @@ -243,14 +249,7 @@ export class UsersService { roles: e.select(e.auth.Role, () => ({ filter_single: { name: "User" } })), ucard_number: ldapLibraryToUcardNumber(ldapUser.shefLibraryNumber), profile_picture, - }); - } - - async createOrFindLdapUser(ldapUser: LdapUser): Promise { - return ( - (await this.findByUcardNumber(ldapLibraryToUcardNumber(ldapUser.shefLibraryNumber))) ?? - (await this.insertLdapUser(ldapUser)) - ); + }; } async idToUsername(id: string) { diff --git a/apps/forge/src/components/signin/actions/QueueDispatcher.tsx b/apps/forge/src/components/signin/actions/QueueDispatcher.tsx index 575f9c6..571f027 100644 --- a/apps/forge/src/components/signin/actions/QueueDispatcher.tsx +++ b/apps/forge/src/components/signin/actions/QueueDispatcher.tsx @@ -1,18 +1,17 @@ -import { useMutation } from "@tanstack/react-query"; import { AppDispatch, AppRootState } from "@/redux/store.ts"; -import { useDispatch, useSelector } from "react-redux"; +import { useMutation } from "@tanstack/react-query"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { useDispatch, useSelector } from "react-redux"; -import { Loader } from "@ui/components/ui/loader.tsx"; -import { Button } from "@ui/components/ui/button.tsx"; -import { useState } from "react"; +import { errorDisplay } from "@/components/errors/ErrorDisplay"; import { signinActions } from "@/redux/signin.slice.ts"; +import { PostQueueInPerson, PostQueueProps } from "@/services/signin/queueService.ts"; import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; +import { Button } from "@ui/components/ui/button.tsx"; +import { Loader } from "@ui/components/ui/loader.tsx"; +import { useState } from "react"; import { toast } from "sonner"; -import { PostQueueInPerson, PostQueueProps } from "@/services/signin/queueService.ts"; -import { errorDisplay } from "@/components/errors/ErrorDisplay"; -import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const QueueDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const dispatch: AppDispatch = useDispatch(); @@ -25,7 +24,7 @@ const QueueDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queueProps: PostQueueProps = { locationName: activeLocation, - uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), + uCardNumber: signInSession?.ucard_number ?? "", signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx b/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx index 9580f20..d817d09 100644 --- a/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx +++ b/apps/forge/src/components/signin/actions/RegisterDispatcher.tsx @@ -24,7 +24,7 @@ const RegisterDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const registerProps: PostRegisterProps = { locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? "0", + uCardNumber: signInSession?.ucard_number ?? "", signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/SignInDispatcher.tsx b/apps/forge/src/components/signin/actions/SignInDispatcher.tsx index 3bab350..afbe892 100644 --- a/apps/forge/src/components/signin/actions/SignInDispatcher.tsx +++ b/apps/forge/src/components/signin/actions/SignInDispatcher.tsx @@ -1,18 +1,17 @@ +import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { PostSignIn, PostSignInProps } from "@/services/signin/signInService.ts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { AppDispatch, AppRootState } from "@/redux/store.ts"; -import { useDispatch, useSelector } from "react-redux"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { useDispatch, useSelector } from "react-redux"; -import { Loader } from "@ui/components/ui/loader.tsx"; -import { Button } from "@ui/components/ui/button.tsx"; -import { useEffect, useState } from "react"; +import { errorDisplay } from "@/components/errors/ErrorDisplay"; import { signinActions } from "@/redux/signin.slice.ts"; import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; +import { Button } from "@ui/components/ui/button.tsx"; +import { Loader } from "@ui/components/ui/loader.tsx"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; -import { errorDisplay } from "@/components/errors/ErrorDisplay"; -import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const SignInDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queryClient = useQueryClient(); @@ -26,10 +25,10 @@ const SignInDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signInProps: PostSignInProps = { locationName: activeLocation, - uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), + uCardNumber: signInSession?.ucard_number ?? "", signal: abortController.signal, postBody: { - ucard_number: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), + ucard_number: signInSession?.ucard_number ?? "", location: activeLocation, reason_id: signInSession?.sign_in_reason?.id ?? "", tools: signInSession?.training?.map((training) => training.name) ?? [], diff --git a/apps/forge/src/components/signin/actions/SignInManager.tsx b/apps/forge/src/components/signin/actions/SignInManager.tsx index 9bea9ba..cf1efa5 100644 --- a/apps/forge/src/components/signin/actions/SignInManager.tsx +++ b/apps/forge/src/components/signin/actions/SignInManager.tsx @@ -1,29 +1,29 @@ -import React, { ReactElement, useEffect, useLayoutEffect, useState } from "react"; -import UCardInput from "@/components/signin/actions/UCardInput.tsx"; +import QueueDispatcher from "@/components/signin/actions/QueueDispatcher.tsx"; +import RegisterDispatcher from "@/components/signin/actions/RegisterDispatcher.tsx"; +import SignInDispatcher from "@/components/signin/actions/SignInDispatcher.tsx"; +import SignInFlowProgress from "@/components/signin/actions/SignInFlowProgress.tsx"; import SignInReasonInput from "@/components/signin/actions/SignInReasonInput.tsx"; -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import SignOutDispatcher from "@/components/signin/actions/SignOutDispatcher.tsx"; +import ToolSelectionInput from "@/components/signin/actions/ToolSelectionInput.tsx"; +import UCardInput from "@/components/signin/actions/UCardInput.tsx"; +import useDoubleTapEscape from "@/hooks/useDoubleTapEscape.ts"; import { signinActions } from "@/redux/signin.slice.ts"; -import { SignInSession } from "@/types/signin.ts"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { AnyStep, EnqueueSteps, FlowConfiguration, FlowStepComponent, FlowType, - flowTypeToPrintTable, RegisterSteps, SignInSteps, SignOutSteps, + flowTypeToPrintTable, } from "@/types/signInActions.ts"; -import ToolSelectionInput from "@/components/signin/actions/ToolSelectionInput.tsx"; -import SignInDispatcher from "@/components/signin/actions/SignInDispatcher.tsx"; -import SignOutDispatcher from "@/components/signin/actions/SignOutDispatcher.tsx"; -import SignInFlowProgress from "@/components/signin/actions/SignInFlowProgress.tsx"; +import { SignInSession } from "@/types/signin.ts"; import { Button } from "@ui/components/ui/button.tsx"; -import useDoubleTapEscape from "@/hooks/useDoubleTapEscape.ts"; -import QueueDispatcher from "@/components/signin/actions/QueueDispatcher.tsx"; -import RegisterDispatcher from "@/components/signin/actions/RegisterDispatcher.tsx"; +import React, { ReactElement, useEffect, useLayoutEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; const flowConfig: FlowConfiguration = { [FlowType.SignIn]: { @@ -47,7 +47,7 @@ const flowConfig: FlowConfiguration = { }; const defaultSignInSession: SignInSession = { - ucard_number: 0, + ucard_number: "", is_rep: false, sign_in_reason: null, training: null, diff --git a/apps/forge/src/components/signin/actions/SignInReasonInput.tsx b/apps/forge/src/components/signin/actions/SignInReasonInput.tsx index 016dd0d..d658bc0 100644 --- a/apps/forge/src/components/signin/actions/SignInReasonInput.tsx +++ b/apps/forge/src/components/signin/actions/SignInReasonInput.tsx @@ -1,9 +1,9 @@ import { ErrorDisplayProps, errorDisplay } from "@/components/errors/ErrorDisplay"; import { Category } from "@/components/icons/SignInReason.tsx"; -import { FlowStepComponent } from "@/types/signInActions.ts"; -import { signinActions } from "@/redux/signin.slice.ts"; -import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { signinActions, useSignInSessionField } from "@/redux/signin.slice.ts"; +import { AppDispatch } from "@/redux/store.ts"; import { useSignInReasons } from "@/services/signin/signInReasonService.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import type { Reason } from "@ignis/types/sign_in.ts"; import { Button } from "@ui/components/ui/button.tsx"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; @@ -11,18 +11,16 @@ import { Input } from "@ui/components/ui/input.tsx"; import { Loader } from "@ui/components/ui/loader.tsx"; import Fuse from "fuse.js"; import React, { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; const SignInReasonInput: FlowStepComponent = ({ onSecondary, onPrimary }) => { const [inputValue, setInputValue] = useState(""); - const [selectedReason, setSelectedReason] = useState( - useSelector((state: AppRootState) => state.signin.session?.sign_in_reason ?? null), - ); + const [selectedReason, setSelectedReason] = useState(useSignInSessionField("sign_in_reason") ?? null); const { data: signInReasons, isLoading, isError, error } = useSignInReasons(); const [fuse, setFuse] = useState>(new Fuse([], { keys: ["name"] })); const [highlightedIndex, setHighlightedIndex] = useState(-1); const [canContinue, setCanContinue] = useState(false); - const hasSessionError = useSelector((state: AppRootState) => state.signin.session?.session_errored ?? false); + const hasSessionError = useSignInSessionField("session_errored") ?? false; const dispatch: AppDispatch = useDispatch(); useEffect(() => { diff --git a/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx b/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx index 7c9319c..d9df804 100644 --- a/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx +++ b/apps/forge/src/components/signin/actions/SignOutDispatcher.tsx @@ -1,18 +1,17 @@ +import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { PostSignOut, PostSignOutProps } from "@/services/signin/signInService.ts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { AppDispatch, AppRootState } from "@/redux/store.ts"; -import { useDispatch, useSelector } from "react-redux"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { useDispatch, useSelector } from "react-redux"; -import { Loader } from "@ui/components/ui/loader.tsx"; -import { Button } from "@ui/components/ui/button.tsx"; -import { useEffect, useState } from "react"; +import { errorDisplay } from "@/components/errors/ErrorDisplay"; import { signinActions } from "@/redux/signin.slice.ts"; import { FlowStepComponent } from "@/types/signInActions.ts"; import { useNavigate } from "@tanstack/react-router"; +import { Button } from "@ui/components/ui/button.tsx"; +import { Loader } from "@ui/components/ui/loader.tsx"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; -import { errorDisplay } from "@/components/errors/ErrorDisplay"; -import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; const SignOutDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const queryClient = useQueryClient(); @@ -26,7 +25,7 @@ const SignOutDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signOutProps: PostSignOutProps = { locationName: activeLocation, - uCardNumber: fullUCardToDBRepresentation(signInSession?.ucard_number ?? "0"), + uCardNumber: signInSession?.ucard_number ?? "", signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx b/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx index e9fb8db..e05a145 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx +++ b/apps/forge/src/components/signin/actions/ToolSelectionInput.tsx @@ -1,10 +1,10 @@ import { errorDisplay } from "@/components/errors/ErrorDisplay"; -import { FlowStepComponent } from "@/types/signInActions.ts"; import { SelectedTrainingPipDisplay } from "@/components/signin/actions/SelectedTrainingPipDisplay.tsx"; import ToolSelectionList from "@/components/signin/actions/TrainingSelectionList.tsx"; -import { signinActions } from "@/redux/signin.slice.ts"; +import { signinActions, useSignInSessionField } from "@/redux/signin.slice.ts"; import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { GetSignIn, GetSignInProps } from "@/services/signin/signInService.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { Training, User } from "@ignis/types/sign_in.ts"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; @@ -16,7 +16,6 @@ import { Loader } from "@ui/components/ui/loader.tsx"; import { ChevronsDownUp, ChevronsUpDown } from "lucide-react"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { fullUCardToDBRepresentation } from "@/lib/utils.ts"; /* three categories of tools that can be selected: @@ -32,7 +31,7 @@ const ToolSelectionInput: FlowStepComponent = ({ onSecondary, onPrimary }) => { const abortController = new AbortController(); // For gracefully cancelling the query const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); - const ucardNumber = useSelector((state: AppRootState) => state.signin.session?.ucard_number); + const ucardNumber = useSignInSessionField("ucard_number"); const [isOpen, setIsOpen] = useState(false); const [trainingMap, setTrainingMap] = useState({ @@ -52,7 +51,7 @@ const ToolSelectionInput: FlowStepComponent = ({ onSecondary, onPrimary }) => { const signInProps: GetSignInProps = { locationName: activeLocation, - uCardNumber: fullUCardToDBRepresentation(ucardNumber ?? "0"), + uCardNumber: ucardNumber ?? "", signal: abortController.signal, }; diff --git a/apps/forge/src/components/signin/actions/UCardInput.tsx b/apps/forge/src/components/signin/actions/UCardInput.tsx index eafcc7e..0aa3ecc 100644 --- a/apps/forge/src/components/signin/actions/UCardInput.tsx +++ b/apps/forge/src/components/signin/actions/UCardInput.tsx @@ -1,6 +1,8 @@ -import { FlowStepComponent } from "@/types/signInActions.ts"; +import { UCARD_LENGTH } from "@/lib/constants"; +import { ucardNumberToString } from "@/lib/utils"; import { signinActions, useSignInSessionField } from "@/redux/signin.slice.ts"; import { AppDispatch } from "@/redux/store.ts"; +import { FlowStepComponent } from "@/types/signInActions.ts"; import { Button } from "@ui/components/ui/button.tsx"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@ui/components/ui/input-otp.tsx"; @@ -8,10 +10,8 @@ import { useEffect, useRef, useState } from "react"; import { useDispatch } from "react-redux"; const UCardInput: FlowStepComponent = ({ onPrimary }) => { - const UCARD_LENGTH = 9; // Total length of the OTP const dispatch = useDispatch(); - const originalUcard = useSignInSessionField("ucard_number"); - const [otp, setOtp] = useState(originalUcard ? originalUcard.toString().padStart(UCARD_LENGTH, "0") : ""); // OTP is now handled as a string + const [otp, setOtp] = useState(useSignInSessionField("ucard_number")); // OTP is now handled as a string const [isOtpValid, setIsOtpValid] = useState(otp.length === UCARD_LENGTH); const handleOtpChange = (value: string) => { diff --git a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/index.tsx b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/index.tsx index 1c62710..111fc23 100644 --- a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/index.tsx +++ b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/index.tsx @@ -6,6 +6,7 @@ import { SignInReasonDisplay } from "@/components/signin/dashboard/components/Si import { TimeDisplay } from "@/components/signin/dashboard/components/SignedInUserCard/TimeDisplay.tsx"; import { iForgeEpoch } from "@/config/constants.ts"; import { REP_OFF_SHIFT, REP_ON_SHIFT } from "@/lib/constants.ts"; +import { ucardNumberToString } from "@/lib/utils"; import { AppRootState } from "@/redux/store.ts"; import { PostSignOut, PostSignOutProps } from "@/services/signin/signInService.ts"; import type { PartialReason } from "@ignis/types/sign_in.ts"; @@ -47,7 +48,7 @@ export const SignedInUserCard: React.FunctionComponent = ({ const signOutProps: PostSignOutProps = { locationName: activeLocation, - uCardNumber: user.ucard_number, + uCardNumber: ucardNumberToString(user.ucard_number), signal: abortController.signal, }; diff --git a/apps/forge/src/lib/constants.ts b/apps/forge/src/lib/constants.ts index 418b9dc..5442e1b 100644 --- a/apps/forge/src/lib/constants.ts +++ b/apps/forge/src/lib/constants.ts @@ -5,3 +5,4 @@ export const LOCATIONS: Location[] = ["mainspace", "heartspace"]; export const REP_ON_SHIFT = "Rep On Shift"; export const REP_OFF_SHIFT = "Rep Off Shift"; export const INFRACTION_TYPES: InfractionType[] = ["WARNING", "TEMP_BAN", "PERM_BAN", "RESTRICTION", "TRAINING_ISSUE"]; +export const UCARD_LENGTH = 9; diff --git a/apps/forge/src/lib/utils.ts b/apps/forge/src/lib/utils.ts index 7d4b939..c1d0454 100644 --- a/apps/forge/src/lib/utils.ts +++ b/apps/forge/src/lib/utils.ts @@ -5,6 +5,7 @@ import { type ClassValue, clsx } from "clsx"; import md5 from "md5"; import { useSelector } from "react-redux"; import { twMerge } from "tailwind-merge"; +import { UCARD_LENGTH } from "./constants"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -46,6 +47,6 @@ export function extractError(error: Error): string { return error?.message || "Unknown Error. Contact the IT Team"; } -export function fullUCardToDBRepresentation(ucard_number: string): number { - return parseInt(ucard_number.slice(3), 10); +export function ucardNumberToString(ucard_number: number): string { + return ucard_number.toString().padStart(UCARD_LENGTH, "0"); } diff --git a/apps/forge/src/services/signin/signInService.ts b/apps/forge/src/services/signin/signInService.ts index 8d97cab..219b0a1 100644 --- a/apps/forge/src/services/signin/signInService.ts +++ b/apps/forge/src/services/signin/signInService.ts @@ -3,7 +3,7 @@ import { FinaliseSignInDto, User } from "@ignis/types/sign_in.ts"; export interface GetSignInProps { locationName: string; - uCardNumber: number; + uCardNumber: string; signal: AbortSignal; } @@ -20,7 +20,7 @@ export const GetSignIn = async ({ locationName, uCardNumber, signal }: GetSignIn export interface PostSignInProps { signal: AbortSignal; locationName: string; - uCardNumber: number; + uCardNumber: string; postBody: FinaliseSignInDto; } @@ -39,7 +39,7 @@ export const PostSignIn = async ({ locationName, uCardNumber, signal, postBody } export interface PostSignOutProps { signal: AbortSignal; locationName: string; - uCardNumber: number; + uCardNumber: string; } export const PostSignOut = async ({ locationName, uCardNumber, signal }: PostSignOutProps): Promise => { diff --git a/packages/types/sign_in.ts b/packages/types/sign_in.ts index 794e4be..08c55e3 100644 --- a/packages/types/sign_in.ts +++ b/packages/types/sign_in.ts @@ -1,11 +1,8 @@ -import e from "@dbschema/edgeql-js"; import { sign_in, std } from "@dbschema/interfaces"; -import { z } from "zod"; import * as users from "./users"; export type { CreateSignInDto, - RegisterUserDto, FinaliseSignInDto, UpdateSignInDto, } from "@/sign-in/dto/sigs-in-dto"; @@ -61,6 +58,8 @@ export type Training = Omit Date: Sat, 27 Apr 2024 02:46:49 +0100 Subject: [PATCH 6/6] chore: non-functional changes/various improvements --- apps/anvil/src/sign-in/sign-in.service.ts | 51 ++++++++++--------- apps/anvil/src/users/users.service.ts | 25 --------- .../SignedInUserCard/InfractionSection.tsx | 2 +- 3 files changed, 28 insertions(+), 50 deletions(-) diff --git a/apps/anvil/src/sign-in/sign-in.service.ts b/apps/anvil/src/sign-in/sign-in.service.ts index 996867d..9846889 100644 --- a/apps/anvil/src/sign-in/sign-in.service.ts +++ b/apps/anvil/src/sign-in/sign-in.service.ts @@ -682,34 +682,37 @@ export class SignInService implements OnModuleInit { throw new HttpException("The queue is currently not in use", HttpStatus.SERVICE_UNAVAILABLE); } - await this.dbService.query( - e.delete(e.sign_in.QueuePlace, (queue_place) => ({ - filter: e.op(queue_place.user.id, "=", e.cast(e.uuid, user_id)), - })), - ); - - await this.dbService.query( - e.update(e.sign_in.QueuePlace, (queue_place) => ({ - filter: e.op(queue_place.location, "=", castLocation(location)), - set: { - position: e.op(queue_place.position, "-", 1), - }, - })), - ); + await this.dbService.client.transaction(async (tx) => { + await e + .delete(e.sign_in.QueuePlace, (queue_place) => ({ + filter: e.op(queue_place.user.id, "=", e.uuid(id)), + })) + .run(tx); + + await e + .update(e.sign_in.QueuePlace, (queue_place) => ({ + filter: e.op(queue_place.location, "=", castLocation(location)), + set: { + position: e.op(queue_place.position, "-", 1), + }, + })) + .run(tx); + }); } async queuedUsersThatCanSignIn(location: Location) { - const queue_places = await this.dbService.query( - e.select(e.sign_in.QueuePlace, (queue_place) => ({ - user: PartialUserProps(queue_place.user), - filter: e.op( - e.op(queue_place.location, "=", e.cast(e.sign_in.SignInLocation, castLocation(location))), - "and", - e.op(queue_place.can_sign_in, "=", true), - ), - })), + return await this.dbService.query( + e.select( + e.select(e.sign_in.QueuePlace, (queue_place) => ({ + filter: e.op( + e.op(queue_place.location, "=", e.cast(e.sign_in.SignInLocation, castLocation(location))), + "and", + e.op(queue_place.can_sign_in, "=", true), + ), + })).user, + PartialUserProps, + ), ); - return queue_places.map((queue_place) => queue_place.user); } async getSignInReasons() { diff --git a/apps/anvil/src/users/users.service.ts b/apps/anvil/src/users/users.service.ts index 2290b4e..e05ca51 100644 --- a/apps/anvil/src/users/users.service.ts +++ b/apps/anvil/src/users/users.service.ts @@ -252,31 +252,6 @@ export class UsersService { }; } - async idToUsername(id: string) { - return await this.dbService.query( - e.select(e.users.User, () => ({ - username: true, - filter_single: { id }, - })).username, - ); - } - - async ucardNumberToUsername(ucard_number: number) { - return await this.dbService.query( - e.select(e.users.User, () => ({ - filter_single: { ucard_number }, - })).username, - ); - } - - async ucardNumberToID(ucard_number: number) { - return await this.dbService.query( - e.select(e.users.User, () => ({ - filter_single: { ucard_number }, - })).id, - ); - } - async signInStats(id: string): Promise { const groupings = await this.dbService.query( e.group( diff --git a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/InfractionSection.tsx b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/InfractionSection.tsx index 22c3df7..bd3890c 100644 --- a/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/InfractionSection.tsx +++ b/apps/forge/src/components/signin/dashboard/components/SignedInUserCard/InfractionSection.tsx @@ -72,7 +72,7 @@ export const InfractionSection: React.FC = ({ user, loca }); switch (type) { - case "TEMP_BAN": // FIXME this insta break + case "TEMP_BAN": buttonDisabled = !(type && date?.from && date?.to); extra_field = ( <>