From 9d20a98dbbc322fa6f0644e8b31e6b97769887ce Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 12 Dec 2024 17:21:28 +0100 Subject: [PATCH] feat: add support for Postgres database provider (#79) --- .env.test | 2 - .github/workflows/e2e-tests.yml | 117 ++++++++++++++-- README.md | 28 ++-- backend/.env.example | 4 +- backend/go.mod | 8 +- backend/go.sum | 56 +++++++- backend/internal/bootstrap/db_bootstrap.go | 43 ++++-- backend/internal/common/env_config.go | 59 +++++--- .../controller/webauthn_controller.go | 4 +- backend/internal/model/types/date_time.go | 7 +- backend/internal/model/user.go | 2 +- backend/internal/model/webauthn.go | 5 +- backend/internal/service/test_service.go | 30 ++++- backend/internal/service/webauthn_service.go | 17 +-- .../postgres/20241211111554_init.up.sql | 126 ++++++++++++++++++ .../{ => sqlite}/20240731203656_init.up.sql | 0 ...0240813211251_passkey_backup_flags..up.sql | 0 ...240813211251_passkey_backup_flags.down.sql | 0 ...0240817191051_rename_config_table.down.sql | 0 .../20240817191051_rename_config_table.up.sql | 0 ...0820205521_multiple_callback_urls.down.sql | 0 ...240820205521_multiple_callback_urls.up.sql | 0 .../20240908123031_audit_log.down.sql | 0 .../20240908123031_audit_log.up.sql | 0 .../20240924202721_user_groups.down.sql | 0 .../20240924202721_user_groups.up.sql | 0 ...20241004092030_audit_log_location.down.sql | 0 .../20241004092030_audit_log_location.up.sql | 0 .../20241023072742_unix-timestamps.down.sql | 0 .../20241023072742_unix-timestamps.up.sql | 0 ...25214824_app_config_default_value.down.sql | 0 ...1025214824_app_config_default_value.up.sql | 0 .../20241028064959_custom_claims.down.sql | 0 .../20241028064959_custom_claims.up.sql | 0 .../{ => sqlite}/20241115131129_pkce.down.sql | 0 .../{ => sqlite}/20241115131129_pkce.up.sql | 0 frontend/tests/user-settings.spec.ts | 2 + scripts/docker/entrypoint.sh | 4 +- 38 files changed, 433 insertions(+), 81 deletions(-) delete mode 100644 .env.test create mode 100644 backend/migrations/postgres/20241211111554_init.up.sql rename backend/migrations/{ => sqlite}/20240731203656_init.up.sql (100%) rename backend/migrations/{ => sqlite}/20240813211251_passkey_backup_flags..up.sql (100%) rename backend/migrations/{ => sqlite}/20240813211251_passkey_backup_flags.down.sql (100%) rename backend/migrations/{ => sqlite}/20240817191051_rename_config_table.down.sql (100%) rename backend/migrations/{ => sqlite}/20240817191051_rename_config_table.up.sql (100%) rename backend/migrations/{ => sqlite}/20240820205521_multiple_callback_urls.down.sql (100%) rename backend/migrations/{ => sqlite}/20240820205521_multiple_callback_urls.up.sql (100%) rename backend/migrations/{ => sqlite}/20240908123031_audit_log.down.sql (100%) rename backend/migrations/{ => sqlite}/20240908123031_audit_log.up.sql (100%) rename backend/migrations/{ => sqlite}/20240924202721_user_groups.down.sql (100%) rename backend/migrations/{ => sqlite}/20240924202721_user_groups.up.sql (100%) rename backend/migrations/{ => sqlite}/20241004092030_audit_log_location.down.sql (100%) rename backend/migrations/{ => sqlite}/20241004092030_audit_log_location.up.sql (100%) rename backend/migrations/{ => sqlite}/20241023072742_unix-timestamps.down.sql (100%) rename backend/migrations/{ => sqlite}/20241023072742_unix-timestamps.up.sql (100%) rename backend/migrations/{ => sqlite}/20241025214824_app_config_default_value.down.sql (100%) rename backend/migrations/{ => sqlite}/20241025214824_app_config_default_value.up.sql (100%) rename backend/migrations/{ => sqlite}/20241028064959_custom_claims.down.sql (100%) rename backend/migrations/{ => sqlite}/20241028064959_custom_claims.up.sql (100%) rename backend/migrations/{ => sqlite}/20241115131129_pkce.down.sql (100%) rename backend/migrations/{ => sqlite}/20241115131129_pkce.up.sql (100%) diff --git a/.env.test b/.env.test deleted file mode 100644 index dad03cc..0000000 --- a/.env.test +++ /dev/null @@ -1,2 +0,0 @@ -APP_ENV=test -PUBLIC_APP_URL=http://localhost \ No newline at end of file diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5aff7f4..ac31e06 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -5,22 +5,43 @@ on: pull_request: branches: [main] jobs: - build-and-test: + build: timeout-minutes: 20 runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and export + uses: docker/build-push-action@v6 + with: + tags: stonith404/pocket-id:test + outputs: type=docker,dest=/tmp/docker-image.tar + + - name: Upload Docker image artifact + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: /tmp/docker-image.tar + + test-sqlite: + runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: lts/* - cache: 'npm' + cache: "npm" cache-dependency-path: frontend/package-lock.json - - name: Build Docker Image - run: docker build -t stonith404/pocket-id . - - - name: Run Docker Container - run: docker run -d --name pocket-id -p 80:80 --env-file .env.test stonith404/pocket-id + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: docker-image + path: /tmp + - name: Load Docker Image + run: docker load -i /tmp/docker-image.tar - name: Install frontend dependencies working-directory: ./frontend @@ -30,6 +51,13 @@ jobs: working-directory: ./frontend run: npx playwright install --with-deps chromium + - name: Run Docker Container with Sqlite DB + run: | + docker run -d --name pocket-id-sqlite \ + -p 80:80 \ + -e APP_ENV=test \ + stonith404/pocket-id:test + - name: Run Playwright tests working-directory: ./frontend run: npx playwright test @@ -37,7 +65,80 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report + name: playwright-report-sqlite + path: frontend/tests/.report + include-hidden-files: true + retention-days: 15 + + test-postgres: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: docker-image + path: /tmp + - name: Load Docker Image + run: docker load -i /tmp/docker-image.tar + + - name: Install frontend dependencies + working-directory: ./frontend + run: npm ci + + - name: Install Playwright Browsers + working-directory: ./frontend + run: npx playwright install --with-deps chromium + + - name: Create Docker network + run: docker network create pocket-id-network + + - name: Start Postgres DB + run: | + docker run -d --name pocket-id-db \ + --network pocket-id-network \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=pocket-id \ + -p 5432:5432 \ + postgres:17 + + - name: Wait for Postgres to start + run: | + for i in {1..10}; do + if docker exec pocket-id-db pg_isready -U postgres; then + echo "Postgres is ready" + break + fi + echo "Waiting for Postgres..." + sleep 2 + done + + - name: Run Docker Container with Postgres DB + run: | + docker run -d --name pocket-id-postgres \ + --network pocket-id-network \ + -p 80:80 \ + -e APP_ENV=test \ + -e DB_PROVIDER=postgres \ + -e POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \ + stonith404/pocket-id:test + + - name: Run Playwright tests + working-directory: ./frontend + run: npx playwright test + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-postgres path: frontend/tests/.report include-hidden-files: true retention-days: 15 diff --git a/README.md b/README.md index 24cacf8..e317f9b 100644 --- a/README.md +++ b/README.md @@ -141,19 +141,21 @@ docker compose up -d ## Environment variables -| Variable | Default Value | Recommended to change | Description | -| ---------------------- | ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. | -| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. | -| `MAXMIND_LICENSE_KEY` | `-` | yes | License Key for the GeoLite2 Database. The license key is required to retrieve the geographical location of IP addresses in the audit log. If the key is not provided, IP locations will be marked as "unknown." You can obtain a license key for free [here](https://www.maxmind.com/en/geolite2/signup). | -| `PUID` and `PGID` | `1000` | yes | The user and group ID of the user who should run Pocket ID inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). | -| `DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. | -| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. | -| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. | -| `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. | -| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. | -| `PORT` | `3000` | no | The port on which the frontend should listen. | -| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | +| Variable | Default Value | Recommended to change | Description | +| ---------------------------- | ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. | +| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. | +| `MAXMIND_LICENSE_KEY` | `-` | yes | License Key for the GeoLite2 Database. The license key is required to retrieve the geographical location of IP addresses in the audit log. If the key is not provided, IP locations will be marked as "unknown." You can obtain a license key for free [here](https://www.maxmind.com/en/geolite2/signup). | +| `PUID` and `PGID` | `1000` | yes | The user and group ID of the user who should run Pocket ID inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). | +| `DB_PROVIDER` | `sqlite` | no | The database provider you want to use. Currently `sqlite` and `postgres` are supported. | +| `SQLITE_DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. This gets ignored if you didn't set `DB_PROVIDER` to `sqlite`. | +| `POSTGRES_CONNECTION_STRING` | `-` | no | The connection string to your Postgres database. This gets ignored if you didn't set `DB_PROVIDER` to `postgres`. A connection string can look like this: `postgresql://user:password@host:5432/pocket-id`. | +| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. | +| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. | +| `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. | +| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. | +| `PORT` | `3000` | no | The port on which the frontend should listen. | +| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | ## Contribute diff --git a/backend/.env.example b/backend/.env.example index 9fd8c8f..c86f02f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,8 @@ APP_ENV=production PUBLIC_APP_URL=http://localhost -DB_PATH=data/pocket-id.db +DB_PROVIDER=sqlite +SQLITE_DB_PATH=data/pocket-id.db +POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id UPLOAD_PATH=data/uploads PORT=8080 HOST=localhost \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 8e8be49..3b61bfe 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,7 +5,6 @@ go 1.23.1 require ( github.com/caarlos0/env/v11 v11.2.2 github.com/fxamacker/cbor/v2 v2.7.0 - github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/go-co-op/gocron/v2 v2.12.1 github.com/go-playground/validator/v10 v10.22.1 @@ -18,6 +17,7 @@ require ( github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 golang.org/x/crypto v0.27.0 golang.org/x/time v0.6.0 + gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.12 ) @@ -36,6 +36,10 @@ require ( github.com/google/go-tpm v0.9.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect @@ -43,6 +47,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.23 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -57,6 +62,7 @@ require ( golang.org/x/arch v0.10.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.29.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/backend/go.sum b/backend/go.sum index ffacd0f..e975d96 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,7 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -13,18 +17,32 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= +github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= -github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= -github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw= github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -39,6 +57,8 @@ github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= @@ -55,6 +75,14 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -85,16 +113,28 @@ github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNG github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc= github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -118,6 +158,14 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -130,6 +178,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= @@ -146,6 +196,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= diff --git a/backend/internal/bootstrap/db_bootstrap.go b/backend/internal/bootstrap/db_bootstrap.go index 3b8830e..bf298e4 100644 --- a/backend/internal/bootstrap/db_bootstrap.go +++ b/backend/internal/bootstrap/db_bootstrap.go @@ -2,9 +2,13 @@ package bootstrap import ( "errors" + "fmt" "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/database" + postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres" + sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/stonith404/pocket-id/backend/internal/common" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -19,15 +23,29 @@ func newDatabase() (db *gorm.DB) { log.Fatalf("failed to connect to database: %v", err) } sqlDb, err := db.DB() - sqlDb.SetMaxOpenConns(1) if err != nil { log.Fatalf("failed to get sql.DB: %v", err) } - driver, err := sqlite3.WithInstance(sqlDb, &sqlite3.Config{}) + // Choose the correct driver for the database provider + var driver database.Driver + switch common.EnvConfig.DbProvider { + case common.DbProviderSqlite: + driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{}) + case common.DbProviderPostgres: + driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{}) + default: + log.Fatalf("unsupported database provider: %s", common.EnvConfig.DbProvider) + } + if err != nil { + log.Fatalf("failed to create migration driver: %v", err) + } + + // Run migrations m, err := migrate.NewWithDatabaseInstance( - "file://migrations", - "postgres", driver) + "file://migrations/"+string(common.EnvConfig.DbProvider), + "pocket-id", driver, + ) if err != nil { log.Fatalf("failed to create migration instance: %v", err) } @@ -41,15 +59,20 @@ func newDatabase() (db *gorm.DB) { } func connectDatabase() (db *gorm.DB, err error) { - dbPath := common.EnvConfig.DBPath + var dialector gorm.Dialector - // Use in-memory database for testing - if common.EnvConfig.AppEnv == "test" { - dbPath = "file::memory:?cache=shared" + // Choose the correct database provider + switch common.EnvConfig.DbProvider { + case common.DbProviderSqlite: + dialector = sqlite.Open(common.EnvConfig.SqliteDBPath) + case common.DbProviderPostgres: + dialector = postgres.Open(common.EnvConfig.PostgresConnectionString) + default: + return nil, fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider) } for i := 1; i <= 3; i++ { - db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + db, err = gorm.Open(dialector, &gorm.Config{ TranslateError: true, Logger: getLogger(), }) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index e8a2eb7..d441146 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -6,32 +6,55 @@ import ( "log" ) +type DbProvider string + +const ( + DbProviderSqlite DbProvider = "sqlite" + DbProviderPostgres DbProvider = "postgres" +) + type EnvConfigSchema struct { - AppEnv string `env:"APP_ENV"` - AppURL string `env:"PUBLIC_APP_URL"` - DBPath string `env:"DB_PATH"` - UploadPath string `env:"UPLOAD_PATH"` - Port string `env:"BACKEND_PORT"` - Host string `env:"HOST"` - EmailTemplatesPath string `env:"EMAIL_TEMPLATES_PATH"` - MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` - GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` + AppEnv string `env:"APP_ENV"` + AppURL string `env:"PUBLIC_APP_URL"` + DbProvider DbProvider `env:"DB_PROVIDER"` + SqliteDBPath string `env:"SQLITE_DB_PATH"` + PostgresConnectionString string `env:"POSTGRES_CONNECTION_STRING"` + UploadPath string `env:"UPLOAD_PATH"` + Port string `env:"BACKEND_PORT"` + Host string `env:"HOST"` + EmailTemplatesPath string `env:"EMAIL_TEMPLATES_PATH"` + MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` + GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` } var EnvConfig = &EnvConfigSchema{ - AppEnv: "production", - DBPath: "data/pocket-id.db", - UploadPath: "data/uploads", - AppURL: "http://localhost", - Port: "8080", - Host: "localhost", - EmailTemplatesPath: "./email-templates", - MaxMindLicenseKey: "", - GeoLiteDBPath: "data/GeoLite2-City.mmdb", + AppEnv: "production", + DbProvider: "sqlite", + SqliteDBPath: "data/pocket-id.db", + PostgresConnectionString: "", + UploadPath: "data/uploads", + AppURL: "http://localhost", + Port: "8080", + Host: "localhost", + EmailTemplatesPath: "./email-templates", + MaxMindLicenseKey: "", + GeoLiteDBPath: "data/GeoLite2-City.mmdb", } func init() { if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil { log.Fatal(err) } + // Validate the environment variables + if EnvConfig.DbProvider != DbProviderSqlite && EnvConfig.DbProvider != DbProviderPostgres { + log.Fatal("Invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'") + } + + if EnvConfig.DbProvider == DbProviderPostgres && EnvConfig.PostgresConnectionString == "" { + log.Fatal("Missing POSTGRES_CONNECTION_STRING environment variable") + } + + if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" { + log.Fatal("Missing SQLITE_DB_PATH environment variable") + } } diff --git a/backend/internal/controller/webauthn_controller.go b/backend/internal/controller/webauthn_controller.go index bff2c29..a37f957 100644 --- a/backend/internal/controller/webauthn_controller.go +++ b/backend/internal/controller/webauthn_controller.go @@ -91,9 +91,7 @@ func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) { return } - userID := c.GetString("userID") - - user, token, err := wc.webAuthnService.VerifyLogin(sessionID, userID, credentialAssertionData, c.ClientIP(), c.Request.UserAgent()) + user, token, err := wc.webAuthnService.VerifyLogin(sessionID, credentialAssertionData, c.ClientIP(), c.Request.UserAgent()) if err != nil { c.Error(err) return diff --git a/backend/internal/model/types/date_time.go b/backend/internal/model/types/date_time.go index 17c5761..e8f53ab 100644 --- a/backend/internal/model/types/date_time.go +++ b/backend/internal/model/types/date_time.go @@ -2,6 +2,7 @@ package datatype import ( "database/sql/driver" + "github.com/stonith404/pocket-id/backend/internal/common" "time" ) @@ -14,7 +15,11 @@ func (date *DateTime) Scan(value interface{}) (err error) { } func (date DateTime) Value() (driver.Value, error) { - return time.Time(date).Unix(), nil + if common.EnvConfig.DbProvider == common.DbProviderSqlite { + return time.Time(date).Unix(), nil + } else { + return time.Time(date), nil + } } func (date DateTime) UTC() time.Time { diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 903f591..7682bd7 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -33,7 +33,7 @@ func (u User) WebAuthnCredentials() []webauthn.Credential { for i, credential := range u.Credentials { credentials[i] = webauthn.Credential{ - ID: []byte(credential.CredentialID), + ID: credential.CredentialID, AttestationType: credential.AttestationType, PublicKey: credential.PublicKey, Transport: credential.Transport, diff --git a/backend/internal/model/webauthn.go b/backend/internal/model/webauthn.go index b785643..f1f7ff1 100644 --- a/backend/internal/model/webauthn.go +++ b/backend/internal/model/webauthn.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "github.com/go-webauthn/webauthn/protocol" + datatype "github.com/stonith404/pocket-id/backend/internal/model/types" "time" ) @@ -12,7 +13,7 @@ type WebauthnSession struct { Base Challenge string - ExpiresAt time.Time + ExpiresAt datatype.DateTime UserVerification string } @@ -20,7 +21,7 @@ type WebauthnCredential struct { Base Name string - CredentialID string + CredentialID []byte PublicKey []byte AttestationType string Transport AuthenticatorTransportList diff --git a/backend/internal/service/test_service.go b/backend/internal/service/test_service.go index 62f0568..446193f 100644 --- a/backend/internal/service/test_service.go +++ b/backend/internal/service/test_service.go @@ -60,7 +60,7 @@ func (s *TestService) SeedDatabase() error { userGroups := []model.UserGroup{ { Base: model.Base{ - ID: "4110f814-56f1-4b28-8998-752b69bc97c0e", + ID: "c7ae7c01-28a3-4f3c-9572-1ee734ea8368", }, Name: "developers", FriendlyName: "Developers", @@ -146,7 +146,7 @@ func (s *TestService) SeedDatabase() error { webauthnCredentials := []model.WebauthnCredential{ { Name: "Passkey 1", - CredentialID: "test-credential-1", + CredentialID: []byte("test-credential-1"), PublicKey: publicKey1, AttestationType: "none", Transport: model.AuthenticatorTransportList{protocol.Internal}, @@ -154,7 +154,7 @@ func (s *TestService) SeedDatabase() error { }, { Name: "Passkey 2", - CredentialID: "test-credential-2", + CredentialID: []byte("test-credential-2"), PublicKey: publicKey2, AttestationType: "none", Transport: model.AuthenticatorTransportList{protocol.Internal}, @@ -169,7 +169,7 @@ func (s *TestService) SeedDatabase() error { webauthnSession := model.WebauthnSession{ Challenge: "challenge", - ExpiresAt: time.Now().Add(1 * time.Hour), + ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), UserVerification: "preferred", } if err := tx.Create(&webauthnSession).Error; err != nil { @@ -183,13 +183,29 @@ func (s *TestService) SeedDatabase() error { func (s *TestService) ResetDatabase() error { err := s.db.Transaction(func(tx *gorm.DB) error { var tables []string - if err := tx.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';").Scan(&tables).Error; err != nil { - return err + + switch common.EnvConfig.DbProvider { + case common.DbProviderSqlite: + // Query to get all tables for SQLite + if err := tx.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';").Scan(&tables).Error; err != nil { + return err + } + case common.DbProviderPostgres: + // Query to get all tables for PostgreSQL + if err := tx.Raw(` + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' AND tablename != 'schema_migrations'; + `).Scan(&tables).Error; err != nil { + return err + } + default: + return fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider) } // Delete all rows from all tables for _, table := range tables { - if err := tx.Exec("DELETE FROM " + table).Error; err != nil { + if err := tx.Exec(fmt.Sprintf("DELETE FROM %s;", table)).Error; err != nil { return err } } diff --git a/backend/internal/service/webauthn_service.go b/backend/internal/service/webauthn_service.go index 6376b1e..1438f91 100644 --- a/backend/internal/service/webauthn_service.go +++ b/backend/internal/service/webauthn_service.go @@ -5,6 +5,7 @@ import ( "github.com/go-webauthn/webauthn/webauthn" "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/model" + datatype "github.com/stonith404/pocket-id/backend/internal/model/types" "github.com/stonith404/pocket-id/backend/internal/utils" "gorm.io/gorm" "net/http" @@ -55,7 +56,7 @@ func (s *WebAuthnService) BeginRegistration(userID string) (*model.PublicKeyCred } sessionToStore := &model.WebauthnSession{ - ExpiresAt: session.Expires, + ExpiresAt: datatype.DateTime(session.Expires), Challenge: session.Challenge, UserVerification: string(session.UserVerification), } @@ -79,7 +80,7 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R session := webauthn.SessionData{ Challenge: storedSession.Challenge, - Expires: storedSession.ExpiresAt, + Expires: storedSession.ExpiresAt.ToTime(), UserID: []byte(userID), } @@ -95,7 +96,7 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R credentialToStore := model.WebauthnCredential{ Name: "New Passkey", - CredentialID: string(credential.ID), + CredentialID: credential.ID, AttestationType: credential.AttestationType, PublicKey: credential.PublicKey, Transport: credential.Transport, @@ -117,7 +118,7 @@ func (s *WebAuthnService) BeginLogin() (*model.PublicKeyCredentialRequestOptions } sessionToStore := &model.WebauthnSession{ - ExpiresAt: session.Expires, + ExpiresAt: datatype.DateTime(session.Expires), Challenge: session.Challenge, UserVerification: string(session.UserVerification), } @@ -133,7 +134,7 @@ func (s *WebAuthnService) BeginLogin() (*model.PublicKeyCredentialRequestOptions }, nil } -func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssertionData *protocol.ParsedCredentialAssertionData, ipAddress, userAgent string) (model.User, string, error) { +func (s *WebAuthnService) VerifyLogin(sessionID string, credentialAssertionData *protocol.ParsedCredentialAssertionData, ipAddress, userAgent string) (model.User, string, error) { var storedSession model.WebauthnSession if err := s.db.First(&storedSession, "id = ?", sessionID).Error; err != nil { return model.User{}, "", err @@ -141,7 +142,7 @@ func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssert session := webauthn.SessionData{ Challenge: storedSession.Challenge, - Expires: storedSession.ExpiresAt, + Expires: storedSession.ExpiresAt.ToTime(), } var user *model.User @@ -156,10 +157,6 @@ func (s *WebAuthnService) VerifyLogin(sessionID, userID string, credentialAssert return model.User{}, "", err } - if err := s.db.Find(&user, "id = ?", userID).Error; err != nil { - return model.User{}, "", err - } - token, err := s.jwtService.GenerateAccessToken(*user) if err != nil { return model.User{}, "", err diff --git a/backend/migrations/postgres/20241211111554_init.up.sql b/backend/migrations/postgres/20241211111554_init.up.sql new file mode 100644 index 0000000..caa625c --- /dev/null +++ b/backend/migrations/postgres/20241211111554_init.up.sql @@ -0,0 +1,126 @@ +CREATE TABLE app_config_variables +( + key VARCHAR(100) NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + type VARCHAR(20) NOT NULL, + is_public BOOLEAN DEFAULT FALSE NOT NULL, + is_internal BOOLEAN DEFAULT FALSE NOT NULL, + default_value TEXT +); + +CREATE TABLE user_groups +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + friendly_name VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE users +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + first_name VARCHAR(100), + last_name VARCHAR(100), + is_admin BOOLEAN DEFAULT FALSE NOT NULL +); + +CREATE TABLE audit_logs +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + event VARCHAR(100) NOT NULL, + ip_address INET NOT NULL, + data JSONB NOT NULL, + user_id UUID REFERENCES users ON DELETE SET NULL, + user_agent TEXT, + country VARCHAR(100), + city VARCHAR(100) +); + +CREATE TABLE custom_claims +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + key VARCHAR(255) NOT NULL, + value TEXT NOT NULL, + user_id UUID REFERENCES users ON DELETE CASCADE, + user_group_id UUID REFERENCES user_groups ON DELETE CASCADE, + CONSTRAINT custom_claims_unique UNIQUE (key, user_id, user_group_id), + CHECK (user_id IS NOT NULL OR user_group_id IS NOT NULL) +); + +CREATE TABLE oidc_authorization_codes +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + code VARCHAR(255) NOT NULL UNIQUE, + scope TEXT NOT NULL, + nonce VARCHAR(255), + expires_at TIMESTAMPTZ NOT NULL, + user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE, + client_id UUID NOT NULL, + code_challenge VARCHAR(255), + code_challenge_method_sha256 BOOLEAN +); + +CREATE TABLE oidc_clients +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + name VARCHAR(255), + secret TEXT, + callback_urls JSONB, + image_type VARCHAR(10), + created_by_id UUID REFERENCES users ON DELETE SET NULL, + is_public BOOLEAN DEFAULT FALSE +); + +CREATE TABLE one_time_access_tokens +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE +); + +CREATE TABLE user_authorized_oidc_clients +( + scope VARCHAR(255), + user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE, + client_id UUID NOT NULL REFERENCES oidc_clients ON DELETE CASCADE, + PRIMARY KEY (user_id, client_id) +); + +CREATE TABLE user_groups_users +( + user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE, + user_group_id UUID NOT NULL REFERENCES user_groups ON DELETE CASCADE, + PRIMARY KEY (user_id, user_group_id) +); + +CREATE TABLE webauthn_credentials +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + name VARCHAR(255) NOT NULL, + credential_id BYTEA NOT NULL UNIQUE, + public_key BYTEA NOT NULL, + attestation_type VARCHAR(20) NOT NULL, + transport JSONB NOT NULL, + user_id UUID REFERENCES users ON DELETE CASCADE, + backup_eligible BOOLEAN DEFAULT FALSE NOT NULL, + backup_state BOOLEAN DEFAULT FALSE NOT NULL +); + +CREATE TABLE webauthn_sessions +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + challenge VARCHAR(255) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + user_verification VARCHAR(255) NOT NULL +); \ No newline at end of file diff --git a/backend/migrations/20240731203656_init.up.sql b/backend/migrations/sqlite/20240731203656_init.up.sql similarity index 100% rename from backend/migrations/20240731203656_init.up.sql rename to backend/migrations/sqlite/20240731203656_init.up.sql diff --git a/backend/migrations/20240813211251_passkey_backup_flags..up.sql b/backend/migrations/sqlite/20240813211251_passkey_backup_flags..up.sql similarity index 100% rename from backend/migrations/20240813211251_passkey_backup_flags..up.sql rename to backend/migrations/sqlite/20240813211251_passkey_backup_flags..up.sql diff --git a/backend/migrations/20240813211251_passkey_backup_flags.down.sql b/backend/migrations/sqlite/20240813211251_passkey_backup_flags.down.sql similarity index 100% rename from backend/migrations/20240813211251_passkey_backup_flags.down.sql rename to backend/migrations/sqlite/20240813211251_passkey_backup_flags.down.sql diff --git a/backend/migrations/20240817191051_rename_config_table.down.sql b/backend/migrations/sqlite/20240817191051_rename_config_table.down.sql similarity index 100% rename from backend/migrations/20240817191051_rename_config_table.down.sql rename to backend/migrations/sqlite/20240817191051_rename_config_table.down.sql diff --git a/backend/migrations/20240817191051_rename_config_table.up.sql b/backend/migrations/sqlite/20240817191051_rename_config_table.up.sql similarity index 100% rename from backend/migrations/20240817191051_rename_config_table.up.sql rename to backend/migrations/sqlite/20240817191051_rename_config_table.up.sql diff --git a/backend/migrations/20240820205521_multiple_callback_urls.down.sql b/backend/migrations/sqlite/20240820205521_multiple_callback_urls.down.sql similarity index 100% rename from backend/migrations/20240820205521_multiple_callback_urls.down.sql rename to backend/migrations/sqlite/20240820205521_multiple_callback_urls.down.sql diff --git a/backend/migrations/20240820205521_multiple_callback_urls.up.sql b/backend/migrations/sqlite/20240820205521_multiple_callback_urls.up.sql similarity index 100% rename from backend/migrations/20240820205521_multiple_callback_urls.up.sql rename to backend/migrations/sqlite/20240820205521_multiple_callback_urls.up.sql diff --git a/backend/migrations/20240908123031_audit_log.down.sql b/backend/migrations/sqlite/20240908123031_audit_log.down.sql similarity index 100% rename from backend/migrations/20240908123031_audit_log.down.sql rename to backend/migrations/sqlite/20240908123031_audit_log.down.sql diff --git a/backend/migrations/20240908123031_audit_log.up.sql b/backend/migrations/sqlite/20240908123031_audit_log.up.sql similarity index 100% rename from backend/migrations/20240908123031_audit_log.up.sql rename to backend/migrations/sqlite/20240908123031_audit_log.up.sql diff --git a/backend/migrations/20240924202721_user_groups.down.sql b/backend/migrations/sqlite/20240924202721_user_groups.down.sql similarity index 100% rename from backend/migrations/20240924202721_user_groups.down.sql rename to backend/migrations/sqlite/20240924202721_user_groups.down.sql diff --git a/backend/migrations/20240924202721_user_groups.up.sql b/backend/migrations/sqlite/20240924202721_user_groups.up.sql similarity index 100% rename from backend/migrations/20240924202721_user_groups.up.sql rename to backend/migrations/sqlite/20240924202721_user_groups.up.sql diff --git a/backend/migrations/20241004092030_audit_log_location.down.sql b/backend/migrations/sqlite/20241004092030_audit_log_location.down.sql similarity index 100% rename from backend/migrations/20241004092030_audit_log_location.down.sql rename to backend/migrations/sqlite/20241004092030_audit_log_location.down.sql diff --git a/backend/migrations/20241004092030_audit_log_location.up.sql b/backend/migrations/sqlite/20241004092030_audit_log_location.up.sql similarity index 100% rename from backend/migrations/20241004092030_audit_log_location.up.sql rename to backend/migrations/sqlite/20241004092030_audit_log_location.up.sql diff --git a/backend/migrations/20241023072742_unix-timestamps.down.sql b/backend/migrations/sqlite/20241023072742_unix-timestamps.down.sql similarity index 100% rename from backend/migrations/20241023072742_unix-timestamps.down.sql rename to backend/migrations/sqlite/20241023072742_unix-timestamps.down.sql diff --git a/backend/migrations/20241023072742_unix-timestamps.up.sql b/backend/migrations/sqlite/20241023072742_unix-timestamps.up.sql similarity index 100% rename from backend/migrations/20241023072742_unix-timestamps.up.sql rename to backend/migrations/sqlite/20241023072742_unix-timestamps.up.sql diff --git a/backend/migrations/20241025214824_app_config_default_value.down.sql b/backend/migrations/sqlite/20241025214824_app_config_default_value.down.sql similarity index 100% rename from backend/migrations/20241025214824_app_config_default_value.down.sql rename to backend/migrations/sqlite/20241025214824_app_config_default_value.down.sql diff --git a/backend/migrations/20241025214824_app_config_default_value.up.sql b/backend/migrations/sqlite/20241025214824_app_config_default_value.up.sql similarity index 100% rename from backend/migrations/20241025214824_app_config_default_value.up.sql rename to backend/migrations/sqlite/20241025214824_app_config_default_value.up.sql diff --git a/backend/migrations/20241028064959_custom_claims.down.sql b/backend/migrations/sqlite/20241028064959_custom_claims.down.sql similarity index 100% rename from backend/migrations/20241028064959_custom_claims.down.sql rename to backend/migrations/sqlite/20241028064959_custom_claims.down.sql diff --git a/backend/migrations/20241028064959_custom_claims.up.sql b/backend/migrations/sqlite/20241028064959_custom_claims.up.sql similarity index 100% rename from backend/migrations/20241028064959_custom_claims.up.sql rename to backend/migrations/sqlite/20241028064959_custom_claims.up.sql diff --git a/backend/migrations/20241115131129_pkce.down.sql b/backend/migrations/sqlite/20241115131129_pkce.down.sql similarity index 100% rename from backend/migrations/20241115131129_pkce.down.sql rename to backend/migrations/sqlite/20241115131129_pkce.down.sql diff --git a/backend/migrations/20241115131129_pkce.up.sql b/backend/migrations/sqlite/20241115131129_pkce.up.sql similarity index 100% rename from backend/migrations/20241115131129_pkce.up.sql rename to backend/migrations/sqlite/20241115131129_pkce.up.sql diff --git a/frontend/tests/user-settings.spec.ts b/frontend/tests/user-settings.spec.ts index bd60938..9c979ce 100644 --- a/frontend/tests/user-settings.spec.ts +++ b/frontend/tests/user-settings.spec.ts @@ -168,6 +168,8 @@ test('Update user custom claims', async ({ page }) => { await page.getByLabel('Remove custom claim').first().click(); await page.getByRole('button', { name: 'Save' }).nth(1).click(); + await expect(page.getByRole('status')).toHaveText('Custom claims updated successfully'); + await page.reload(); // Check if custom claim is removed diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh index 0f694ec..95f59cf 100644 --- a/scripts/docker/entrypoint.sh +++ b/scripts/docker/entrypoint.sh @@ -8,9 +8,9 @@ echo "Starting Caddy..." # Check if TRUST_PROXY is set to true and use the appropriate Caddyfile if [ "$TRUST_PROXY" = "true" ]; then - caddy start --config /etc/caddy/Caddyfile.trust-proxy & + caddy start --adapter caddyfile --config /etc/caddy/Caddyfile.trust-proxy & else - caddy start --config /etc/caddy/Caddyfile & + caddy start --adapter caddyfile --config /etc/caddy/Caddyfile & fi wait \ No newline at end of file