From 1d1d92a5cfdf5b5203ebc54684bd9cf76287fd7b Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sun, 8 Mar 2026 21:57:13 +0100 Subject: [PATCH 01/19] fix: release.yml --- .github/workflows/docker.yml | 6 ++++++ .github/workflows/release.yml | 1 + app/api/agent/[agentId]/restore/route.ts | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a5ed0f6e..47de990e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,6 +6,9 @@ on: version: required: true type: string + ref: + required: true + type: string image_name: required: false type: string @@ -39,6 +42,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68e4bee3..f5fa024b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,6 +88,7 @@ jobs: uses: ./.github/workflows/docker.yml with: version: ${{ needs.create-release.outputs.version }} + ref: ${{ needs.create-release.outputs.version }} add_latest: true secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }} diff --git a/app/api/agent/[agentId]/restore/route.ts b/app/api/agent/[agentId]/restore/route.ts index d93e945b..0fbc3dcc 100644 --- a/app/api/agent/[agentId]/restore/route.ts +++ b/app/api/agent/[agentId]/restore/route.ts @@ -66,8 +66,8 @@ export async function POST( await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore"); const response = { - message: true, - details: "Restoration successfully updated" + status: true, + message: "Restoration successfully updated" } From 5d4a1248e4c5a14874d800466a7a94d5a157ce55 Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Mon, 16 Mar 2026 08:54:13 +0100 Subject: [PATCH 02/19] fix: S3ChannelConfigSchema type port mismatch --- .../channel-form/providers/storages/forms/s3.schema.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.schema.ts b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.schema.ts index a732a84b..2b4d6b4c 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.schema.ts +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.schema.ts @@ -6,6 +6,10 @@ export const S3ChannelConfigSchema = z.object({ accessKey: z.string().min(1, "Access Key is required"), secretKey: z.string().min(1, "Secret Key is required"), bucketName: z.string().min(1, "Bucket name is required"), - port: z.coerce.number().optional(), + port: z.union([ + z.literal("").transform(() => ""), + z.literal("").transform(() => ""), + z.coerce.number() + ]).optional(), ssl: z.boolean().optional().default(true), }); From e575ea1a8fce06c3f5e4fe3d2d5828f15bc41fe5 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 15:17:13 +0100 Subject: [PATCH 03/19] fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme. --- .env.example | 1 - .github/workflows/e2e.yml | 84 +++ .gitignore | 8 + e2e/auth.spec.ts | 136 ++++ package.json | 1 + playwright.config.ts | 79 +++ pnpm-lock.yaml | 76 ++- seeds/keycloak/.gitkeep | 1 - seeds/pocket-id/.gitkeep | 1 - src/lib/auth/auth.ts | 1313 ++++++++++++++++++------------------- 10 files changed, 1016 insertions(+), 684 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/auth.spec.ts create mode 100644 playwright.config.ts delete mode 100644 seeds/keycloak/.gitkeep delete mode 100644 seeds/pocket-id/.gitkeep diff --git a/.env.example b/.env.example index 904979e5..e30239f9 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,6 @@ DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public # Project PROJECT_NAME="Portabase" -PROJECT_DESCRIPTION="Portabase is a powerful database manager" PROJECT_URL=http://localhost:8887 PROJECT_SECRET= diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..d3ad4514 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,84 @@ +name: End-to-end testing + +on: + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: devuser + POSTGRES_PASSWORD: changeme + POSTGRES_DB: devdb + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U devuser -d devdb" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/dockerfile/Dockerfile + push: false + load: true + tags: portabase/portabase:test + + - name: Run app container + run: | + docker run -d --name myapp \ + -p 8887:8887 \ + -e DATABASE_URL=postgres://devuser:changeme@postgres:5432/devdb \ + -e NODE_ENV=production \ + -e PROJECT_URL=http://localhost:8887 \ + portabase/portabase:test + + - name: Wait for app port to be listening + run: | + for i in {1..40}; do + if nc -z localhost 8887; then + echo "Port 8887 is open → app is likely listening" + exit 0 + fi + echo "Waiting for port 8887... ($i/40)" + sleep 2 + done + echo "Timeout: port 8887 never opened" + docker logs myapp + exit 1 + + - name: Setup Node + Playwright + uses: actions/setup-node@v4 + with: + node-version: 22 + + - run: npm ci + - run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npx playwright test + env: + BASE_URL: http://localhost:8887 + + - name: Upload report if failed + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Cleanup + if: always() + run: docker stop myapp && docker rm myapp || true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 50ec4bb3..38625915 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,11 @@ private/keys/* seeds/pocket-id/*.db certificates + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts new file mode 100644 index 00000000..401734ed --- /dev/null +++ b/e2e/auth.spec.ts @@ -0,0 +1,136 @@ +import {test, expect, Page} from '@playwright/test'; + + +type UserCredentials = { + username: string + email: string + password: string +} + + +const users: Record = { + admin: {username: 'Admin', email: 'admin5@example.com', password: 'testPASS123456!'}, + normal: {username: 'John Doe', email: 'john5@example.comm', password: 'testPASS123456!'}, +} + +async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) { + await page.locator('input[name="name"]').fill(name) + await page.locator('input[name="email"]').fill(email) + await page.locator('input[name="password"]').fill(password) + await page.locator('input[name="confirmPassword"]').fill(confirmPassword) + + await page.click('button[type="submit"]') +} + +async function login(page: Page, email: string, password: string) { + await page.locator('input[name="email"]').fill(email) + await page.locator('input[name="password"]').fill(password) + + // await page.click('button[type="submit"]') + await page.locator('button:has-text("Login")').click() + // await page.click('text=Login') +} + +async function logout() { +} + +test.describe.serial('User signup and login flows', () => { + + test('Redirect to login if not connected', async ({page}) => { + await page.goto('dashboard/projects'); + await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects"); + }); + + test('Password too short', async ({page}) => { + await page.goto('') + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + let password = '123456' + await register(page, users["admin"].username, users["admin"].email, password, password) + + const toast = page.locator('text=Must have at least 8 character') + await expect(toast).toBeVisible() + }) + + test('Password too simple', async ({page}) => { + await page.goto('') + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + let password = '12345678' + await register(page, users["admin"].username, users["admin"].email, password, password) + + const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.') + await expect(toast).toBeVisible() + }) + + test('Password and confirm password mismatch', async ({page}) => { + await page.goto('') + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + let password = 'testPASS123456!' + let confirmPassword = 'testPASS123456!!' + await register(page, users["admin"].username, users["admin"].email, password, confirmPassword) + + const toast = page.locator('text=The passwords did not match') + await expect(toast).toBeVisible() + }) + + test('Successful register for admin', async ({page}) => { + await page.goto('') + + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) + + await expect(page).toHaveURL('login') + }) + + test('User already exists.', async ({page}) => { + await page.goto('') + + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) + + const toast = page.locator('text=User already exists. Use another email.') + await expect(toast).toBeVisible() + }) + + test('Successful register for normal', async ({page}) => { + await page.goto('') + + await page.click('text=Sign up') + await expect(page).toHaveURL('register') + + await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password) + + await expect(page).toHaveURL('login') + }) + + test('Failed login because account not active', async ({page}) => { + await page.goto('login') + await login(page, users["normal"].email, users["normal"].password) + + const toast = page.locator('text=Your account is not active.') + await expect(toast).toBeVisible() + }) + + test('Successful login', async ({page}) => { + await page.goto('login') + await login(page, users["admin"].email, users["admin"].password) + + await expect(page).toHaveURL('dashboard') + }) + +}) + + +test('Successful logout', async ({page}) => { + await logout() + await expect(page).toHaveURL('login') +}) \ No newline at end of file diff --git a/package.json b/package.json index bc12df9a..004bd2fe 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ }, "devDependencies": { "@iconify/react": "^6.0.2", + "@playwright/test": "^1.58.2", "@react-email/preview-server": "4.3.2", "@react-email/render": "^2.0.4", "@release-it/bumper": "^7.0.5", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..6ddd59a3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:8887', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'docker compose up -d && make up', + // url: 'http://localhost:8887', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28e010e8..7bdc3275 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@better-auth/passkey': specifier: ^1.4.19 - version: 1.4.19(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0) + version: 1.4.19(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0) '@better-auth/sso': specifier: ^1.4.19 - version: 1.4.19(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76)) + version: 1.4.19(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76)) '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.71.2(react@19.2.4)) @@ -130,7 +130,7 @@ importers: version: 6.0.0 better-auth: specifier: 1.4.18 - version: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -175,10 +175,10 @@ importers: version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: 16.1.5 - version: 16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-safe-action: specifier: ^7.10.8 - version: 7.10.8(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) + version: 7.10.8(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -267,9 +267,12 @@ importers: '@iconify/react': specifier: ^6.0.2 version: 6.0.2(react@19.2.4) + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@react-email/preview-server': specifier: 4.3.2 - version: 4.3.2(postcss@8.5.6) + version: 4.3.2(@playwright/test@1.58.2)(postcss@8.5.6) '@react-email/render': specifier: ^2.0.4 version: 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1952,6 +1955,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@prisma/client@6.7.0': resolution: {integrity: sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==} engines: {node: '>=18.18'} @@ -4909,6 +4917,11 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6215,6 +6228,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -7695,23 +7718,23 @@ snapshots: nanostores: 1.1.0 zod: 4.3.6 - '@better-auth/passkey@1.4.19(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0)': + '@better-auth/passkey@1.4.19(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0)': dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@simplewebauthn/browser': 13.2.2 '@simplewebauthn/server': 13.2.3 - better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-call: 1.1.8(zod@3.25.76) nanostores: 1.1.0 zod: 4.3.6 - '@better-auth/sso@1.4.19(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))': + '@better-auth/sso@1.4.19(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-call: 1.1.8(zod@3.25.76) fast-xml-parser: 5.3.9 jose: 6.1.3 @@ -8807,6 +8830,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)': optionalDependencies: prisma: 6.7.0(typescript@5.9.3) @@ -10186,7 +10213,7 @@ snapshots: md-to-react-email: 5.0.5(react@19.2.4) react: 19.2.4 - '@react-email/preview-server@4.3.2(postcss@8.5.6)': + '@react-email/preview-server@4.3.2(@playwright/test@1.58.2)(postcss@8.5.6)': dependencies: '@babel/core': 7.26.10 '@babel/parser': 7.27.0 @@ -10212,7 +10239,7 @@ snapshots: json5: 2.2.3 log-symbols: 4.1.0 module-punycode: punycode@2.3.1 - next: 15.5.2(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.5.2(@babel/core@7.26.10)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) node-html-parser: 7.0.1 ora: 5.4.1 pretty-bytes: 6.1.1 @@ -11187,7 +11214,7 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -11205,7 +11232,7 @@ snapshots: '@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3) drizzle-kit: 0.31.9 drizzle-orm: 0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)) - next: 16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pg: 8.18.0 prisma: 6.7.0(typescript@5.9.3) react: 19.2.4 @@ -12564,6 +12591,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -13437,9 +13467,9 @@ snapshots: dependencies: type-fest: 2.19.0 - next-safe-action@7.10.8(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76): + next-safe-action@7.10.8(next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76): dependencies: - next: 16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: @@ -13450,7 +13480,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@15.5.2(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.5.2(@babel/core@7.26.10)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.5.2 '@swc/helpers': 0.5.15 @@ -13468,12 +13498,13 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.2 '@next/swc-win32-arm64-msvc': 15.5.2 '@next/swc-win32-x64-msvc': 15.5.2 + '@playwright/test': 1.58.2 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.5(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.1.5 '@swc/helpers': 0.5.15 @@ -13492,6 +13523,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.5 '@next/swc-win32-arm64-msvc': 16.1.5 '@next/swc-win32-x64-msvc': 16.1.5 + '@playwright/test': 1.58.2 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -13860,6 +13892,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} diff --git a/seeds/keycloak/.gitkeep b/seeds/keycloak/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/seeds/keycloak/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/seeds/pocket-id/.gitkeep b/seeds/pocket-id/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/seeds/pocket-id/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index 83db23f5..bf741033 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -1,539 +1,522 @@ -import { betterAuth } from "better-auth"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import {betterAuth} from "better-auth"; +import {drizzleAdapter} from "better-auth/adapters/drizzle"; import * as drizzleDb from "@/db"; -import { db } from "@/db"; -import { env } from "@/env.mjs"; -import { nextCookies } from "better-auth/next-js"; -import { - admin as adminPlugin, - openAPI, - Organization, - organization, - twoFactor, -} from "better-auth/plugins"; -import { - ac, - admin, - orgAdmin, - orgMember, - orgOwner, - pending, - superadmin, - user, -} from "@/lib/auth/permissions"; -import { headers } from "next/headers"; -import { count, eq } from "drizzle-orm"; -import { - MemberWithUser, - OrganizationWithMembersAndUsers, -} from "@/db/schema/03_organization"; -import { sendEmail } from "@/lib/email"; -import { render } from "@react-email/render"; -import { withUpdatedAt } from "@/db/utils"; +import {db} from "@/db"; +import {env} from "@/env.mjs"; +import {nextCookies} from "better-auth/next-js"; +import {admin as adminPlugin, openAPI, Organization, organization, twoFactor} from "better-auth/plugins"; +import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions"; +import {headers} from "next/headers"; +import {count, eq} from "drizzle-orm"; +import {MemberWithUser, OrganizationWithMembersAndUsers,} from "@/db/schema/03_organization"; +import {sendEmail} from "@/lib/email"; +import {render} from "@react-email/render"; +import {withUpdatedAt} from "@/db/utils"; import EmailVerification from "@/components/emails/auth/email-verification"; import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"; -import { getDeviceDetails } from "@/utils/detection"; +import {getDeviceDetails} from "@/utils/detection"; import EmailNewLogin from "@/components/emails/auth/email-new-login"; -import { sso } from "@better-auth/sso"; -import { SUPPORTED_PROVIDERS } from "@/lib/auth/config"; -import { passkey } from "@better-auth/passkey"; -import { getOidcProviders } from "./oidc"; -import { APIError } from "better-auth/api"; -import { getOAuthProviders } from "./oauth"; +import {sso} from "@better-auth/sso"; +import {SUPPORTED_PROVIDERS} from "@/lib/auth/config"; +import {passkey} from "@better-auth/passkey"; +import {getOidcProviders} from "./oidc"; +import {APIError} from "better-auth/api"; +import {getOAuthProviders} from "./oauth"; + const oidcProviders = getOidcProviders(); export const auth = betterAuth({ - onAPIError: { - errorURL: "/error", - onError: (error, ctx) => { - //todo: capture errors in a monitoring service - }, - }, - database: drizzleAdapter(db, { - provider: "pg", - schema: drizzleDb.schemas, - }), - appName: env.PROJECT_NAME!, - baseURL: env.PROJECT_URL, - secret: env.PROJECT_SECRET, - emailAndPassword: { - enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", - requireEmailVerification: false, - sendResetPassword: async ({ user, token }, request) => { - if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { - throw new APIError("FORBIDDEN", { - message: "Password reset is disabled.", - }); - } - await db - .update(drizzleDb.schemas.user) - .set( - withUpdatedAt({ - emailVerified: true, - }), - ) - .where(eq(drizzleDb.schemas.user.id, user.id)) - .returning(); - - await sendEmail({ - to: user.email, - subject: "Reset your password", - html: await render( - EmailForgotPassword({ - firstname: user.name!, - token, - }), - {}, - ), - }); - }, - }, - emailVerification: { - async sendVerificationEmail({ user, token, url }) { - await sendEmail({ - to: user.email, - subject: "Portabase Email Verification", - html: await render( - EmailVerification({ - firstname: user.name, - url: url, - }), - ), - }); - - await ( - await auth.$context - ).internalAdapter.updateUser(user.id, { - emailVerified: false, - }); + onAPIError: { + errorURL: "/error", + onError: (error, ctx) => { + //todo: capture errors in a monitoring service + }, }, - async afterEmailVerification(user) { - await ( - await auth.$context - ).internalAdapter.updateUser(user.id, { - emailVerified: true, - }); + database: drizzleAdapter(db, { + provider: "pg", + schema: drizzleDb.schemas, + }), + appName: env.PROJECT_NAME!, + baseURL: env.PROJECT_URL, + secret: env.PROJECT_SECRET, + emailAndPassword: { + enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", + requireEmailVerification: false, + sendResetPassword: async ({user, token}, request) => { + if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { + throw new APIError("FORBIDDEN", { + message: "Password reset is disabled.", + }); + } + await db + .update(drizzleDb.schemas.user) + .set( + withUpdatedAt({ + emailVerified: true, + }), + ) + .where(eq(drizzleDb.schemas.user.id, user.id)) + .returning(); + + await sendEmail({ + to: user.email, + subject: "Reset your password", + html: await render( + EmailForgotPassword({ + firstname: user.name!, + token, + }), + {}, + ), + }); + }, }, - }, - socialProviders: getOAuthProviders().reduce< - Record - >((acc, provider) => { - const configEntry = SUPPORTED_PROVIDERS.find((p) => p.id === provider.id); - - if (!configEntry?.isActive) { - return acc; - } + emailVerification: { + async sendVerificationEmail({user, token, url}) { + await sendEmail({ + to: user.email, + subject: "Portabase Email Verification", + html: await render( + EmailVerification({ + firstname: user.name, + url: url, + }), + ), + }); - acc[provider.id] = { - clientId: provider.client, - clientSecret: provider.secret, - ...(provider.id === "apple" && provider.appleBundleIdentifier - ? { appBundleIdentifier: provider.appleBundleIdentifier } - : {}), - }; - return acc; - }, {}), - account: { - accountLinking: { - enabled: true, - trustedProviders: [ - "credential", - ...getOAuthProviders().map((p) => p.id), - ...oidcProviders.map((p) => p.id), - ], - allowDifferentEmails: true, - }, - }, - - plugins: [ - sso({ - defaultSSO: oidcProviders.map((p) => ({ - oidcConfig: { - issuer: p.issuerUrl, - discoveryEndpoint: p.discoveryEndpoint, - jwksEndpoint: p.jwksEndpoint, - clientId: p.client, - clientSecret: p.secret, - scopes: p.scopes?.split(" ") ?? ["openid", "profile", "email"], - pkce: p.pkce, - mapping: { - extraFields: { - groups: "groups", - }, - }, + await ( + await auth.$context + ).internalAdapter.updateUser(user.id, { + emailVerified: false, + }); }, - providerId: p.id, - domain: p.host, - //@ts-ignore - issuer: p.issuerUrl, - })), - provisionUser: async ({ user: usr, userInfo, provider }) => { - const existingUser = await db.query.user.findFirst({ - where: eq(drizzleDb.schemas.user.email, usr.email), - }); - - if (existingUser && existingUser.role === "superadmin") { - return; - } - - if (existingUser && env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN === "false") { - return; - } - - const providerId = provider.providerId; - const oidcProvider = oidcProviders.find((p) => p.id === providerId); - const allowedGroup = oidcProvider?.allowedGroup || env.ALLOWED_GROUP; - const roleMapStr = oidcProvider?.roleMap; - - const rawGroups = userInfo?.groups || userInfo?.roles || []; - - const userGroups: string[] = Array.isArray(rawGroups) - ? rawGroups.reduce((carry: string[], group) => { - carry.push(String(group).toLowerCase()); - return carry; - }, []) - : [String(rawGroups).toLowerCase()]; - - let roleToAssign: string | undefined; - - if (roleMapStr) { - const mappings = roleMapStr.split(",").map((m) => m.split(":")); - for (const [group, role] of mappings) { - if (userGroups.includes(group.trim())) { - roleToAssign = role.trim(); - break; - } - } - } - - if (!roleToAssign && allowedGroup) { - const hasAccess = userGroups.includes(allowedGroup); - - if (hasAccess) { - const userCount = ( - await db.select({ count: count() }).from(drizzleDb.schemas.user) - )[0].count; - const isSuperadmin = userCount === 0; - - roleToAssign = - allowedGroup.includes("admin") || - allowedGroup.includes("superadmin") - ? isSuperadmin - ? "superadmin" - : "admin" - : "pending"; - } - } - - if (!roleToAssign && oidcProvider?.defaultRole) { - roleToAssign = oidcProvider.defaultRole; - } - - if (!roleToAssign) { - console.warn( - `Access Denied for user ${usr.email}: No matching group/role found in ${providerId} config.`, - ); - throw new APIError("FORBIDDEN", { - message: `Access Denied: No matching roles found (${roleToAssign})`, - }); - } + async afterEmailVerification(user) { + await ( + await auth.$context + ).internalAdapter.updateUser(user.id, { + emailVerified: true, + }); + }, + }, + socialProviders: getOAuthProviders().reduce< + Record + >((acc, provider) => { + const configEntry = SUPPORTED_PROVIDERS.find((p) => p.id === provider.id); - const userCount = ( - await db.select({ count: count() }).from(drizzleDb.schemas.user) - )[0].count; - if ( - userCount === 0 && - (roleToAssign === "admin" || roleToAssign === "superadmin") - ) { - roleToAssign = "superadmin"; + if (!configEntry?.isActive) { + return acc; } - if (existingUser) { - await db - .update(drizzleDb.schemas.user) - .set({ role: roleToAssign, emailVerified: true }) - .where(eq(drizzleDb.schemas.user.id, existingUser.id)); - } else { - return { - ...usr, - role: roleToAssign, - emailVerified: true, - }; - } - }, - }), - ...(env.AUTH_PASSKEY_ENABLED === "true" - ? [ - passkey({ - rpName: env.PROJECT_NAME || "Portabase", - rpID: env.PROJECT_URL - ? new URL(env.PROJECT_URL).hostname - : "localhost", - }), - ] - : []), - openAPI(), - nextCookies(), - twoFactor(), - organization({ - ac, - roles: { - owner: orgOwner, - admin: orgAdmin, - member: orgMember, - }, - }), - adminPlugin({ - adminRoles: ["admin", "superadmin"], - defaultRole: "pending", - ac, - roles: { - admin, - user, - pending, - superadmin, - }, - }), - ], - advanced: { - database: { - generateId: false, - }, - cookies: { - state: { - attributes: { - sameSite: "none", - secure: true, + acc[provider.id] = { + clientId: provider.client, + clientSecret: provider.secret, + ...(provider.id === "apple" && provider.appleBundleIdentifier + ? {appBundleIdentifier: provider.appleBundleIdentifier} + : {}), + }; + return acc; + }, {}), + account: { + accountLinking: { + enabled: true, + trustedProviders: [ + "credential", + ...getOAuthProviders().map((p) => p.id), + ...oidcProviders.map((p) => p.id), + ], + allowDifferentEmails: true, }, - }, }, - }, - user: { - deleteUser: { - enabled: true, - }, - changeEmail: { - enabled: true, - }, - additionalFields: { - deletedAt: { - type: "number", - nullable: true, - required: false, - }, - theme: { - type: "string", - }, - lastConnectedAt: { - type: "date", - }, - lastChangedPasswordAt: { - type: "date", - }, - }, - }, - databaseHooks: { - account: { - create: { - before: async (account) => { - const provider = SUPPORTED_PROVIDERS.find( - (p) => p.id === account.providerId, - ); - - if (provider && provider.allowLinking === false) { - throw new APIError("FORBIDDEN", { - message: "Linking is disabled for this provider.", - }); - } + + plugins: [ + sso({ + defaultSSO: oidcProviders.map((p) => ({ + oidcConfig: { + issuer: p.issuerUrl, + discoveryEndpoint: p.discoveryEndpoint, + jwksEndpoint: p.jwksEndpoint, + clientId: p.client, + clientSecret: p.secret, + scopes: p.scopes?.split(" ") ?? ["openid", "profile", "email"], + pkce: p.pkce, + mapping: { + extraFields: { + groups: "groups", + }, + }, + }, + providerId: p.id, + domain: p.host, + //@ts-ignore + issuer: p.issuerUrl, + })), + provisionUser: async ({user: usr, userInfo, provider}) => { + const existingUser = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.email, usr.email), + }); + + if (existingUser && existingUser.role === "superadmin") { + return; + } + + if (existingUser && env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN === "false") { + return; + } + + const providerId = provider.providerId; + const oidcProvider = oidcProviders.find((p) => p.id === providerId); + const allowedGroup = oidcProvider?.allowedGroup || env.ALLOWED_GROUP; + const roleMapStr = oidcProvider?.roleMap; + + const rawGroups = userInfo?.groups || userInfo?.roles || []; + + const userGroups: string[] = Array.isArray(rawGroups) + ? rawGroups.reduce((carry: string[], group) => { + carry.push(String(group).toLowerCase()); + return carry; + }, []) + : [String(rawGroups).toLowerCase()]; + + let roleToAssign: string | undefined; + + if (roleMapStr) { + const mappings = roleMapStr.split(",").map((m) => m.split(":")); + for (const [group, role] of mappings) { + if (userGroups.includes(group.trim())) { + roleToAssign = role.trim(); + break; + } + } + } + + if (!roleToAssign && allowedGroup) { + const hasAccess = userGroups.includes(allowedGroup); + + if (hasAccess) { + const userCount = ( + await db.select({count: count()}).from(drizzleDb.schemas.user) + )[0].count; + const isSuperadmin = userCount === 0; + + roleToAssign = + allowedGroup.includes("admin") || + allowedGroup.includes("superadmin") + ? isSuperadmin + ? "superadmin" + : "admin" + : "pending"; + } + } + + if (!roleToAssign && oidcProvider?.defaultRole) { + roleToAssign = oidcProvider.defaultRole; + } + + if (!roleToAssign) { + console.warn( + `Access Denied for user ${usr.email}: No matching group/role found in ${providerId} config.`, + ); + throw new APIError("FORBIDDEN", { + message: `Access Denied: No matching roles found (${roleToAssign})`, + }); + } + + const userCount = ( + await db.select({count: count()}).from(drizzleDb.schemas.user) + )[0].count; + if ( + userCount === 0 && + (roleToAssign === "admin" || roleToAssign === "superadmin") + ) { + roleToAssign = "superadmin"; + } + + if (existingUser) { + await db + .update(drizzleDb.schemas.user) + .set({role: roleToAssign, emailVerified: true}) + .where(eq(drizzleDb.schemas.user.id, existingUser.id)); + } else { + return { + ...usr, + role: roleToAssign, + emailVerified: true, + }; + } + }, + }), + ...(env.AUTH_PASSKEY_ENABLED === "true" + ? [ + passkey({ + rpName: env.PROJECT_NAME || "Portabase", + rpID: env.PROJECT_URL + ? new URL(env.PROJECT_URL).hostname + : "localhost", + }), + ] + : []), + openAPI(), + nextCookies(), + twoFactor(), + organization({ + ac, + roles: { + owner: orgOwner, + admin: orgAdmin, + member: orgMember, + }, + }), + adminPlugin({ + adminRoles: ["admin", "superadmin"], + defaultRole: "pending", + ac, + roles: { + admin, + user, + pending, + superadmin, + }, + }), + ], + advanced: { + database: { + generateId: false, + }, + cookies: { + state: { + attributes: { + sameSite: "none", + secure: true, + }, + }, }, - }, }, user: { - update: { - async before(user, context) { - if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { - if (user.password || user.lastChangedPasswordAt) { - throw new APIError("FORBIDDEN", { - message: "Password updates are disabled", - }); - } - } - return { - data: user, - }; + deleteUser: { + enabled: true, }, - }, - create: { - async before(user, context) { - const userCount = ( - await db.select({ count: count() }).from(drizzleDb.schemas.user) - )[0].count; - - if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) { - throw new APIError("FORBIDDEN", { - message: "Sign up is disabled", - }); - } - - const role = userCount === 0 ? "superadmin" : "pending"; - - return { - data: { - ...user, - role, - }, - }; + changeEmail: { + enabled: true, }, - async after(user, context) { - const userCount = ( - await db.select({ count: count() }).from(drizzleDb.schemas.user) - )[0].count; - const role = userCount === 0 ? "owner" : "admin"; - - const defaultOrgSlug = "default"; - const defaultOrg = await db.query.organization.findFirst({ - where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), - }); - - if (defaultOrg) { - await db.insert(drizzleDb.schemas.member).values({ - userId: user.id, - organizationId: defaultOrg.id, - role: role, - }); - } else { - console.warn( - "Default organization not found. Cannot assign member.", - ); - } + additionalFields: { + deletedAt: { + type: "number", + nullable: true, + required: false, + }, + theme: { + type: "string", + }, + lastConnectedAt: { + type: "date", + }, + lastChangedPasswordAt: { + type: "date", + }, }, - }, }, - session: { - create: { - before: async (session, context) => { - const userId = session.userId; - - const memberships = await db.query.member.findMany({ - where: eq(drizzleDb.schemas.member.userId, userId), - }); - - const url = - context?.request?.url || context?.headers?.get("referer") || ""; - - let providerId: string | undefined; - - if (url) { - const urlPath = new URL(url).pathname; - const pathParts = urlPath.split("/"); - const lastPathPart = pathParts[pathParts.length - 1]; + databaseHooks: { + account: { + create: { + before: async (account) => { + const provider = SUPPORTED_PROVIDERS.find( + (p) => p.id === account.providerId, + ); + + if (provider && provider.allowLinking === false) { + throw new APIError("FORBIDDEN", { + message: "Linking is disabled for this provider.", + }); + } + }, + }, + }, + user: { + update: { + async before(user, context) { + if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { + if (user.password || user.lastChangedPasswordAt) { + throw new APIError("FORBIDDEN", { + message: "Password updates are disabled", + }); + } + } + return { + data: user, + }; + }, + }, + create: { + async before(user, context) { + const userCount = ( + await db.select({count: count()}).from(drizzleDb.schemas.user) + )[0].count; + + if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) { + throw new APIError("FORBIDDEN", { + message: "Sign up is disabled", + }); + } - if (urlPath.startsWith("/api/auth/sso/callback/")) { - providerId = lastPathPart; - } else if (urlPath.startsWith("/api/auth/callback/")) { - providerId = lastPathPart; - } - } + const role = userCount === 0 ? "superadmin" : "pending"; - return { - data: { - activeOrganizationId: memberships[0]?.organizationId, - providerId: providerId, + return { + data: { + ...user, + role, + }, + }; + }, + async after(user, context) { + const userCount = ( + await db.select({count: count()}).from(drizzleDb.schemas.user) + )[0].count; + const role = userCount === 0 ? "owner" : "admin"; + + const defaultOrgSlug = "default"; + const defaultOrg = await db.query.organization.findFirst({ + where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), + }); + + if (defaultOrg) { + await db.insert(drizzleDb.schemas.member).values({ + userId: user.id, + organizationId: defaultOrg.id, + role: role, + }); + } else { + console.warn( + "Default organization not found. Cannot assign member.", + ); + } + }, }, - }; - }, - after: async (session) => { - const user = await db.query.user.findFirst({ - where: eq(drizzleDb.schemas.user.id, session.userId), - }); - - if (!user) return; - - const createdAtDiff = - new Date(session.createdAt).getTime() - - new Date(user.createdAt).getTime(); - - if (createdAtDiff < 5000) { - console.log( - `Skipping new login email for freshly created user ${user.email}`, - ); - return; - } - - const lastDiff = user.lastConnectedAt - ? new Date(session.createdAt).getTime() - - new Date(user.lastConnectedAt).getTime() - : Infinity; - - if (lastDiff < 30000) return; - - if (user.role === "pending") return; - - const deviceInfo = getDeviceDetails(session.userAgent); - - await sendEmail({ - to: user.email, - subject: "New login to your account", - html: await render( - EmailNewLogin({ - firstname: user.name!, - os: deviceInfo.os, - browser: deviceInfo.browser, - ipAddress: session.ipAddress!, - }), - {}, - ), - }); - - (await auth.$context).internalAdapter.updateUser(user.id, { - lastConnectedAt: new Date(), - }); }, - }, - }, - }, - session: { - additionalFields: { - activeOrganizationId: { - type: "string", - required: false, - }, - providerId: { - type: "string", - required: false, - }, - }, - }, - /* databaseHooks: { session: { create: { - before: async (session) => { - const organizationId = await getLastOrganizationOrFirst(session.userId); + before: async (session, context) => { + const userId = session.userId; - if (!organizationId) { - return { - ...session, - }; - } + const memberships = await db.query.member.findMany({ + where: eq(drizzleDb.schemas.member.userId, userId), + }); + const url = + context?.request?.url || context?.headers?.get("referer") || ""; - const [aa] = await db - .update(drizzleUser.session) - .set({ activeOrganizationId: organizationId }) - .where(eq(drizzleUser.session.id, session.id)) - .returning(); + let providerId: string | undefined; + if (url) { + const urlPath = new URL(url).pathname; + const pathParts = urlPath.split("/"); + const lastPathPart = pathParts[pathParts.length - 1]; + + if (urlPath.startsWith("/api/auth/sso/callback/")) { + providerId = lastPathPart; + } else if (urlPath.startsWith("/api/auth/callback/")) { + providerId = lastPathPart; + } + } return { - ...session, - activeOrganizationId: organizationId, + data: { + activeOrganizationId: memberships[0]?.organizationId, + providerId: providerId, + }, }; }, + after: async (session) => { + const user = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.id, session.userId), + }); + + if (!user) return; + + const createdAtDiff = + new Date(session.createdAt).getTime() - + new Date(user.createdAt).getTime(); + + if (createdAtDiff < 5000) { + console.log( + `Skipping new login email for freshly created user ${user.email}`, + ); + return; + } + + const lastDiff = user.lastConnectedAt + ? new Date(session.createdAt).getTime() - + new Date(user.lastConnectedAt).getTime() + : Infinity; + + if (lastDiff < 30000) return; + + if (user.role === "pending") return; + + const deviceInfo = getDeviceDetails(session.userAgent); + + await sendEmail({ + to: user.email, + subject: "New login to your account", + html: await render( + EmailNewLogin({ + firstname: user.name!, + os: deviceInfo.os, + browser: deviceInfo.browser, + ipAddress: session.ipAddress!, + }), + {}, + ), + }); + + (await auth.$context).internalAdapter.updateUser(user.id, { + lastConnectedAt: new Date(), + }); + }, }, }, - },*/ - trustedOrigins: [env.PROJECT_URL!, "http://app"], + }, + session: { + additionalFields: { + activeOrganizationId: { + type: "string", + required: false, + }, + providerId: { + type: "string", + required: false, + }, + }, + }, + /* databaseHooks: { + session: { + create: { + before: async (session) => { + const organizationId = await getLastOrganizationOrFirst(session.userId); + + if (!organizationId) { + return { + ...session, + }; + } + + + const [aa] = await db + .update(drizzleUser.session) + .set({ activeOrganizationId: organizationId }) + .where(eq(drizzleUser.session.id, session.id)) + .returning(); + + + return { + ...session, + activeOrganizationId: organizationId, + }; + }, + }, + }, + },*/ + trustedOrigins: [env.PROJECT_URL!, "http://app"], }); /*export const signUpUser = async (email: string, password: string, name: string) => { @@ -560,227 +543,231 @@ export const signInUser = async (email: string, password: string) => { };*/ export const createUser = async ( - name: string, - email: string, - password: string, - role: "user" | "pending" | "admin" | "superadmin" = "pending", + name: string, + email: string, + password: string, + role: "user" | "pending" | "admin" | "superadmin" = "pending", ) => { - return await auth.api.createUser({ - headers: await headers(), - body: { - name, - email, - password, - role, - }, - }); + return await auth.api.createUser({ + headers: await headers(), + body: { + name, + email, + password, + role, + }, + }); }; export const getSessions = async () => { - return await auth.api.listSessions({ - headers: await headers(), - }); + return await auth.api.listSessions({ + headers: await headers(), + }); }; export const getSession = async () => { - return await auth.api.getSession({ - headers: await headers(), - }); + return await auth.api.getSession({ + headers: await headers(), + }); }; export const revokeSession = async (e: string) => { - try { - const { status } = await auth.api.revokeSession({ - body: { - token: e, - }, - headers: await headers(), - }); - return status; - } catch (e) {} + try { + const {status} = await auth.api.revokeSession({ + body: { + token: e, + }, + headers: await headers(), + }); + return status; + } catch (e) { + } }; export const getAccounts = async () => { - return await auth.api.listUserAccounts({ - headers: await headers(), - }); + return await auth.api.listUserAccounts({ + headers: await headers(), + }); }; export const unlinkAccount = async (provider: string, account: string) => { - try { - const { status } = await auth.api.unlinkAccount({ - body: { - providerId: provider, - accountId: account, - }, - headers: await headers(), - }); + try { + const {status} = await auth.api.unlinkAccount({ + body: { + providerId: provider, + accountId: account, + }, + headers: await headers(), + }); - return status; - } catch (e) {} + return status; + } catch (e) { + } }; export const getOrganization = async ({ - organizationId, - organizationSlug, -}: { - organizationId?: string; - organizationSlug?: string; + organizationId, + organizationSlug, + }: { + organizationId?: string; + organizationSlug?: string; } = {}): Promise => { - const query = - organizationId != null - ? { organizationId } - : organizationSlug != null - ? { organizationSlug } - : undefined; - - try { - const response = await auth.api.getFullOrganization({ - headers: await headers(), - ...(query ? { query } : {}), - }); + const query = + organizationId != null + ? {organizationId} + : organizationSlug != null + ? {organizationSlug} + : undefined; + + try { + const response = await auth.api.getFullOrganization({ + headers: await headers(), + ...(query ? {query} : {}), + }); - return response as OrganizationWithMembersAndUsers; - } catch (e) { - console.error(e); - return null; - } + return response as OrganizationWithMembersAndUsers; + } catch (e) { + console.error(e); + return null; + } }; export const getPasskeys = async () => { - if (env.AUTH_PASSKEY_ENABLED !== "true") return []; - const passkeys = await auth.api.listPasskeys({ - headers: await headers(), - }); + if (env.AUTH_PASSKEY_ENABLED !== "true") return []; + const passkeys = await auth.api.listPasskeys({ + headers: await headers(), + }); - return passkeys; + return passkeys; }; export const revokePasskey = async (e: string) => { - if (env.AUTH_PASSKEY_ENABLED !== "true") return; - await auth.api.deletePasskey({ - body: { - id: e, - }, - headers: await headers(), - }); + if (env.AUTH_PASSKEY_ENABLED !== "true") return; + await auth.api.deletePasskey({ + body: { + id: e, + }, + headers: await headers(), + }); }; export const listOrganizations = async (): Promise => { - try { - return (await auth.api.listOrganizations({ - headers: await headers(), - })) as Organization[]; - } catch (e) { - return null; - } + try { + return (await auth.api.listOrganizations({ + headers: await headers(), + })) as Organization[]; + } catch (e) { + return null; + } }; export const getLastOrganizationOrFirst = async (userId: string) => { - try { - const organizations = await db.query.organization.findMany({ - where: eq(drizzleDb.schemas.member.userId, userId), - }); + try { + const organizations = await db.query.organization.findMany({ + where: eq(drizzleDb.schemas.member.userId, userId), + }); - if (organizations.length > 0) { - return organizations[0].id; - } + if (organizations.length > 0) { + return organizations[0].id; + } - return null; - } catch (e) { - return null; - } + return null; + } catch (e) { + return null; + } }; export const createOrganization = async (name: string, slug: string) => { - try { - return await auth.api.createOrganization({ - headers: await headers(), - body: { - name, - slug, - }, - }); - } catch (e: any) { - const errorMessage = - e?.response?.data?.message || e?.message || "Unknown auth error"; - const status = e?.response?.status || 500; - - console.error("Auth API createOrganization error:", { - message: errorMessage, - status, - raw: e, - }); + try { + return await auth.api.createOrganization({ + headers: await headers(), + body: { + name, + slug, + }, + }); + } catch (e: any) { + const errorMessage = + e?.response?.data?.message || e?.message || "Unknown auth error"; + const status = e?.response?.status || 500; + + console.error("Auth API createOrganization error:", { + message: errorMessage, + status, + raw: e, + }); - throw { - name: "AuthCreateOrganizationError", - message: errorMessage, - status, - cause: e, - }; - } + throw { + name: "AuthCreateOrganizationError", + message: errorMessage, + status, + cause: e, + }; + } }; export const deleteOrganization = async (organizationId: string) => { - try { - return await auth.api.deleteOrganization({ - body: { - organizationId, - }, - headers: await headers(), - }); - } catch (e: any) { - const errorMessage = - e?.response?.data?.message || e?.message || "Unknown auth error"; - const status = e?.response?.status || 500; - - console.error("Auth API deleteOrganization error:", { - message: errorMessage, - status, - raw: e, - }); + try { + return await auth.api.deleteOrganization({ + body: { + organizationId, + }, + headers: await headers(), + }); + } catch (e: any) { + const errorMessage = + e?.response?.data?.message || e?.message || "Unknown auth error"; + const status = e?.response?.status || 500; + + console.error("Auth API deleteOrganization error:", { + message: errorMessage, + status, + raw: e, + }); - throw { - name: "AuthDeleteOrganizationError", - message: errorMessage, - status, - cause: e, - }; - } + throw { + name: "AuthDeleteOrganizationError", + message: errorMessage, + status, + cause: e, + }; + } }; export const checkSlugOrganization = async (slug: string) => { - try { - const { status } = await auth.api.checkOrganizationSlug({ - headers: await headers(), - body: { - slug, - }, - }); + try { + const {status} = await auth.api.checkOrganizationSlug({ + headers: await headers(), + body: { + slug, + }, + }); - return status; - } catch {} + return status; + } catch { + } }; export const getActiveMember = async () => { - try { - const member = await auth.api.getActiveMember({ - headers: await headers(), - }); + try { + const member = await auth.api.getActiveMember({ + headers: await headers(), + }); - return member as MemberWithUser; - } catch (e) { - console.log("err", e); - } + return member as MemberWithUser; + } catch (e) { + console.log("err", e); + } }; export const setActiveOrganization = async (slug: string) => { - try { - return await auth.api.setActiveOrganization({ - headers: await headers(), - body: { - organizationSlug: slug, - }, - }); - } catch {} + try { + return await auth.api.setActiveOrganization({ + headers: await headers(), + body: { + organizationSlug: slug, + }, + }); + } catch { + } }; From 014ad35127e583387620d161c3d4ead6555ff71f Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 15:31:06 +0100 Subject: [PATCH 04/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d3ad4514..95a9e170 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -26,6 +26,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Build Docker image uses: docker/build-push-action@v6 with: @@ -58,16 +72,11 @@ jobs: docker logs myapp exit 1 - - name: Setup Node + Playwright - uses: actions/setup-node@v4 - with: - node-version: 22 - - - run: npm ci - - run: npx playwright install --with-deps + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps - name: Run Playwright tests - run: npx playwright test + run: pnpm exec playwright test env: BASE_URL: http://localhost:8887 From 67d9de61a4f19263daf4f21628ace0779593754d Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 15:40:31 +0100 Subject: [PATCH 05/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 2 -- .gitignore | 1 - e2e/auth.spec.ts | 43 +++++++++++++++++++-------------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 95a9e170..7ec2609a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -28,8 +28,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 38625915..07983545 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,6 @@ seeds/pocket-id/*.db certificates # Playwright -node_modules/ /test-results/ /playwright-report/ /blob-report/ diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index 401734ed..49b385a0 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -9,8 +9,8 @@ type UserCredentials = { const users: Record = { - admin: {username: 'Admin', email: 'admin5@example.com', password: 'testPASS123456!'}, - normal: {username: 'John Doe', email: 'john5@example.comm', password: 'testPASS123456!'}, + admin: {username: 'Admin', email: 'admin@example.com', password: 'testPASS123456!'}, + normal: {username: 'John Doe', email: 'john.doe@example.com', password: 'testPASS123456!'}, } async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) { @@ -26,25 +26,24 @@ async function login(page: Page, email: string, password: string) { await page.locator('input[name="email"]').fill(email) await page.locator('input[name="password"]').fill(password) - // await page.click('button[type="submit"]') await page.locator('button:has-text("Login")').click() - // await page.click('text=Login') } async function logout() { + //TODO } test.describe.serial('User signup and login flows', () => { test('Redirect to login if not connected', async ({page}) => { - await page.goto('dashboard/projects'); + await page.goto('/dashboard/projects'); await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects"); }); test('Password too short', async ({page}) => { await page.goto('') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') let password = '123456' await register(page, users["admin"].username, users["admin"].email, password, password) @@ -56,7 +55,7 @@ test.describe.serial('User signup and login flows', () => { test('Password too simple', async ({page}) => { await page.goto('') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') let password = '12345678' await register(page, users["admin"].username, users["admin"].email, password, password) @@ -66,9 +65,9 @@ test.describe.serial('User signup and login flows', () => { }) test('Password and confirm password mismatch', async ({page}) => { - await page.goto('') + await page.goto('/') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') let password = 'testPASS123456!' let confirmPassword = 'testPASS123456!!' @@ -79,21 +78,21 @@ test.describe.serial('User signup and login flows', () => { }) test('Successful register for admin', async ({page}) => { - await page.goto('') + await page.goto('/') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) - await expect(page).toHaveURL('login') + await expect(page).toHaveURL('/login') }) test('User already exists.', async ({page}) => { - await page.goto('') + await page.goto('/') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) @@ -102,10 +101,10 @@ test.describe.serial('User signup and login flows', () => { }) test('Successful register for normal', async ({page}) => { - await page.goto('') + await page.goto('/') await page.click('text=Sign up') - await expect(page).toHaveURL('register') + await expect(page).toHaveURL('/register') await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password) @@ -113,7 +112,7 @@ test.describe.serial('User signup and login flows', () => { }) test('Failed login because account not active', async ({page}) => { - await page.goto('login') + await page.goto('/login') await login(page, users["normal"].email, users["normal"].password) const toast = page.locator('text=Your account is not active.') @@ -124,13 +123,13 @@ test.describe.serial('User signup and login flows', () => { await page.goto('login') await login(page, users["admin"].email, users["admin"].password) - await expect(page).toHaveURL('dashboard') + await expect(page).toHaveURL('/dashboard') }) }) -test('Successful logout', async ({page}) => { - await logout() - await expect(page).toHaveURL('login') -}) \ No newline at end of file +// test('Successful logout', async ({page}) => { +// await logout() +// await expect(page).toHaveURL('/login') +// }) \ No newline at end of file From a2193eb3f4c3098bc6fac456cab466bee3d8e8ab Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 15:50:10 +0100 Subject: [PATCH 06/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7ec2609a..02e0c026 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,7 +51,7 @@ jobs: run: | docker run -d --name myapp \ -p 8887:8887 \ - -e DATABASE_URL=postgres://devuser:changeme@postgres:5432/devdb \ + -e DATABASE_URL=postgres://devuser:changeme@localhost:5432/devdb \ -e NODE_ENV=production \ -e PROJECT_URL=http://localhost:8887 \ portabase/portabase:test From b901e8bf10f0e351701bd03ba884f34d7bc7b2ac Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 15:57:37 +0100 Subject: [PATCH 07/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 1 + app/layout.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 02e0c026..907b8175 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -50,6 +50,7 @@ jobs: - name: Run app container run: | docker run -d --name myapp \ + --network host \ -p 8887:8887 \ -e DATABASE_URL=postgres://devuser:changeme@localhost:5432/devdb \ -e NODE_ENV=production \ diff --git a/app/layout.tsx b/app/layout.tsx index ce4d8e2f..0b62b03b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,7 +6,7 @@ import { author, geistMono, poppins } from "@/fonts/fonts"; import { cn } from "@/lib/utils"; import { Providers } from "./providers"; -const title = process.env.PROJECT_NAME ?? "App Title"; +const title = process.env.PROJECT_NAME ?? "Portabase"; export const metadata: Metadata = { title: { From 6ade15d94d83be2b5b2f6c76353a88311e93944d Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 16:20:00 +0100 Subject: [PATCH 08/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 907b8175..6cc2325e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -52,9 +52,10 @@ jobs: docker run -d --name myapp \ --network host \ -p 8887:8887 \ - -e DATABASE_URL=postgres://devuser:changeme@localhost:5432/devdb \ + -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ -e NODE_ENV=production \ -e PROJECT_URL=http://localhost:8887 \ + -e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \ portabase/portabase:test - name: Wait for app port to be listening From a9fa97674c53904d668a051d63ad0a21c27d3c54 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 16:31:53 +0100 Subject: [PATCH 09/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6cc2325e..6fbc212a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,24 +51,24 @@ jobs: run: | docker run -d --name myapp \ --network host \ - -p 8887:8887 \ + -p 8887:80 \ -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ -e NODE_ENV=production \ - -e PROJECT_URL=http://localhost:8887 \ + -e PROJECT_URL=http://localhost:80 \ -e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \ portabase/portabase:test - name: Wait for app port to be listening run: | for i in {1..40}; do - if nc -z localhost 8887; then + if nc -z localhost 80; then echo "Port 8887 is open → app is likely listening" exit 0 fi - echo "Waiting for port 8887... ($i/40)" + echo "Waiting for port 80... ($i/40)" sleep 2 done - echo "Timeout: port 8887 never opened" + echo "Timeout: port 80 never opened" docker logs myapp exit 1 @@ -78,7 +78,7 @@ jobs: - name: Run Playwright tests run: pnpm exec playwright test env: - BASE_URL: http://localhost:8887 + BASE_URL: http://localhost:80 - name: Upload report if failed if: failure() From 495a1c6edd6019be9c2dba7a3d1e435bce9a297f Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 16:50:41 +0100 Subject: [PATCH 10/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 12 ++++++------ playwright.config.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6fbc212a..8021b4ca 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,7 +51,6 @@ jobs: run: | docker run -d --name myapp \ --network host \ - -p 8887:80 \ -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ -e NODE_ENV=production \ -e PROJECT_URL=http://localhost:80 \ @@ -61,14 +60,15 @@ jobs: - name: Wait for app port to be listening run: | for i in {1..40}; do - if nc -z localhost 80; then - echo "Port 8887 is open → app is likely listening" + + if curl -fsS http://localhost:80/ >/dev/null; then + echo "App is responding on port 80" exit 0 fi - echo "Waiting for port 80... ($i/40)" + echo "Waiting for app readiness... ($i/40)" sleep 2 done - echo "Timeout: port 80 never opened" + echo "Timeout: app never became ready" docker logs myapp exit 1 @@ -78,7 +78,7 @@ jobs: - name: Run Playwright tests run: pnpm exec playwright test env: - BASE_URL: http://localhost:80 + PROJECT_URL: http://localhost:80 - name: Upload report if failed if: failure() diff --git a/playwright.config.ts b/playwright.config.ts index 6ddd59a3..308f8273 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('')`. */ - baseURL: 'http://localhost:8887', + baseURL: process.env.PROJECT_URL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', From 73c664b7eab404b7453c70f67ddf3e19069645ee Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 17:07:09 +0100 Subject: [PATCH 11/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 41 ++++++++++++++++++++------------------- e2e/auth.spec.ts | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8021b4ca..0af4f03e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,20 +8,20 @@ jobs: build-and-test: runs-on: ubuntu-latest - services: - postgres: - image: postgres:17 - env: - POSTGRES_USER: devuser - POSTGRES_PASSWORD: changeme - POSTGRES_DB: devdb - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U devuser -d devdb" - --health-interval 10s - --health-timeout 5s - --health-retries 5 +# services: +# postgres: +# image: postgres:17 +# env: +# POSTGRES_USER: devuser +# POSTGRES_PASSWORD: changeme +# POSTGRES_DB: devdb +# ports: +# - 5432:5432 +# options: >- +# --health-cmd "pg_isready -U devuser -d devdb" +# --health-interval 10s +# --health-timeout 5s +# --health-retries 5 steps: - uses: actions/checkout@v4 @@ -50,10 +50,11 @@ jobs: - name: Run app container run: | docker run -d --name myapp \ - --network host \ - -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ +# --network host \ + -p 8887:80 +# -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ -e NODE_ENV=production \ - -e PROJECT_URL=http://localhost:80 \ + -e PROJECT_URL=http://localhost:8887 \ -e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \ portabase/portabase:test @@ -61,8 +62,8 @@ jobs: run: | for i in {1..40}; do - if curl -fsS http://localhost:80/ >/dev/null; then - echo "App is responding on port 80" + if curl -fsS http://localhost:8887/ >/dev/null; then + echo "App is responding on port 8887" exit 0 fi echo "Waiting for app readiness... ($i/40)" @@ -78,7 +79,7 @@ jobs: - name: Run Playwright tests run: pnpm exec playwright test env: - PROJECT_URL: http://localhost:80 + PROJECT_URL: http://localhost:8887 - name: Upload report if failed if: failure() diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index 49b385a0..d615ba44 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -123,7 +123,7 @@ test.describe.serial('User signup and login flows', () => { await page.goto('login') await login(page, users["admin"].email, users["admin"].password) - await expect(page).toHaveURL('/dashboard') + await expect(page).toHaveURL('/dashboard/home') }) }) From 9aaac4d52e3d7e04700d5f6ea9e9d3b4d88a16f0 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 17:15:11 +0100 Subject: [PATCH 12/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0af4f03e..1986fa93 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,21 +8,6 @@ jobs: build-and-test: runs-on: ubuntu-latest -# services: -# postgres: -# image: postgres:17 -# env: -# POSTGRES_USER: devuser -# POSTGRES_PASSWORD: changeme -# POSTGRES_DB: devdb -# ports: -# - 5432:5432 -# options: >- -# --health-cmd "pg_isready -U devuser -d devdb" -# --health-interval 10s -# --health-timeout 5s -# --health-retries 5 - steps: - uses: actions/checkout@v4 @@ -50,9 +35,7 @@ jobs: - name: Run app container run: | docker run -d --name myapp \ -# --network host \ - -p 8887:80 -# -e DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public \ + -p 8887:80 \ -e NODE_ENV=production \ -e PROJECT_URL=http://localhost:8887 \ -e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \ From d0ff0b0955a092370879fd44242a4b770f170d0a Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:02:37 +0100 Subject: [PATCH 13/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 4 +--- docker-compose.prod.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1986fa93..9d4e36cd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,6 +7,7 @@ on: jobs: build-and-test: runs-on: ubuntu-latest + container: mcr.microsoft.com/playwright:v1.58.2-noble steps: - uses: actions/checkout@v4 @@ -56,9 +57,6 @@ jobs: docker logs myapp exit 1 - - name: Install Playwright browsers - run: pnpm exec playwright install --with-deps - - name: Run Playwright tests run: pnpm exec playwright test env: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 68ab343a..7e4d50cd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,7 +4,7 @@ services: # context: . # dockerfile: docker/dockerfile/Dockerfile # target: prod - image: portabase/portabase:1.4.0 + image: portabase/portabase:1.7.1 ports: - '8887:80' environment: diff --git a/package.json b/package.json index eec19a8a..bc62d742 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ }, "devDependencies": { "@iconify/react": "^6.0.2", - "@playwright/test": "^1.58.2", + "@playwright/test": "1.58.2", "@react-email/preview-server": "4.3.2", "@react-email/render": "^2.0.4", "@release-it/bumper": "^7.0.5", From 30e053c6d9fb1b289b1d3c954c66362899f21a13 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:05:25 +0100 Subject: [PATCH 14/19] chore: fix bug in github action. --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bdc3275..13304cfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,7 +268,7 @@ importers: specifier: ^6.0.2 version: 6.0.2(react@19.2.4) '@playwright/test': - specifier: ^1.58.2 + specifier: 1.58.2 version: 1.58.2 '@react-email/preview-server': specifier: 4.3.2 From cda0a44433366d70e30d67e83b2e114eafd972f0 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:10:03 +0100 Subject: [PATCH 15/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9d4e36cd..2fe37405 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,6 +24,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Docker image uses: docker/build-push-action@v6 with: From 8e995048a48dad43a2e6deec11dea2e94690a740 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:25:36 +0100 Subject: [PATCH 16/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2fe37405..7c8f5185 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,6 @@ on: jobs: build-and-test: runs-on: ubuntu-latest - container: mcr.microsoft.com/playwright:v1.58.2-noble steps: - uses: actions/checkout@v4 @@ -24,9 +23,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker image uses: docker/build-push-action@v6 with: @@ -60,6 +56,23 @@ jobs: docker logs myapp exit 1 + - name: Install Playwright system dependencies (Chromium) + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq --no-install-recommends \ + ca-certificates \ + fonts-liberation \ + libasound2t64 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libatspi2.0-0 \ + libcairo2 \ + libcups2 \ + libdbus-1 + + - name: Install Playwright browsers + run: pnpm exec playwright install chromium firefox webkit + - name: Run Playwright tests run: pnpm exec playwright test env: From 66bc0c4461fe3d5e217f7539416c0ec57139600b Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:36:28 +0100 Subject: [PATCH 17/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7c8f5185..2d392a0c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -71,7 +71,7 @@ jobs: libdbus-1 - name: Install Playwright browsers - run: pnpm exec playwright install chromium firefox webkit + run: pnpm exec playwright install --with-deps chromium firefox webkit - name: Run Playwright tests run: pnpm exec playwright test From 50b3557da69e7c9a9dad2b98992a424066c3981b Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:36:37 +0100 Subject: [PATCH 18/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2d392a0c..a8ade14e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -56,20 +56,6 @@ jobs: docker logs myapp exit 1 - - name: Install Playwright system dependencies (Chromium) - run: | - sudo apt-get update -qq - sudo apt-get install -y -qq --no-install-recommends \ - ca-certificates \ - fonts-liberation \ - libasound2t64 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libatspi2.0-0 \ - libcairo2 \ - libcups2 \ - libdbus-1 - - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium firefox webkit From eedb704a9a1c15382ecfe6b1b9aa9a5ddcb044f9 Mon Sep 17 00:00:00 2001 From: killianlarcher Date: Wed, 18 Mar 2026 18:58:29 +0100 Subject: [PATCH 19/19] chore: fix bug in github action. --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a8ade14e..d06c8843 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -60,7 +60,7 @@ jobs: run: pnpm exec playwright install --with-deps chromium firefox webkit - name: Run Playwright tests - run: pnpm exec playwright test + run: pnpm exec playwright test --project chromium env: PROJECT_URL: http://localhost:8887