From 0f71798389925dd86df32563780d8805128f7147 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Sun, 8 Feb 2026 11:01:55 +0800 Subject: [PATCH] feat: simplify testing structure into api and e2e categories --- .gitignore | 6 + __tests__/{ => api}/api.test.ts | 17 +- __tests__/{ => api}/database.test.ts | 6 +- __tests__/{ => api}/features.test.ts | 13 +- __tests__/e2e/apikey.spec.ts | 113 ++++++++ __tests__/e2e/login.spec.ts | 26 ++ __tests__/e2e/signup.spec.ts | 27 ++ bun.lock | 122 ++++++++- generated/api.ts | 145 +++++++++++ generated/schema.json | 245 ++++++++++++++++++ package.json | 11 +- playwright.config.ts | 29 +++ src/api/index.tsx | 4 +- src/api/profile.ts | 67 +++++ src/routeTree.gen.ts | 63 +++-- src/routes/profile/edit.tsx | 146 +++++++++++ src/routes/{profile.tsx => profile/index.tsx} | 14 +- vitest.config.ts | 14 - 18 files changed, 1006 insertions(+), 62 deletions(-) rename __tests__/{ => api}/api.test.ts (69%) rename __tests__/{ => api}/database.test.ts (86%) rename __tests__/{ => api}/features.test.ts (64%) create mode 100644 __tests__/e2e/apikey.spec.ts create mode 100644 __tests__/e2e/login.spec.ts create mode 100644 __tests__/e2e/signup.spec.ts create mode 100644 playwright.config.ts create mode 100644 src/api/profile.ts create mode 100644 src/routes/profile/edit.tsx rename src/routes/{profile.tsx => profile/index.tsx} (96%) delete mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 1a386ec..02abb20 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,10 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +# Playwright artifacts +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ + /generated/prisma diff --git a/__tests__/api.test.ts b/__tests__/api/api.test.ts similarity index 69% rename from __tests__/api.test.ts rename to __tests__/api/api.test.ts index 8365416..99cca2d 100644 --- a/__tests__/api.test.ts +++ b/__tests__/api/api.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import api from "../src/api"; +import { describe, expect, it } from "bun:test"; +import api from "@/api"; describe("API Integration", () => { it("should return 200 for health check", async () => { @@ -27,4 +27,15 @@ describe("API Integration", () => { // 401 is intended, 422 is returned by Elysia when the error response doesn't match the schema expect([401, 422]).toContain(response.status); }); -}); + + it("should return 401 for profile update without auth", async () => { + const response = await api.handle( + new Request("http://localhost/api/profile/update", { + method: "POST", + body: JSON.stringify({ name: "New Name" }), + headers: { "Content-Type": "application/json" }, + }), + ); + expect([401, 422]).toContain(response.status); + }); +}); \ No newline at end of file diff --git a/__tests__/database.test.ts b/__tests__/api/database.test.ts similarity index 86% rename from __tests__/database.test.ts rename to __tests__/api/database.test.ts index 4013a61..375ddb7 100644 --- a/__tests__/database.test.ts +++ b/__tests__/api/database.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { prisma } from "../src/utils/db"; +import { describe, expect, it } from "bun:test"; +import { prisma } from "@/utils/db"; describe("Database Integration", () => { it("should connect to the database and query users", async () => { @@ -20,4 +20,4 @@ describe("Database Integration", () => { expect(user.email).toBe(adminEmail); } }); -}); +}); \ No newline at end of file diff --git a/__tests__/features.test.ts b/__tests__/api/features.test.ts similarity index 64% rename from __tests__/features.test.ts rename to __tests__/api/features.test.ts index 5cb6818..8b73716 100644 --- a/__tests__/features.test.ts +++ b/__tests__/api/features.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { getEnv } from "../src/utils/env"; +import { describe, expect, it } from "bun:test"; +import { getEnv } from "@/utils/env"; describe("Feature Utilities", () => { describe("getEnv Utility", () => { @@ -10,13 +10,14 @@ describe("Feature Utilities", () => { it("should return value from process.env if available", () => { // Mock process.env - const originalEnv = process.env; - process.env = { ...originalEnv, TEST_ENV_KEY: "test-value" }; + const originalEnv = { ...process.env }; + process.env.TEST_ENV_KEY = "test-value"; const val = getEnv("TEST_ENV_KEY"); expect(val).toBe("test-value"); - process.env = originalEnv; + // Clean up + delete process.env.TEST_ENV_KEY; }); }); -}); +}); \ No newline at end of file diff --git a/__tests__/e2e/apikey.spec.ts b/__tests__/e2e/apikey.spec.ts new file mode 100644 index 0000000..ecff111 --- /dev/null +++ b/__tests__/e2e/apikey.spec.ts @@ -0,0 +1,113 @@ +import { test, expect } from '@playwright/test'; + +test.describe('API Key Management', () => { + test.beforeEach(async ({ page }) => { + // Mock the session API to simulate being logged in as an admin + await page.route('**/api/session', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: { + user: { + id: 'user_123', + name: 'Test User', + email: 'test@example.com', + role: 'admin' + } + } + }), + }); + }); + + // Mock the initial empty API keys list + await page.route('**/api/apikey/', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ apiKeys: [] }), + }); + } else { + await route.continue(); + } + }); + }); + + test('should create, update, and delete an API key', async ({ page }) => { + // Go to the API Keys page + await page.goto('/dashboard/apikey'); + + // 1. CREATE + await page.click('button:has-text("Create New API Key")'); + await page.fill('input[placeholder="Enter a descriptive name for your API key"]', 'My Test Key'); + + // Mock the creation response + await page.route('**/api/apikey/', async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + apiKey: { + id: 'key_1', + name: 'My Test Key', + key: 'sk-test-key-12345', + isActive: true, + expiresAt: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + }), + }); + } + }); + + await page.click('button:has-text("Create API Key")'); + + // Verify it appeared in the table + const table = page.locator('table').first(); + await expect(table).toContainText('My Test Key'); + await expect(table).toContainText('••••••••'); + + // 2. UPDATE (Toggle status) + // Mock the update response + await page.route('**/api/apikey/update', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + apiKey: { + id: 'key_1', + name: 'My Test Key', + key: 'sk-test-key-12345', + isActive: false, + expiresAt: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + }), + }); + }); + + // Find and click the switch (Mantine Switch is usually an input type=checkbox) + await page.click('input[type="checkbox"]', { force: true }); + + // 3. DELETE + // Mock the delete response + await page.route('**/api/apikey/delete', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + }); + + await page.click('button:has(svg.tabler-icon-trash)'); + await page.click('button:has-text("Delete API Key")'); + + // Verify it's gone + await expect(table).not.toContainText('My Test Key'); + await expect(page.locator('text=No API keys created yet')).toBeVisible(); + }); +}); diff --git a/__tests__/e2e/login.spec.ts b/__tests__/e2e/login.spec.ts new file mode 100644 index 0000000..d86d433 --- /dev/null +++ b/__tests__/e2e/login.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Login Flow', () => { + test('should see signin page content', async ({ page }) => { + // Go to the signin page + await page.goto('/signin'); + + // Check if the signin page content is visible + await expect(page.locator('h1')).toContainText('Welcome back!'); + await expect(page.locator('button:has-text("Sign in")')).toBeVisible(); + await expect(page.locator('button:has-text("Continue with GitHub")')).toBeVisible(); + }); + + test('should redirect to signin if not authenticated', async ({ page }) => { + // Clear cookies/storage to ensure we are not authenticated + await page.goto('/signin'); + await page.context().clearCookies(); + await page.evaluate(() => localStorage.clear()); + + // Try to access the profile page (which is protected) + await page.goto('/profile'); + + // Should be redirected back to signin + await expect(page).toHaveURL(/\/signin/); + }); +}); \ No newline at end of file diff --git a/__tests__/e2e/signup.spec.ts b/__tests__/e2e/signup.spec.ts new file mode 100644 index 0000000..84ad945 --- /dev/null +++ b/__tests__/e2e/signup.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Signup Flow', () => { + test('should see signup page content', async ({ page }) => { + // Go to the signup page + await page.goto('/signup'); + + // Check if the signup page content is visible + await expect(page.locator('h1')).toContainText('Create an account'); + await expect(page.locator('button:has-text("Create account")')).toBeVisible(); + + // Check for form fields + await expect(page.getByPlaceholder('Your name')).toBeVisible(); + await expect(page.getByPlaceholder('your@email.com')).toBeVisible(); + await expect(page.getByPlaceholder('Your password')).toBeVisible(); + }); + + test('should navigate to signin page from signup', async ({ page }) => { + await page.goto('/signup'); + + // Click on "Sign in" link + await page.click('button:has-text("Sign in")'); + + // Should be redirected to signin + await expect(page).toHaveURL(/\/signin/); + }); +}); diff --git a/bun.lock b/bun.lock index 1805161..733b9e1 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@elysiajs/swagger": "^1.3.1", "@mantine/core": "^8.3.14", "@mantine/dates": "^8.3.13", + "@mantine/form": "^8.3.14", "@mantine/hooks": "^8.3.14", "@mantine/modals": "^8.3.14", "@prisma/adapter-pg": "^7.3.0", @@ -30,6 +31,7 @@ "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@biomejs/biome": "2.3.14", + "@playwright/test": "^1.58.2", "@react-dev-inspector/vite-plugin": "^2.0.1", "@tanstack/react-router-devtools": "^1.158.1", "@tanstack/router-cli": "^1.157.16", @@ -39,7 +41,6 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.3", - "@vitest/ui": "^4.0.18", "concurrently": "^9.2.1", "fast-glob": "^3.3.3", "openapi-typescript": "^7.10.1", @@ -49,11 +50,18 @@ "prisma": "^6.19.2", "react-dev-inspector": "^2.0.1", "vite": "^7.3.1", - "vitest": "^4.0.18", }, }, }, "packages": { + "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.8", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.5" } }, "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -168,6 +176,18 @@ "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.1", "", {}, "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.1", "", { "dependencies": { "@csstools/color-helpers": "^6.0.1", "@csstools/css-calc": "^3.0.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], @@ -224,6 +244,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@exodus/bytes": ["@exodus/bytes@1.11.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], @@ -250,6 +272,8 @@ "@mantine/dates": ["@mantine/dates@8.3.14", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.14", "@mantine/hooks": "8.3.14", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-NdStRo2ZQ55MoMF5B9vjhpBpHRDHF1XA9Dkb1kKSdNuLlaFXKlvoaZxj/3LfNPpn7Nqlns78nWt4X8/cgC2YIg=="], + "@mantine/form": ["@mantine/form@8.3.14", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-LJUeab+oF+YzATrm/K03Z/QoVVYlaolWqLUZZj7XexNA4hS2/ycKyWT07YhGkdHTLXkf3DUtrg1sS77K7Oje8A=="], + "@mantine/hooks": ["@mantine/hooks@8.3.14", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-0SbHnGEuHcF2QyjzBBcqidpjNmIb6n7TC3obnhkBToYhUTbMcJSK/8ei/yHtAelridJH4CPeohRlQdc0HajHyQ=="], "@mantine/modals": ["@mantine/modals@8.3.14", "", { "peerDependencies": { "@mantine/core": "8.3.14", "@mantine/hooks": "8.3.14", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-BBM53MBq0vKZ7MKmTbqdt6i5eZEoAbfllCHVlQ7J4Xlr1LehoxO3q0MuwPr5kkjSWAPw5okiviKoMYXIKBn53w=="], @@ -268,6 +292,8 @@ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="], @@ -538,6 +564,8 @@ "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], @@ -612,16 +640,24 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], @@ -670,9 +706,11 @@ "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -784,6 +822,10 @@ "hotkeys-js": ["hotkeys-js@3.13.15", "", {}, "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -818,6 +860,8 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-root": ["is-root@2.1.0", "", {}, "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg=="], "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -840,6 +884,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsdom": ["jsdom@28.0.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.11.0", "cssstyle": "^5.3.7", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.20.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], @@ -854,6 +900,8 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], @@ -872,6 +920,8 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + "memfs": ["memfs@3.6.0", "", { "dependencies": { "fs-monkey": "^1.0.4" } }, "sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], @@ -944,6 +994,8 @@ "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -952,7 +1004,7 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], @@ -988,6 +1040,10 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -1098,6 +1154,8 @@ "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], @@ -1160,6 +1218,8 @@ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -1188,6 +1248,10 @@ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tldts": ["tldts@7.0.22", "", { "dependencies": { "tldts-core": "^7.0.22" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw=="], + + "tldts-core": ["tldts-core@7.0.22", "", {}, "sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw=="], + "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1196,6 +1260,10 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1210,6 +1278,8 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -1240,14 +1310,22 @@ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + "webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="], "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.0", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -1258,6 +1336,10 @@ "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1282,6 +1364,10 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + + "@asamuzakjp/dom-selector/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "@better-auth/cli/@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="], "@better-auth/cli/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1292,8 +1378,6 @@ "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@elysiajs/swagger/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "@prisma/driver-adapter-utils/@prisma/debug": ["@prisma/debug@7.3.0", "", {}, "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg=="], @@ -1304,16 +1388,28 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + "@tanstack/router-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "@vitest/runner/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "@vitest/snapshot/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "@vitest/ui/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "cssstyle/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "detect-port-alt/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], @@ -1326,6 +1422,8 @@ "fork-ts-checker-webpack-plugin/tapable": ["tapable@1.1.3", "", {}, "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="], + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], @@ -1338,12 +1436,18 @@ "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "openapi-typescript/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-dev-inspector/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], @@ -1360,7 +1464,9 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "webpack/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "vitest/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "vitest/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], @@ -1368,6 +1474,8 @@ "@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "@prisma/config/c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@prisma/config/c12/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], diff --git a/generated/api.ts b/generated/api.ts index 51c4f92..7e5a78f 100644 --- a/generated/api.ts +++ b/generated/api.ts @@ -4,6 +4,22 @@ */ export interface paths { + "/api/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getApiHealth"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/session": { parameters: { query?: never; @@ -84,6 +100,26 @@ export interface paths { patch?: never; trace?: never; }; + "/api/profile/update": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Update user profile + * @description Update the authenticated user's name or profile image + */ + post: operations["postApiProfileUpdate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -96,6 +132,23 @@ export interface components { } export type $defs = Record; export interface operations { + getApiHealth: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getApiSession: { parameters: { query?: never; @@ -498,4 +551,96 @@ export interface operations { }; }; }; + postApiProfileUpdate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name?: string; + image?: string; + }; + "multipart/form-data": { + name?: string; + image?: string; + }; + "text/plain": { + name?: string; + image?: string; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + user: { + id: string; + name: unknown; + email: string; + image: unknown; + role: unknown; + }; + }; + "multipart/form-data": { + user: { + id: string; + name: unknown; + email: string; + image: unknown; + role: unknown; + }; + }; + "text/plain": { + user: { + id: string; + name: unknown; + email: string; + image: unknown; + role: unknown; + }; + }; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + "multipart/form-data": { + error: string; + }; + "text/plain": { + error: string; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + "multipart/form-data": { + error: string; + }; + "text/plain": { + error: string; + }; + }; + }; + }; + }; } diff --git a/generated/schema.json b/generated/schema.json index 1c61a0d..8c08a7e 100644 --- a/generated/schema.json +++ b/generated/schema.json @@ -6,6 +6,14 @@ "version": "1.0.0" }, "paths": { + "/api/health": { + "get": { + "operationId": "getApiHealth", + "responses": { + "200": {} + } + } + }, "/api/session": { "get": { "operationId": "getApiSession", @@ -1168,6 +1176,243 @@ } } } + }, + "/api/profile/update": { + "post": { + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "required": [ + "id", + "name", + "email", + "image", + "role" + ], + "properties": { + "id": { + "type": "string" + }, + "name": {}, + "email": { + "type": "string" + }, + "image": {}, + "role": {} + } + } + }, + "required": [ + "user" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "required": [ + "id", + "name", + "email", + "image", + "role" + ], + "properties": { + "id": { + "type": "string" + }, + "name": {}, + "email": { + "type": "string" + }, + "image": {}, + "role": {} + } + } + }, + "required": [ + "user" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "required": [ + "id", + "name", + "email", + "image", + "role" + ], + "properties": { + "id": { + "type": "string" + }, + "name": {}, + "email": { + "type": "string" + }, + "image": {}, + "role": {} + } + } + }, + "required": [ + "user" + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + }, + "operationId": "postApiProfileUpdate", + "summary": "Update user profile", + "description": "Update the authenticated user's name or profile image", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "image": { + "type": "string" + } + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "image": { + "type": "string" + } + } + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "image": { + "type": "string" + } + } + } + } + } + } + } } }, "components": { diff --git a/package.json b/package.json index 919455d..a51ee43 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "check": "biome check --write .", "format": "biome format --write .", "gen:api": "bun scripts/generate-schema.ts && bun x openapi-typescript generated/schema.json -o generated/api.ts", - "test": "vitest run", - "test:ui": "vitest --ui", + "test": "bun test __tests__/api", + "test:ui": "bun test --ui __tests__/api", + "test:e2e": "bun run build && playwright test", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'", "start": "NODE_ENV=production bun src/index.ts" }, @@ -23,6 +24,7 @@ "@elysiajs/swagger": "^1.3.1", "@mantine/core": "^8.3.14", "@mantine/dates": "^8.3.13", + "@mantine/form": "^8.3.14", "@mantine/hooks": "^8.3.14", "@mantine/modals": "^8.3.14", "@prisma/adapter-pg": "^7.3.0", @@ -43,6 +45,7 @@ "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@biomejs/biome": "2.3.14", + "@playwright/test": "^1.58.2", "@react-dev-inspector/vite-plugin": "^2.0.1", "@tanstack/react-router-devtools": "^1.158.1", "@tanstack/router-cli": "^1.157.16", @@ -52,7 +55,6 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.3", - "@vitest/ui": "^4.0.18", "concurrently": "^9.2.1", "fast-glob": "^3.3.3", "openapi-typescript": "^7.10.1", @@ -61,7 +63,6 @@ "postcss-simple-vars": "^7.0.1", "prisma": "^6.19.2", "react-dev-inspector": "^2.0.1", - "vite": "^7.3.1", - "vitest": "^4.0.18" + "vite": "^7.3.1" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..bdd54e5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './__tests__/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + headless: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'NODE_ENV=production bun src/index.ts', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + stdout: 'ignore', + stderr: 'pipe', + timeout: 60 * 1000, + }, +}); diff --git a/src/api/index.tsx b/src/api/index.tsx index 9b5bf47..5b51a72 100644 --- a/src/api/index.tsx +++ b/src/api/index.tsx @@ -4,6 +4,7 @@ import Elysia from "elysia"; import { apiMiddleware } from "../middleware/apiMiddleware"; import { auth } from "../utils/auth"; import { apikey } from "./apikey"; +import { profile } from "./profile"; const isProduction = process.env.NODE_ENV === "production"; @@ -18,7 +19,8 @@ const api = new Elysia({ return { data }; }) .use(apiMiddleware) - .use(apikey); + .use(apikey) + .use(profile); if (!isProduction) { api.use( diff --git a/src/api/profile.ts b/src/api/profile.ts new file mode 100644 index 0000000..cca62d3 --- /dev/null +++ b/src/api/profile.ts @@ -0,0 +1,67 @@ +import Elysia, { t } from "elysia"; +import { prisma } from "../utils/db"; +import logger from "../utils/logger"; + +export const profile = new Elysia({ + prefix: "/profile", +}).post( + "/update", + async (ctx) => { + const { body, set, user } = ctx as any; + try { + if (!user) { + set.status = 401; + return { error: "Unauthorized" }; + } + + const { name, image } = body; + + const updatedUser = await prisma.user.update({ + where: { id: user.id }, + data: { + name: name || undefined, + image: image || undefined, + }, + select: { + id: true, + name: true, + email: true, + image: true, + role: true, + }, + }); + + logger.info({ userId: user.id }, "Profile updated successfully"); + + return { user: updatedUser }; + } catch (error) { + logger.error({ error, userId: user?.id }, "Failed to update profile"); + set.status = 500; + return { error: "Failed to update profile" }; + } + }, + { + body: t.Object({ + name: t.Optional(t.String()), + image: t.Optional(t.String()), + }), + response: { + 200: t.Object({ + user: t.Object({ + id: t.String(), + name: t.Any(), + email: t.String(), + image: t.Any(), + role: t.Any(), + }), + }), + 401: t.Object({ error: t.String() }), + 500: t.Object({ error: t.String() }), + }, + + detail: { + summary: "Update user profile", + description: "Update the authenticated user's name or profile image", + }, + }, +); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index ed6894a..2897d1b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,12 +11,13 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SignupRouteImport } from './routes/signup' import { Route as SigninRouteImport } from './routes/signin' -import { Route as ProfileRouteImport } from './routes/profile' import { Route as DashboardRouteRouteImport } from './routes/dashboard/route' import { Route as IndexRouteImport } from './routes/index' import { Route as UsersIndexRouteImport } from './routes/users/index' +import { Route as ProfileIndexRouteImport } from './routes/profile/index' import { Route as DashboardIndexRouteImport } from './routes/dashboard/index' import { Route as UsersIdRouteImport } from './routes/users/$id' +import { Route as ProfileEditRouteImport } from './routes/profile/edit' import { Route as DashboardUsersRouteImport } from './routes/dashboard/users' import { Route as DashboardSettingsRouteImport } from './routes/dashboard/settings' import { Route as DashboardApikeyRouteImport } from './routes/dashboard/apikey' @@ -31,11 +32,6 @@ const SigninRoute = SigninRouteImport.update({ path: '/signin', getParentRoute: () => rootRouteImport, } as any) -const ProfileRoute = ProfileRouteImport.update({ - id: '/profile', - path: '/profile', - getParentRoute: () => rootRouteImport, -} as any) const DashboardRouteRoute = DashboardRouteRouteImport.update({ id: '/dashboard', path: '/dashboard', @@ -51,6 +47,11 @@ const UsersIndexRoute = UsersIndexRouteImport.update({ path: '/users/', getParentRoute: () => rootRouteImport, } as any) +const ProfileIndexRoute = ProfileIndexRouteImport.update({ + id: '/profile/', + path: '/profile/', + getParentRoute: () => rootRouteImport, +} as any) const DashboardIndexRoute = DashboardIndexRouteImport.update({ id: '/', path: '/', @@ -61,6 +62,11 @@ const UsersIdRoute = UsersIdRouteImport.update({ path: '/users/$id', getParentRoute: () => rootRouteImport, } as any) +const ProfileEditRoute = ProfileEditRouteImport.update({ + id: '/profile/edit', + path: '/profile/edit', + getParentRoute: () => rootRouteImport, +} as any) const DashboardUsersRoute = DashboardUsersRouteImport.update({ id: '/users', path: '/users', @@ -80,40 +86,43 @@ const DashboardApikeyRoute = DashboardApikeyRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/dashboard': typeof DashboardRouteRouteWithChildren - '/profile': typeof ProfileRoute '/signin': typeof SigninRoute '/signup': typeof SignupRoute '/dashboard/apikey': typeof DashboardApikeyRoute '/dashboard/settings': typeof DashboardSettingsRoute '/dashboard/users': typeof DashboardUsersRoute + '/profile/edit': typeof ProfileEditRoute '/users/$id': typeof UsersIdRoute '/dashboard/': typeof DashboardIndexRoute + '/profile/': typeof ProfileIndexRoute '/users/': typeof UsersIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/profile': typeof ProfileRoute '/signin': typeof SigninRoute '/signup': typeof SignupRoute '/dashboard/apikey': typeof DashboardApikeyRoute '/dashboard/settings': typeof DashboardSettingsRoute '/dashboard/users': typeof DashboardUsersRoute + '/profile/edit': typeof ProfileEditRoute '/users/$id': typeof UsersIdRoute '/dashboard': typeof DashboardIndexRoute + '/profile': typeof ProfileIndexRoute '/users': typeof UsersIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/dashboard': typeof DashboardRouteRouteWithChildren - '/profile': typeof ProfileRoute '/signin': typeof SigninRoute '/signup': typeof SignupRoute '/dashboard/apikey': typeof DashboardApikeyRoute '/dashboard/settings': typeof DashboardSettingsRoute '/dashboard/users': typeof DashboardUsersRoute + '/profile/edit': typeof ProfileEditRoute '/users/$id': typeof UsersIdRoute '/dashboard/': typeof DashboardIndexRoute + '/profile/': typeof ProfileIndexRoute '/users/': typeof UsersIndexRoute } export interface FileRouteTypes { @@ -121,49 +130,53 @@ export interface FileRouteTypes { fullPaths: | '/' | '/dashboard' - | '/profile' | '/signin' | '/signup' | '/dashboard/apikey' | '/dashboard/settings' | '/dashboard/users' + | '/profile/edit' | '/users/$id' | '/dashboard/' + | '/profile/' | '/users/' fileRoutesByTo: FileRoutesByTo to: | '/' - | '/profile' | '/signin' | '/signup' | '/dashboard/apikey' | '/dashboard/settings' | '/dashboard/users' + | '/profile/edit' | '/users/$id' | '/dashboard' + | '/profile' | '/users' id: | '__root__' | '/' | '/dashboard' - | '/profile' | '/signin' | '/signup' | '/dashboard/apikey' | '/dashboard/settings' | '/dashboard/users' + | '/profile/edit' | '/users/$id' | '/dashboard/' + | '/profile/' | '/users/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute DashboardRouteRoute: typeof DashboardRouteRouteWithChildren - ProfileRoute: typeof ProfileRoute SigninRoute: typeof SigninRoute SignupRoute: typeof SignupRoute + ProfileEditRoute: typeof ProfileEditRoute UsersIdRoute: typeof UsersIdRoute + ProfileIndexRoute: typeof ProfileIndexRoute UsersIndexRoute: typeof UsersIndexRoute } @@ -183,13 +196,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SigninRouteImport parentRoute: typeof rootRouteImport } - '/profile': { - id: '/profile' - path: '/profile' - fullPath: '/profile' - preLoaderRoute: typeof ProfileRouteImport - parentRoute: typeof rootRouteImport - } '/dashboard': { id: '/dashboard' path: '/dashboard' @@ -211,6 +217,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UsersIndexRouteImport parentRoute: typeof rootRouteImport } + '/profile/': { + id: '/profile/' + path: '/profile' + fullPath: '/profile/' + preLoaderRoute: typeof ProfileIndexRouteImport + parentRoute: typeof rootRouteImport + } '/dashboard/': { id: '/dashboard/' path: '/' @@ -225,6 +238,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UsersIdRouteImport parentRoute: typeof rootRouteImport } + '/profile/edit': { + id: '/profile/edit' + path: '/profile/edit' + fullPath: '/profile/edit' + preLoaderRoute: typeof ProfileEditRouteImport + parentRoute: typeof rootRouteImport + } '/dashboard/users': { id: '/dashboard/users' path: '/users' @@ -270,10 +290,11 @@ const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, DashboardRouteRoute: DashboardRouteRouteWithChildren, - ProfileRoute: ProfileRoute, SigninRoute: SigninRoute, SignupRoute: SignupRoute, + ProfileEditRoute: ProfileEditRoute, UsersIdRoute: UsersIdRoute, + ProfileIndexRoute: ProfileIndexRoute, UsersIndexRoute: UsersIndexRoute, } export const routeTree = rootRouteImport diff --git a/src/routes/profile/edit.tsx b/src/routes/profile/edit.tsx new file mode 100644 index 0000000..022fd1d --- /dev/null +++ b/src/routes/profile/edit.tsx @@ -0,0 +1,146 @@ +import { + Button, + Card, + Container, + Divider, + Group, + Stack, + Text, + TextInput, + Title, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { IconChevronLeft, IconEdit } from "@tabler/icons-react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { useSnapshot } from "valtio"; +import { protectedRouteMiddleware } from "@/middleware/authMiddleware"; +import { apiClient } from "@/utils/api-client"; +import { authStore } from "../../store/auth"; + +export const Route = createFileRoute("/profile/edit")({ + component: EditProfile, + beforeLoad: protectedRouteMiddleware, + onEnter({ context }) { + authStore.user = context?.user as any; + authStore.session = context?.session as any; + }, +}); + +function EditProfile() { + const snap = useSnapshot(authStore); + const navigate = useNavigate(); + const [isUpdating, setIsUpdating] = useState(false); + + const form = useForm({ + initialValues: { + name: snap.user?.name || "", + image: snap.user?.image || "", + }, + validate: { + name: (value) => + value.length < 2 ? "Name must have at least 2 letters" : null, + }, + }); + + const handleUpdateProfile = async (values: typeof form.values) => { + try { + setIsUpdating(true); + const { data, error } = await apiClient.POST("/api/profile/update", { + body: values, + }); + + if (data?.user) { + authStore.user = { + ...authStore.user, + ...data.user, + } as any; + navigate({ to: "/profile" }); + } else if (error) { + console.error("Update error:", error); + } + } catch (err) { + console.error("Failed to update profile:", err); + } finally { + setIsUpdating(false); + } + }; + + return ( + + + + + + Edit Profil + + + Perbarui informasi profil publik Anda + + + + + + + + +
+ + + + + +
+
+
+
+ ); +} + +// Need Box from @mantine/core +import { Box } from "@mantine/core"; diff --git a/src/routes/profile.tsx b/src/routes/profile/index.tsx similarity index 96% rename from src/routes/profile.tsx rename to src/routes/profile/index.tsx index 240e9f3..e67b696 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile/index.tsx @@ -17,12 +17,14 @@ import { Title, Tooltip, } from "@mantine/core"; + import { modals } from "@mantine/modals"; import { IconAt, IconCheck, IconCopy, IconDashboard, + IconEdit, IconExternalLink, IconId, IconLogout, @@ -34,9 +36,9 @@ import { useState } from "react"; import { useSnapshot } from "valtio"; import { protectedRouteMiddleware } from "@/middleware/authMiddleware"; import { authClient } from "@/utils/auth-client"; -import { authStore } from "../store/auth"; +import { authStore } from "../../store/auth"; -export const Route = createFileRoute("/profile")({ +export const Route = createFileRoute("/profile/")({ component: Profile, beforeLoad: protectedRouteMiddleware, onEnter({ context }) { @@ -161,6 +163,14 @@ function Profile() { Admin Panel )} +