Compare commits

...

15 Commits

Author SHA1 Message Date
Elias Schneider
74eb2ac0b9 release: 1.9.1 2025-08-24 23:17:31 +02:00
Elias Schneider
51222f5607 tests: add no tx wrap to unit tests 2025-08-24 23:16:49 +02:00
Elias Schneider
d6d1a4ced2 fix: sqlite migration drops allowed user groups 2025-08-24 23:07:50 +02:00
Elias Schneider
4b086cebcd release: 1.9.0 2025-08-24 20:54:03 +02:00
Alessandro (Ale) Segala
1f3550c9bd fix: ensure SQLite has a writable temporary directory (#876)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-24 20:50:51 +02:00
dependabot[bot]
912008b048 chore(deps): bump golang.org/x/oauth2 from 0.26.0 to 0.27.0 in /backend in the go_modules group across 1 directory (#879)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-24 20:50:30 +02:00
Elias Schneider
5ad8b03831 chore(translations): update translations via Crowdin (#878) 2025-08-24 20:42:58 +02:00
Elias Schneider
c1e515a05f ci/cd: use matrix for e2e tests 2025-08-24 20:35:30 +02:00
Elias Schneider
654593b4b6 chore(migrations): use TEXT instead of VARCHAR for client ID 2025-08-24 20:22:06 +02:00
Elias Schneider
8999173aa0 ci/cd: fix playwright browsers not installed 2025-08-24 20:16:57 +02:00
Elias Schneider
10b087640f tests: fix postgres e2e tests (#877) 2025-08-24 19:15:26 +02:00
Elias Schneider
d0392d25ed fix: sort order incorrect for apps when using postgres 2025-08-24 19:08:33 +02:00
Elias Schneider
2ffc6ba42a fix: don't force uuid for client id in postgres 2025-08-24 18:29:41 +02:00
Elias Schneider
c114a2edaa feat: support automatic db migration rollbacks (#874) 2025-08-24 16:56:28 +02:00
Elias Schneider
63db4d5120 chore(migrations): add postgres down migration to 20250822000000 2025-08-24 15:30:18 +02:00
91 changed files with 683 additions and 201 deletions

View File

@@ -3,15 +3,15 @@ on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '**.md'
- '.github/**'
- "docs/**"
- "**.md"
- ".github/**"
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- '**.md'
- '.github/**'
- "docs/**"
- "**.md"
- ".github/**"
jobs:
build:
@@ -45,23 +45,29 @@ jobs:
path: /tmp/docker-image.tar
retention-days: 1
test-sqlite:
test:
if: github.event.pull_request.head.ref != 'i18n_crowdin'
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
db: [sqlite, postgres]
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Cache Playwright Browsers
@@ -70,100 +76,8 @@ jobs:
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- 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: Cache LLDAP Docker image
uses: actions/cache@v3
id: lldap-cache
with:
path: /tmp/lldap-image.tar
key: lldap-stable-${{ runner.os }}
- name: Pull and save LLDAP image
if: steps.lldap-cache.outputs.cache-hit != 'true'
run: |
docker pull nitnelave/lldap:stable
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
- name: Load LLDAP image from cache
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Install test dependencies
run: pnpm --filter pocket-id-tests install --frozen-lockfile
- name: Install Playwright Browsers
working-directory: ./tests
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm dlx playwright install --with-deps chromium
- name: Run Docker Container with Sqlite DB and LDAP
working-directory: ./tests/setup
run: |
docker compose up -d
docker compose logs -f pocket-id &> /tmp/backend.log &
- name: Run Playwright tests
working-directory: tests
run: pnpm exec playwright test
- name: Upload Test Report
uses: actions/upload-artifact@v4
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
with:
name: playwright-report-sqlite
path: tests/.report
include-hidden-files: true
retention-days: 15
- name: Upload Backend Test Report
uses: actions/upload-artifact@v4
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
with:
name: backend-sqlite
path: /tmp/backend.log
include-hidden-files: true
retention-days: 15
test-postgres:
if: github.event.pull_request.head.ref != 'i18n_crowdin'
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
- name: Cache Playwright Browsers
uses: actions/cache@v3
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Cache PostgreSQL Docker image
if: matrix.db == 'postgres'
uses: actions/cache@v3
id: postgres-cache
with:
@@ -171,15 +85,14 @@ jobs:
key: postgres-17-${{ runner.os }}
- name: Pull and save PostgreSQL image
if: steps.postgres-cache.outputs.cache-hit != 'true'
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit != 'true'
run: |
docker pull postgres:17
docker save postgres:17 > /tmp/postgres-image.tar
- name: Load PostgreSQL image from cache
if: steps.postgres-cache.outputs.cache-hit == 'true'
if: matrix.db == 'postgres' && steps.postgres-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/postgres-image.tar
- name: Cache LLDAP Docker image
uses: actions/cache@v3
id: lldap-cache
@@ -196,7 +109,6 @@ jobs:
- name: Load LLDAP image from cache
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
@@ -207,14 +119,21 @@ jobs:
run: docker load -i /tmp/docker-image.tar
- name: Install test dependencies
run: pnpm --filter pocket-id-tests install
run: pnpm --filter pocket-id-tests install --frozen-lockfile
- name: Install Playwright Browsers
working-directory: ./tests
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm dlx playwright install --with-deps chromium
run: pnpm exec playwright install --with-deps chromium
- name: Run Docker Container (sqlite) with LDAP
if: matrix.db == 'sqlite'
working-directory: ./tests/setup
run: |
docker compose up -d
docker compose logs -f pocket-id &> /tmp/backend.log &
- name: Run Docker Container with Postgres DB and LDAP
- name: Run Docker Container (postgres) with LDAP
if: matrix.db == 'postgres'
working-directory: ./tests/setup
run: |
docker compose -f docker-compose-postgres.yml up -d
@@ -228,8 +147,8 @@ jobs:
uses: actions/upload-artifact@v4
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
with:
name: playwright-report-postgres
path: frontend/tests/.report
name: playwright-report-${{ matrix.db }}
path: tests/.report
include-hidden-files: true
retention-days: 15
@@ -237,7 +156,7 @@ jobs:
uses: actions/upload-artifact@v4
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
with:
name: backend-postgres
name: backend-${{ matrix.db }}
path: /tmp/backend.log
include-hidden-files: true
retention-days: 15

View File

@@ -1 +1 @@
1.8.1
1.9.1

View File

@@ -1,3 +1,24 @@
## [](https://github.com/pocket-id/pocket-id/compare/v1.9.0...v) (2025-08-24)
### Bug Fixes
* sqlite migration drops allowed user groups ([d6d1a4c](https://github.com/pocket-id/pocket-id/commit/d6d1a4ced23886f255a9c2048d19ad3599a17f26))
## [](https://github.com/pocket-id/pocket-id/compare/v1.8.1...v) (2025-08-24)
### Features
* support automatic db migration rollbacks ([#874](https://github.com/pocket-id/pocket-id/issues/874)) ([c114a2e](https://github.com/pocket-id/pocket-id/commit/c114a2edaae4c007c75c34c02e8b0bb011845cae))
### Bug Fixes
* don't force uuid for client id in postgres ([2ffc6ba](https://github.com/pocket-id/pocket-id/commit/2ffc6ba42af4742a13b77543142b66b3e826ab88))
* ensure SQLite has a writable temporary directory ([#876](https://github.com/pocket-id/pocket-id/issues/876)) ([1f3550c](https://github.com/pocket-id/pocket-id/commit/1f3550c9bd3aafd3bd2272ef47f3ed8736037d81))
* sort order incorrect for apps when using postgres ([d0392d2](https://github.com/pocket-id/pocket-id/commit/d0392d25edcaa5f3c7da2aad70febf63b47763fa))
## [](https://github.com/pocket-id/pocket-id/compare/v1.8.0...v) (2025-08-24)

View File

@@ -74,6 +74,8 @@ require (
github.com/go-webauthn/x v0.1.23 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/google/go-github/v39 v39.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -132,6 +134,7 @@ require (
golang.org/x/arch v0.20.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect

View File

@@ -95,11 +95,19 @@ github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJD
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -319,6 +327,7 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -339,6 +348,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -352,6 +362,9 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -385,6 +398,7 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -406,6 +420,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"log/slog"
"net/url"
"os"
"path/filepath"
"strings"
"time"
@@ -13,6 +15,7 @@ import (
"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/golang-migrate/migrate/v4/source/github"
"github.com/golang-migrate/migrate/v4/source/iofs"
slogGorm "github.com/orandin/slog-gorm"
"gorm.io/driver/postgres"
@@ -20,6 +23,7 @@ import (
gormLogger "gorm.io/gorm/logger"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/utils"
sqliteutil "github.com/pocket-id/pocket-id/backend/internal/utils/sqlite"
"github.com/pocket-id/pocket-id/backend/resources"
)
@@ -38,7 +42,9 @@ func NewDatabase() (db *gorm.DB, err error) {
var driver database.Driver
switch common.EnvConfig.DbProvider {
case common.DbProviderSqlite:
driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{})
driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{
NoTxWrap: true,
})
case common.DbProviderPostgres:
driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{})
default:
@@ -58,8 +64,9 @@ func NewDatabase() (db *gorm.DB, err error) {
}
func migrateDatabase(driver database.Driver) error {
// Use the embedded migrations
source, err := iofs.New(resources.FS, "migrations/"+string(common.EnvConfig.DbProvider))
// Embedded migrations via iofs
path := "migrations/" + string(common.EnvConfig.DbProvider)
source, err := iofs.New(resources.FS, path)
if err != nil {
return fmt.Errorf("failed to create embedded migration source: %w", err)
}
@@ -69,14 +76,66 @@ func migrateDatabase(driver database.Driver) error {
return fmt.Errorf("failed to create migration instance: %w", err)
}
err = m.Up()
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed to apply migrations: %w", err)
requiredVersion, err := getRequiredMigrationVersion(path)
if err != nil {
return fmt.Errorf("failed to get last migration version: %w", err)
}
currentVersion, _, _ := m.Version()
if currentVersion > requiredVersion {
slog.Warn("Database version is newer than the application supports, possible downgrade detected", slog.Uint64("db_version", uint64(currentVersion)), slog.Uint64("app_version", uint64(requiredVersion)))
if !common.EnvConfig.AllowDowngrade {
return fmt.Errorf("database version (%d) is newer than application version (%d), downgrades are not allowed (set ALLOW_DOWNGRADE=true to enable)", currentVersion, requiredVersion)
}
slog.Info("Fetching migrations from GitHub to handle possible downgrades")
return migrateDatabaseFromGitHub(driver, requiredVersion)
}
if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed to apply embedded migrations: %w", err)
}
return nil
}
func migrateDatabaseFromGitHub(driver database.Driver, version uint) error {
srcURL := "github://pocket-id/pocket-id/backend/resources/migrations/" + string(common.EnvConfig.DbProvider)
m, err := migrate.NewWithDatabaseInstance(srcURL, "pocket-id", driver)
if err != nil {
return fmt.Errorf("failed to create GitHub migration instance: %w", err)
}
if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed to apply GitHub migrations: %w", err)
}
return nil
}
// getRequiredMigrationVersion reads the embedded migration files and returns the highest version number found.
func getRequiredMigrationVersion(path string) (uint, error) {
entries, err := resources.FS.ReadDir(path)
if err != nil {
return 0, fmt.Errorf("failed to read migration directory: %w", err)
}
var maxVersion uint
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
var version uint
n, err := fmt.Sscanf(name, "%d_", &version)
if err == nil && n == 1 {
if version > maxVersion {
maxVersion = version
}
}
}
return maxVersion, nil
}
func connectDatabase() (db *gorm.DB, err error) {
var dialector gorm.Dialector
@@ -86,11 +145,20 @@ func connectDatabase() (db *gorm.DB, err error) {
if common.EnvConfig.DbConnectionString == "" {
return nil, errors.New("missing required env var 'DB_CONNECTION_STRING' for SQLite database")
}
sqliteutil.RegisterSqliteFunctions()
connString, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
connString, dbPath, err := parseSqliteConnectionString(common.EnvConfig.DbConnectionString)
if err != nil {
return nil, err
}
// Before we connect, also make sure that there's a temporary folder for SQLite to write its data
err = ensureSqliteTempDir(filepath.Dir(dbPath))
if err != nil {
return nil, err
}
dialector = sqlite.Open(connString)
case common.DbProviderPostgres:
if common.EnvConfig.DbConnectionString == "" {
@@ -120,7 +188,7 @@ func connectDatabase() (db *gorm.DB, err error) {
return nil, err
}
func parseSqliteConnectionString(connString string) (string, error) {
func parseSqliteConnectionString(connString string) (parsedConnString string, dbPath string, err error) {
if !strings.HasPrefix(connString, "file:") {
connString = "file:" + connString
}
@@ -131,7 +199,7 @@ func parseSqliteConnectionString(connString string) (string, error) {
// Parse the connection string
connStringUrl, err := url.Parse(connString)
if err != nil {
return "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
return "", "", fmt.Errorf("failed to parse SQLite connection string: %w", err)
}
// Convert options for the old SQLite driver to the new one
@@ -140,10 +208,19 @@ func parseSqliteConnectionString(connString string) (string, error) {
// Add the default and required params
err = addSqliteDefaultParameters(connStringUrl, isMemoryDB)
if err != nil {
return "", fmt.Errorf("invalid SQLite connection string: %w", err)
return "", "", fmt.Errorf("invalid SQLite connection string: %w", err)
}
return connStringUrl.String(), nil
// Get the absolute path to the database
// Here, we know for a fact that the ? is present
parsedConnString = connStringUrl.String()
idx := strings.IndexRune(parsedConnString, '?')
dbPath, err = filepath.Abs(parsedConnString[len("file:"):idx])
if err != nil {
return "", "", fmt.Errorf("failed to determine absolute path to the database: %w", err)
}
return parsedConnString, dbPath, nil
}
// The official C implementation of SQLite allows some additional properties in the connection string
@@ -296,6 +373,48 @@ func isSqliteInMemory(connString string) bool {
return len(qs["mode"]) > 0 && qs["mode"][0] == "memory"
}
// ensureSqliteTempDir ensures that SQLite has a directory where it can write temporary files if needed
// The default directory may not be writable when using a container with a read-only root file system
// See: https://www.sqlite.org/tempfiles.html
func ensureSqliteTempDir(dbPath string) error {
// Per docs, SQLite tries these folders in order (excluding those that aren't applicable to us):
//
// - The SQLITE_TMPDIR environment variable
// - The TMPDIR environment variable
// - /var/tmp
// - /usr/tmp
// - /tmp
//
// Source: https://www.sqlite.org/tempfiles.html#temporary_file_storage_locations
//
// First, let's check if SQLITE_TMPDIR or TMPDIR are set, in which case we trust the user has taken care of the problem already
if os.Getenv("SQLITE_TMPDIR") != "" || os.Getenv("TMPDIR") != "" {
return nil
}
// Now, let's check if /var/tmp, /usr/tmp, or /tmp exist and are writable
for _, dir := range []string{"/var/tmp", "/usr/tmp", "/tmp"} {
ok, err := utils.IsWritableDir(dir)
if err != nil {
return fmt.Errorf("failed to check if %s is writable: %w", dir, err)
}
if ok {
// We found a folder that's writable
return nil
}
}
// If we're here, there's no temporary directory that's writable (not unusual for containers with a read-only root file system), so we set SQLITE_TMPDIR to the folder where the SQLite database is set
err := os.Setenv("SQLITE_TMPDIR", dbPath)
if err != nil {
return fmt.Errorf("failed to set SQLITE_TMPDIR environmental variable: %w", err)
}
slog.Debug("Set SQLITE_TMPDIR to the database directory", "path", dbPath)
return nil
}
func getGormLogger() gormLogger.Interface {
loggerOpts := make([]slogGorm.Option, 0, 5)
loggerOpts = append(loggerOpts,

View File

@@ -52,6 +52,7 @@ type EnvConfigSchema struct {
LogJSON bool `env:"LOG_JSON"`
TrustProxy bool `env:"TRUST_PROXY"`
AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"`
AllowDowngrade bool `env:"ALLOW_DOWNGRADE"`
}
var EnvConfig = defaultConfig()
@@ -87,6 +88,7 @@ func defaultConfig() EnvConfigSchema {
TracingEnabled: false,
TrustProxy: false,
AnalyticsDisabled: false,
AllowDowngrade: false,
}
}

View File

@@ -343,7 +343,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
},
{
Base: model.Base{
ID: "b2c3d4e5-f6g7-8901-bcde-f12345678901",
ID: "dc3c9c96-714e-48eb-926e-2d7c7858e6cf",
},
Token: "PARTIAL567890ABC",
ExpiresAt: datatype.DateTime(time.Now().Add(7 * 24 * time.Hour)),
@@ -352,7 +352,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
},
{
Base: model.Base{
ID: "c3d4e5f6-g7h8-9012-cdef-123456789012",
ID: "44de1863-ffa5-4db1-9507-4887cd7a1e3f",
},
Token: "EXPIRED34567890B",
ExpiresAt: datatype.DateTime(time.Now().Add(-24 * time.Hour)), // Expired
@@ -361,7 +361,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
},
{
Base: model.Base{
ID: "d4e5f6g7-h8i9-0123-def0-234567890123",
ID: "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b",
},
Token: "FULLYUSED567890C",
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),

View File

@@ -1379,8 +1379,7 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
query := tx.
WithContext(ctx).
Model(&model.OidcClient{}).
Preload("UserAuthorizedOidcClients", "user_id = ?", userID).
Distinct()
Preload("UserAuthorizedOidcClients", "user_id = ?", userID)
// If user has no groups, only return clients with no allowed user groups
if len(userGroupIDs) == 0 {
@@ -1401,7 +1400,7 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
if sortedPaginationRequest.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(sortedPaginationRequest.Sort.Direction) {
query = query.
Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction)
Order("user_authorized_oidc_clients.last_used_at " + sortedPaginationRequest.Sort.Direction + " NULLS LAST")
}
response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &clients)

View File

@@ -1,12 +1,15 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"syscall"
"github.com/google/uuid"
"github.com/pocket-id/pocket-id/backend/resources"
@@ -136,3 +139,41 @@ func FileExists(path string) (bool, error) {
}
return !s.IsDir(), nil
}
// IsWritableDir checks if a directory exists and is writable
func IsWritableDir(dir string) (bool, error) {
// Check if directory exists and it's actually a directory
info, err := os.Stat(dir)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("failed to stat '%s': %w", dir, err)
}
if !info.IsDir() {
return false, nil
}
// Generate a random suffix for the test file to avoid conflicts
randomBytes := make([]byte, 8)
_, err = io.ReadFull(rand.Reader, randomBytes)
if err != nil {
return false, fmt.Errorf("failed to generate random bytes: %w", err)
}
// Check if directory is writable by trying to create a temporary file
testFile := filepath.Join(dir, ".pocketid_test_write_"+hex.EncodeToString(randomBytes))
defer os.Remove(testFile)
file, err := os.Create(testFile)
if err != nil {
if os.IsPermission(err) || errors.Is(err, syscall.EROFS) {
return false, nil
}
return false, fmt.Errorf("failed to create test file: %w", err)
}
_ = file.Close()
return true, nil
}

View File

@@ -55,7 +55,9 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
// Perform migrations with the embedded migrations
sqlDB, err := db.DB()
require.NoError(t, err, "Failed to get sql.DB")
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{
NoTxWrap: true,
})
require.NoError(t, err, "Failed to create migration driver")
source, err := iofs.New(resources.FS, "migrations/sqlite")
require.NoError(t, err, "Failed to create embedded migration source")
@@ -63,6 +65,8 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
require.NoError(t, err, "Failed to create migration instance")
err = m.Up()
require.NoError(t, err, "Failed to perform migrations")
_, err = sqlDB.Exec("PRAGMA foreign_keys = OFF;")
require.NoError(t, err, "Failed to disable foreign keys")
return db
}

View File

@@ -1 +1,7 @@
-- No-op
ALTER TABLE public.audit_logs
DROP CONSTRAINT IF EXISTS audit_logs_user_id_fkey,
ADD CONSTRAINT audit_logs_user_id_fkey
FOREIGN KEY (user_id) REFERENCES public.users (id);
ALTER TABLE public.oidc_authorization_codes
DROP CONSTRAINT IF EXISTS oidc_authorization_codes_client_fk;

View File

@@ -0,0 +1 @@
-- No-op because strings can't be converted to UUIDs

View File

@@ -0,0 +1,57 @@
-- Drop foreign keys that reference oidc_clients(id)
ALTER TABLE oidc_authorization_codes
DROP CONSTRAINT IF EXISTS oidc_authorization_codes_client_fk;
ALTER TABLE user_authorized_oidc_clients
DROP CONSTRAINT IF EXISTS user_authorized_oidc_clients_client_id_fkey;
ALTER TABLE oidc_refresh_tokens
DROP CONSTRAINT IF EXISTS oidc_refresh_tokens_client_id_fkey;
ALTER TABLE oidc_device_codes
DROP CONSTRAINT IF EXISTS oidc_device_codes_client_id_fkey;
ALTER TABLE oidc_clients_allowed_user_groups
DROP CONSTRAINT IF EXISTS oidc_clients_allowed_user_groups_oidc_client_id_fkey;
-- Alter child columns to TEXT
ALTER TABLE oidc_authorization_codes
ALTER COLUMN client_id TYPE TEXT USING client_id::text;
ALTER TABLE user_authorized_oidc_clients
ALTER
COLUMN client_id TYPE TEXT USING client_id::text;
ALTER TABLE oidc_refresh_tokens
ALTER
COLUMN client_id TYPE TEXT USING client_id::text;
ALTER TABLE oidc_device_codes
ALTER
COLUMN client_id TYPE TEXT USING client_id::text;
ALTER TABLE oidc_clients_allowed_user_groups
ALTER
COLUMN oidc_client_id TYPE TEXT USING oidc_client_id::text;
-- Alter parent primary key column to TEXT
ALTER TABLE oidc_clients
ALTER
COLUMN id TYPE TEXT USING id::text;
-- Recreate foreign keys with the new type
ALTER TABLE oidc_authorization_codes
ADD CONSTRAINT oidc_authorization_codes_client_fk
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
ALTER TABLE user_authorized_oidc_clients
ADD CONSTRAINT user_authorized_oidc_clients_client_id_fkey
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
ALTER TABLE oidc_refresh_tokens
ADD CONSTRAINT oidc_refresh_tokens_client_id_fkey
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
ALTER TABLE oidc_device_codes
ADD CONSTRAINT oidc_device_codes_client_id_fkey
FOREIGN KEY (client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;
ALTER TABLE oidc_clients_allowed_user_groups
ADD CONSTRAINT oidc_clients_allowed_user_groups_oidc_client_id_fkey
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE users
(
id TEXT NOT NULL PRIMARY KEY,
@@ -77,4 +79,6 @@ CREATE TABLE application_configuration_variables
type TEXT NOT NULL,
is_public NUMERIC DEFAULT FALSE NOT NULL,
is_internal NUMERIC DEFAULT FALSE NOT NULL
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE webauthn_credentials ADD COLUMN backup_eligible BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE webauthn_credentials ADD COLUMN backup_state BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE webauthn_credentials ADD COLUMN backup_state BOOLEAN NOT NULL DEFAULT FALSE;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE webauthn_credentials DROP COLUMN backup_eligible;
ALTER TABLE webauthn_credentials DROP COLUMN backup_state;
ALTER TABLE webauthn_credentials DROP COLUMN backup_state;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE app_config_variables
RENAME TO application_configuration_variables;
RENAME TO application_configuration_variables;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE application_configuration_variables
RENAME TO app_config_variables;
RENAME TO app_config_variables;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
create table oidc_clients
(
id TEXT not null primary key,
@@ -20,4 +22,6 @@ select id,
created_by_id
from oidc_clients_dg_tmp;
drop table oidc_clients_dg_tmp;
drop table oidc_clients_dg_tmp;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
create table oidc_clients_dg_tmp
(
id TEXT not null primary key,
@@ -23,4 +25,6 @@ from oidc_clients;
drop table oidc_clients;
alter table oidc_clients_dg_tmp
rename to oidc_clients;
rename to oidc_clients;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
DROP TABLE audit_logs;
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE audit_logs;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE audit_logs
(
id TEXT NOT NULL PRIMARY KEY,
@@ -7,4 +9,6 @@ CREATE TABLE audit_logs
user_agent TEXT NOT NULL,
data BLOB NOT NULL,
user_id TEXT REFERENCES users
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE user_groups;
DROP TABLE user_groups_users;
DROP TABLE user_groups_users;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE user_groups
(
id TEXT NOT NULL PRIMARY KEY,
@@ -13,4 +15,6 @@ CREATE TABLE user_groups_users
PRIMARY KEY (user_id, user_group_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE audit_logs DROP COLUMN country;
ALTER TABLE audit_logs DROP COLUMN city;
ALTER TABLE audit_logs DROP COLUMN city;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE audit_logs ADD COLUMN country TEXT;
ALTER TABLE audit_logs ADD COLUMN city TEXT;
ALTER TABLE audit_logs ADD COLUMN city TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- Convert the Unix timestamps back to DATETIME format
UPDATE user_groups
@@ -25,4 +27,6 @@ SET created_at = datetime(created_at, 'unixepoch');
UPDATE webauthn_sessions
SET created_at = datetime(created_at, 'unixepoch'),
expires_at = datetime(expires_at, 'unixepoch');
expires_at = datetime(expires_at, 'unixepoch');
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- Convert the DATETIME fields to Unix timestamps (in seconds)
UPDATE user_groups
SET created_at = strftime('%s', created_at);
@@ -24,4 +26,6 @@ SET created_at = strftime('%s', created_at);
UPDATE webauthn_sessions
SET created_at = strftime('%s', created_at),
expires_at = strftime('%s', expires_at);
expires_at = strftime('%s', expires_at);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE app_config_variables DROP COLUMN default_value;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE app_config_variables DROP COLUMN default_value;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE app_config_variables ADD COLUMN default_value TEXT;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE app_config_variables ADD COLUMN default_value TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
DROP TABLE custom_claims;
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE custom_claims;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE custom_claims
(
id TEXT NOT NULL PRIMARY KEY,
@@ -12,4 +14,6 @@ CREATE TABLE custom_claims
CONSTRAINT custom_claims_unique UNIQUE (key, user_id, user_group_id),
CHECK (user_id IS NOT NULL OR user_group_id IS NOT NULL)
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_authorization_codes DROP COLUMN code_challenge;
ALTER TABLE oidc_authorization_codes DROP COLUMN code_challenge_method_sha256;
ALTER TABLE oidc_clients DROP COLUMN is_public;
ALTER TABLE oidc_clients DROP COLUMN is_public;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_authorization_codes ADD COLUMN code_challenge TEXT;
ALTER TABLE oidc_authorization_codes ADD COLUMN code_challenge_method_sha256 NUMERIC;
ALTER TABLE oidc_clients ADD COLUMN is_public BOOLEAN DEFAULT FALSE;
ALTER TABLE oidc_clients ADD COLUMN is_public BOOLEAN DEFAULT FALSE;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE oidc_clients DROP COLUMN pkce_enabled;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN pkce_enabled;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE oidc_clients ADD COLUMN pkce_enabled BOOLEAN DEFAULT FALSE;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN pkce_enabled BOOLEAN DEFAULT FALSE;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE users DROP COLUMN ldap_id;
ALTER TABLE user_groups DROP COLUMN ldap_id;
ALTER TABLE user_groups DROP COLUMN ldap_id;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,5 +1,9 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE users ADD COLUMN ldap_id TEXT;
ALTER TABLE user_groups ADD COLUMN ldap_id TEXT;
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id);
CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
UPDATE app_config_variables SET key = 'emailEnabled' WHERE key = 'emailLoginNotificationEnabled';
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE app_config_variables SET key = 'emailEnabled' WHERE key = 'emailLoginNotificationEnabled';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
UPDATE app_config_variables SET key = 'emailLoginNotificationEnabled' WHERE key = 'emailEnabled';
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE app_config_variables SET key = 'emailLoginNotificationEnabled' WHERE key = 'emailEnabled';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE users SET ldap_id = '' WHERE ldap_id IS NULL;
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE users SET ldap_id = null WHERE ldap_id = '';
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
DROP TABLE oidc_clients_allowed_user_groups;
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE oidc_clients_allowed_user_groups;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE oidc_clients_allowed_user_groups
(
user_group_id TEXT NOT NULL,
@@ -5,4 +7,6 @@ CREATE TABLE oidc_clients_allowed_user_groups
PRIMARY KEY (oidc_client_id, user_group_id),
FOREIGN KEY (oidc_client_id) REFERENCES oidc_clients (id) ON DELETE CASCADE,
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE oidc_clients DROP COLUMN logout_callback_urls;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN logout_callback_urls;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE oidc_clients ADD COLUMN logout_callback_urls BLOB;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN logout_callback_urls BLOB;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,7 +1,11 @@
PRAGMA foreign_keys=OFF;
BEGIN;
UPDATE app_config_variables
SET value = CASE
WHEN value = 'true' AND (SELECT value FROM app_config_variables WHERE key = 'smtpPort' LIMIT 1) = '587' THEN 'starttls'
WHEN value = 'true' THEN 'tls'
ELSE 'none'
END
WHERE key = 'smtpTls';
WHERE key = 'smtpTls';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX IF EXISTS idx_api_keys_key;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS api_keys;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
@@ -9,4 +11,6 @@ CREATE TABLE api_keys (
user_id TEXT REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_api_keys_key ON api_keys(key);
CREATE INDEX idx_api_keys_key ON api_keys(key);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE users DROP COLUMN locale;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE users DROP COLUMN locale;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
ALTER TABLE users ADD COLUMN locale TEXT;
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE users ADD COLUMN locale TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_token;
DROP TABLE IF EXISTS oidc_refresh_tokens;
DROP TABLE IF EXISTS oidc_refresh_tokens;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE oidc_refresh_tokens (
id TEXT NOT NULL PRIMARY KEY,
created_at DATETIME,
@@ -8,4 +10,6 @@ CREATE TABLE oidc_refresh_tokens (
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE
);
CREATE INDEX idx_oidc_refresh_tokens_token ON oidc_refresh_tokens(token);
CREATE INDEX idx_oidc_refresh_tokens_token ON oidc_refresh_tokens(token);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,8 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX IF EXISTS idx_audit_logs_event;
DROP INDEX IF EXISTS idx_audit_logs_user_id;
DROP INDEX IF EXISTS idx_audit_logs_client_name;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,8 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE INDEX idx_audit_logs_event ON audit_logs(event);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,4 +1,9 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE app_config_variables ADD type VARCHAR(20) NOT NULL,;
ALTER TABLE app_config_variables ADD is_public BOOLEAN DEFAULT FALSE NOT NULL,;
ALTER TABLE app_config_variables ADD is_internal BOOLEAN DEFAULT FALSE NOT NULL,;
ALTER TABLE app_config_variables ADD default_value TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,4 +1,8 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE app_config_variables DROP type;
ALTER TABLE app_config_variables DROP is_public;
ALTER TABLE app_config_variables DROP is_internal;
ALTER TABLE app_config_variables DROP default_value;
ALTER TABLE app_config_variables DROP default_value;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,4 +1,8 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX idx_users_disabled;
ALTER TABLE users
DROP COLUMN disabled;
DROP COLUMN disabled;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE users
ADD COLUMN disabled NUMERIC DEFAULT FALSE NOT NULL;
ADD COLUMN disabled NUMERIC DEFAULT FALSE NOT NULL;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE api_keys
DROP COLUMN expiration_email_sent;
DROP COLUMN expiration_email_sent;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,2 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE api_keys
ADD COLUMN expiration_email_sent BOOLEAN NOT NULL DEFAULT 0;
ADD COLUMN expiration_email_sent BOOLEAN NOT NULL DEFAULT 0;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
DROP TABLE oidc_device_codes;
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE oidc_device_codes;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE oidc_device_codes
(
id TEXT NOT NULL PRIMARY KEY,
@@ -9,4 +11,6 @@ CREATE TABLE oidc_device_codes
is_authorized BOOLEAN NOT NULL DEFAULT FALSE,
user_id TEXT REFERENCES users ON DELETE CASCADE,
client_id TEXT NOT NULL REFERENCES oidc_clients ON DELETE CASCADE
);
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1 @@
-- No rollback is needed for this migration.
-- No-op

View File

@@ -1 +1,5 @@
DELETE FROM app_config_variables WHERE value = '';
PRAGMA foreign_keys=OFF;
BEGIN;
DELETE FROM app_config_variables WHERE value = '';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN credentials;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN credentials TEXT NULL;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
DROP INDEX idx_audit_logs_country;
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX idx_audit_logs_country;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,5 @@
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP INDEX IF EXISTS idx_signup_tokens_expires_at;
DROP INDEX IF EXISTS idx_signup_tokens_token;
DROP TABLE IF EXISTS signup_tokens;
DROP TABLE IF EXISTS signup_tokens;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE signup_tokens (
id TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
@@ -8,4 +10,6 @@ CREATE TABLE signup_tokens (
);
CREATE INDEX idx_signup_tokens_token ON signup_tokens(token);
CREATE INDEX idx_signup_tokens_expires_at ON signup_tokens(expires_at);
CREATE INDEX idx_signup_tokens_expires_at ON signup_tokens(expires_at);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- Re-create the table with non-nullable ip_address
-- We then move the data and rename the table
CREATE TABLE audit_logs_new
@@ -28,3 +30,6 @@ CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_user_agent ON audit_logs(user_agent);
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- Re-create the table with nullable ip_address
-- We then move the data and rename the table
CREATE TABLE audit_logs_new
@@ -28,3 +30,6 @@ CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_user_agent ON audit_logs(user_agent);
CREATE INDEX idx_audit_logs_client_name ON audit_logs((json_extract(data, '$.clientName')));
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
DROP TABLE kv;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,6 +1,11 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- The "kv" tables contains miscellaneous key-value pairs
CREATE TABLE kv
(
"key" TEXT NOT NULL PRIMARY KEY,
"value" TEXT NOT NULL
);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
-- Normalize (form NFC) all existing values in the database
UPDATE api_keys SET
name = normalize(name, 'nfc'),
@@ -23,3 +25,6 @@ UPDATE users SET
UPDATE user_groups SET
friendly_name = normalize(friendly_name, 'nfc'),
"name" = normalize("name", 'nfc');
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN launch_url;
ALTER TABLE user_authorized_oidc_clients DROP COLUMN created_at;
ALTER TABLE user_authorized_oidc_clients DROP COLUMN created_at;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN launch_url TEXT;
CREATE TABLE user_authorized_oidc_clients_new
@@ -14,3 +16,6 @@ SELECT scope, user_id, client_id, unixepoch() FROM user_authorized_oidc_clients;
DROP TABLE user_authorized_oidc_clients;
ALTER TABLE user_authorized_oidc_clients_new RENAME TO user_authorized_oidc_clients;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN requires_reauthentication;
DROP INDEX IF EXISTS idx_reauthentication_tokens_token;
DROP TABLE IF EXISTS reauthentication_tokens;
DROP TABLE IF EXISTS reauthentication_tokens;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,3 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients ADD COLUMN requires_reauthentication BOOLEAN NOT NULL DEFAULT FALSE;
CREATE TABLE reauthentication_tokens (
@@ -8,4 +10,6 @@ CREATE TABLE reauthentication_tokens (
user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);
CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens(token);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,4 +1,6 @@
PRAGMA foreign_keys=OFF;
BEGIN;
PRAGMA foreign_keys=OFF;
---------------------------
-- Delete all orphaned rows
---------------------------
@@ -174,4 +176,6 @@ DROP TABLE webauthn_credentials;
ALTER TABLE webauthn_credentials_new RENAME TO webauthn_credentials;
PRAGMA foreign_keys=ON;
PRAGMA foreign_key_check;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -276,8 +276,8 @@
"public_clients_description": "공개 클라이언트는 클라이언트 시크릿이 없습니다. 이들은 시크릿을 안전하게 보관할 수 없는 모바일, 웹, 네이티브 애플리케이션을 위해 설계되었습니다.",
"pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "공개 키 코드 교환은 CSRF 및 승인 코드 가로채기 공격을 방지하기 위한 보안 기능입니다.",
"requires_reauthentication": "재인증이 필요합니다.",
"requires_users_to_authenticate_again_on_each_authorization": "사용자가 이미 로그인한 상태에서도 각 권한 부여 시마다 다시 인증을 요구합니다.",
"requires_reauthentication": "재인증 요구",
"requires_users_to_authenticate_again_on_each_authorization": "사용자가 이미 로그인한 상태에서도 승인할 때마다 다시 인증을 요구합니다.",
"name_logo": "{name} 로고",
"change_logo": "로고 변경",
"upload_logo": "로고 업로드",
@@ -439,6 +439,6 @@
"revoke_access_successful": "{clientName}의 접근이 성공적으로 취소되었습니다.",
"last_signed_in_ago": "{time} 전에 로그인함",
"invalid_client_id": "고객 ID에는 영문자, 숫자, 밑줄, 하이픈만 포함될 수 있습니다.",
"custom_client_id_description": "응용 프로그램에서 이 정보가 필요한 경우 사용자 정의 클라이언트 ID 설정하세요. 그렇지 않은 경우 빈 상태로 두 무작위로 생성니다.",
"custom_client_id_description": "애플리케이션에서 사용자 정의 클라이언트 ID가 요구되는 경우 설정하세요. 그렇지 않으면 빈 상태로 두어서 무작위로 생성할 수 있습니다.",
"generated": "생성됨"
}

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "1.8.1",
"version": "1.9.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -40,7 +40,7 @@ export const oidcClients = {
id: '7c21a609-96b5-4011-9900-272b8d31a9d1',
name: 'Tailscale',
callbackUrl: 'http://tailscale/auth/callback',
secret: 'n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo',
secret: 'n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo'
},
federated: {
id: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b',
@@ -116,7 +116,7 @@ export const signupTokens = {
createdAt: new Date().toISOString()
},
partiallyUsed: {
id: 'b2c3d4e5-f6g7-8901-bcde-f12345678901',
id: 'dc3c9c96-714e-48eb-926e-2d7c7858e6cf',
token: 'PARTIAL567890ABC',
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
usageLimit: 5,
@@ -124,7 +124,7 @@ export const signupTokens = {
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
},
expired: {
id: 'c3d4e5f6-g7h8-9012-cdef-123456789012',
id: '44de1863-ffa5-4db1-9507-4887cd7a1e3f',
token: 'EXPIRED34567890B',
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
usageLimit: 3,
@@ -132,7 +132,7 @@ export const signupTokens = {
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString()
},
fullyUsed: {
id: 'd4e5f6g7-h8i9-0123-def0-234567890123',
id: 'f1b1678b-7720-4d8b-8f91-1dbff1e2d02b',
token: 'FULLYUSED567890C',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
usageLimit: 1,

View File

@@ -19,6 +19,9 @@ services:
extends:
file: docker-compose.yml
service: pocket-id
environment:
- DB_PROVIDER=postgres
- DB_CONNECTION_STRING=postgres://postgres:postgres@postgres:5432/pocket-id
depends_on:
postgres:
condition: service_healthy

View File

@@ -215,7 +215,7 @@ test('Refresh token fails when used for the wrong user', async ({ request }) =>
data: {
rt: token,
client: clientId,
user: 'bad-user'
user: '44cb5d71-db31-4555-9a1b-5484650f6002'
}
})
.then((r) => r.text());