admin: Superforms + Zod validation, three-address-defaults schema, profiles toast fix

- Add Superforms + Zod 4 for all admin form validation
- Replace address isDefault with three separate boolean defaults (Mailing, Billing, Shipping)
- Update profiles page with three-checkbox address form and toast improvements
- Convert all admin page server actions to pass request directly to crud
- Export superFormErrorsToFieldErrors helper for profiles server actions
- Regenerate Prisma client for new schema fields
This commit is contained in:
NeilClack
2026-07-07 13:36:23 -05:00
parent 38347f5f24
commit 38d1752f5b
98 changed files with 3408 additions and 1758 deletions
@@ -23,15 +23,18 @@ description: Configure Better Auth server and client, set up database adapters,
## Quick Reference
### Environment Variables
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
Only define `baseURL`/`secret` in config if env vars are NOT set.
### File Location
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
### CLI Commands
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
@@ -43,7 +46,7 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Core Config Options
| Option | Notes |
|--------|-------|
| ------------------ | ---------------------------------------------- |
| `appName` | Optional display name |
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
| `basePath` | Default `/api/auth`. Set `/` for root. |
@@ -70,11 +73,13 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Session Management
**Storage priority:**
1. If `secondaryStorage` defined → sessions go there (not DB)
2. Set `session.storeSessionInDatabase: true` to also persist to DB
3. No database + `cookieCache` → fully stateless mode
**Cookie cache strategies:**
- `compact` (default) - Base64url + HMAC. Smallest.
- `jwt` - Standard JWT. Readable but signed.
- `jwe` - Encrypted. Maximum security.
@@ -104,6 +109,7 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Security
**In `advanced`:**
- `useSecureCookies` - Force HTTPS cookies
- `disableCSRFCheck` - ⚠️ Security risk
- `disableOriginCheck` - ⚠️ Security risk
@@ -128,9 +134,11 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Plugins
**Import from dedicated paths for tree-shaking:**
```
import { twoFactor } from "better-auth/plugins/two-factor"
```
NOT `from "better-auth/plugins"`.
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`.
@@ -8,14 +8,15 @@ description: Configure rate limiting, manage auth secrets, set up CSRF protectio
### Configuring the Secret
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env
secret: process.env.BETTER_AUTH_SECRET // or via `BETTER_AUTH_SECRET` env
});
```
Better Auth looks for secrets in this order:
1. `options.secret` in your config
2. `BETTER_AUTH_SECRET` environment variable
3. `AUTH_SECRET` environment variable
@@ -34,14 +35,14 @@ Enabled in production by default. Applies to all endpoints. Plugins can override
### Default Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
rateLimit: {
enabled: true, // Default: true in production
window: 10, // Time window in seconds (default: 10)
max: 100, // Max requests per window (default: 100)
},
max: 100 // Max requests per window (default: 100)
}
});
```
@@ -95,12 +96,12 @@ Multi-layer protection: origin header validation, Fetch Metadata checks, and fir
### Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
disableCSRFCheck: false, // Default: false (keep enabled)
},
disableCSRFCheck: false // Default: false (keep enabled)
}
});
```
@@ -111,14 +112,11 @@ Only disable for testing or with an alternative CSRF mechanism.
### Configuring Trusted Origins
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://admin.example.com",
],
baseURL: 'https://api.example.com',
trustedOrigins: ['https://app.example.com', 'https://admin.example.com']
});
```
@@ -128,10 +126,10 @@ The `baseURL` origin is automatically trusted. Also configurable via env: `BETTE
```ts
trustedOrigins: [
"*.example.com", // Matches any subdomain
"https://*.example.com", // Protocol-specific wildcard
"exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo)
]
'*.example.com', // Matches any subdomain
'https://*.example.com', // Protocol-specific wildcard
'exp://192.168.*.*:*/*' // Custom schemes (e.g., Expo)
];
```
### Dynamic Trusted Origins
@@ -143,7 +141,7 @@ trustedOrigins: async (request) => {
// Validate against database, header, etc.
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.myapp.com`];
}
};
```
Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403.
@@ -153,13 +151,13 @@ Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`,
### Session Expiration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default)
},
updateAge: 60 * 60 * 24 // Refresh session every 24 hours (default)
}
});
```
@@ -186,17 +184,17 @@ Defaults: `secure: true` (HTTPS/production), `sameSite: "lax"`, `httpOnly: true`
### Custom Cookie Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
useSecureCookies: true, // Force secure cookies
cookiePrefix: "myapp", // Custom prefix (default: "better-auth")
cookiePrefix: 'myapp', // Custom prefix (default: "better-auth")
defaultCookieAttributes: {
sameSite: "strict", // Stricter CSRF protection
path: "/auth", // Limit cookie scope
},
},
sameSite: 'strict', // Stricter CSRF protection
path: '/auth' // Limit cookie scope
}
}
});
```
@@ -221,12 +219,12 @@ PKCE is automatic for all OAuth flows. State tokens are 32-char random strings e
### State Parameter Storage
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
account: {
storeStateStrategy: "cookie", // Options: "cookie" (default), "database"
},
storeStateStrategy: 'cookie' // Options: "cookie" (default), "database"
}
});
```
@@ -245,15 +243,15 @@ Enable if storing OAuth tokens for API access on behalf of users. Use `skipState
### IP Address Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check
disableIpTracking: false, // Keep enabled for rate limiting
},
},
ipAddressHeaders: ['x-forwarded-for', 'x-real-ip'], // Headers to check
disableIpTracking: false // Keep enabled for rate limiting
}
}
});
```
@@ -262,50 +260,50 @@ Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `
## Database Hooks for Security Auditing
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog("session.created", {
await auditLog('session.created', {
userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"),
userAgent: ctx?.request?.headers.get("user-agent"),
ip: ctx?.request?.headers.get('x-forwarded-for'),
userAgent: ctx?.request?.headers.get('user-agent')
});
},
}
},
delete: {
before: async ({ data }) => {
await auditLog("session.revoked", { sessionId: data.id });
},
},
await auditLog('session.revoked', { sessionId: data.id });
}
}
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
await auditLog("user.email_changed", {
await auditLog('user.email_changed', {
userId: data.id,
oldEmail: oldData?.email,
newEmail: data.email,
newEmail: data.email
});
}
},
},
}
}
},
account: {
create: {
after: async ({ data }) => {
await auditLog("account.linked", {
await auditLog('account.linked', {
userId: data.userId,
provider: data.providerId,
provider: data.providerId
});
},
},
},
},
}
}
}
}
});
```
@@ -314,7 +312,7 @@ Return `false` from a `before` hook to prevent an operation.
## Background Tasks
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
@@ -324,9 +322,9 @@ export const auth = betterAuth({
// Vercel: waitUntil(promise)
// Cloudflare: ctx.waitUntil(promise)
waitUntil(promise);
},
},
},
}
}
}
});
```
@@ -339,24 +337,21 @@ Built-in: consistent response messages, dummy operations on invalid requests, ba
## Complete Security Configuration Example
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://*.preview.example.com",
],
baseURL: 'https://api.example.com',
trustedOrigins: ['https://app.example.com', 'https://*.preview.example.com'],
// Rate limiting
rateLimit: {
enabled: true,
storage: "secondary-storage",
storage: 'secondary-storage',
customRules: {
"/api/auth/sign-in/email": { window: 60, max: 5 },
"/api/auth/sign-up/email": { window: 60, max: 3 },
},
'/api/auth/sign-in/email': { window: 60, max: 5 },
'/api/auth/sign-up/email': { window: 60, max: 3 }
}
},
// Session security
@@ -367,31 +362,30 @@ export const auth = betterAuth({
cookieCache: {
enabled: true,
maxAge: 300,
strategy: "jwe", // Encrypted session data
},
strategy: 'jwe' // Encrypted session data
}
},
// OAuth security
account: {
encryptOAuthTokens: true,
storeStateStrategy: "cookie",
storeStateStrategy: 'cookie'
},
// Advanced settings
advanced: {
useSecureCookies: true,
cookiePrefix: "myapp",
cookiePrefix: 'myapp',
defaultCookieAttributes: {
sameSite: "lax",
sameSite: 'lax'
},
ipAddress: {
ipAddressHeaders: ["x-forwarded-for"],
ipv6Subnet: 64,
ipAddressHeaders: ['x-forwarded-for'],
ipv6Subnet: 64
},
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
handler: (promise) => waitUntil(promise)
}
},
// Security auditing
@@ -400,8 +394,8 @@ export const auth = betterAuth({
create: {
after: async ({ data, ctx }) => {
console.log(`New session for user ${data.userId}`);
},
},
}
}
},
user: {
update: {
@@ -409,10 +403,10 @@ export const auth = betterAuth({
if (oldData?.email !== data.email) {
console.log(`Email changed for user ${data.id}`);
}
},
},
},
},
}
}
}
}
});
```
+11 -5
View File
@@ -18,6 +18,7 @@ Before writing any code, gather requirements by scanning the project and asking
### Step 1: Scan the project
Analyze the codebase to auto-detect:
- **Framework** — Look for `next.config`, `svelte.config`, `nuxt.config`, `astro.config`, `vite.config`, or Express/Hono entry files.
- **Database/ORM** — Look for `prisma/schema.prisma`, `drizzle.config`, `package.json` deps (`pg`, `mysql2`, `better-sqlite3`, `mongoose`, `mongodb`).
- **Existing auth** — Look for existing auth libraries (`next-auth`, `lucia`, `clerk`, `supabase/auth`, `firebase/auth`) in `package.json` or imports.
@@ -75,6 +76,7 @@ Use the `AskQuestion` tool to ask the user **all applicable questions in a singl
- `allow_multiple: true`
10. **Auth UI style** (always ask)
- Prompt: "What style do you want for the auth pages? Pick one or describe your own."
- Options: Minimal & clean | Centered card with background | Split layout (form + hero image) | Floating / glassmorphism | Other (I'll describe)
@@ -180,15 +182,18 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
**Location:** `lib/auth.ts` or `src/lib/auth.ts`
**Minimal config needs:**
- `database` - Connection or adapter
- `emailAndPassword: { enabled: true }` - For email/password auth
**Standard config adds:**
- `socialProviders` - OAuth providers (google, github, etc.)
- `emailVerification.sendVerificationEmail` - Email verification handler
- `emailAndPassword.sendResetPassword` - Password reset handler
**Full config adds:**
- `plugins` - Array of feature plugins
- `session` - Expiry, cookie cache settings
- `account.accountLinking` - Multi-provider linking
@@ -218,7 +223,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Route Handler Setup
| Framework | File | Handler |
|-----------|------|---------|
| ------------------ | -------------------------------- | ------------------------------------------------ |
| Next.js App Router | `app/api/auth/[...all]/route.ts` | `toNextJsHandler(auth)` → export `{ GET, POST }` |
| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
@@ -233,7 +238,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Database Migrations
| Adapter | Command |
|---------|---------|
| --------------- | -------------------------------------------------------------------------------------------------- |
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) |
| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` |
| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
@@ -245,7 +250,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Database Adapters
| Database | Setup |
|----------|-------|
| ---------- | -------------------------------------------------------------------------------------- |
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
| PostgreSQL | Pass `pg.Pool` instance directly |
| MySQL | Pass `mysql2` pool directly |
@@ -258,7 +263,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Common Plugins
| Plugin | Server Import | Client Import | Purpose |
|--------|---------------|---------------|---------|
| -------------- | ---------------------- | -------------------- | ----------------- |
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
| `admin` | `better-auth/plugins` | `adminClient` | User management |
@@ -274,6 +279,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Auth UI Implementation
**Sign in flow:**
1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })`
2. Handle `error` in response
3. Redirect on success
@@ -303,7 +309,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Troubleshooting
| Issue | Fix |
|-------|-----|
| ------------------------------- | ------------------------------------------------------------- |
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
| "Invalid Origin" | Add domain to `trustedOrigins` |
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
@@ -18,19 +18,19 @@ description: Configure email verification, implement password reset flows, set p
Configure `emailVerification.sendVerificationEmail` to verify user email addresses.
```ts
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // your email sending function
import { betterAuth } from 'better-auth';
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
subject: 'Verify your email address',
text: `Click the link to verify your email: ${url}`
});
},
},
}
}
});
```
@@ -43,8 +43,8 @@ For stricter security, enable `emailAndPassword.requireEmailVerification` to blo
```ts
export const auth = betterAuth({
emailAndPassword: {
requireEmailVerification: true,
},
requireEmailVerification: true
}
});
```
@@ -60,7 +60,7 @@ Always use absolute URLs (including the origin) for callback URLs in sign-up and
```ts
const { data, error } = await authClient.signUp.email({
callbackURL: "https://example.com/callback", // absolute URL with origin
callbackURL: 'https://example.com/callback' // absolute URL with origin
});
```
@@ -69,8 +69,8 @@ const { data, error } = await authClient.signUp.email({
Provide `sendResetPassword` in the email and password config to enable password resets.
```ts
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // your email sending function
import { betterAuth } from 'better-auth';
import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({
emailAndPassword: {
@@ -79,16 +79,16 @@ export const auth = betterAuth({
sendResetPassword: async ({ user, url, token }, request) => {
void sendEmail({
to: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
subject: 'Reset your password',
text: `Click the link to reset your password: ${url}`
});
},
// Optional event hook
onPasswordReset: async ({ user }, request) => {
// your logic here
console.log(`Password for user ${user.email} has been reset.`);
},
},
}
}
});
```
@@ -105,9 +105,9 @@ export const auth = betterAuth({
handler: (promise) => {
// Use platform-specific methods like waitUntil
waitUntil(promise);
},
},
},
}
}
}
});
```
@@ -119,8 +119,8 @@ Tokens expire after 1 hour by default. Configure with `resetPasswordTokenExpires
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
resetPasswordTokenExpiresIn: 60 * 30, // 30 minutes
},
resetPasswordTokenExpiresIn: 60 * 30 // 30 minutes
}
});
```
@@ -134,8 +134,8 @@ Enable `revokeSessionsOnPasswordReset` to invalidate all existing sessions on pa
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
revokeSessionsOnPasswordReset: true,
},
revokeSessionsOnPasswordReset: true
}
});
```
@@ -148,8 +148,8 @@ export const auth = betterAuth({
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
maxPasswordLength: 256,
},
maxPasswordLength: 256
}
});
```
@@ -160,9 +160,9 @@ Call `requestPasswordReset` to send the reset link. Triggers the `sendResetPassw
```ts
const data = await auth.api.requestPasswordReset({
body: {
email: "john.doe@example.com", // required
redirectTo: "https://example.com/reset-password",
},
email: 'john.doe@example.com', // required
redirectTo: 'https://example.com/reset-password'
}
});
```
@@ -170,8 +170,8 @@ Or authClient:
```ts
const { data, error } = await authClient.requestPasswordReset({
email: "john.doe@example.com", // required
redirectTo: "https://example.com/reset-password",
email: 'john.doe@example.com', // required
redirectTo: 'https://example.com/reset-password'
});
```
@@ -186,15 +186,15 @@ Default: `scrypt` (Node.js native, no external dependencies).
To use Argon2id or another algorithm, provide custom `hash` and `verify` functions:
```ts
import { betterAuth } from "better-auth";
import { hash, verify, type Options } from "@node-rs/argon2";
import { betterAuth } from 'better-auth';
import { hash, verify, type Options } from '@node-rs/argon2';
const argon2Options: Options = {
memoryCost: 65536, // 64 MiB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel lanes
outputLen: 32, // 32 byte output
algorithm: 2, // Argon2id variant
algorithm: 2 // Argon2id variant
};
export const auth = betterAuth({
@@ -202,10 +202,9 @@ export const auth = betterAuth({
enabled: true,
password: {
hash: (password) => hash(password, argon2Options),
verify: ({ password, hash: storedHash }) =>
verify(storedHash, password, argon2Options),
},
},
verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options)
}
}
});
```
@@ -11,28 +11,28 @@ description: Configure multi-tenant organizations, manage members and invitation
4. Verify: check that organization, member, invitation tables exist in your database
```ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5, // Max orgs per user
membershipLimit: 100, // Max members per org
}),
],
membershipLimit: 100 // Max members per org
})
]
});
```
### Client-Side Setup
```ts
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
import { createAuthClient } from 'better-auth/client';
import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
plugins: [organizationClient()],
plugins: [organizationClient()]
});
```
@@ -43,10 +43,10 @@ The creator is automatically assigned the `owner` role.
```ts
const createOrg = async () => {
const { data, error } = await authClient.organization.create({
name: "My Company",
slug: "my-company",
logo: "https://example.com/logo.png",
metadata: { plan: "pro" },
name: 'My Company',
slug: 'my-company',
logo: 'https://example.com/logo.png',
metadata: { plan: 'pro' }
});
};
```
@@ -62,8 +62,8 @@ organization({
},
organizationLimit: async (user) => {
// Premium users get more organizations
return user.plan === "premium" ? 20 : 3;
},
return user.plan === 'premium' ? 20 : 3;
}
});
```
@@ -74,16 +74,15 @@ Administrators can create organizations for other users (server-side only):
```ts
await auth.api.createOrganization({
body: {
name: "Client Organization",
slug: "client-org",
userId: "user-id-who-will-be-owner", // `userId` is required
},
name: 'Client Organization',
slug: 'client-org',
userId: 'user-id-who-will-be-owner' // `userId` is required
}
});
```
**Note**: The `userId` parameter cannot be used alongside session headers.
## Active Organizations
Stored in the session and scopes subsequent API calls. Set after user selects one.
@@ -91,7 +90,7 @@ Stored in the session and scopes subsequent API calls. Set after user selects on
```ts
const setActive = async (organizationId: string) => {
const { data, error } = await authClient.organization.setActive({
organizationId,
organizationId
});
};
```
@@ -107,10 +106,10 @@ Use `getFullOrganization()` to retrieve the active org with all members, invitat
```ts
await auth.api.addMember({
body: {
userId: "user-id",
role: "member",
organizationId: "org-id",
},
userId: 'user-id',
role: 'member',
organizationId: 'org-id'
}
});
```
@@ -121,10 +120,10 @@ For client-side member additions, use the invitation system instead.
```ts
await auth.api.addMember({
body: {
userId: "user-id",
role: ["admin", "moderator"],
organizationId: "org-id",
},
userId: 'user-id',
role: ['admin', 'moderator'],
organizationId: 'org-id'
}
});
```
@@ -141,11 +140,11 @@ Use `updateMemberRole({ memberId, role })`.
```ts
organization({
membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") {
if (organization.metadata?.plan === 'enterprise') {
return 1000;
}
return 50;
},
}
});
```
@@ -154,9 +153,9 @@ organization({
### Setting Up Invitation Emails
```ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
plugins: [
@@ -172,11 +171,11 @@ export const auth = betterAuth({
<a href="https://yourapp.com/accept-invite?id=${invitation.id}">
Accept Invitation
</a>
`,
`
});
},
}),
],
}
})
]
});
```
@@ -184,8 +183,8 @@ export const auth = betterAuth({
```ts
await authClient.organization.inviteMember({
email: "newuser@example.com",
role: "member",
email: 'newuser@example.com',
role: 'member'
});
```
@@ -193,9 +192,9 @@ await authClient.organization.inviteMember({
```ts
const { data } = await authClient.organization.getInvitationURL({
email: "newuser@example.com",
role: "member",
callbackURL: "https://yourapp.com/dashboard",
email: 'newuser@example.com',
role: 'member',
callbackURL: 'https://yourapp.com/dashboard'
});
// Share data.url via any channel
@@ -209,7 +208,7 @@ This endpoint does not call `sendInvitationEmail` — handle delivery yourself.
organization({
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days (default: 48 hours)
invitationLimit: 100, // Max pending invitations per org
cancelPendingInvitationsOnReInvite: true, // Cancel old invites when re-inviting
cancelPendingInvitationsOnReInvite: true // Cancel old invites when re-inviting
});
```
@@ -221,7 +220,7 @@ Default roles: `owner` (full access), `admin` (manage members/invitations/settin
```ts
const { data } = await authClient.organization.hasPermission({
permission: "member:write",
permission: 'member:write'
});
if (data?.hasPermission) {
@@ -236,7 +235,7 @@ Use `checkRolePermission({ role, permissions })` for client-side UI rendering (s
### Enabling Teams
```ts
import { organization } from "better-auth/plugins";
import { organization } from 'better-auth/plugins';
export const auth = betterAuth({
plugins: [
@@ -244,8 +243,8 @@ export const auth = betterAuth({
teams: {
enabled: true
}
}),
],
})
]
});
```
@@ -253,7 +252,7 @@ export const auth = betterAuth({
```ts
const { data } = await authClient.organization.createTeam({
name: "Engineering",
name: 'Engineering'
});
```
@@ -270,7 +269,7 @@ organization({
teams: {
maximumTeams: 20, // Max teams per org
maximumMembersPerTeam: 50, // Max members per team
allowRemovingAllTeams: false, // Prevent removing last team
allowRemovingAllTeams: false // Prevent removing last team
}
});
```
@@ -280,8 +279,8 @@ organization({
### Enabling Dynamic Access Control
```ts
import { organization } from "better-auth/plugins";
import { dynamicAccessControl } from "@better-auth/organization/addons";
import { organization } from 'better-auth/plugins';
import { dynamicAccessControl } from '@better-auth/organization/addons';
export const auth = betterAuth({
plugins: [
@@ -289,8 +288,8 @@ export const auth = betterAuth({
dynamicAccessControl: {
enabled: true
}
}),
],
})
]
});
```
@@ -298,11 +297,11 @@ export const auth = betterAuth({
```ts
await authClient.organization.createRole({
role: "moderator",
role: 'moderator',
permission: {
member: ["read"],
invitation: ["read"],
},
member: ['read'],
invitation: ['read']
}
});
```
@@ -321,8 +320,8 @@ organization({
return {
data: {
...data,
metadata: { ...data.metadata, createdBy: user.id },
},
metadata: { ...data.metadata, createdBy: user.id }
}
};
},
afterCreate: async ({ organization, member }) => {
@@ -332,19 +331,19 @@ organization({
beforeDelete: async ({ organization }) => {
// Cleanup before deletion
await archiveOrganizationData(organization.id);
},
}
},
member: {
afterCreate: async ({ member, organization }) => {
await notifyAdmins(organization.id, `New member joined`);
},
}
},
invitation: {
afterCreate: async ({ invitation, organization, inviter }) => {
await logInvitation(invitation);
},
},
},
}
}
}
});
```
@@ -356,30 +355,30 @@ Customize table names, field names, and add additional fields:
organization({
schema: {
organization: {
modelName: "workspace", // Rename table
modelName: 'workspace', // Rename table
fields: {
name: "workspaceName", // Rename fields
name: 'workspaceName' // Rename fields
},
additionalFields: {
billingId: {
type: "string",
required: false,
},
},
type: 'string',
required: false
}
}
},
member: {
additionalFields: {
department: {
type: "string",
required: false,
type: 'string',
required: false
},
title: {
type: "string",
required: false,
},
},
},
},
type: 'string',
required: false
}
}
}
}
});
```
@@ -396,8 +395,8 @@ Always ensure ownership transfer before removing the current owner:
```ts
// Transfer ownership first
await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id",
role: "owner",
memberId: 'new-owner-member-id',
role: 'owner'
});
// Then the previous owner can be demoted or removed
@@ -409,7 +408,7 @@ Deleting an organization removes all associated data (members, invitations, team
```ts
organization({
disableOrganizationDeletion: true, // Disable via config
disableOrganizationDeletion: true // Disable via config
});
```
@@ -422,10 +421,10 @@ organization({
beforeDelete: async ({ organization }) => {
// Archive instead of delete
await archiveOrganization(organization.id);
throw new Error("Organization archived, not deleted");
},
},
},
throw new Error('Organization archived, not deleted');
}
}
}
});
```
@@ -438,9 +437,9 @@ organization({
## Complete Configuration Example
```ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
plugins: [
@@ -449,10 +448,10 @@ export const auth = betterAuth({
allowUserToCreateOrganization: true,
organizationLimit: 10,
membershipLimit: 100,
creatorRole: "owner",
creatorRole: 'owner',
// Slugs
defaultOrganizationIdField: "slug",
defaultOrganizationIdField: 'slug',
// Invitations
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days
@@ -461,7 +460,7 @@ export const auth = betterAuth({
await sendEmail({
to: data.email,
subject: `Join ${data.organization.name}`,
html: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`,
html: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`
});
},
@@ -470,10 +469,10 @@ export const auth = betterAuth({
organization: {
afterCreate: async ({ organization }) => {
console.log(`Organization ${organization.name} created`);
},
},
},
}),
],
}
}
}
})
]
});
```
@@ -11,33 +11,33 @@ description: Configure TOTP authenticator apps, send OTP codes via email/SMS, ma
4. Verify: check that `twoFactorSecret` column exists on user table
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
export const auth = betterAuth({
appName: "My App",
appName: 'My App',
plugins: [
twoFactor({
issuer: "My App",
}),
],
issuer: 'My App'
})
]
});
```
### Client-Side Setup
```ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from 'better-auth/client';
import { twoFactorClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
],
window.location.href = '/2fa';
}
})
]
});
```
@@ -48,7 +48,7 @@ Requires password verification. Returns TOTP URI (for QR code) and backup codes.
```ts
const enable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({
password,
password
});
if (data) {
@@ -65,7 +65,7 @@ const enable2FA = async (password: string) => {
### Displaying the QR Code
```tsx
import QRCode from "react-qr-code";
import QRCode from 'react-qr-code';
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
return <QRCode value={totpURI} />;
@@ -80,7 +80,7 @@ Accepts codes from one period before/after current time:
const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true,
trustDevice: true
});
};
```
@@ -91,8 +91,8 @@ const verifyTotp = async (code: string) => {
twoFactor({
totpOptions: {
digits: 6, // 6 or 8 digits (default: 6)
period: 30, // Code validity period in seconds (default: 30)
},
period: 30 // Code validity period in seconds (default: 30)
}
});
```
@@ -101,9 +101,9 @@ twoFactor({
### Configuring OTP Delivery
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
plugins: [
@@ -112,16 +112,16 @@ export const auth = betterAuth({
sendOTP: async ({ user, otp }, ctx) => {
await sendEmail({
to: user.email,
subject: "Your verification code",
text: `Your code is: ${otp}`,
subject: 'Your verification code',
text: `Your code is: ${otp}`
});
},
period: 5, // Code validity in minutes (default: 3)
digits: 6, // Number of digits (default: 6)
allowedAttempts: 5, // Max verification attempts (default: 5)
},
}),
],
allowedAttempts: 5 // Max verification attempts (default: 5)
}
})
]
});
```
@@ -136,8 +136,8 @@ Configure how OTP codes are stored in the database:
```ts
twoFactor({
otpOptions: {
storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
},
storeOTP: 'encrypted' // Options: "plain", "encrypted", "hashed"
}
});
```
@@ -148,9 +148,9 @@ twoFactor({
otpOptions: {
storeOTP: {
encrypt: async (token) => myEncrypt(token),
decrypt: async (token) => myDecrypt(token),
},
},
decrypt: async (token) => myDecrypt(token)
}
}
});
```
@@ -182,7 +182,7 @@ Invalidates all previous codes:
```ts
const regenerateBackupCodes = async (password: string) => {
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password,
password
});
// data.backupCodes contains the new codes
};
@@ -194,7 +194,7 @@ const regenerateBackupCodes = async (password: string) => {
const verifyBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code,
trustDevice: true,
trustDevice: true
});
};
```
@@ -206,8 +206,8 @@ twoFactor({
backupCodeOptions: {
amount: 10, // Number of codes to generate (default: 10)
length: 10, // Length of each code (default: 10)
storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
},
storeBackupCodes: 'encrypted' // Options: "plain", "encrypted"
}
});
```
@@ -230,9 +230,9 @@ const signIn = async (email: string, password: string) => {
{
onSuccess(context) {
if (context.data.twoFactorRedirect) {
window.location.href = "/2fa";
window.location.href = '/2fa';
}
}
},
}
);
};
@@ -252,7 +252,7 @@ Flow: credentials → session removed → temporary 2FA cookie (10 min default)
```ts
twoFactor({
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
twoFactorCookieMaxAge: 600 // 10 minutes in seconds (default)
});
```
@@ -263,8 +263,8 @@ Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional at
```ts
twoFactor({
otpOptions: {
allowedAttempts: 5, // Max attempts per OTP code (default: 5)
},
allowedAttempts: 5 // Max attempts per OTP code (default: 5)
}
});
```
@@ -281,7 +281,7 @@ Requires password confirmation. Revokes trusted device records:
```ts
const disable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.disable({
password,
password
});
};
```
@@ -289,43 +289,43 @@ const disable2FA = async (password: string) => {
## Complete Configuration Example
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
appName: "My App",
appName: 'My App',
plugins: [
twoFactor({
// TOTP settings
issuer: "My App",
issuer: 'My App',
totpOptions: {
digits: 6,
period: 30,
period: 30
},
// OTP settings
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({
to: user.email,
subject: "Your verification code",
text: `Your code is: ${otp}`,
subject: 'Your verification code',
text: `Your code is: ${otp}`
});
},
period: 5,
allowedAttempts: 5,
storeOTP: "encrypted",
storeOTP: 'encrypted'
},
// Backup code settings
backupCodeOptions: {
amount: 10,
length: 10,
storeBackupCodes: "encrypted",
storeBackupCodes: 'encrypted'
},
// Session settings
twoFactorCookieMaxAge: 600, // 10 minutes
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days
}),
],
trustDeviceMaxAge: 30 * 24 * 60 * 60 // 30 days
})
]
});
```
+1 -4
View File
@@ -4,10 +4,7 @@
"trailingComma": "none",
"semi": true,
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
+1 -1
View File
@@ -15,7 +15,7 @@ Requires `.env` with at least `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH
## Commands
| npm run ... | What it does |
|----------------|-------------|
| -------------------------- | ------------------------------------------------ |
| `check` | `svelte-kit sync && svelte-check` (typecheck) |
| `lint` | `prettier --check . && eslint .` (order matters) |
| `format` | `prettier --write .` |
+20
View File
@@ -1 +1,21 @@
# Rhema Wave Technologies - User Portal
SvelteKit 2 + Svelte 5 + Tailwind CSS v4 + Prisma 7 + PostgreSQL + Better-Auth.
## Admin
Admin CRUD pages at `/admin`. 9 models have per-page routes with modals for create/edit:
| Included | Excluded |
| -------- | --------------------- |
| users | Session (Better Auth) |
| profiles | Account (Better Auth) |
| addresses| Verification (Better Auth) |
| user-roles | UserProclamations (composite key) |
| proclamations | |
| solfeggio-frequencies | |
| proclamation-categories | |
| license-keys (list-only) | |
| subscription-levels | |
See `src/lib/svelte-admin/README.md` for architecture and configuration docs.
+724 -361
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -42,6 +42,7 @@
"svelte": "^5.45.6",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17",
"tsx": "^4.23.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
@@ -52,10 +53,15 @@
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@tiptap/core": "^3.15.3",
"@tiptap/extension-color": "^3.27.3",
"@tiptap/extension-highlight": "^3.27.3",
"@tiptap/extension-text-style": "^3.27.3",
"@tiptap/starter-kit": "^3.15.3",
"better-auth": "^1.4.10",
"dotenv": "^17.2.3",
"pg": "^8.16.3",
"resend": "^6.17.1"
"resend": "^6.17.1",
"sveltekit-superforms": "^2.30.2",
"zod": "^4.4.3"
}
}
+2 -1
View File
@@ -6,7 +6,8 @@ import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations'
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts'
},
datasource: {
url: process.env['DATABASE_URL']
@@ -0,0 +1,5 @@
-- AlterTable: Remove price from Proclamation
ALTER TABLE "Proclamation" DROP COLUMN "price";
-- AlterTable: Change SolfeggioFrequency value from Float to Int
ALTER TABLE "SolfeggioFrequency" ALTER COLUMN "value" SET DATA TYPE INTEGER;
@@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the column `isDefault` on the `Address` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Address" DROP COLUMN "isDefault",
ADD COLUMN "isDefaultBilling" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "isDefaultMailing" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "isDefaultShipping" BOOLEAN NOT NULL DEFAULT false;
+3 -1
View File
@@ -56,7 +56,9 @@ model Address {
postalCode String
country String
type String @default("Mailing")
isDefault Boolean @default(false)
isDefaultMailing Boolean @default(false)
isDefaultBilling Boolean @default(false)
isDefaultShipping Boolean @default(false)
}
model Session {
+111
View File
@@ -0,0 +1,111 @@
import 'dotenv/config';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/lib/generated/prisma/client';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const userRoles = [
{ id: 1, name: 'Super' },
{ id: 2, name: 'Admin' },
{ id: 3, name: 'Staff' },
{ id: 4, name: 'User' }
];
const solfeggioFrequencies = [
{ id: 1, name: 'Crown', value: 936 },
{ id: 2, name: '3rd Eye', value: 852 },
{ id: 3, name: 'Throat', value: 741 },
{ id: 4, name: 'Heart', value: 639 },
{ id: 5, name: 'Solar Plexus', value: 528 },
{ id: 6, name: 'Sacral', value: 417 },
{ id: 7, name: 'Root', value: 396 }
];
const subscriptionLevels = [
{ id: 1, name: 'Basic', description: 'General only' },
{ id: 2, name: 'Advanced', description: 'General + Specific' },
{ id: 3, name: 'Elite', description: 'General + Specific + Precise' }
];
const proclamationCategories = [
{ id: 4, name: 'Spiritual', parentId: null },
{ id: 5, name: 'General', parentId: 4 },
{ id: 9, name: 'Specific', parentId: 4 },
{ id: 40, name: 'Precise', parentId: 4 },
{ id: 2, name: 'Physical', parentId: null },
{ id: 3, name: 'General', parentId: 2 },
{ id: 10, name: 'Specific', parentId: 2 },
{ id: 32, name: 'Precise', parentId: 2 },
{ id: 16, name: 'Mental', parentId: null },
{ id: 30, name: 'General', parentId: 16 },
{ id: 31, name: 'Specific', parentId: 16 },
{ id: 38, name: 'Precise', parentId: 16 },
{ id: 17, name: 'Custom (Client Specific)', parentId: null },
{ id: 19, name: 'Financial', parentId: null },
{ id: 28, name: 'General', parentId: 19 },
{ id: 29, name: 'Specific', parentId: 19 },
{ id: 37, name: 'Precise', parentId: 19 },
{ id: 20, name: 'Emotional', parentId: null },
{ id: 24, name: 'General', parentId: 20 },
{ id: 26, name: 'Specific', parentId: 20 },
{ id: 45, name: 'Precise', parentId: 20 },
{ id: 21, name: 'Social', parentId: null },
{ id: 22, name: 'General', parentId: 21 },
{ id: 23, name: 'Specific', parentId: 21 },
{ id: 39, name: 'Precise', parentId: 21 },
{ id: 41, name: 'Biblical', parentId: null },
{ id: 42, name: 'Personal Growth & Development', parentId: null },
{ id: 43, name: 'Prayers', parentId: 41 },
{ id: 44, name: 'Scripture', parentId: 41 },
{ id: 46, name: 'Sports', parentId: null }
];
async function main() {
console.log('Seeding UserRole...');
for (const role of userRoles) {
await prisma.userRole.upsert({
where: { id: role.id },
create: role,
update: role
});
}
console.log('Seeding SolfeggioFrequency...');
for (const freq of solfeggioFrequencies) {
await prisma.solfeggioFrequency.upsert({
where: { id: freq.id },
create: freq,
update: freq
});
}
console.log('Seeding ProclamationCategory...');
for (const cat of proclamationCategories) {
await prisma.proclamationCategory.upsert({
where: { id: cat.id },
create: cat,
update: cat
});
}
console.log('Seeding SubscriptionLevel...');
for (const level of subscriptionLevels) {
await prisma.subscriptionLevel.upsert({
where: { id: level.id },
create: level,
update: level
});
}
console.log('Seed complete.');
}
main()
.catch((e) => {
console.error('Seed failed:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Session, User } from "better-auth";
import type { Session, User } from 'better-auth';
declare global {
namespace App {
+5 -4
View File
@@ -1,13 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
@@ -18,7 +20,6 @@
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="h-dvh w-dvw flex flex-col">%sveltekit.body%</div>
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
</body>
</html>
+1 -3
View File
@@ -87,9 +87,7 @@ describe('hooks.server handle', () => {
const event = createMockEvent();
await handle({ event, resolve: event.resolve });
expect(svelteKitHandler).toHaveBeenCalledWith(
expect.objectContaining({ building: false })
);
expect(svelteKitHandler).toHaveBeenCalledWith(expect.objectContaining({ building: false }));
});
it('calls svelteKitHandler with auth instance', async () => {
+4 -4
View File
@@ -1,11 +1,11 @@
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from "$app/environment";
import { auth } from '$lib/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
export async function handle({ event, resolve }) {
// Fetch current session from Better Auth
const session = await auth.api.getSession({
headers: event.request.headers,
headers: event.request.headers
});
// Make session and user available on server
+1 -1
View File
@@ -1,3 +1,3 @@
import { createAuthClient } from "better-auth/svelte"
import { createAuthClient } from 'better-auth/svelte';
export const authClient = createAuthClient({});
+1 -4
View File
@@ -46,9 +46,6 @@ export const auth = betterAuth({
window: 60,
max: 100
},
trustedOrigins: [
'http://localhost:5173',
'https://portal.rhemawavetech.com'
],
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
plugins: [sveltekitCookies(getRequestEvent)]
});
+92
View File
@@ -0,0 +1,92 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Highlight from '@tiptap/extension-highlight';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
let { initialContent = '', name = 'content' }: { initialContent?: string; name?: string } =
$props();
let element: HTMLElement;
let editor: Editor;
let content = $state('');
onMount(() => {
content = initialContent;
editor = new Editor({
element,
extensions: [
StarterKit.configure({
bulletList: false,
orderedList: false
}),
TextStyle,
Color,
Highlight.configure({ multicolor: true })
],
content: initialContent,
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none outline-none h-full min-h-40 p-3 dark:prose-invert dark:text-gray-100'
}
},
onUpdate: ({ editor }) => {
content = editor.getHTML();
}
});
editor.setEditable(true);
return () => editor.destroy();
});
function toggleBold() { editor?.chain().focus().toggleBold().run(); }
function toggleItalic() { editor?.chain().focus().toggleItalic().run(); }
function toggleHighlight() { editor?.chain().focus().toggleHighlight().run(); }
function setColor() {
const current = editor?.getAttributes('textStyle').color ?? '#000000';
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
const idx = colors.indexOf(current);
const next = colors[(idx + 1) % colors.length];
editor?.chain().focus().setColor(next).run();
}
</script>
<div class="border border-gray-300 dark:border-gray-600">
<div
class="flex gap-1 border-b border-gray-300 bg-gray-100 p-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300"
>
<button
type="button"
onclick={toggleBold}
class="min-w-8 px-2 py-1 font-bold hover:bg-gray-200 dark:hover:bg-gray-700"
title="Bold">B</button>
<button
type="button"
onclick={toggleItalic}
class="min-w-8 px-2 py-1 italic hover:bg-gray-200 dark:hover:bg-gray-700"
title="Italic">I</button>
<button
type="button"
onclick={toggleHighlight}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Highlight"><span class="bg-yellow-200 px-1 dark:bg-yellow-600">H</span></button>
<button
type="button"
onclick={setColor}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Cycle text color">A</button>
</div>
<div
bind:this={element}
role="textbox"
tabindex="0"
contenteditable="true"
class="min-h-40 cursor-text bg-white dark:bg-gray-900"
></div>
</div>
<input type="hidden" {name} value={content} />
+3 -1
View File
@@ -28,7 +28,9 @@ export * from "./enums.ts"
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
@@ -202,33 +202,6 @@ export type IntWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -430,20 +403,4 @@ export type NestedFloatFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
File diff suppressed because one or more lines are too long
@@ -80,12 +80,12 @@ export type PrismaVersion = {
}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/
export const prismaVersion: PrismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
}
/**
@@ -1436,7 +1436,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber'
phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -1449,7 +1450,11 @@ export const AddressScalarFieldEnum = {
city: 'city',
state: 'state',
postalCode: 'postalCode',
country: 'country'
country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -1514,7 +1519,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId'
} as const
@@ -1754,6 +1758,21 @@ export type PrismaClientOptions = ({
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
/**
* Optional maximum size for the query plan cache. If not provided, a default size will be used.
* A value of `0` can be used to disable the cache entirely. A higher cache size can improve
* performance for applications that execute a large number of unique queries, while a smaller
* cache size can reduce memory usage.
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* queryPlanCacheMaxSize: 100,
* })
* ```
*/
queryPlanCacheMaxSize?: number
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
@@ -72,12 +72,12 @@ export type ModelName = (typeof ModelName)[keyof typeof ModelName]
* Enums
*/
export const TransactionIsolationLevel = {
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
@@ -101,7 +101,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber'
phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -114,7 +115,11 @@ export const AddressScalarFieldEnum = {
city: 'city',
state: 'state',
postalCode: 'postalCode',
country: 'country'
country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -179,7 +184,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId'
} as const
+6 -1
View File
@@ -214,7 +214,7 @@ export type AccountGroupByOutputType = {
_max: AccountMaxAggregateOutputType | null
}
type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise<
export type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<AccountGroupByOutputType, T['by']> &
{
@@ -1406,6 +1406,11 @@ export type AccountFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Accounts.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Accounts.
*/
distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
}
+147 -2
View File
@@ -32,6 +32,10 @@ export type AddressMinAggregateOutputType = {
state: string | null
postalCode: string | null
country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
}
export type AddressMaxAggregateOutputType = {
@@ -42,6 +46,10 @@ export type AddressMaxAggregateOutputType = {
state: string | null
postalCode: string | null
country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
}
export type AddressCountAggregateOutputType = {
@@ -52,6 +60,10 @@ export type AddressCountAggregateOutputType = {
state: number
postalCode: number
country: number
type: number
isDefaultMailing: number
isDefaultBilling: number
isDefaultShipping: number
_all: number
}
@@ -64,6 +76,10 @@ export type AddressMinAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
}
export type AddressMaxAggregateInputType = {
@@ -74,6 +90,10 @@ export type AddressMaxAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
}
export type AddressCountAggregateInputType = {
@@ -84,6 +104,10 @@ export type AddressCountAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
_all?: true
}
@@ -167,12 +191,16 @@ export type AddressGroupByOutputType = {
state: string
postalCode: string
country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
_count: AddressCountAggregateOutputType | null
_min: AddressMinAggregateOutputType | null
_max: AddressMaxAggregateOutputType | null
}
type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise<
export type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<AddressGroupByOutputType, T['by']> &
{
@@ -198,6 +226,10 @@ export type AddressWhereInput = {
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
}
@@ -209,6 +241,10 @@ export type AddressOrderByWithRelationInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
profile?: Prisma.ProfileOrderByWithRelationInput
}
@@ -223,6 +259,10 @@ export type AddressWhereUniqueInput = Prisma.AtLeast<{
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
}, "id">
@@ -234,6 +274,10 @@ export type AddressOrderByWithAggregationInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
_count?: Prisma.AddressCountOrderByAggregateInput
_max?: Prisma.AddressMaxOrderByAggregateInput
_min?: Prisma.AddressMinOrderByAggregateInput
@@ -250,6 +294,10 @@ export type AddressScalarWhereWithAggregatesInput = {
state?: Prisma.StringWithAggregatesFilter<"Address"> | string
postalCode?: Prisma.StringWithAggregatesFilter<"Address"> | string
country?: Prisma.StringWithAggregatesFilter<"Address"> | string
type?: Prisma.StringWithAggregatesFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
}
export type AddressCreateInput = {
@@ -259,6 +307,10 @@ export type AddressCreateInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile: Prisma.ProfileCreateNestedOneWithoutAddressesInput
}
@@ -270,6 +322,10 @@ export type AddressUncheckedCreateInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateInput = {
@@ -279,6 +335,10 @@ export type AddressUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
profile?: Prisma.ProfileUpdateOneRequiredWithoutAddressesNestedInput
}
@@ -290,6 +350,10 @@ export type AddressUncheckedUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressCreateManyInput = {
@@ -300,6 +364,10 @@ export type AddressCreateManyInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateManyMutationInput = {
@@ -309,6 +377,10 @@ export type AddressUpdateManyMutationInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateManyInput = {
@@ -319,6 +391,10 @@ export type AddressUncheckedUpdateManyInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressListRelationFilter = {
@@ -339,6 +415,10 @@ export type AddressCountOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressMaxOrderByAggregateInput = {
@@ -349,6 +429,10 @@ export type AddressMaxOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressMinOrderByAggregateInput = {
@@ -359,6 +443,10 @@ export type AddressMinOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressCreateNestedManyWithoutProfileInput = {
@@ -410,6 +498,10 @@ export type AddressCreateWithoutProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUncheckedCreateWithoutProfileInput = {
@@ -419,6 +511,10 @@ export type AddressUncheckedCreateWithoutProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressCreateOrConnectWithoutProfileInput = {
@@ -458,6 +554,10 @@ export type AddressScalarWhereInput = {
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
}
export type AddressCreateManyProfileInput = {
@@ -467,6 +567,10 @@ export type AddressCreateManyProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateWithoutProfileInput = {
@@ -476,6 +580,10 @@ export type AddressUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateWithoutProfileInput = {
@@ -485,6 +593,10 @@ export type AddressUncheckedUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateManyWithoutProfileInput = {
@@ -494,6 +606,10 @@ export type AddressUncheckedUpdateManyWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
@@ -506,6 +622,10 @@ export type AddressSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -517,6 +637,10 @@ export type AddressSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -528,6 +652,10 @@ export type AddressSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -539,9 +667,13 @@ export type AddressSelectScalar = {
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "profileId" | "street" | "city" | "state" | "postalCode" | "country", ExtArgs["result"]["address"]>
export type AddressOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "profileId" | "street" | "city" | "state" | "postalCode" | "country" | "type" | "isDefaultMailing" | "isDefaultBilling" | "isDefaultShipping", ExtArgs["result"]["address"]>
export type AddressInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}
@@ -565,6 +697,10 @@ export type $AddressPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
state: string
postalCode: string
country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
}, ExtArgs["result"]["address"]>
composites: {}
}
@@ -996,6 +1132,10 @@ export interface AddressFieldRefs {
readonly state: Prisma.FieldRef<"Address", 'String'>
readonly postalCode: Prisma.FieldRef<"Address", 'String'>
readonly country: Prisma.FieldRef<"Address", 'String'>
readonly type: Prisma.FieldRef<"Address", 'String'>
readonly isDefaultMailing: Prisma.FieldRef<"Address", 'Boolean'>
readonly isDefaultBilling: Prisma.FieldRef<"Address", 'Boolean'>
readonly isDefaultShipping: Prisma.FieldRef<"Address", 'Boolean'>
}
@@ -1192,6 +1332,11 @@ export type AddressFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Addresses.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Addresses.
*/
distinct?: Prisma.AddressScalarFieldEnum | Prisma.AddressScalarFieldEnum[]
}
@@ -158,7 +158,7 @@ export type LicenseKeyGroupByOutputType = {
_max: LicenseKeyMaxAggregateOutputType | null
}
type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise<
export type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<LicenseKeyGroupByOutputType, T['by']> &
{
@@ -1122,6 +1122,11 @@ export type LicenseKeyFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Skip the first `n` LicenseKeys.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of LicenseKeys.
*/
distinct?: Prisma.LicenseKeyScalarFieldEnum | Prisma.LicenseKeyScalarFieldEnum[]
}
@@ -29,14 +29,12 @@ export type AggregateProclamation = {
export type ProclamationAvgAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
export type ProclamationSumAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -46,7 +44,6 @@ export type ProclamationMinAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -56,7 +53,6 @@ export type ProclamationMaxAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -66,7 +62,6 @@ export type ProclamationCountAggregateOutputType = {
content: number
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
_all: number
}
@@ -75,14 +70,12 @@ export type ProclamationCountAggregateOutputType = {
export type ProclamationAvgAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
export type ProclamationSumAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -92,7 +85,6 @@ export type ProclamationMinAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -102,7 +94,6 @@ export type ProclamationMaxAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -112,7 +103,6 @@ export type ProclamationCountAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
_all?: true
}
@@ -209,7 +199,6 @@ export type ProclamationGroupByOutputType = {
content: string
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
_count: ProclamationCountAggregateOutputType | null
_avg: ProclamationAvgAggregateOutputType | null
@@ -218,7 +207,7 @@ export type ProclamationGroupByOutputType = {
_max: ProclamationMaxAggregateOutputType | null
}
type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise<
export type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProclamationGroupByOutputType, T['by']> &
{
@@ -242,7 +231,6 @@ export type ProclamationWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -256,7 +244,6 @@ export type ProclamationOrderByWithRelationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
category?: Prisma.ProclamationCategoryOrderByWithRelationInput
@@ -273,7 +260,6 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -287,7 +273,6 @@ export type ProclamationOrderByWithAggregationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
_count?: Prisma.ProclamationCountOrderByAggregateInput
_avg?: Prisma.ProclamationAvgOrderByAggregateInput
@@ -305,7 +290,6 @@ export type ProclamationScalarWhereWithAggregatesInput = {
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
price?: Prisma.FloatWithAggregatesFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
}
@@ -313,7 +297,6 @@ export type ProclamationCreateInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -326,7 +309,6 @@ export type ProclamationUncheckedCreateInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -335,7 +317,6 @@ export type ProclamationUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -348,7 +329,6 @@ export type ProclamationUncheckedUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -359,7 +339,6 @@ export type ProclamationCreateManyInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -367,7 +346,6 @@ export type ProclamationUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
}
export type ProclamationUncheckedUpdateManyInput = {
@@ -376,7 +354,6 @@ export type ProclamationUncheckedUpdateManyInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -386,14 +363,12 @@ export type ProclamationCountOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
export type ProclamationAvgOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -403,7 +378,6 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -413,14 +387,12 @@ export type ProclamationMinOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
export type ProclamationSumOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -439,14 +411,6 @@ export type ProclamationOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type FloatFieldUpdateOperationsInput = {
set?: number
increment?: number
decrement?: number
multiply?: number
divide?: number
}
export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
@@ -591,7 +555,6 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -603,7 +566,6 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -627,7 +589,6 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -639,7 +600,6 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -647,7 +607,6 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
id?: string
name: string
content: string
price?: number
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -658,7 +617,6 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -698,7 +656,6 @@ export type ProclamationScalarWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
}
@@ -706,7 +663,6 @@ export type ProclamationCreateWithoutCategoryInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -717,7 +673,6 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -752,7 +707,6 @@ export type ProclamationCreateWithoutSubscriptionLevelInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -764,7 +718,6 @@ export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -799,7 +752,6 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -807,7 +759,6 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -818,7 +769,6 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -828,7 +778,6 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -837,7 +786,6 @@ export type ProclamationCreateManyCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number
}
@@ -845,7 +793,6 @@ export type ProclamationUpdateWithoutCategoryInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -856,7 +803,6 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -866,7 +812,6 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -876,14 +821,12 @@ export type ProclamationCreateManySubscriptionLevelInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
}
export type ProclamationUpdateWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -895,7 +838,6 @@ export type ProclamationUncheckedUpdateWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -905,7 +847,6 @@ export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
}
@@ -945,7 +886,6 @@ export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.Internal
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -960,7 +900,6 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -973,7 +912,6 @@ export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -986,11 +924,10 @@ export type ProclamationSelectScalar = {
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
}
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "price" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -1023,7 +960,6 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
content: string
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
}, ExtArgs["result"]["proclamation"]>
composites: {}
@@ -1457,7 +1393,6 @@ export interface ProclamationFieldRefs {
readonly content: Prisma.FieldRef<"Proclamation", 'String'>
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly price: Prisma.FieldRef<"Proclamation", 'Float'>
readonly subscriptionLevelId: Prisma.FieldRef<"Proclamation", 'Int'>
}
@@ -1655,6 +1590,11 @@ export type ProclamationFindManyArgs<ExtArgs extends runtime.Types.Extensions.In
* Skip the first `n` Proclamations.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Proclamations.
*/
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
}
@@ -182,7 +182,7 @@ export type ProclamationCategoryGroupByOutputType = {
_max: ProclamationCategoryMaxAggregateOutputType | null
}
type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise<
export type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProclamationCategoryGroupByOutputType, T['by']> &
{
@@ -1297,6 +1297,11 @@ export type ProclamationCategoryFindManyArgs<ExtArgs extends runtime.Types.Exten
* Skip the first `n` ProclamationCategories.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of ProclamationCategories.
*/
distinct?: Prisma.ProclamationCategoryScalarFieldEnum | Prisma.ProclamationCategoryScalarFieldEnum[]
}
+43 -2
View File
@@ -30,6 +30,7 @@ export type ProfileMinAggregateOutputType = {
firstName: string | null
lastName: string | null
phoneNumber: string | null
ghlContactId: string | null
}
export type ProfileMaxAggregateOutputType = {
@@ -38,6 +39,7 @@ export type ProfileMaxAggregateOutputType = {
firstName: string | null
lastName: string | null
phoneNumber: string | null
ghlContactId: string | null
}
export type ProfileCountAggregateOutputType = {
@@ -46,6 +48,7 @@ export type ProfileCountAggregateOutputType = {
firstName: number
lastName: number
phoneNumber: number
ghlContactId: number
_all: number
}
@@ -56,6 +59,7 @@ export type ProfileMinAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
}
export type ProfileMaxAggregateInputType = {
@@ -64,6 +68,7 @@ export type ProfileMaxAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
}
export type ProfileCountAggregateInputType = {
@@ -72,6 +77,7 @@ export type ProfileCountAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
_all?: true
}
@@ -153,12 +159,13 @@ export type ProfileGroupByOutputType = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId: string | null
_count: ProfileCountAggregateOutputType | null
_min: ProfileMinAggregateOutputType | null
_max: ProfileMaxAggregateOutputType | null
}
type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise<
export type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProfileGroupByOutputType, T['by']> &
{
@@ -182,6 +189,7 @@ export type ProfileWhereInput = {
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter
}
@@ -192,6 +200,7 @@ export type ProfileOrderByWithRelationInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
addresses?: Prisma.AddressOrderByRelationAggregateInput
}
@@ -205,6 +214,7 @@ export type ProfileWhereUniqueInput = Prisma.AtLeast<{
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter
}, "id" | "userId">
@@ -215,6 +225,7 @@ export type ProfileOrderByWithAggregationInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.ProfileCountOrderByAggregateInput
_max?: Prisma.ProfileMaxOrderByAggregateInput
_min?: Prisma.ProfileMinOrderByAggregateInput
@@ -229,6 +240,7 @@ export type ProfileScalarWhereWithAggregatesInput = {
firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
}
export type ProfileCreateInput = {
@@ -236,6 +248,7 @@ export type ProfileCreateInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
}
@@ -246,6 +259,7 @@ export type ProfileUncheckedCreateInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
}
@@ -254,6 +268,7 @@ export type ProfileUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
@@ -264,6 +279,7 @@ export type ProfileUncheckedUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
}
@@ -273,6 +289,7 @@ export type ProfileCreateManyInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
}
export type ProfileUpdateManyMutationInput = {
@@ -280,6 +297,7 @@ export type ProfileUpdateManyMutationInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileUncheckedUpdateManyInput = {
@@ -288,6 +306,7 @@ export type ProfileUncheckedUpdateManyInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileNullableScalarRelationFilter = {
@@ -301,6 +320,7 @@ export type ProfileCountOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileMaxOrderByAggregateInput = {
@@ -309,6 +329,7 @@ export type ProfileMaxOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileMinOrderByAggregateInput = {
@@ -317,6 +338,7 @@ export type ProfileMinOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileScalarRelationFilter = {
@@ -375,6 +397,7 @@ export type ProfileCreateWithoutUserInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
}
@@ -383,6 +406,7 @@ export type ProfileUncheckedCreateWithoutUserInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
}
@@ -407,6 +431,7 @@ export type ProfileUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
@@ -415,6 +440,7 @@ export type ProfileUncheckedUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
}
@@ -423,6 +449,7 @@ export type ProfileCreateWithoutAddressesInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
}
@@ -432,6 +459,7 @@ export type ProfileUncheckedCreateWithoutAddressesInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
}
export type ProfileCreateOrConnectWithoutAddressesInput = {
@@ -455,6 +483,7 @@ export type ProfileUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
}
@@ -464,6 +493,7 @@ export type ProfileUncheckedUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -503,6 +533,7 @@ export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
@@ -514,6 +545,7 @@ export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
@@ -523,6 +555,7 @@ export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
@@ -532,9 +565,10 @@ export type ProfileSelectScalar = {
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
}
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber", ExtArgs["result"]["profile"]>
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
export type ProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
@@ -559,6 +593,7 @@ export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArg
firstName: string
lastName: string
phoneNumber: string
ghlContactId: string | null
}, ExtArgs["result"]["profile"]>
composites: {}
}
@@ -989,6 +1024,7 @@ export interface ProfileFieldRefs {
readonly firstName: Prisma.FieldRef<"Profile", 'String'>
readonly lastName: Prisma.FieldRef<"Profile", 'String'>
readonly phoneNumber: Prisma.FieldRef<"Profile", 'String'>
readonly ghlContactId: Prisma.FieldRef<"Profile", 'String'>
}
@@ -1185,6 +1221,11 @@ export type ProfileFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Profiles.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Profiles.
*/
distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
}
+6 -1
View File
@@ -179,7 +179,7 @@ export type SessionGroupByOutputType = {
_max: SessionMaxAggregateOutputType | null
}
type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise<
export type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SessionGroupByOutputType, T['by']> &
{
@@ -1227,6 +1227,11 @@ export type SessionFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Sessions.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Sessions.
*/
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
}
@@ -182,7 +182,7 @@ export type SolfeggioFrequencyGroupByOutputType = {
_max: SolfeggioFrequencyMaxAggregateOutputType | null
}
type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise<
export type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SolfeggioFrequencyGroupByOutputType, T['by']> &
{
@@ -203,7 +203,7 @@ export type SolfeggioFrequencyWhereInput = {
NOT?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[]
id?: Prisma.IntFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatFilter<"SolfeggioFrequency"> | number
value?: Prisma.IntFilter<"SolfeggioFrequency"> | number
proclamations?: Prisma.ProclamationListRelationFilter
}
@@ -241,7 +241,7 @@ export type SolfeggioFrequencyScalarWhereWithAggregatesInput = {
NOT?: Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput | Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringWithAggregatesFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatWithAggregatesFilter<"SolfeggioFrequency"> | number
value?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
}
export type SolfeggioFrequencyCreateInput = {
@@ -259,14 +259,14 @@ export type SolfeggioFrequencyUncheckedCreateInput = {
export type SolfeggioFrequencyUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUpdateManyWithoutSolfeggioFrequencyNestedInput
}
export type SolfeggioFrequencyUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput
}
@@ -278,13 +278,13 @@ export type SolfeggioFrequencyCreateManyInput = {
export type SolfeggioFrequencyUpdateManyMutationInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyScalarRelationFilter = {
@@ -363,13 +363,13 @@ export type SolfeggioFrequencyUpdateToOneWithWhereWithoutProclamationsInput = {
export type SolfeggioFrequencyUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -872,7 +872,7 @@ export interface Prisma__SolfeggioFrequencyClient<T, Null = never, ExtArgs exten
export interface SolfeggioFrequencyFieldRefs {
readonly id: Prisma.FieldRef<"SolfeggioFrequency", 'Int'>
readonly name: Prisma.FieldRef<"SolfeggioFrequency", 'String'>
readonly value: Prisma.FieldRef<"SolfeggioFrequency", 'Float'>
readonly value: Prisma.FieldRef<"SolfeggioFrequency", 'Int'>
}
@@ -1069,6 +1069,11 @@ export type SolfeggioFrequencyFindManyArgs<ExtArgs extends runtime.Types.Extensi
* Skip the first `n` SolfeggioFrequencies.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of SolfeggioFrequencies.
*/
distinct?: Prisma.SolfeggioFrequencyScalarFieldEnum | Prisma.SolfeggioFrequencyScalarFieldEnum[]
}
@@ -178,7 +178,7 @@ export type SubscriptionLevelGroupByOutputType = {
_max: SubscriptionLevelMaxAggregateOutputType | null
}
type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise<
export type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SubscriptionLevelGroupByOutputType, T['by']> &
{
@@ -1063,6 +1063,11 @@ export type SubscriptionLevelFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` SubscriptionLevels.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of SubscriptionLevels.
*/
distinct?: Prisma.SubscriptionLevelScalarFieldEnum | Prisma.SubscriptionLevelScalarFieldEnum[]
}
+6 -1
View File
@@ -213,7 +213,7 @@ export type UserGroupByOutputType = {
_max: UserMaxAggregateOutputType | null
}
type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise<
export type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserGroupByOutputType, T['by']> &
{
@@ -1883,6 +1883,11 @@ export type UserFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
* Skip the first `n` Users.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Users.
*/
distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[]
}
@@ -151,7 +151,7 @@ export type UserProclamationsGroupByOutputType = {
_max: UserProclamationsMaxAggregateOutputType | null
}
type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise<
export type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserProclamationsGroupByOutputType, T['by']> &
{
@@ -1207,6 +1207,11 @@ export type UserProclamationsFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` UserProclamations.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of UserProclamations.
*/
distinct?: Prisma.UserProclamationsScalarFieldEnum | Prisma.UserProclamationsScalarFieldEnum[]
}
+6 -1
View File
@@ -171,7 +171,7 @@ export type UserRoleGroupByOutputType = {
_max: UserRoleMaxAggregateOutputType | null
}
type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise<
export type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserRoleGroupByOutputType, T['by']> &
{
@@ -1041,6 +1041,11 @@ export type UserRoleFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Skip the first `n` UserRoles.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of UserRoles.
*/
distinct?: Prisma.UserRoleScalarFieldEnum | Prisma.UserRoleScalarFieldEnum[]
}
@@ -165,7 +165,7 @@ export type VerificationGroupByOutputType = {
_max: VerificationMaxAggregateOutputType | null
}
type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise<
export type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<VerificationGroupByOutputType, T['by']> &
{
@@ -982,6 +982,11 @@ export type VerificationFindManyArgs<ExtArgs extends runtime.Types.Extensions.In
* Skip the first `n` Verifications.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Verifications.
*/
distinct?: Prisma.VerificationScalarFieldEnum | Prisma.VerificationScalarFieldEnum[]
}
+13 -9
View File
@@ -10,15 +10,15 @@
// ✔ Delete a card
// ✔ Issue a refund
import type { Decimal } from "@prisma/client/runtime/client";
import type { Decimal } from '@prisma/client/runtime/client';
export interface CreditDebitCard {
first_name: string,
last_name: string,
card_number: number,
expiration_month: number,
expiration_year: number,
security_code: number,
first_name: string;
last_name: string;
card_number: number;
expiration_month: number;
expiration_year: number;
security_code: number;
}
export async function chargeCreditDebitCard(card: CreditDebitCard, amount: Decimal): Promise<null> {
@@ -27,11 +27,15 @@ export async function chargeCreditDebitCard (card: CreditDebitCard, amount: Deci
* for the amount specified.
*/
console.log(`charged card ${card} for $${amount}`);
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
return null;
}
export async function chargedStoredCard(cardNumber: number, profileID: string, amount: Decimal): Promise<null> {
export async function chargedStoredCard(
cardNumber: number,
profileID: string,
amount: Decimal
): Promise<null> {
/**
* chargedStoredCard attempts to request payment from a valid card,
* for the amount specified.
+1 -4
View File
@@ -1,10 +1,7 @@
import { BETTER_AUTH_URL } from '$env/static/private';
import { sendMail } from './resend';
export async function sendVerificationEmail(options: {
email: string;
token: string;
}) {
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`;
await sendMail({
+96 -75
View File
@@ -1,8 +1,8 @@
# svelte-admin
A config-driven admin panel framework for SvelteKit + Prisma. Define your models once in a
registry and get full CRUD UI with dynamic routing, role-based access control, searchable
dropdowns, and related-record panels — no per-model route files needed.
registry and get full CRUD UI with per-model static pages, role-based access control, searchable
tables, FK dropdowns, TipTap editor for rich text, and related-record panels.
## Architecture
@@ -13,27 +13,30 @@ src/lib/svelte-admin/ # Framework core
├── roles.ts # Role hierarchy and access helpers
├── prisma-delegate.ts # Dynamic Prisma delegate lookup
├── introspect.ts # Field label generation and dot-notation resolution
├── form-data.ts # FormData → typed object parser
├── form-data.ts # FormData -> typed object parser
├── crud.ts # CRUD action factory (load, upsert, delete)
└── index.ts # Barrel exports
src/routes/(protected)/admin/ # Dynamic admin routes
src/routes/(protected)/admin/ # Per-model admin pages (no dynamic [model] routing)
├── +layout.server.ts # Role-based access guard
├── +layout.svelte # Layout shell
├── +page.server.ts # Admin home (model grid)
├── +page.svelte # Admin home UI
├── [model]/
│ ├── +page.server.ts # List view + create action
│ ├── +page.svelte # List view UI
│ └── [id]/
│ ├── +page.server.ts # Edit view + update/delete actions
│ └── +page.svelte # Edit view UI
├── users/ # Each model gets its own page directory
├── profiles/
├── addresses/
├── user-roles/
├── proclamations/
├── solfeggio-frequencies/
├── proclamation-categories/
├── subscription-levels/
└── license-keys/ # listOnly — no create/edit modal
src/lib/components/admin/ # Reusable admin UI components
├── AdminTable.svelte # Sortable, searchable data table
├── AdminForm.svelte # Dynamic form with type-aware inputs
├── ComboBox.svelte # Searchable dropdown for FK fields
├── RelatedHasOne.svelte # Display panel for 1:1 related records
└── RelatedHasMany.svelte # Display table for 1:N related records
├── AdminModal.svelte # Modal overlay for CRUD forms
├── TipTap.svelte # Rich-text editor (in components/tiptap/)
```
## Adding a New Model
@@ -74,10 +77,11 @@ src/lib/components/admin/ # Reusable admin UI components
}
```
3. **Done.** Visit `/admin/widgets`. The dynamic `[model]` routes handle list, create, edit,
and delete automatically.
3. **Create the page directory** `src/routes/(protected)/admin/widgets/` with:
- `+page.server.ts` — load records + form actions using `crud.ts` helpers
- `+page.svelte` — render AdminTable + AdminModal with AdminForm
No new route files, no new components. The registry is the only thing you touch.
4. **Done.** Visit `/admin/widgets`. The page handles list, create, edit via modal.
## Configuration Reference
@@ -95,25 +99,26 @@ No new route files, no new components. The registry is the only thing you touch.
| `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. |
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table. |
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table and no modal. |
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to scalar types. Controls input rendering and FormData parsing. |
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as ComboBox dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. |
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as `<select>` dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages (display only via related records table). |
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
### PrismaScalarType
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime'`
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Tiptap'`
Controls both form input rendering and FormData parsing:
| Type | HTML Input | Parse Logic |
| ---------- | ----------------------------------- | ------------------------------ |
| ---------- | ------------------------------------- | ------------------------------ |
| `String` | `<input type="text">` | `raw.toString()` |
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty → `null` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty → `null` |
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` → `true` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` |
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty -> `null` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty -> `null` |
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` -> `true` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty -> `null` |
| `Tiptap` | TipTap rich-text editor (hidden input)| `raw.toString()` |
### RelationOptionConfig
@@ -142,14 +147,18 @@ Controls both form input rendering and FormData parsing:
The `fieldTypes` mapping is the central mechanism connecting Prisma scalar types to HTML inputs
and form data parsing. Every field in `formFields` must have a corresponding entry in `fieldTypes`.
When a field also has a `relationOptions` entry, the framework renders a `ComboBox` (searchable
dropdown) instead of the default input for that type.
When a field also has a `relationOptions` entry, the framework renders a `<select>` dropdown
instead of the default input for that type. The options are fetched server-side by `crud.ts`
and passed to the client as `relationOptionsData`.
For `Tiptap` fields, the TipTap rich-text editor is rendered. The editor content is stored in a
hidden input and submitted with the form.
## Relations
### Foreign Key Dropdowns (relationOptions)
When a form field is a foreign key, add a `relationOptions` entry to render a searchable ComboBox:
When a form field is a foreign key, add a `relationOptions` entry to render a `<select>` dropdown:
```ts
{
@@ -165,7 +174,8 @@ When a form field is a foreign key, add a `relationOptions` entry to render a se
}
```
The route loader automatically queries the related model and passes options to the ComboBox.
The `crud.ts` `loadRecords` function automatically queries the related model and passes options
to the page as `relationOptionsData`.
### Eager Loading (include)
@@ -200,9 +210,7 @@ relatedRecords: [
];
```
The `relationKey` must match a key in `include` so the data is actually loaded. `has-one` renders
a key-value panel via `RelatedHasOne`; `has-many` renders a table via `RelatedHasMany`. Both
support an optional `editSlug` to link to the related record's edit page.
The `relationKey` must match a key in `include` so the data is actually loaded.
## Access Control
@@ -228,23 +236,23 @@ Use `accessFilter` to restrict which records a viewer can see:
```ts
accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= ROLES.SUPER) return undefined; // no filter
if (viewerRoleId <= ROLES.SUPER) return undefined;
if (viewerRoleId <= ROLES.ADMIN) {
return { roleId: { gte: ROLES.STAFF } }; // can't see other admins
return { roleId: { gte: ROLES.STAFF } };
}
return { roleId: { gt: ROLES.STAFF } }; // staff sees only regular users
return { roleId: { gt: ROLES.STAFF } };
};
```
## How Dynamic Routing Works
## How Static Pages Work
1. User visits `/admin/proclamations` (or `/admin/proclamations/abc123`)
2. SvelteKit matches the `[model]` (and `[id]`) route params
3. `+page.server.ts` calls `getModelConfig(params.model)` to look up the config
4. `getDelegate(config)` returns `prisma[lcfirst(config.modelName)]` (e.g., `prisma.proclamation`)
5. The delegate runs `findMany` (list), `findUnique` (edit), `create`, `update`, or `delete`
6. Data + config are passed to the Svelte page component
7. Components (`AdminTable`, `AdminForm`, etc.) render based on config
1. User visits `/admin/proclamations`
2. SvelteKit matches the static route `/admin/proclamations/+page.server.ts`
3. `+page.server.ts` calls `getModelConfig('proclamations')` to look up the config
4. `loadRecords(config)` calls `prisma.proclamation.findMany()` with the config's `include` and `accessFilter`
5. For each FK field with a `relationOptions`, it also queries the related model for dropdown options
6. Data + config are passed to `+page.svelte`
7. The page renders `AdminTable` with records and `AdminModal` + `AdminForm` for create/edit
## Component API
@@ -255,11 +263,13 @@ accessFilter: (viewerRoleId: number) => {
| `records` | `Record<string, unknown>[]` | Data rows |
| `listFields` | `string[]` | Column keys (dot-notation supported) |
| `searchFields` | `string[]` | Fields for client-side search |
| `modelSlug` | `string` | Used for building edit links |
| `listOnly` | `boolean?` | Suppress edit links |
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for cell formatting |
| `singular` | `string` | Singular label for UI text |
| `onRowClick` | `(record) => void?` | Click handler for edit modal |
| `onNew` | `() => void?` | Click handler for create modal |
Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
decimals), filtered count display.
decimals), empty state.
### AdminForm
@@ -270,46 +280,31 @@ decimals), filtered count display.
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
| `form` | `ActionData?` | Carries validation errors |
| `action` | `string` | Form action (e.g., `'?/create'`) |
| `readonlyFields` | `string[]?` | Read-only field names |
| `action` | `string` | Form action (e.g., `'?/upsert'`) |
| `submitLabel` | `string?` | Button text |
### ComboBox
### AdminModal
| Prop | Type | Description |
| ------------- | -------------------- | ------------------ |
| `id` | `string` | Element ID |
| `name` | `string` | Form field name |
| `options` | `{ label; value }[]` | Dropdown items |
| `placeholder` | `string?` | Placeholder text |
| `value` | `string \| number?` | Pre-selected value |
| --------- | ----------------- | ------------------------ |
| `open` | `boolean` | Show/hide modal |
| `title` | `string` | Modal heading |
| `onClose` | `() => void` | Close handler |
| `children`| `Snippet` | Modal body content |
Features: type-to-search filtering, keyboard navigation (ArrowUp/Down, Enter, Escape),
clear button, scroll-to-highlight.
### RelatedHasOne
### TipTap
| Prop | Type | Description |
| --------------- | --------------------------------- | ------------------------------------ |
| `label` | `string` | Section heading |
| `record` | `Record<string, unknown> \| null` | Related record |
| `displayFields` | `string[]` | Fields to display as key-value pairs |
| `editSlug` | `string?` | Links to edit the related record |
### RelatedHasMany
| Prop | Type | Description |
| --------------- | --------------------------- | ----------------------------- |
| `label` | `string` | Section heading (shows count) |
| `records` | `Record<string, unknown>[]` | Related records |
| `displayFields` | `string[]` | Table columns |
| `editSlug` | `string?` | Per-row edit links |
| ---------------- | -------- | --------------------------------- |
| `initialContent` | `string` | Initial HTML content |
| `name` | `string` | Hidden input name for form submit |
## Utility Functions
| Function | Module | Description |
| ------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` → `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` -> `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
| `resolveFieldValue(record, path)` | `introspect.ts` | Resolves dot-notation paths like `'category.name'` on nested objects |
| `extractFormData(formData, fields, types)` | `form-data.ts` | Parses `FormData` with type-aware conversion per `PrismaScalarType` |
| `getDelegate(config)` | `prisma-delegate.ts` | Returns `prisma[lcfirst(modelName)]` delegate |
@@ -318,6 +313,32 @@ clear button, scroll-to-highlight.
| `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` |
| `loadRecords(locals, config)` | `crud.ts` | Fetches records + FK dropdown options for a model |
| `upsertRecord(formData, config, locals)` | `crud.ts` | Creates or updates a record based on presence of `id` in form data |
| `deleteRecord(formData, config, locals)` | `crud.ts` | Deletes a record by ID |
## Models in Admin
| Registry Slug | Prisma Model | Notes |
| -------------------------- | ---------------------- | ---------------------------------- |
| `users` | User | Role assignment, view-only fields |
| `profiles` | Profile | Linked to user FK |
| `addresses` | Address | Linked to profile FK |
| `user-roles` | UserRole | |
| `proclamations` | Proclamation | TipTap rich-text editor for content|
| `solfeggio-frequencies` | SolfeggioFrequency | |
| `proclamation-categories` | ProclamationCategory | Parent category hierarchy |
| `license-keys` | LicenseKey | listOnly — read-only table, no CRUD|
| `subscription-levels` | SubscriptionLevel | |
### Excluded from Admin
| Model | Reason |
| ------------------ | -------------------------------------------- |
| Session | Better Auth internal — not for manual editing|
| Account | Better Auth internal |
| Verification | Better Auth internal |
| UserProclamations | Composite key — managed via other models |
## Complete Example
@@ -364,7 +385,7 @@ A hypothetical `Product` model showing all configuration features:
],
accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= 2) return undefined;
return { active: true }; // staff only sees active products
return { active: true };
}
}
```
+124
View File
@@ -0,0 +1,124 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createLocals, mockUser } from '$lib/test-utils';
import { getModelConfig } from './registry';
vi.mock('$lib/db', () => ({
prisma: {
userRole: {
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn()
}
}
}));
import { prisma } from '$lib/db';
import { loadRecords, upsertRecord, deleteRecord } from './crud';
const findMany = prisma.userRole.findMany as ReturnType<typeof vi.fn>;
const create = prisma.userRole.create as ReturnType<typeof vi.fn>;
const update = prisma.userRole.update as ReturnType<typeof vi.fn>;
const deletes = prisma.userRole.delete as ReturnType<typeof vi.fn>;
const findUnique = prisma.userRole.findUnique as ReturnType<typeof vi.fn>;
const findFirst = prisma.userRole.findFirst as ReturnType<typeof vi.fn>;
const config = getModelConfig('user-roles')!;
describe('crud loadRecords', () => {
beforeEach(() => {
findMany.mockReset();
});
it('returns records', async () => {
findMany.mockResolvedValue([{ id: 1, name: 'Admin' }]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([{ id: 1, name: 'Admin' }]);
});
it('returns empty records array', async () => {
findMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([]);
});
});
describe('crud upsertRecord', () => {
beforeEach(() => {
create.mockReset();
update.mockReset();
findUnique.mockReset();
findFirst.mockReset();
});
it('creates a new record when no id', async () => {
create.mockResolvedValue({ id: 2, name: 'Editor' });
const fd = new FormData();
fd.set('name', 'Editor');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(create).toHaveBeenCalledWith({ data: { name: 'Editor' } });
expect(result).toEqual({ success: true });
});
it('updates an existing record when id is present', async () => {
findUnique.mockResolvedValue({ id: 1, name: 'Old Name' });
const fd = new FormData();
fd.set('id', '1');
fd.set('name', 'New Name');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { name: 'New Name' } });
expect(result).toEqual({ success: true });
});
it('returns error when updating non-existent record', async () => {
findUnique.mockResolvedValue(null);
const fd = new FormData();
fd.set('id', '999');
fd.set('name', 'Ghost');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Record not found' });
});
});
describe('crud deleteRecord', () => {
beforeEach(() => {
deletes.mockReset();
});
it('deletes a record by id', async () => {
deletes.mockResolvedValue({ id: 1, name: 'Admin' });
const fd = new FormData();
fd.set('id', '1');
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(deletes).toHaveBeenCalledWith({ where: { id: 1 } });
expect(result).toEqual({ success: true });
});
it('returns error when id is missing', async () => {
const fd = new FormData();
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Missing id' });
});
});
+202
View File
@@ -0,0 +1,202 @@
import { getDelegate, parseId } from './prisma-delegate';
import { extractFormData } from './form-data';
import type { AdminModelConfig } from './types';
import { superValidate } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import * as Schemas from './schemas';
import type { z } from 'zod';
const modelSchemas: Record<string, z.ZodObject<any>> = {
User: Schemas.User,
Profile: Schemas.Profile,
UserRole: Schemas.UserRole,
Proclamation: Schemas.Proclamation,
ProclamationCategory: Schemas.ProclamationCategory,
SolfeggioFrequency: Schemas.SolfeggioFrequency,
SubscriptionLevel: Schemas.SubscriptionLevel,
LicenseKey: Schemas.LicenseKey
};
export function superFormErrorsToFieldErrors(superErrors: Record<string, unknown>): {
error: string;
fieldErrors: Record<string, string>;
} {
const fieldErrors: Record<string, string> = {};
let formError = '';
for (const [key, value] of Object.entries(superErrors)) {
if (key === '_errors' && Array.isArray(value)) {
formError = (value[0] as string) ?? '';
} else if (
value &&
typeof value === 'object' &&
'_errors' in value &&
Array.isArray((value as Record<string, unknown>)._errors)
) {
const errMsg = ((value as Record<string, unknown>)._errors as string[])[0];
fieldErrors[key] = errMsg;
}
}
return { error: formError, fieldErrors };
}
export async function loadRecords(locals: App.Locals, config: AdminModelConfig) {
const delegate = getDelegate(config);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
const records = await delegate.findMany({
where,
include: config.include,
orderBy: { id: 'asc' }
});
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
// For cascade child fields: maps field -> { childValue -> parentValue }
const cascadeOptionParentMap: Record<string, Record<string | number, string | number>> = {};
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
const selectFields: Record<string, boolean> = {
[relConfig.valueField]: true,
[relConfig.labelField]: true
};
// Check if this field is a cascade child (needs parentId)
const cascadeEntry = Object.entries(config.cascadeFields ?? {}).find(
([, c]) => c.parentField === field
);
if (cascadeEntry) {
selectFields['parentId'] = true;
}
const options = await relDelegate.findMany({
select: selectFields,
orderBy: { [relConfig.labelField]: 'asc' }
});
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
label: String(opt[relConfig.labelField]),
value: opt[relConfig.valueField] as string | number
}));
relationOptionsData[field] = mapped;
// Build parent map if cascade child
if (cascadeEntry) {
const childField = cascadeEntry[0];
const parentMap: Record<string | number, string | number> = {};
const rows = options as Record<string, unknown>[];
for (const row of rows) {
const childVal = row[relConfig.valueField] as string | number;
const parentVal = row['parentId'] as number | null;
if (parentVal != null) {
parentMap[childVal] = parentVal;
}
}
cascadeOptionParentMap[childField] = parentMap;
}
}
return {
records: records as Record<string, unknown>[],
relationOptionsData,
cascadeOptionParentMap
};
}
export async function upsertRecord(
requestOrFormData: Request | FormData,
config: AdminModelConfig,
locals: App.Locals
) {
const schema = modelSchemas[config.modelName];
const form = schema ? await superValidate(requestOrFormData, zod4(schema)) : null;
if (form && !form.valid) {
const { error, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { ...form, success: false, error, fieldErrors };
}
const formData =
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
const rawId = formData.get('id')?.toString();
const data =
form?.data ??
(extractFormData(formData, config.formFields, config.fieldTypes) as Record<string, unknown>);
// Strip virtual/UI-only fields from Prisma payload
for (const f of config.dataOnlyFields ?? []) {
delete data[f];
}
const delegate = getDelegate(config);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
if (rawId) {
const id = parseId(config, rawId);
const existing = await delegate.findUnique({
where: { id },
...((config.include ? { include: config.include } : {}) as object)
});
if (!existing) {
return { success: false, error: 'Record not found' };
}
if (where) {
const match = await delegate.findFirst({
where: { id, ...where }
});
if (!match) {
return { success: false, error: 'Access denied' };
}
}
await delegate.update({
where: { id },
data
});
return { success: true };
} else {
try {
await delegate.create({ data });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
const meta = (e as { meta?: { target?: string[] } }).meta;
const fields = meta?.target ?? [];
return {
success: false,
error: `A record with this ${fields.join(', ')} already exists.`,
fieldErrors: Object.fromEntries(fields.map((f) => [f, 'Already taken']))
};
}
throw e;
}
return { success: true };
}
}
export async function deleteRecord(
requestOrFormData: Request | FormData,
config: AdminModelConfig,
locals: App.Locals
) {
const delegate = getDelegate(config);
const formData =
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' };
const id = parseId(config, rawId);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
if (where) {
const match = await delegate.findFirst({ where: { id, ...where } });
if (!match) return { success: false, error: 'Access denied' };
}
await delegate.delete({ where: { id } });
return { success: true };
}
+4
View File
@@ -31,6 +31,10 @@ export function extractFormData(
data[field] = val === '' ? null : new Date(val);
break;
}
case 'Tiptap': {
data[field] = raw?.toString() ?? '';
break;
}
default: {
data[field] = raw?.toString() ?? '';
break;
+74
View File
@@ -0,0 +1,74 @@
import { z } from 'zod';
const email = z.string().email('Invalid email address');
const phone = z
.string()
.min(10, 'Phone number must be at least 10 digits')
.regex(/^[\d\-\+\(\) ]+$/, 'Invalid phone number format');
const name = z.string().min(1, 'Required').max(100, 'Too long');
export const UserRole = z.object({
name: z.string().min(1, 'Required')
});
export const User = z.object({
name: z.string().min(1, 'Required'),
email,
roleId: z.coerce.number().int().min(1)
});
export const Profile = z.object({
userId: z.string().min(1, 'Required'),
firstName: name,
lastName: name,
phoneNumber: phone
});
export const Address = z.object({
profileId: z.string().min(1),
street: z.string().min(1, 'Street is required'),
city: z.string().min(1, 'City is required'),
state: z.string().min(1, 'State is required'),
postalCode: z.string().min(1, 'Postal code is required'),
country: z.string().min(1, 'Country is required'),
type: z.enum(['Mailing', 'Billing', 'Shipping']).default('Mailing'),
isDefaultMailing: z.coerce.boolean().default(false),
isDefaultBilling: z.coerce.boolean().default(false),
isDefaultShipping: z.coerce.boolean().default(false)
});
export const SolfeggioFrequency = z.object({
name: z.string().min(1, 'Required'),
value: z.coerce.number().int('Must be a whole number')
});
export const SubscriptionLevel = z.object({
name: z.string().min(1, 'Required'),
description: z.string().optional().default('')
});
export const ProclamationCategory = z.object({
name: z.string().min(1, 'Required'),
parentId: z.coerce.number().int().optional()
});
export const Proclamation = z.object({
name: z.string().min(1, 'Required'),
solfeggioFrequencyId: z.coerce.number().int(),
parentCategoryId: z.coerce.number().int(),
categoryId: z.coerce.number().int(),
subscriptionLevelId: z.coerce.number().int(),
content: z.string().optional().default('')
});
export const LicenseKey = z.object({
active: z.coerce.boolean(),
level: z.string().min(1, 'Required'),
userId: z.string().min(1, 'Required')
});
export const UserUpdate = z.object({
id: z.string().min(1),
name: z.string().min(1, 'Required').optional(),
email: z.string().email().optional()
});
+9
View File
@@ -19,6 +19,12 @@ export type RelatedRecordConfig = {
editSlug?: string;
};
export type ActionResult = {
success: boolean;
error?: string;
fieldErrors?: Record<string, string>;
};
export type AdminModelConfig = {
slug: string;
modelName: string;
@@ -35,4 +41,7 @@ export type AdminModelConfig = {
relationOptions?: Record<string, RelationOptionConfig>;
relatedRecords?: RelatedRecordConfig[];
accessFilter?: (viewerRoleId: number) => Record<string, unknown> | undefined;
/** Fields that should be excluded from Prisma create/update (virtual/UI-only) */
dataOnlyFields?: string[];
cascadeFields?: Record<string, { parentField: string }>;
};
@@ -37,12 +37,16 @@
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Profile updated successfully.
</div>
{/if}
{#if form?.action === 'updateProfile' && form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -115,12 +119,16 @@
</div>
{#if form?.action === 'addAddress' && form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Address added successfully.
</div>
{/if}
{#if form?.action === 'addAddress' && form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -133,7 +141,9 @@
>
<div class="flex flex-col gap-3">
<div>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Street</label>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Street</label
>
<input
type="text"
id="street"
@@ -144,7 +154,9 @@
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>City</label
>
<input
type="text"
id="city"
@@ -154,7 +166,9 @@
/>
</div>
<div>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300">State</label>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>State</label
>
<input
type="text"
id="state"
@@ -166,7 +180,10 @@
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="postalCode" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label
for="postalCode"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</label>
<input
@@ -178,7 +195,10 @@
/>
</div>
<div>
<label for="country" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
<label
for="country"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
>
<input
type="text"
id="country"
@@ -204,10 +224,13 @@
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3">
{#each data.user.profile.addresses as address}
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
<div
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{address.city}, {address.state} {address.postalCode}
{address.city}, {address.state}
{address.postalCode}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
</div>
@@ -168,9 +168,9 @@ describe('(protected)/account page server', () => {
country: 'US'
});
const err = await Promise.resolve(
invokeAction('addAddress', request, createLocals())
).catch((e: unknown) => e);
const err = await Promise.resolve(invokeAction('addAddress', request, createLocals())).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
@@ -201,9 +201,9 @@ describe('(protected)/account page server', () => {
});
const locals = createLocals(mockUser() as any);
const err = await Promise.resolve(
invokeAction('addAddress', request, locals)
).catch((e: unknown) => e);
const err = await Promise.resolve(invokeAction('addAddress', request, locals)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 400)).toBe(true);
});
@@ -1,4 +1,3 @@
<!--
There should be a form in the middle
rounded corners
@@ -44,17 +43,29 @@
<Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
<p class="mx-auto text-center my-4 italic">Please finish setting up your profile</p>
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
{#if data.user?.image}
<img src={data.user?.image} class="rounded-full w-[8rem] mx-auto" alt="Profile" />
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
{:else}
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
{/if}
<p class="mx-auto text-center my-4">{data.user?.email}</p>
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
<div class="flex flex-col">
<input type="text" name="userId" value={data.user?.id} class="hidden" />
<input type="text" name="firstName" placeholder="First Name" class="my-2" bind:value={firstName} />
<input type="text" name="lastName" placeholder="Last Name" class="my-2" bind:value={lastName} />
<input
type="text"
name="firstName"
placeholder="First Name"
class="my-2"
bind:value={firstName}
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
class="my-2"
bind:value={lastName}
/>
<input
type="tel"
name="phoneNumber"
@@ -64,6 +75,8 @@
oninput={handlePhoneInput}
/>
</div>
<button type="submit" class="p-2 px-4 bg-blue-300 dark:bg-gray-700 dark:text-gray-100">Submit</button>
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
>Submit</button
>
</form>
</Window>
@@ -33,9 +33,9 @@ describe('account setup page server load', () => {
});
it('throws 403 when user is not logged in', async () => {
const err = await Promise.resolve(
load({ locals: createLocals() } as any)
).catch((e: unknown) => e);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
@@ -5,7 +5,9 @@
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Welcome, {data.firstName}!</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
Welcome, {data.firstName}!
</h1>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<a
@@ -22,7 +24,9 @@
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.proclamationCount}</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
{data.proclamationCount}
</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
</a>
@@ -12,13 +12,19 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Key
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Level
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Status
</th>
</tr>
@@ -26,15 +32,17 @@
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
{#each data.licenseKeys as key}
<tr>
<td class="whitespace-nowrap px-6 py-4 font-mono text-sm text-gray-900 dark:text-gray-100">
<td
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
>
{key.key}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
{key.level}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
<td class="px-6 py-4 text-sm whitespace-nowrap">
<span
class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 {key.active
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
>
@@ -23,7 +23,7 @@ export const load: PageServerLoad = async ({ url }) => {
const [proclamations, categories] = await Promise.all([
prisma.proclamation.findMany({
where,
include: { solfeggioFrequency: true, category: { include: { parent: true } } },
include: { solfeggioFrequency: true, category: { include: { parent: true } }, subscriptionLevel: true },
orderBy: { name: 'asc' }
}),
prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } })
@@ -40,7 +40,9 @@
<!-- Filters -->
<div class="flex flex-wrap items-end gap-4">
<div>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Search</label
>
<input
type="text"
id="search"
@@ -51,7 +53,9 @@
/>
</div>
<div>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Category</label>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Category</label
>
<select
id="category"
bind:value={categoryId}
@@ -91,14 +95,16 @@
class="flex flex-col rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">{proclamation.name}</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{displayCategoryName(proclamation.category)}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{displayCategoryName(proclamation.category)}
</p>
<div class="mt-auto flex items-center justify-between pt-4">
<span class="text-sm text-purple-600 dark:text-gray-400">
{proclamation.solfeggioFrequency.value} Hz &mdash;
{proclamation.solfeggioFrequency.name}
</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
${proclamation.price.toFixed(2)}
<span class="text-xs text-gray-500 dark:text-gray-400">
{proclamation.subscriptionLevel?.name ?? 'Standard'}
</span>
</div>
</a>
@@ -14,7 +14,8 @@ export const load: PageServerLoad = async ({ params, parent }) => {
where: { id: params.id },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } }
category: { include: { parent: true } },
subscriptionLevel: true
}
});
@@ -19,27 +19,35 @@
</a>
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">{data.proclamation.name}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{data.proclamation.name}
</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{categoryBreadcrumb}</p>
</div>
<div class="flex flex-wrap gap-4">
<div class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800">
<div
class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-purple-600 dark:text-gray-400">Frequency</p>
<p class="text-sm font-semibold text-purple-900 dark:text-gray-100">
{data.proclamation.solfeggioFrequency.value} Hz &mdash;
{data.proclamation.solfeggioFrequency.name}
</p>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Price</p>
<div
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Level</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
${data.proclamation.price.toFixed(2)}
{data.proclamation.subscriptionLevel?.name ?? 'Standard'}
</p>
</div>
</div>
<div class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert">
<div
class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert"
>
{@html data.proclamation.content}
</div>
</div>
@@ -21,9 +21,9 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
it('throws 404 when proclamation is not found', async () => {
findUnique.mockResolvedValue(null);
const err = await Promise.resolve(
load({ params: { id: 'nonexistent' } } as any)
).catch((e: unknown) => e);
const err = await Promise.resolve(load({ params: { id: 'nonexistent' } } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 404)).toBe(true);
});
@@ -44,7 +44,8 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
where: { id: 'proc-1' },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } }
category: { include: { parent: true } },
subscriptionLevel: true
}
});
});
@@ -29,10 +29,12 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
const url = createUrl('/admin/proclamations');
const result = (await load({ url } as any)) as any;
expect(result.proclamations).toEqual(proclamations);
expect(result.categories[0]).toEqual(expect.objectContaining({
expect(result.categories[0]).toEqual(
expect.objectContaining({
...categories[0],
displayName: categories[0].name
}));
})
);
expect(result.search).toBe('');
expect(result.categoryId).toBeNull();
});
@@ -109,7 +111,7 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
include: { solfeggioFrequency: true, category: { include: { parent: true } } }
include: { solfeggioFrequency: true, category: { include: { parent: true } }, subscriptionLevel: true }
})
);
});
+1 -3
View File
@@ -48,9 +48,7 @@
{#each adminModels as model}
<a
href="/admin/{model.slug}"
class="rounded px-3 py-1 text-sm transition-colors {isActive(
'/admin/' + model.slug
)
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
@@ -18,7 +18,9 @@ export const load: LayoutServerLoad = async ({ locals }) => {
});
if (!user || !canAccessAdmin(user.roleId)) {
console.log(`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`);
console.log(
`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`
);
throw error(403, 'not authorized');
}
@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
export const load: PageServerLoad = async () => {
const models = getAllModelConfigs().filter((m) => m.slug !== 'license-keys');
return { models };
};
+21 -1
View File
@@ -1 +1,21 @@
<h1 class="p-6 dark:text-gray-100">Admin Dashboard</h1>
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let models = $derived(data.models);
</script>
<h1 class="p-6 text-xl font-bold text-gray-900 dark:text-gray-100">Admin Dashboard</h1>
<div class="grid grid-cols-1 gap-4 px-6 pb-6 sm:grid-cols-2 lg:grid-cols-3">
{#each models as model (model.slug)}
<a
href={'/admin/' + model.slug}
class="block border border-gray-200 p-4 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{model.displayName}</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{model.listFields.length} fields
</p>
</a>
{/each}
</div>
@@ -1,10 +0,0 @@
import type { PageServerLoad } from "./$types";
import { prisma } from "$lib/db";
export const load: PageServerLoad = async ({ locals }) => {
console.log(locals.user?.name);
// Load proclamation categories
const categories = await prisma.proclamationCategory.findMany();
console.log(categories);
return { categories };
}
@@ -1,41 +0,0 @@
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
function getCategoryNameFromId(id: number): string {
if (data.categories) {
const target = data.categories.find((category) => category.id === id);
return target ? target.name : '';
}
return '';
}
</script>
<!-- Display a list of categories -->
<div class="p-6 dark:text-gray-100">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="hidden">ID</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500 dark:text-gray-400">Name</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500 dark:text-gray-400">Parent Category</th>
</tr>
</thead>
<tbody>
{#if data.categories}
{#each data.categories as category, i (category.id)}
<tr class="border-b border-gray-200 hover:bg-gray-300 dark:border-gray-700 dark:hover:bg-gray-800">
<td class="hidden">{i}</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-100">{category.name}</td>
{#if category.parentId}
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">{getCategoryNameFromId(category.parentId)}</td>
{:else}
<td class="px-4 py-2"></td>
{/if}
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
@@ -0,0 +1,68 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords } from '$lib/svelte-admin/crud';
import { prisma } from '$lib/db';
import { sendMail } from '$lib/server/resend';
import { error } from '@sveltejs/kit';
const config = getModelConfig('license-keys')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
createKey: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const email = formData.get('email')?.toString();
const deactivateOld = formData.get('deactivateOld') === 'on';
if (!email) {
return { success: false, error: 'Email is required' };
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return { success: false, error: 'No user found with that email' };
}
// Deactivate old keys if requested
if (deactivateOld) {
await prisma.licenseKey.updateMany({
where: { userId: user.id, active: true },
data: { active: false }
});
}
// Generate 16-char hex UUID grouped as 4-4-4-4
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
const licenseKey = await prisma.licenseKey.create({
data: {
key,
active: true,
level: 'standard',
userId: user.id
}
});
// Send email with license key
try {
await sendMail({
to: email,
subject: 'Your License Key',
data: {
firstName: user.name ?? 'Valued Customer',
licenseKey: key
}
});
} catch (e) {
console.error('Failed to send license key email:', e);
}
return { success: true, error: undefined };
}
};
@@ -0,0 +1,100 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { enhance } from '$app/forms';
let { data, form }: PageProps = $props();
const config = getModelConfig('license-keys')!;
let showCreateForm = $state(false);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let createError = $state('');
$effect(() => {
if (form && 'success' in (form as ActionResult)) {
const result = form as ActionResult;
if (result.success) {
toastMessage = 'License key created and emailed';
toastType = 'success';
showCreateForm = false;
} else {
createError = result.error ?? 'Failed to create license key';
}
}
});
function toggleForm() {
showCreateForm = !showCreateForm;
createError = '';
}
</script>
<div class="p-6">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<button
type="button"
onclick={toggleForm}
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300"
>
{showCreateForm ? 'Cancel' : 'Create Key +'}
</button>
</div>
{#if showCreateForm}
<form method="POST" action="?/createKey" use:enhance class="mb-6 border border-gray-200 p-4 dark:border-gray-700">
<h2 class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-300">
Create New License Key
</h2>
{#if createError}
<div class="mb-3 border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{createError}
</div>
{/if}
<div class="flex flex-col gap-3">
<div>
<label for="email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
User Email
</label>
<input
type="email"
id="email"
name="email"
required
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
placeholder="user@example.com"
/>
</div>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="deactivateOld" class="h-4 w-4" />
Deactivate existing active keys for this user
</label>
<div class="flex justify-end">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Create & Email Key
</button>
</div>
</div>
</form>
{/if}
<AdminTable
records={data.records}
listFields={config.listFields}
searchFields={config.searchFields}
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
/>
</div>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
{/if}
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('proclamation-categories')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="proclamation-categories" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('proclamations')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="proclamations" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -1,8 +1,16 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
import {
loadRecords,
upsertRecord,
deleteRecord,
superFormErrorsToFieldErrors
} from '$lib/svelte-admin/crud';
import { prisma } from '$lib/db';
import { error } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import * as Schemas from '$lib/svelte-admin/schemas';
const config = getModelConfig('profiles')!;
@@ -12,67 +20,73 @@ export const load: PageServerLoad = async ({ locals }) => {
export const actions = {
upsert: async ({ request, locals }) => {
const formData = await request.formData();
return upsertRecord(formData, config, locals);
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
const formData = await request.formData();
return deleteRecord(formData, config, locals);
return deleteRecord(request, config, locals);
},
upsertAddress: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const id = formData.get('id')?.toString();
const profileId = formData.get('profileId')?.toString();
const form = await superValidate(request, zod4(Schemas.Address));
if (!form.valid) {
const { error: addrError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, addressError: addrError, addressFieldErrors: fieldErrors };
}
const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } =
form.data;
if (!profileId) {
return { success: false, addressError: 'Missing profile ID' };
}
const type = formData.get('type')?.toString() || 'Mailing';
const street = formData.get('street')?.toString() || '';
const city = formData.get('city')?.toString() || '';
const state = formData.get('state')?.toString() || '';
const postalCode = formData.get('postalCode')?.toString() || '';
const country = formData.get('country')?.toString() || '';
const isDefault = formData.get('isDefault') === 'on';
if (!street || !city || !state || !postalCode || !country) {
return {
success: false,
addressError: 'All address fields are required'
};
}
if (isDefault) {
// Unique-default enforcement: clear defaults of same type before setting
if (isDefaultMailing) {
await prisma.address.updateMany({
where: { profileId, type, isDefault: true },
data: { isDefault: false }
where: { profileId, isDefaultMailing: true },
data: { isDefaultMailing: false }
});
}
if (isDefaultBilling) {
await prisma.address.updateMany({
where: { profileId, isDefaultBilling: true },
data: { isDefaultBilling: false }
});
}
if (isDefaultShipping) {
await prisma.address.updateMany({
where: { profileId, isDefaultShipping: true },
data: { isDefaultShipping: false }
});
}
if (id) {
const updated = await prisma.address.update({
where: { id },
data: { type, street, city, state, postalCode, country, isDefault }
});
return { success: true, address: updated };
}
const payload = {
...addressData,
profileId,
isDefaultMailing,
isDefaultBilling,
isDefaultShipping
};
const created = await prisma.address.create({
data: { profileId, type, street, city, state, postalCode, country, isDefault }
});
const created = await prisma.address.create({ data: payload });
return { success: true, address: created };
},
updateUser: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const userId = formData.get('id')?.toString();
const email = formData.get('email')?.toString();
const name = formData.get('name')?.toString();
const form = await superValidate(request, zod4(Schemas.UserUpdate));
if (!form.valid) {
const { error: userErr, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, userError: userErr, userFieldErrors: fieldErrors };
}
if (!userId) {
const { id, email, name } = form.data;
if (!id) {
return { success: false, userError: 'Missing user ID' };
}
@@ -83,7 +97,7 @@ export const actions = {
return { success: false, userError: 'No fields to update' };
}
await prisma.user.update({ where: { id: userId }, data });
await prisma.user.update({ where: { id }, data });
return { success: true };
}
};
@@ -30,8 +30,9 @@
state: '',
postalCode: '',
country: '',
type: 'Mailing',
isDefault: 'false'
isDefaultMailing: 'false',
isDefaultBilling: 'false',
isDefaultShipping: 'false'
});
let userEmail = $state('');
@@ -44,11 +45,9 @@
} else if ((form as any).userError) {
userError = (form as any).userError;
} else if (form.success) {
toastMessage = (form as any).addressError
? 'Address saved'
: 'Saved successfully';
toastType = 'success';
if ((form as any).address) {
toastMessage = 'Address saved';
const addr = (form as any).address as Record<string, unknown>;
if (editingRecord) {
const addrs = (editingRecord.addresses as Record<string, unknown>[]) ?? [];
@@ -59,7 +58,8 @@
}
addressFormOpen = false;
addressError = '';
} else if (!(form as any).addressError && !(form as any).userError) {
} else {
toastMessage = 'Saved successfully';
closeModal();
}
} else if (!(form as any).addressError && !(form as any).userError) {
@@ -86,8 +86,8 @@
userFormOpen = false;
addressError = '';
userError = '';
userEmail = (record.user as Record<string, unknown>)?.email as string ?? '';
userName = (record.user as Record<string, unknown>)?.name as string ?? '';
userEmail = ((record.user as Record<string, unknown>)?.email as string) ?? '';
userName = ((record.user as Record<string, unknown>)?.name as string) ?? '';
modalOpen = true;
}
@@ -162,11 +162,16 @@
</button>
</div>
<p class="mb-1 text-sm text-gray-600 dark:text-gray-400">
{user?.name as string ?? ''} ({user?.email as string ?? ''})
{(user?.name as string) ?? ''} ({(user?.email as string) ?? ''})
</p>
{#if userFormOpen}
<form method="POST" action="?/updateUser" use:enhance class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700">
<form
method="POST"
action="?/updateUser"
use:enhance
class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<input type="hidden" name="id" value={String(user?.id ?? '')} />
{#if userError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
@@ -175,7 +180,10 @@
{/if}
<div class="flex flex-col gap-2">
<div>
<label for="user-name" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="user-name"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Name
</label>
<input
@@ -187,7 +195,10 @@
/>
</div>
<div>
<label for="user-email" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="user-email"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Email
</label>
<input
@@ -227,7 +238,12 @@
</div>
{#if addressFormOpen}
<form method="POST" action="?/upsertAddress" use:enhance class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700">
<form
method="POST"
action="?/upsertAddress"
use:enhance
class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<input type="hidden" name="profileId" value={String(editingRecord.id)} />
{#if addressError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
@@ -236,21 +252,10 @@
{/if}
<div class="grid grid-cols-2 gap-2">
<div>
<label for="addr-type" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
Type
</label>
<select
id="addr-type"
name="type"
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
<label
for="addr-street"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
<option value="Mailing">Mailing</option>
<option value="Billing">Billing</option>
<option value="Shipping">Shipping</option>
</select>
</div>
<div>
<label for="addr-street" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
Street
</label>
<input
@@ -262,7 +267,10 @@
/>
</div>
<div>
<label for="addr-city" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-city"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
City
</label>
<input
@@ -274,7 +282,10 @@
/>
</div>
<div>
<label for="addr-state" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-state"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
State
</label>
<input
@@ -286,7 +297,10 @@
/>
</div>
<div>
<label for="addr-postal" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-postal"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Postal Code
</label>
<input
@@ -298,7 +312,10 @@
/>
</div>
<div>
<label for="addr-country" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-country"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Country
</label>
<input
@@ -310,10 +327,20 @@
/>
</div>
</div>
<label class="mt-2 flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefault" class="h-3.5 w-3.5" />
Set as default for this type
<div class="mt-2 flex flex-wrap gap-4">
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultMailing" class="h-3.5 w-3.5" />
Default Mailing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultBilling" class="h-3.5 w-3.5" />
Default Billing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultShipping" class="h-3.5 w-3.5" />
Default Shipping
</label>
</div>
<div class="mt-2 flex justify-end">
<button
type="submit"
@@ -329,25 +356,30 @@
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th
>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">City</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">State</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Zip</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Country</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Default</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase"
>Country</th
>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">M</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">B</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">S</th>
</tr>
</thead>
<tbody>
{#each addresses as addr}
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="px-2 py-1 text-xs">{addr.type as string}</td>
<td class="px-2 py-1">{addr.street as string}</td>
<td class="px-2 py-1">{addr.city as string}</td>
<td class="px-2 py-1">{addr.state as string}</td>
<td class="px-2 py-1">{addr.postalCode as string}</td>
<td class="px-2 py-1">{addr.country as string}</td>
<td class="px-2 py-1">{addr.isDefault ? 'Yes' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultMailing ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultBilling ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultShipping ? '✓' : ''}</td>
</tr>
{/each}
</tbody>
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('solfeggio-frequencies')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="solfeggio-frequencies" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('subscription-levels')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="subscription-levels" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('user-roles')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="user-roles" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad, Actions } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('users')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions: Actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="users" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
+3 -1
View File
@@ -25,7 +25,9 @@ describe('(protected) layout server load', () => {
it('throws 403 when locals.user is undefined', async () => {
findUnique.mockResolvedValue(null);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch((e: unknown) => e);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
+4 -1
View File
@@ -7,7 +7,10 @@
{#if $page.status === 403}
<h1 class="dark:text-gray-100">Not Authorized</h1>
<p class="dark:text-gray-300">
You don't have permission to view this page. <a href="/" class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a>.
You don't have permission to view this page. <a
href="/"
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
>.
</p>
{/if}
</Window>
@@ -1,4 +1,4 @@
import type { RequestHandler } from "./$types";
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async () => {
// Request validation (400)
@@ -17,5 +17,5 @@ export const POST: RequestHandler = async () => {
// Success (200/201)
// 7. Send response
return new Response(JSON.stringify({ "msg": "success" }))
}
return new Response(JSON.stringify({ msg: 'success' }));
};
+3 -3
View File
@@ -2,6 +2,6 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => {
const categories = await prisma.proclamationCategory.findMany()
return new Response(JSON.stringify(categories))
}
const categories = await prisma.proclamationCategory.findMany();
return new Response(JSON.stringify(categories));
};
+3 -3
View File
@@ -2,6 +2,6 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => {
const frequencies = await prisma.solfeggioFrequency.findMany()
return new Response(JSON.stringify(frequencies))
}
const frequencies = await prisma.solfeggioFrequency.findMany();
return new Response(JSON.stringify(frequencies));
};
+10 -3
View File
@@ -9,13 +9,17 @@
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Forgot Password</h1>
{#if form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
If an account with that email exists, we've sent a password reset link.
</div>
{/if}
{#if form?.error && !form?.success}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -36,7 +40,10 @@
</button>
</form>
<a href={resolve('/login')} class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200">
<a
href={resolve('/login')}
class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Back to Login
</a>
</div>
+11 -7
View File
@@ -37,20 +37,24 @@
{#if error}
<span class="text-red-600 dark:text-gray-300">{error}</span>
{/if}
<form
onsubmit={handleSubmit}
class="my-6 flex flex-col items-center justify-center"
>
<form onsubmit={handleSubmit} class="my-6 flex flex-col items-center justify-center">
<p>Login</p>
<input type="email" placeholder="Email Address" bind:value={email} required class="my-2" />
<input type="password" placeholder="Password" bind:value={password} required class="my-2" />
<button class="mt-4 rounded bg-blue-200 px-4 py-2 dark:bg-gray-700 dark:text-gray-100" type="submit">Sign In</button>
<button
class="mt-4 rounded bg-blue-200 px-4 py-2 dark:bg-gray-700 dark:text-gray-100"
type="submit">Sign In</button
>
</form>
<div class="mt-2 flex flex-col items-center gap-2">
<a href="/forgot-password" class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
<a
href="/forgot-password"
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>Forgot Password?</a
>
<a href="/register" class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
<a
href="/register"
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>Don't have an account? Register</a
>
</div>
+13 -3
View File
@@ -23,7 +23,9 @@
{:else}
<div class="flex flex-col items-start justify-center">
{#if form?.error}
<div class="mb-4 w-full rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 w-full rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -32,8 +34,16 @@
<input type="text" name="lastName" placeholder="Last Name" required class="mb-2" />
<input type="email" name="email" placeholder="Email Address" required class="mb-2" />
<input type="password" name="password" placeholder="Password" required class="mb-2" />
<input type="password" name="password2" placeholder="Verify Password" required class="mb-2" />
<button type="submit" class="mx-auto rounded-full bg-blue-600 px-6 py-2 text-white dark:bg-gray-700 dark:text-gray-100"
<input
type="password"
name="password2"
placeholder="Verify Password"
required
class="mb-2"
/>
<button
type="submit"
class="mx-auto rounded-full bg-blue-600 px-6 py-2 text-white dark:bg-gray-700 dark:text-gray-100"
>Submit</button
>
</form>
+3 -1
View File
@@ -8,7 +8,9 @@
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Reset Password</h1>
{#if form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
+3 -10
View File
@@ -89,8 +89,7 @@ describe('reset-password page server action', () => {
resetPassword.mockReset();
});
const invokeDefault = (url: URL, request: Request) =>
actions.default({ request, url } as any);
const invokeDefault = (url: URL, request: Request) => actions.default({ request, url } as any);
it('returns fail(400) when token is missing', async () => {
const url = createUrl('/reset-password');
@@ -120,10 +119,7 @@ describe('reset-password page server action', () => {
it('returns fail(400) when password is empty', async () => {
const url = createUrl('/reset-password', { token: 'token-1' });
const result = await invokeDefault(
url,
createFormRequest({ password: '', password2: '' })
);
const result = await invokeDefault(url, createFormRequest({ password: '', password2: '' }));
expect(isActionFailure(result)).toBe(true);
if (isActionFailure(result)) {
expect(result.data).toEqual({ error: 'Password must be at least 8 characters.' });
@@ -181,10 +177,7 @@ describe('reset-password page server action', () => {
const url = createUrl('/reset-password', { token: 'token-1' });
const err = await Promise.resolve(
invokeDefault(
url,
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
)
invokeDefault(url, createFormRequest({ password: 'longenough1', password2: 'longenough1' }))
).catch((e: unknown) => e);
expect(isRedirect(err)).toBe(true);
if (isRedirect(err)) {