Merge main into dev
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
+1
-1
@@ -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=
|
||||
|
||||
@@ -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
|
||||
+57
@@ -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"]
|
||||
@@ -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:
|
||||
|
||||
@@ -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-----
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
npx prisma migrate deploy
|
||||
|
||||
echo "Starting application..."
|
||||
exec node build/index.js
|
||||
Generated
+3
@@ -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": {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -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.
|
||||
+4
-3
@@ -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: {
|
||||
|
||||
+2
-11
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string, unknown> | 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)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import Window from '$lib/components/containers/Window.svelte';
|
||||
</script>
|
||||
|
||||
<Window>
|
||||
<div class="flex flex-col items-center gap-4 p-6 text-center">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
Admin Error ({$page.status})
|
||||
</h1>
|
||||
<p class="max-w-md text-sm text-gray-600 dark:text-gray-400">
|
||||
{#if $page.error?.message}
|
||||
{$page.error.message}
|
||||
{:else}
|
||||
An unexpected error occurred while loading this admin page.
|
||||
{/if}
|
||||
</p>
|
||||
<a
|
||||
href="/admin"
|
||||
class="mt-2 border border-gray-200 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
</Window>
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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 } });
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
Reference in New Issue
Block a user