diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be97dc3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.svelte-kit +build +.git +.env +.env.* +npm-debug.log* +.DS_Store \ No newline at end of file diff --git a/.env.example b/.env.example index aaae207..9fe22a7 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ DATABASE_URL= BETTER_AUTH_SECRET= BETTER_AUTH_URL= -GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_ID=6 GOOGLE_CLIENT_SECRET= RESEND_API_KEY= EMAIL_FROM= diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..f033828 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,32 @@ +name: Build and Push to Digital Ocean Container Registry + +on: + push: + branches: [main] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: | + docker build --platform linux/amd64 \ + -t registry.digitalocean.com/radiantwave-portal-container-registry/portal:${{ gitea.sha }} \ + -t registry.digitalocean.com/radiantwave-portal-container-registry/portal:latest \ + . + + - name: Login to DigitalOcean Registry + uses: docker/login-action@v3 + with: + registry: registry.digitalocean.com + username: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + password: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + + - name: Push image + run: | + docker push registry.digitalocean.com/radiantwave-portal-container-registry/portal:${{ gitea.sha }} + docker push registry.digitalocean.com/radiantwave-portal-container-registry/portal:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d647f67 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# --- Build stage --- +FROM node:24-slim AS build +WORKDIR /app + +# Placeholder values for build-time $env/static/* and better-auth's module-scope +# secret validation. These are NOT the real runtime values — App Platform +# injects the real ones as env vars when the container runs. +ARG DATABASE_URL=postgres://placeholder:placeholder@localhost:5432/placeholder +ARG ORIGIN=http://localhost:3000 +ARG BETTER_AUTH_URL=http://localhost:3000 +ARG BETTER_AUTH_BASE_URL=http://localhost:3000 +ARG BETTER_AUTH_SECRET=placeholder-build-time-secret-not-used-at-runtime +ENV DATABASE_URL=$DATABASE_URL +ENV ORIGIN=$ORIGIN +ENV BETTER_AUTH_URL=$BETTER_AUTH_URL +ENV BETTER_AUTH_BASE_URL=$BETTER_AUTH_BASE_URL +ENV BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Drop devDependencies now that the build is done +RUN npm prune --omit=dev + +# --- Runtime stage --- +FROM node:24-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 + +# Install the DigitalOcean managed DB CA cert into the system trust store. +# `ca-certificates` provides update-ca-certificates; certs placed in +# /usr/local/share/ca-certificates/ must end in .crt to be picked up. +COPY digital-ocean.crt /usr/local/share/ca-certificates/digital-ocean.crt +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Node still doesn't read the system trust store unless --use-system-ca is +# passed explicitly (available on 22.19+/24.6+, but off by default even here). +# NODE_EXTRA_CA_CERTS is simpler and works regardless of Node version. +ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/digital-ocean.crt + +COPY --from=build /app/build ./build +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/src/lib/generated/prisma ./src/lib/generated/prisma +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/prisma.config.ts ./prisma.config.ts +COPY docker-entrypoint.sh ./ + +EXPOSE 3000 +CMD ["./docker-entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index ed3ded6..9997a89 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,39 @@ # Rhema Wave Technologies - User Portal +A combination application that combines the main Radiant Wave API with a user-facing account management application, +and an admin-facing platform management application. An all-in-one management platform for Radiant Wave and Radiant Wave Technologies. + SvelteKit 2 + Svelte 5 + Tailwind CSS v4 + Prisma 7 + PostgreSQL + Better-Auth. +## Deployment + +This application runs inside of a Docker container, defined by the Dockerfile found within this repo. +Codeberg/Gitea actions are triggered on `push` actions to the `main` branch, and build the docker container. +The docker container is then uploaded to the Digital Ocean Container Registry, where it is pulled into an +App Platform application and run. + +To build the Docker container locally (with podman), ensuring builds pass prior to pushing to `main`: + +```sh +podman build --platform linux/amd64 -t registry.digitalocean.com/radiantwave-portal-container-registry/portal:latest . +``` + +To build the Docker container on a Linux machine, such as a Forgejo Action Runner: + +```sh +docker build -t registry.digitalocean.com/radiantwave-portal-container-registry/portal:latest . +``` + +Then push to the container registry: + +```sh +podman push registry.digitalocean.com/radiantwave-portal-container-registry/portal:latest +``` + +--- + +## Development + ## Admin Admin CRUD pages at `/admin`. 9 models have per-page routes with modals for create/edit: diff --git a/digital-ocean.crt b/digital-ocean.crt new file mode 100644 index 0000000..8dff6b2 --- /dev/null +++ b/digital-ocean.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIETTCCArWgAwIBAgIUSiKrBdf9KHXL2om54yA9sQTxeawwDQYJKoZIhvcNAQEM +BQAwQDE+MDwGA1UEAww1MTdmNmMxNDktZmQ1Yy00MTRmLTlkNjctYjIxYTc2MWE3 +ZDlhIEdFTiAxIFByb2plY3QgQ0EwHhcNMjUwNDI5MTc0NDA5WhcNMzUwNDI3MTc0 +NDA5WjBAMT4wPAYDVQQDDDUxN2Y2YzE0OS1mZDVjLTQxNGYtOWQ2Ny1iMjFhNzYx +YTdkOWEgR0VOIDEgUHJvamVjdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCC +AYoCggGBANMf/9zhlInHTm5CrsnxHZQcqwy2sr4i4w2aWbpCa9j3rWcSyS7PAd+i +p2u1xTEoboJcbUghZYdmS92epVgejAHsHn4yx8LUsmdQvVF8imNlJfZA5mV09Voj +MpRwmXhQWCxyNqn/pmjMEKtpTm7TCr2GjEPDJC4gHSmPZF1EqJ3wWK+leoeZvRuE +omamd2oP60CxtHxt+8zcI6V80sC4QDze2qqRQ3rv1u2MkymUTAQT8kkb1pGmC/E+ +wanXI1gI5nLqhMcoY55WujA/BOgGLWM1fNCIUWkAoQXUV6G0rM8aYDbTUvuTR0Fk +6LP96/bhlxuhrkTSbezO7kwGngiD0rKL4zKFjdwqBbbL1ZjAgfncmzb9zE6NW6Ad +Z8zcLgGCC0Z74GhXNsqNuxY2Mmqo0B/YUa1ahiFjNeXAk14mMXVGG7pbdu+YUUpU +I3duyxwefZsr3KHvz31HhX2jtcaVFrbOF/Svu23MqRhvj9cCUhiF34h7BqEqMFJP +AeIJ/O8xjwIDAQABoz8wPTAdBgNVHQ4EFgQUQyY3hBxq86mcYi8vRRIkCcDsmkMw +DwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggGB +AJqSrBQ8Qi6nIT1N0jo4WXQ5J9p9cyxYcaoRKkdOnvtnB9IXYwX6Ea4w7btZI3O5 +eYamItTqNAC9ZZmwc2i16v1Nfpm50vGJjZZUMXYkxx1e9/kzOsUn4h6Q6H6+490Y +PAl7kn4Ngi7xU3bwxt0K80dU5p3a16r0A93y8GYW/WDuGRuGgrmRz6+psic4U8j4 +nzHPFeJqtg9I00XYtzsFIIYrbAZT+jMuViVWCNAHJPyQYip8LkEzAfnlrszo9P9G +ieFN/wZbaPaVdju8wsSXN35yxy/eOsJ1p6gA4vzNdTtOmbMAlTQJ65QLu+HJrPpA +M5gjIf6QDSJyNpvWcjVROn6DiUnhsbe6EhRIPf6e8R0xwTe6y59XiU3sdsSDC0xp +3R2v7khln6IUimCsgs9lFvhOSZp4DmGuMs+CVCbqr8rtPTj5xWkeitkjlmiVSkg/ +z2IItxzG4pGmlPc17TCgKzI6bkNoCZvJ92Wvq85q5LcQegJFQjASZKPFXNOm3SDq +wQ== +-----END CERTIFICATE----- diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..d48fb8b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +echo "Running database migrations..." +npx prisma migrate deploy + +echo "Starting application..." +exec node build/index.js diff --git a/package-lock.json b/package-lock.json index 1f4c4b9..f309115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,9 @@ "vite": "^7.2.6", "vitest": "^4.0.15", "vitest-browser-svelte": "^2.0.1" + }, + "engines": { + "node": ">=24" } }, "node_modules/@ark/schema": { diff --git a/package.json b/package.json index 061834f..8385f9f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "private": true, "version": "0.0.1", "type": "module", + "engines": { + "node": ">=24" + }, "scripts": { "dev": "vite dev", "build": "vite build", @@ -64,4 +67,4 @@ "sveltekit-superforms": "^2.30.2", "zod": "^4.4.3" } -} +} \ No newline at end of file diff --git a/prisma/migrations/20260325223339_remove_sub_constraint/migration.sql b/prisma/migrations/20260325223339_remove_sub_constraint/migration.sql new file mode 100644 index 0000000..9ff2a51 --- /dev/null +++ b/prisma/migrations/20260325223339_remove_sub_constraint/migration.sql @@ -0,0 +1,3 @@ +-- Already applied directly to production database on 2026-03-25. +-- Drift migration: removed a constraint on Proclamation.subscriptionLevelId +-- Effect is reflected in the current schema. diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ae91a39..a32201b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -5,9 +5,10 @@ import { sveltekitCookies } from 'better-auth/svelte-kit'; import { getRequestEvent } from '$app/server'; import { sendMail } from './server/resend'; import { sendVerificationEmail } from './server/email-verification'; +import { env } from '$env/dynamic/private'; export const auth = betterAuth({ - baseURL: process.env.BETTER_AUTH_URL, + baseURL: env.BETTER_AUTH_URL, database: prismaAdapter(prisma, { provider: 'postgresql' }), @@ -41,8 +42,8 @@ export const auth = betterAuth({ socialProviders: { google: { prompt: 'select_account', - clientId: process.env.GOOGLE_CLIENT_ID as string, - clientSecret: process.env.GOOGLE_CLIENT_SECRET as string + clientId: env.GOOGLE_CLIENT_ID as string, + clientSecret: env.GOOGLE_CLIENT_SECRET as string } }, rateLimit: { diff --git a/src/lib/db.ts b/src/lib/db.ts index d33a2da..10cd777 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,23 +1,14 @@ -import 'dotenv/config'; -import fs from 'fs'; import pg from 'pg'; import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '$lib/generated/prisma/client'; +import { env } from '$env/dynamic/private'; let _prisma: PrismaClient | undefined; function getPrisma(): PrismaClient { if (!_prisma) { - const ca_cert = process.env.DO_CA_CERTIFICATE ?? ''; - const pool = new pg.Pool({ - connectionString: process.env.DATABASE_URL, - ssl: ca_cert - ? { - rejectUnauthorized: true, - ca: fs.readFileSync(ca_cert).toString() - } - : undefined + connectionString: env.DATABASE_URL }); const adapter = new PrismaPg(pool); diff --git a/src/lib/server/email-verification.ts b/src/lib/server/email-verification.ts index d0122a1..b5dfdb9 100644 --- a/src/lib/server/email-verification.ts +++ b/src/lib/server/email-verification.ts @@ -1,8 +1,8 @@ -import { BETTER_AUTH_URL } from '$env/static/private'; +import { env } from '$env/dynamic/private'; import { sendMail } from './resend'; export async function sendVerificationEmail(options: { email: string; token: string }) { - const verificationUrl = `${BETTER_AUTH_URL}/api/auth/verify-email?token=${options.token}&callbackURL=/login?verified=true`; + const verificationUrl = `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${options.token}&callbackURL=/login?verified=true`; await sendMail({ to: options.email, diff --git a/src/lib/server/resend.ts b/src/lib/server/resend.ts index dc8d7f4..790fb12 100644 --- a/src/lib/server/resend.ts +++ b/src/lib/server/resend.ts @@ -9,9 +9,16 @@ * */ import { Resend } from 'resend'; -import { RESEND_API_KEY, EMAIL_FROM, RESEND_TEMPLATE_ID } from '$env/static/private'; +import { env } from '$env/dynamic/private'; -export const resend = new Resend(RESEND_API_KEY); +let _resend: Resend; + +function getResend(): Resend { + if (!_resend) { + _resend = new Resend(env.RESEND_API_KEY); + } + return _resend; +} /** * Send a transactional email using a Resend-hosted template. @@ -38,12 +45,12 @@ export async function sendMail(options: { }) { const { to, subject, templateId, data } = options; - const result = await resend.emails.send({ - from: EMAIL_FROM, + const result = await getResend().emails.send({ + from: env.EMAIL_FROM, to: [to], subject: subject ?? 'Template Email', template: { - id: templateId ?? RESEND_TEMPLATE_ID, + id: templateId ?? env.RESEND_TEMPLATE_ID, variables: data ?? {} } }); diff --git a/src/lib/svelte-admin/index.ts b/src/lib/svelte-admin/index.ts index f20f9bd..b3cce53 100644 --- a/src/lib/svelte-admin/index.ts +++ b/src/lib/svelte-admin/index.ts @@ -3,4 +3,5 @@ export { ROLES, hasRole, canAccessAdmin } from './roles'; export { fieldNameToLabel, resolveFieldValue } from './introspect'; export { getDelegate, parseId } from './prisma-delegate'; export { extractFormData } from './form-data'; -export { getModelConfig, getAllModelConfigs } from './registry'; +export { getModelConfig, getAllModelConfigs, getSerializableModelConfigs } from './registry'; +export type { SerializableModelConfig } from './registry'; diff --git a/src/lib/svelte-admin/registry.ts b/src/lib/svelte-admin/registry.ts index b4b895a..dfe200f 100644 --- a/src/lib/svelte-admin/registry.ts +++ b/src/lib/svelte-admin/registry.ts @@ -267,3 +267,24 @@ export function getModelConfig(slug: string): AdminModelConfig | undefined { export function getAllModelConfigs(): AdminModelConfig[] { return models; } + +export type SerializableModelConfig = { + slug: string; + displayName: string; + listFields: string[]; + accessFilterResult: Record | undefined; +}; + +export function getSerializableModelConfigs( + viewerRoleId: number, + excludeSlugs?: string[] +): SerializableModelConfig[] { + return models + .filter((m) => !excludeSlugs?.includes(m.slug)) + .map((m) => ({ + slug: m.slug, + displayName: m.displayName, + listFields: m.listFields, + accessFilterResult: m.accessFilter?.(viewerRoleId) + })); +} diff --git a/src/routes/(protected)/admin/+error.svelte b/src/routes/(protected)/admin/+error.svelte new file mode 100644 index 0000000..52c7b81 --- /dev/null +++ b/src/routes/(protected)/admin/+error.svelte @@ -0,0 +1,25 @@ + + + +
+

+ Admin Error ({$page.status}) +

+

+ {#if $page.error?.message} + {$page.error.message} + {:else} + An unexpected error occurred while loading this admin page. + {/if} +

+ + Back to Admin + +
+
diff --git a/src/routes/(protected)/admin/+page.server.ts b/src/routes/(protected)/admin/+page.server.ts index 4c57c8e..20a8a46 100644 --- a/src/routes/(protected)/admin/+page.server.ts +++ b/src/routes/(protected)/admin/+page.server.ts @@ -1,7 +1,8 @@ import type { PageServerLoad } from './$types'; -import { getAllModelConfigs } from '$lib/svelte-admin/registry'; +import { getSerializableModelConfigs } from '$lib/svelte-admin/registry'; -export const load: PageServerLoad = async () => { - const models = getAllModelConfigs().filter((m) => m.slug !== 'license-keys'); +export const load: PageServerLoad = async ({ locals }) => { + const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4; + const models = getSerializableModelConfigs(viewerRoleId, ['license-keys']); return { models }; }; diff --git a/src/routes/(protected)/admin/page.server.spec.ts b/src/routes/(protected)/admin/page.server.spec.ts new file mode 100644 index 0000000..08e1883 --- /dev/null +++ b/src/routes/(protected)/admin/page.server.spec.ts @@ -0,0 +1,107 @@ +import { describe, it, expect, vi } from 'vitest'; +import { getSerializableModelConfigs } from '$lib/svelte-admin/registry'; +import { createLocals, mockUser } from '$lib/test-utils'; + +vi.mock('$lib/db', () => ({})); + +import { load } from './+page.server'; + +function localsWithRole(roleId: number) { + return createLocals(mockUser({ roleId }) as any); +} + +describe('getSerializableModelConfigs', () => { + it('returns only serializable fields (no functions)', () => { + const models = getSerializableModelConfigs(1); + const serialized = JSON.stringify(models); + expect(serialized).toBeDefined(); + + for (const model of models) { + expect(typeof model.slug).toBe('string'); + expect(typeof model.displayName).toBe('string'); + expect(Array.isArray(model.listFields)).toBe(true); + expect( + typeof model.accessFilterResult === 'object' || model.accessFilterResult === undefined + ).toBe(true); + } + }); + + it('evaluates accessFilter for SUPER (roleId 1) — returns undefined for users', () => { + const models = getSerializableModelConfigs(1); + const users = models.find((m) => m.slug === 'users')!; + expect(users.accessFilterResult).toBeUndefined(); + }); + + it('evaluates accessFilter for ADMIN (roleId 2) — restricts users to STAFF+', () => { + const models = getSerializableModelConfigs(2); + const users = models.find((m) => m.slug === 'users')!; + expect(users.accessFilterResult).toEqual({ roleId: { gte: 3 } }); + }); + + it('evaluates accessFilter for STAFF (roleId 3) — restricts users to USER-only', () => { + const models = getSerializableModelConfigs(3); + const users = models.find((m) => m.slug === 'users')!; + expect(users.accessFilterResult).toEqual({ roleId: { gt: 3 } }); + }); + + it('models without accessFilter have undefined accessFilterResult', () => { + const models = getSerializableModelConfigs(1); + const roles = models.find((m) => m.slug === 'user-roles')!; + expect(roles.accessFilterResult).toBeUndefined(); + }); + + it('excludes specified slugs', () => { + const models = getSerializableModelConfigs(1, ['license-keys', 'users']); + expect(models.find((m) => m.slug === 'license-keys')).toBeUndefined(); + expect(models.find((m) => m.slug === 'users')).toBeUndefined(); + expect(models.length).toBeGreaterThan(0); + }); +}); + +describe('admin index page load', () => { + it('returns models with evaluated accessFilter results', async () => { + const result = (await load({ locals: localsWithRole(1) } as any)) as any; + expect(result.models).toBeDefined(); + expect(Array.isArray(result.models)).toBe(true); + + const users = result.models.find((m: any) => m.slug === 'users'); + expect(users).toBeDefined(); + expect(users.accessFilterResult).toBeUndefined(); + }); + + it('returns different accessFilter results per role', async () => { + const superResult = (await load({ locals: localsWithRole(1) } as any)) as any; + const adminResult = (await load({ locals: localsWithRole(2) } as any)) as any; + const staffResult = (await load({ locals: localsWithRole(3) } as any)) as any; + + const superUsers = superResult.models.find((m: any) => m.slug === 'users'); + const adminUsers = adminResult.models.find((m: any) => m.slug === 'users'); + const staffUsers = staffResult.models.find((m: any) => m.slug === 'users'); + + expect(superUsers.accessFilterResult).toBeUndefined(); + expect(adminUsers.accessFilterResult).toEqual({ roleId: { gte: 3 } }); + expect(staffUsers.accessFilterResult).toEqual({ roleId: { gt: 3 } }); + }); + + it('excludes license-keys model', async () => { + const result = (await load({ locals: localsWithRole(1) } as any)) as any; + const licenseKeys = result.models.find((m: any) => m.slug === 'license-keys'); + expect(licenseKeys).toBeUndefined(); + }); + + it('returns fully JSON-serializable data (no functions)', async () => { + const result = (await load({ locals: localsWithRole(2) } as any)) as any; + const serialized = JSON.stringify(result); + const parsed = JSON.parse(serialized); + expect(parsed.models).toBeDefined(); + expect(parsed.models.length).toBeGreaterThan(0); + }); + + it('defaults to roleId 4 when user has no roleId', async () => { + const result = (await load({ + locals: createLocals(mockUser({ roleId: null }) as any) + } as any)) as any; + const users = result.models.find((m: any) => m.slug === 'users'); + expect(users.accessFilterResult).toEqual({ roleId: { gt: 3 } }); + }); +}); diff --git a/src/routes/api/v1/automations/create/account/+server.ts b/src/routes/api/v1/automations/create/account/+server.ts index f62e994..dc11696 100644 --- a/src/routes/api/v1/automations/create/account/+server.ts +++ b/src/routes/api/v1/automations/create/account/+server.ts @@ -3,7 +3,7 @@ import { json, error } from '@sveltejs/kit'; import { auth } from '$lib/auth'; import { prisma } from '$lib/db'; import { sendMail } from '$lib/server/resend'; -import { BETTER_AUTH_URL } from '$env/static/private'; +import { env } from '$env/dynamic/private'; function generateLicenseKey(): string { const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase(); @@ -53,8 +53,8 @@ export const POST: RequestHandler = async ({ request }) => { }); const verifyUrl = verification - ? `${BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true` - : `${BETTER_AUTH_URL}/login`; + ? `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true` + : `${env.BETTER_AUTH_URL}/login`; await sendMail({ to: email, @@ -64,7 +64,7 @@ export const POST: RequestHandler = async ({ request }) => { firstName: name ?? email.split('@')[0], tempPassword, verifyUrl, - loginUrl: `${BETTER_AUTH_URL}/login` + loginUrl: `${env.BETTER_AUTH_URL}/login` } }); } catch (err) { diff --git a/vite.config.ts b/vite.config.ts index 5a478a5..04f79b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,15 @@ import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ plugins: [tailwindcss(), sveltekit()], + build: { + rollupOptions: { + external: [ + '@prisma/client/runtime/query_compiler_bg.postgresql.mjs', + '@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs' + ] + } + }, + test: { expect: { requireAssertions: true },