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 ## Quick Reference
### Environment Variables ### Environment Variables
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32` - `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`) - `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
Only define `baseURL`/`secret` in config if env vars are NOT set. Only define `baseURL`/`secret` in config if env vars are NOT set.
### File Location ### File Location
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path. CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
### CLI Commands ### CLI Commands
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter) - `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@latest generate` - Generate schema for Prisma/Drizzle
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools - `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
@@ -42,18 +45,18 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Core Config Options ## Core Config Options
| Option | Notes | | Option | Notes |
|--------|-------| | ------------------ | ---------------------------------------------- |
| `appName` | Optional display name | | `appName` | Optional display name |
| `baseURL` | Only if `BETTER_AUTH_URL` not set | | `baseURL` | Only if `BETTER_AUTH_URL` not set |
| `basePath` | Default `/api/auth`. Set `/` for root. | | `basePath` | Default `/api/auth`. Set `/` for root. |
| `secret` | Only if `BETTER_AUTH_SECRET` not set | | `secret` | Only if `BETTER_AUTH_SECRET` not set |
| `database` | Required for most features. See adapters docs. | | `database` | Required for most features. See adapters docs. |
| `secondaryStorage` | Redis/KV for sessions & rate limits | | `secondaryStorage` | Redis/KV for sessions & rate limits |
| `emailAndPassword` | `{ enabled: true }` to activate | | `emailAndPassword` | `{ enabled: true }` to activate |
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` | | `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
| `plugins` | Array of plugins | | `plugins` | Array of plugins |
| `trustedOrigins` | CSRF whitelist | | `trustedOrigins` | CSRF whitelist |
--- ---
@@ -70,11 +73,13 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Session Management ## Session Management
**Storage priority:** **Storage priority:**
1. If `secondaryStorage` defined → sessions go there (not DB) 1. If `secondaryStorage` defined → sessions go there (not DB)
2. Set `session.storeSessionInDatabase: true` to also persist to DB 2. Set `session.storeSessionInDatabase: true` to also persist to DB
3. No database + `cookieCache` → fully stateless mode 3. No database + `cookieCache` → fully stateless mode
**Cookie cache strategies:** **Cookie cache strategies:**
- `compact` (default) - Base64url + HMAC. Smallest. - `compact` (default) - Base64url + HMAC. Smallest.
- `jwt` - Standard JWT. Readable but signed. - `jwt` - Standard JWT. Readable but signed.
- `jwe` - Encrypted. Maximum security. - `jwe` - Encrypted. Maximum security.
@@ -104,6 +109,7 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Security ## Security
**In `advanced`:** **In `advanced`:**
- `useSecureCookies` - Force HTTPS cookies - `useSecureCookies` - Force HTTPS cookies
- `disableCSRFCheck` - ⚠️ Security risk - `disableCSRFCheck` - ⚠️ Security risk
- `disableOriginCheck` - ⚠️ Security risk - `disableOriginCheck` - ⚠️ Security risk
@@ -128,9 +134,11 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
## Plugins ## Plugins
**Import from dedicated paths for tree-shaking:** **Import from dedicated paths for tree-shaking:**
``` ```
import { twoFactor } from "better-auth/plugins/two-factor" import { twoFactor } from "better-auth/plugins/two-factor"
``` ```
NOT `from "better-auth/plugins"`. NOT `from "better-auth/plugins"`.
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`. **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 ### Configuring the Secret
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ 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: Better Auth looks for secrets in this order:
1. `options.secret` in your config 1. `options.secret` in your config
2. `BETTER_AUTH_SECRET` environment variable 2. `BETTER_AUTH_SECRET` environment variable
3. `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 ### Default Configuration
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
rateLimit: { rateLimit: {
enabled: true, // Default: true in production enabled: true, // Default: true in production
window: 10, // Time window in seconds (default: 10) 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 ### Configuration
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
advanced: { 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 ### Configuring Trusted Origins
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
baseURL: "https://api.example.com", baseURL: 'https://api.example.com',
trustedOrigins: [ trustedOrigins: ['https://app.example.com', 'https://admin.example.com']
"https://app.example.com",
"https://admin.example.com",
],
}); });
``` ```
@@ -128,10 +126,10 @@ The `baseURL` origin is automatically trusted. Also configurable via env: `BETTE
```ts ```ts
trustedOrigins: [ trustedOrigins: [
"*.example.com", // Matches any subdomain '*.example.com', // Matches any subdomain
"https://*.example.com", // Protocol-specific wildcard 'https://*.example.com', // Protocol-specific wildcard
"exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo) 'exp://192.168.*.*:*/*' // Custom schemes (e.g., Expo)
] ];
``` ```
### Dynamic Trusted Origins ### Dynamic Trusted Origins
@@ -140,10 +138,10 @@ Compute trusted origins based on the request:
```ts ```ts
trustedOrigins: async (request) => { trustedOrigins: async (request) => {
// Validate against database, header, etc. // Validate against database, header, etc.
const tenant = getTenantFromRequest(request); const tenant = getTenantFromRequest(request);
return [`https://${tenant}.myapp.com`]; return [`https://${tenant}.myapp.com`];
} };
``` ```
Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403. 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 ### Session Expiration
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
session: { session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default) 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 ### Custom Cookie Configuration
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
advanced: { advanced: {
useSecureCookies: true, // Force secure cookies useSecureCookies: true, // Force secure cookies
cookiePrefix: "myapp", // Custom prefix (default: "better-auth") cookiePrefix: 'myapp', // Custom prefix (default: "better-auth")
defaultCookieAttributes: { defaultCookieAttributes: {
sameSite: "strict", // Stricter CSRF protection sameSite: 'strict', // Stricter CSRF protection
path: "/auth", // Limit cookie scope 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 ### State Parameter Storage
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
account: { 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 ### IP Address Configuration
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
advanced: { advanced: {
ipAddress: { ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check ipAddressHeaders: ['x-forwarded-for', 'x-real-ip'], // Headers to check
disableIpTracking: false, // Keep enabled for rate limiting 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 ## Database Hooks for Security Auditing
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
databaseHooks: { databaseHooks: {
session: { session: {
create: { create: {
after: async ({ data, ctx }) => { after: async ({ data, ctx }) => {
await auditLog("session.created", { await auditLog('session.created', {
userId: data.userId, userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"), ip: ctx?.request?.headers.get('x-forwarded-for'),
userAgent: ctx?.request?.headers.get("user-agent"), userAgent: ctx?.request?.headers.get('user-agent')
}); });
}, }
}, },
delete: { delete: {
before: async ({ data }) => { before: async ({ data }) => {
await auditLog("session.revoked", { sessionId: data.id }); await auditLog('session.revoked', { sessionId: data.id });
}, }
}, }
}, },
user: { user: {
update: { update: {
after: async ({ data, oldData }) => { after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) { if (oldData?.email !== data.email) {
await auditLog("user.email_changed", { await auditLog('user.email_changed', {
userId: data.id, userId: data.id,
oldEmail: oldData?.email, oldEmail: oldData?.email,
newEmail: data.email, newEmail: data.email
}); });
} }
}, }
}, }
}, },
account: { account: {
create: { create: {
after: async ({ data }) => { after: async ({ data }) => {
await auditLog("account.linked", { await auditLog('account.linked', {
userId: data.userId, userId: data.userId,
provider: data.providerId, provider: data.providerId
}); });
}, }
}, }
}, }
}, }
}); });
``` ```
@@ -314,19 +312,19 @@ Return `false` from a `before` hook to prevent an operation.
## Background Tasks ## Background Tasks
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
advanced: { advanced: {
backgroundTasks: { backgroundTasks: {
handler: (promise) => { handler: (promise) => {
// Platform-specific handler // Platform-specific handler
// Vercel: waitUntil(promise) // Vercel: waitUntil(promise)
// Cloudflare: ctx.waitUntil(promise) // Cloudflare: ctx.waitUntil(promise)
waitUntil(promise); waitUntil(promise);
}, }
}, }
}, }
}); });
``` ```
@@ -339,80 +337,76 @@ Built-in: consistent response messages, dummy operations on invalid requests, ba
## Complete Security Configuration Example ## Complete Security Configuration Example
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
export const auth = betterAuth({ export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET, secret: process.env.BETTER_AUTH_SECRET,
baseURL: "https://api.example.com", baseURL: 'https://api.example.com',
trustedOrigins: [ trustedOrigins: ['https://app.example.com', 'https://*.preview.example.com'],
"https://app.example.com",
"https://*.preview.example.com",
],
// Rate limiting // Rate limiting
rateLimit: { rateLimit: {
enabled: true, enabled: true,
storage: "secondary-storage", storage: 'secondary-storage',
customRules: { customRules: {
"/api/auth/sign-in/email": { window: 60, max: 5 }, '/api/auth/sign-in/email': { window: 60, max: 5 },
"/api/auth/sign-up/email": { window: 60, max: 3 }, '/api/auth/sign-up/email': { window: 60, max: 3 }
}, }
}, },
// Session security // Session security
session: { session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 24 hours updateAge: 60 * 60 * 24, // 24 hours
freshAge: 60 * 60, // 1 hour for sensitive actions freshAge: 60 * 60, // 1 hour for sensitive actions
cookieCache: { cookieCache: {
enabled: true, enabled: true,
maxAge: 300, maxAge: 300,
strategy: "jwe", // Encrypted session data strategy: 'jwe' // Encrypted session data
}, }
}, },
// OAuth security // OAuth security
account: { account: {
encryptOAuthTokens: true, encryptOAuthTokens: true,
storeStateStrategy: "cookie", storeStateStrategy: 'cookie'
}, },
// Advanced settings
advanced: {
useSecureCookies: true,
cookiePrefix: 'myapp',
defaultCookieAttributes: {
sameSite: 'lax'
},
ipAddress: {
ipAddressHeaders: ['x-forwarded-for'],
ipv6Subnet: 64
},
backgroundTasks: {
handler: (promise) => waitUntil(promise)
}
},
// Advanced settings // Security auditing
advanced: { databaseHooks: {
useSecureCookies: true, session: {
cookiePrefix: "myapp", create: {
defaultCookieAttributes: { after: async ({ data, ctx }) => {
sameSite: "lax", console.log(`New session for user ${data.userId}`);
}, }
ipAddress: { }
ipAddressHeaders: ["x-forwarded-for"], },
ipv6Subnet: 64, user: {
}, update: {
backgroundTasks: { after: async ({ data, oldData }) => {
handler: (promise) => waitUntil(promise), if (oldData?.email !== data.email) {
}, console.log(`Email changed for user ${data.id}`);
}, }
}
// Security auditing }
databaseHooks: { }
session: { }
create: {
after: async ({ data, ctx }) => {
console.log(`New session for user ${data.userId}`);
},
},
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
console.log(`Email changed for user ${data.id}`);
}
},
},
},
},
}); });
``` ```
+44 -38
View File
@@ -18,6 +18,7 @@ Before writing any code, gather requirements by scanning the project and asking
### Step 1: Scan the project ### Step 1: Scan the project
Analyze the codebase to auto-detect: Analyze the codebase to auto-detect:
- **Framework** — Look for `next.config`, `svelte.config`, `nuxt.config`, `astro.config`, `vite.config`, or Express/Hono entry files. - **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`). - **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. - **Existing auth** — Look for existing auth libraries (`next-auth`, `lucia`, `clerk`, `supabase/auth`, `firebase/auth`) in `package.json` or imports.
@@ -75,8 +76,9 @@ Use the `AskQuestion` tool to ask the user **all applicable questions in a singl
- `allow_multiple: true` - `allow_multiple: true`
10. **Auth UI style** (always ask) 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) - 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)
### Step 3: Summarize the plan ### Step 3: Summarize the plan
@@ -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` **Location:** `lib/auth.ts` or `src/lib/auth.ts`
**Minimal config needs:** **Minimal config needs:**
- `database` - Connection or adapter - `database` - Connection or adapter
- `emailAndPassword: { enabled: true }` - For email/password auth - `emailAndPassword: { enabled: true }` - For email/password auth
**Standard config adds:** **Standard config adds:**
- `socialProviders` - OAuth providers (google, github, etc.) - `socialProviders` - OAuth providers (google, github, etc.)
- `emailVerification.sendVerificationEmail` - Email verification handler - `emailVerification.sendVerificationEmail` - Email verification handler
- `emailAndPassword.sendResetPassword` - Password reset handler - `emailAndPassword.sendResetPassword` - Password reset handler
**Full config adds:** **Full config adds:**
- `plugins` - Array of feature plugins - `plugins` - Array of feature plugins
- `session` - Expiry, cookie cache settings - `session` - Expiry, cookie cache settings
- `account.accountLinking` - Multi-provider linking - `account.accountLinking` - Multi-provider linking
@@ -217,14 +222,14 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Route Handler Setup ## Route Handler Setup
| Framework | File | Handler | | Framework | File | Handler |
|-----------|------|---------| | ------------------ | -------------------------------- | ------------------------------------------------ |
| Next.js App Router | `app/api/auth/[...all]/route.ts` | `toNextJsHandler(auth)` → export `{ GET, POST }` | | 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 | | Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` | | Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` | | SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` |
| SolidStart | Route file | `solidStartHandler(auth)` | | SolidStart | Route file | `solidStartHandler(auth)` |
| Hono | Route file | `auth.handler(c.req.raw)` | | Hono | Route file | `auth.handler(c.req.raw)` |
**Next.js Server Components:** Add `nextCookies()` plugin to auth config. **Next.js Server Components:** Add `nextCookies()` plugin to auth config.
@@ -232,11 +237,11 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Database Migrations ## Database Migrations
| Adapter | Command | | Adapter | Command |
|---------|---------| | --------------- | -------------------------------------------------------------------------------------------------- |
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) | | 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` | | 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` | | Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
**Re-run after adding plugins.** **Re-run after adding plugins.**
@@ -244,28 +249,28 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Database Adapters ## Database Adapters
| Database | Setup | | Database | Setup |
|----------|-------| | ---------- | -------------------------------------------------------------------------------------- |
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly | | SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
| PostgreSQL | Pass `pg.Pool` instance directly | | PostgreSQL | Pass `pg.Pool` instance directly |
| MySQL | Pass `mysql2` pool directly | | MySQL | Pass `mysql2` pool directly |
| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` | | Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` |
| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` | | Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` |
| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` | | MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` |
--- ---
## Common Plugins ## Common Plugins
| Plugin | Server Import | Client Import | Purpose | | Plugin | Server Import | Client Import | Purpose |
|--------|---------------|---------------|---------| | -------------- | ---------------------- | -------------------- | ----------------- |
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP | | `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs | | `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
| `admin` | `better-auth/plugins` | `adminClient` | User management | | `admin` | `better-auth/plugins` | `adminClient` | User management |
| `bearer` | `better-auth/plugins` | - | API token auth | | `bearer` | `better-auth/plugins` | - | API token auth |
| `openAPI` | `better-auth/plugins` | - | API docs | | `openAPI` | `better-auth/plugins` | - | API docs |
| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn | | `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn |
| `sso` | `@better-auth/sso` | - | Enterprise SSO | | `sso` | `@better-auth/sso` | - | Enterprise SSO |
**Plugin pattern:** Server plugin + client plugin + run migrations. **Plugin pattern:** Server plugin + client plugin + run migrations.
@@ -274,6 +279,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Auth UI Implementation ## Auth UI Implementation
**Sign in flow:** **Sign in flow:**
1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })` 1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })`
2. Handle `error` in response 2. Handle `error` in response
3. Redirect on success 3. Redirect on success
@@ -302,13 +308,13 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
## Troubleshooting ## Troubleshooting
| Issue | Fix | | Issue | Fix |
|-------|-----| | ------------------------------- | ------------------------------------------------------------- |
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var | | "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
| "Invalid Origin" | Add domain to `trustedOrigins` | | "Invalid Origin" | Add domain to `trustedOrigins` |
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod | | Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
| OAuth callback errors | Verify redirect URIs in provider dashboard | | OAuth callback errors | Verify redirect URIs in provider dashboard |
| Type errors after adding plugin | Re-run CLI generate/migrate | | Type errors after adding plugin | Re-run CLI generate/migrate |
--- ---
@@ -18,19 +18,19 @@ description: Configure email verification, implement password reset flows, set p
Configure `emailVerification.sendVerificationEmail` to verify user email addresses. Configure `emailVerification.sendVerificationEmail` to verify user email addresses.
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { sendEmail } from "./email"; // your email sending function import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({ export const auth = betterAuth({
emailVerification: { emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => { sendVerificationEmail: async ({ user, url, token }, request) => {
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Verify your email address", subject: 'Verify your email address',
text: `Click the link to verify your email: ${url}`, text: `Click the link to verify your email: ${url}`
}); });
}, }
}, }
}); });
``` ```
@@ -42,9 +42,9 @@ For stricter security, enable `emailAndPassword.requireEmailVerification` to blo
```ts ```ts
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
requireEmailVerification: true, requireEmailVerification: true
}, }
}); });
``` ```
@@ -60,7 +60,7 @@ Always use absolute URLs (including the origin) for callback URLs in sign-up and
```ts ```ts
const { data, error } = await authClient.signUp.email({ 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,26 +69,26 @@ const { data, error } = await authClient.signUp.email({
Provide `sendResetPassword` in the email and password config to enable password resets. Provide `sendResetPassword` in the email and password config to enable password resets.
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { sendEmail } from "./email"; // your email sending function import { sendEmail } from './email'; // your email sending function
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
// Custom email sending function to send reset-password email // Custom email sending function to send reset-password email
sendResetPassword: async ({ user, url, token }, request) => { sendResetPassword: async ({ user, url, token }, request) => {
void sendEmail({ void sendEmail({
to: user.email, to: user.email,
subject: "Reset your password", subject: 'Reset your password',
text: `Click the link to reset your password: ${url}`, text: `Click the link to reset your password: ${url}`
}); });
}, },
// Optional event hook // Optional event hook
onPasswordReset: async ({ user }, request) => { onPasswordReset: async ({ user }, request) => {
// your logic here // your logic here
console.log(`Password for user ${user.email} has been reset.`); console.log(`Password for user ${user.email} has been reset.`);
}, }
}, }
}); });
``` ```
@@ -100,14 +100,14 @@ On serverless platforms, configure a background task handler:
```ts ```ts
export const auth = betterAuth({ export const auth = betterAuth({
advanced: { advanced: {
backgroundTasks: { backgroundTasks: {
handler: (promise) => { handler: (promise) => {
// Use platform-specific methods like waitUntil // Use platform-specific methods like waitUntil
waitUntil(promise); waitUntil(promise);
}, }
}, }
}, }
}); });
``` ```
@@ -117,10 +117,10 @@ Tokens expire after 1 hour by default. Configure with `resetPasswordTokenExpires
```ts ```ts
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
resetPasswordTokenExpiresIn: 60 * 30, // 30 minutes resetPasswordTokenExpiresIn: 60 * 30 // 30 minutes
}, }
}); });
``` ```
@@ -132,10 +132,10 @@ Enable `revokeSessionsOnPasswordReset` to invalidate all existing sessions on pa
```ts ```ts
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
revokeSessionsOnPasswordReset: true, revokeSessionsOnPasswordReset: true
}, }
}); });
``` ```
@@ -145,11 +145,11 @@ Password length limits (configurable):
```ts ```ts
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
minPasswordLength: 12, minPasswordLength: 12,
maxPasswordLength: 256, maxPasswordLength: 256
}, }
}); });
``` ```
@@ -159,10 +159,10 @@ Call `requestPasswordReset` to send the reset link. Triggers the `sendResetPassw
```ts ```ts
const data = await auth.api.requestPasswordReset({ const data = await auth.api.requestPasswordReset({
body: { body: {
email: "john.doe@example.com", // required email: 'john.doe@example.com', // required
redirectTo: "https://example.com/reset-password", redirectTo: 'https://example.com/reset-password'
}, }
}); });
``` ```
@@ -170,8 +170,8 @@ Or authClient:
```ts ```ts
const { data, error } = await authClient.requestPasswordReset({ const { data, error } = await authClient.requestPasswordReset({
email: "john.doe@example.com", // required email: 'john.doe@example.com', // required
redirectTo: "https://example.com/reset-password", redirectTo: 'https://example.com/reset-password'
}); });
``` ```
@@ -186,26 +186,25 @@ Default: `scrypt` (Node.js native, no external dependencies).
To use Argon2id or another algorithm, provide custom `hash` and `verify` functions: To use Argon2id or another algorithm, provide custom `hash` and `verify` functions:
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { hash, verify, type Options } from "@node-rs/argon2"; import { hash, verify, type Options } from '@node-rs/argon2';
const argon2Options: Options = { const argon2Options: Options = {
memoryCost: 65536, // 64 MiB memoryCost: 65536, // 64 MiB
timeCost: 3, // 3 iterations timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel lanes parallelism: 4, // 4 parallel lanes
outputLen: 32, // 32 byte output outputLen: 32, // 32 byte output
algorithm: 2, // Argon2id variant algorithm: 2 // Argon2id variant
}; };
export const auth = betterAuth({ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
password: { password: {
hash: (password) => hash(password, argon2Options), hash: (password) => hash(password, argon2Options),
verify: ({ password, hash: storedHash }) => verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options)
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 4. Verify: check that organization, member, invitation tables exist in your database
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { organization } from "better-auth/plugins"; import { organization } from 'better-auth/plugins';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
organization({ organization({
allowUserToCreateOrganization: true, allowUserToCreateOrganization: true,
organizationLimit: 5, // Max orgs per user organizationLimit: 5, // Max orgs per user
membershipLimit: 100, // Max members per org membershipLimit: 100 // Max members per org
}), })
], ]
}); });
``` ```
### Client-Side Setup ### Client-Side Setup
```ts ```ts
import { createAuthClient } from "better-auth/client"; import { createAuthClient } from 'better-auth/client';
import { organizationClient } from "better-auth/client/plugins"; import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({ export const authClient = createAuthClient({
plugins: [organizationClient()], plugins: [organizationClient()]
}); });
``` ```
@@ -42,12 +42,12 @@ The creator is automatically assigned the `owner` role.
```ts ```ts
const createOrg = async () => { const createOrg = async () => {
const { data, error } = await authClient.organization.create({ const { data, error } = await authClient.organization.create({
name: "My Company", name: 'My Company',
slug: "my-company", slug: 'my-company',
logo: "https://example.com/logo.png", logo: 'https://example.com/logo.png',
metadata: { plan: "pro" }, metadata: { plan: 'pro' }
}); });
}; };
``` ```
@@ -57,13 +57,13 @@ Restrict who can create organizations based on user attributes:
```ts ```ts
organization({ organization({
allowUserToCreateOrganization: async (user) => { allowUserToCreateOrganization: async (user) => {
return user.emailVerified === true; return user.emailVerified === true;
}, },
organizationLimit: async (user) => { organizationLimit: async (user) => {
// Premium users get more organizations // Premium users get more organizations
return user.plan === "premium" ? 20 : 3; return user.plan === 'premium' ? 20 : 3;
}, }
}); });
``` ```
@@ -73,26 +73,25 @@ Administrators can create organizations for other users (server-side only):
```ts ```ts
await auth.api.createOrganization({ await auth.api.createOrganization({
body: { body: {
name: "Client Organization", name: 'Client Organization',
slug: "client-org", slug: 'client-org',
userId: "user-id-who-will-be-owner", // `userId` is required userId: 'user-id-who-will-be-owner' // `userId` is required
}, }
}); });
``` ```
**Note**: The `userId` parameter cannot be used alongside session headers. **Note**: The `userId` parameter cannot be used alongside session headers.
## Active Organizations ## Active Organizations
Stored in the session and scopes subsequent API calls. Set after user selects one. Stored in the session and scopes subsequent API calls. Set after user selects one.
```ts ```ts
const setActive = async (organizationId: string) => { const setActive = async (organizationId: string) => {
const { data, error } = await authClient.organization.setActive({ const { data, error } = await authClient.organization.setActive({
organizationId, organizationId
}); });
}; };
``` ```
@@ -106,11 +105,11 @@ Use `getFullOrganization()` to retrieve the active org with all members, invitat
```ts ```ts
await auth.api.addMember({ await auth.api.addMember({
body: { body: {
userId: "user-id", userId: 'user-id',
role: "member", role: 'member',
organizationId: "org-id", organizationId: 'org-id'
}, }
}); });
``` ```
@@ -120,11 +119,11 @@ For client-side member additions, use the invitation system instead.
```ts ```ts
await auth.api.addMember({ await auth.api.addMember({
body: { body: {
userId: "user-id", userId: 'user-id',
role: ["admin", "moderator"], role: ['admin', 'moderator'],
organizationId: "org-id", organizationId: 'org-id'
}, }
}); });
``` ```
@@ -140,12 +139,12 @@ Use `updateMemberRole({ memberId, role })`.
```ts ```ts
organization({ organization({
membershipLimit: async (user, organization) => { membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") { if (organization.metadata?.plan === 'enterprise') {
return 1000; return 1000;
} }
return 50; return 50;
}, }
}); });
``` ```
@@ -154,29 +153,29 @@ organization({
### Setting Up Invitation Emails ### Setting Up Invitation Emails
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { organization } from "better-auth/plugins"; import { organization } from 'better-auth/plugins';
import { sendEmail } from "./email"; import { sendEmail } from './email';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
organization({ organization({
sendInvitationEmail: async (data) => { sendInvitationEmail: async (data) => {
const { email, organization, inviter, invitation } = data; const { email, organization, inviter, invitation } = data;
await sendEmail({ await sendEmail({
to: email, to: email,
subject: `Join ${organization.name}`, subject: `Join ${organization.name}`,
html: ` html: `
<p>${inviter.user.name} invited you to join ${organization.name}</p> <p>${inviter.user.name} invited you to join ${organization.name}</p>
<a href="https://yourapp.com/accept-invite?id=${invitation.id}"> <a href="https://yourapp.com/accept-invite?id=${invitation.id}">
Accept Invitation Accept Invitation
</a> </a>
`, `
}); });
}, }
}), })
], ]
}); });
``` ```
@@ -184,8 +183,8 @@ export const auth = betterAuth({
```ts ```ts
await authClient.organization.inviteMember({ await authClient.organization.inviteMember({
email: "newuser@example.com", email: 'newuser@example.com',
role: "member", role: 'member'
}); });
``` ```
@@ -193,9 +192,9 @@ await authClient.organization.inviteMember({
```ts ```ts
const { data } = await authClient.organization.getInvitationURL({ const { data } = await authClient.organization.getInvitationURL({
email: "newuser@example.com", email: 'newuser@example.com',
role: "member", role: 'member',
callbackURL: "https://yourapp.com/dashboard", callbackURL: 'https://yourapp.com/dashboard'
}); });
// Share data.url via any channel // Share data.url via any channel
@@ -207,9 +206,9 @@ This endpoint does not call `sendInvitationEmail` — handle delivery yourself.
```ts ```ts
organization({ organization({
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days (default: 48 hours) invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days (default: 48 hours)
invitationLimit: 100, // Max pending invitations per org invitationLimit: 100, // Max pending invitations per org
cancelPendingInvitationsOnReInvite: true, // Cancel old invites when re-inviting cancelPendingInvitationsOnReInvite: true // Cancel old invites when re-inviting
}); });
``` ```
@@ -221,11 +220,11 @@ Default roles: `owner` (full access), `admin` (manage members/invitations/settin
```ts ```ts
const { data } = await authClient.organization.hasPermission({ const { data } = await authClient.organization.hasPermission({
permission: "member:write", permission: 'member:write'
}); });
if (data?.hasPermission) { if (data?.hasPermission) {
// User can manage members // User can manage members
} }
``` ```
@@ -236,16 +235,16 @@ Use `checkRolePermission({ role, permissions })` for client-side UI rendering (s
### Enabling Teams ### Enabling Teams
```ts ```ts
import { organization } from "better-auth/plugins"; import { organization } from 'better-auth/plugins';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
organization({ organization({
teams: { teams: {
enabled: true enabled: true
} }
}), })
], ]
}); });
``` ```
@@ -253,7 +252,7 @@ export const auth = betterAuth({
```ts ```ts
const { data } = await authClient.organization.createTeam({ const { data } = await authClient.organization.createTeam({
name: "Engineering", name: 'Engineering'
}); });
``` ```
@@ -267,11 +266,11 @@ Set active team with `setActiveTeam({ teamId })`.
```ts ```ts
organization({ organization({
teams: { teams: {
maximumTeams: 20, // Max teams per org maximumTeams: 20, // Max teams per org
maximumMembersPerTeam: 50, // Max members per team maximumMembersPerTeam: 50, // Max members per team
allowRemovingAllTeams: false, // Prevent removing last team allowRemovingAllTeams: false // Prevent removing last team
} }
}); });
``` ```
@@ -280,17 +279,17 @@ organization({
### Enabling Dynamic Access Control ### Enabling Dynamic Access Control
```ts ```ts
import { organization } from "better-auth/plugins"; import { organization } from 'better-auth/plugins';
import { dynamicAccessControl } from "@better-auth/organization/addons"; import { dynamicAccessControl } from '@better-auth/organization/addons';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
organization({ organization({
dynamicAccessControl: { dynamicAccessControl: {
enabled: true enabled: true
} }
}), })
], ]
}); });
``` ```
@@ -298,11 +297,11 @@ export const auth = betterAuth({
```ts ```ts
await authClient.organization.createRole({ await authClient.organization.createRole({
role: "moderator", role: 'moderator',
permission: { permission: {
member: ["read"], member: ['read'],
invitation: ["read"], invitation: ['read']
}, }
}); });
``` ```
@@ -314,37 +313,37 @@ Execute custom logic at various points in the organization lifecycle:
```ts ```ts
organization({ organization({
hooks: { hooks: {
organization: { organization: {
beforeCreate: async ({ data, user }) => { beforeCreate: async ({ data, user }) => {
// Validate or modify data before creation // Validate or modify data before creation
return { return {
data: { data: {
...data, ...data,
metadata: { ...data.metadata, createdBy: user.id }, metadata: { ...data.metadata, createdBy: user.id }
}, }
}; };
}, },
afterCreate: async ({ organization, member }) => { afterCreate: async ({ organization, member }) => {
// Post-creation logic (e.g., send welcome email, create default resources) // Post-creation logic (e.g., send welcome email, create default resources)
await createDefaultResources(organization.id); await createDefaultResources(organization.id);
}, },
beforeDelete: async ({ organization }) => { beforeDelete: async ({ organization }) => {
// Cleanup before deletion // Cleanup before deletion
await archiveOrganizationData(organization.id); await archiveOrganizationData(organization.id);
}, }
}, },
member: { member: {
afterCreate: async ({ member, organization }) => { afterCreate: async ({ member, organization }) => {
await notifyAdmins(organization.id, `New member joined`); await notifyAdmins(organization.id, `New member joined`);
}, }
}, },
invitation: { invitation: {
afterCreate: async ({ invitation, organization, inviter }) => { afterCreate: async ({ invitation, organization, inviter }) => {
await logInvitation(invitation); await logInvitation(invitation);
}, }
}, }
}, }
}); });
``` ```
@@ -354,32 +353,32 @@ Customize table names, field names, and add additional fields:
```ts ```ts
organization({ organization({
schema: { schema: {
organization: { organization: {
modelName: "workspace", // Rename table modelName: 'workspace', // Rename table
fields: { fields: {
name: "workspaceName", // Rename fields name: 'workspaceName' // Rename fields
}, },
additionalFields: { additionalFields: {
billingId: { billingId: {
type: "string", type: 'string',
required: false, required: false
}, }
}, }
}, },
member: { member: {
additionalFields: { additionalFields: {
department: { department: {
type: "string", type: 'string',
required: false, required: false
}, },
title: { title: {
type: "string", type: 'string',
required: false, required: false
}, }
}, }
}, }
}, }
}); });
``` ```
@@ -396,8 +395,8 @@ Always ensure ownership transfer before removing the current owner:
```ts ```ts
// Transfer ownership first // Transfer ownership first
await authClient.organization.updateMemberRole({ await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id", memberId: 'new-owner-member-id',
role: "owner", role: 'owner'
}); });
// Then the previous owner can be demoted or removed // Then the previous owner can be demoted or removed
@@ -409,7 +408,7 @@ Deleting an organization removes all associated data (members, invitations, team
```ts ```ts
organization({ organization({
disableOrganizationDeletion: true, // Disable via config disableOrganizationDeletion: true // Disable via config
}); });
``` ```
@@ -417,15 +416,15 @@ Or implement soft delete via hooks:
```ts ```ts
organization({ organization({
hooks: { hooks: {
organization: { organization: {
beforeDelete: async ({ organization }) => { beforeDelete: async ({ organization }) => {
// Archive instead of delete // Archive instead of delete
await archiveOrganization(organization.id); await archiveOrganization(organization.id);
throw new Error("Organization archived, not deleted"); throw new Error('Organization archived, not deleted');
}, }
}, }
}, }
}); });
``` ```
@@ -438,42 +437,42 @@ organization({
## Complete Configuration Example ## Complete Configuration Example
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { organization } from "better-auth/plugins"; import { organization } from 'better-auth/plugins';
import { sendEmail } from "./email"; import { sendEmail } from './email';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
organization({ organization({
// Organization limits // Organization limits
allowUserToCreateOrganization: true, allowUserToCreateOrganization: true,
organizationLimit: 10, organizationLimit: 10,
membershipLimit: 100, membershipLimit: 100,
creatorRole: "owner", creatorRole: 'owner',
// Slugs // Slugs
defaultOrganizationIdField: "slug", defaultOrganizationIdField: 'slug',
// Invitations // Invitations
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days
invitationLimit: 50, invitationLimit: 50,
sendInvitationEmail: async (data) => { sendInvitationEmail: async (data) => {
await sendEmail({ await sendEmail({
to: data.email, to: data.email,
subject: `Join ${data.organization.name}`, 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>`
}); });
}, },
// Hooks // Hooks
hooks: { hooks: {
organization: { organization: {
afterCreate: async ({ organization }) => { afterCreate: async ({ organization }) => {
console.log(`Organization ${organization.name} created`); 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 4. Verify: check that `twoFactorSecret` column exists on user table
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { twoFactor } from "better-auth/plugins"; import { twoFactor } from 'better-auth/plugins';
export const auth = betterAuth({ export const auth = betterAuth({
appName: "My App", appName: 'My App',
plugins: [ plugins: [
twoFactor({ twoFactor({
issuer: "My App", issuer: 'My App'
}), })
], ]
}); });
``` ```
### Client-Side Setup ### Client-Side Setup
```ts ```ts
import { createAuthClient } from "better-auth/client"; import { createAuthClient } from 'better-auth/client';
import { twoFactorClient } from "better-auth/client/plugins"; import { twoFactorClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({ export const authClient = createAuthClient({
plugins: [ plugins: [
twoFactorClient({ twoFactorClient({
onTwoFactorRedirect() { onTwoFactorRedirect() {
window.location.href = "/2fa"; window.location.href = '/2fa';
}, }
}), })
], ]
}); });
``` ```
@@ -47,14 +47,14 @@ Requires password verification. Returns TOTP URI (for QR code) and backup codes.
```ts ```ts
const enable2FA = async (password: string) => { const enable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({ const { data, error } = await authClient.twoFactor.enable({
password, password
}); });
if (data) { if (data) {
// data.totpURI — generate a QR code from this // data.totpURI — generate a QR code from this
// data.backupCodes — display to user // data.backupCodes — display to user
} }
}; };
``` ```
@@ -65,10 +65,10 @@ const enable2FA = async (password: string) => {
### Displaying the QR Code ### Displaying the QR Code
```tsx ```tsx
import QRCode from "react-qr-code"; import QRCode from 'react-qr-code';
const TotpSetup = ({ totpURI }: { totpURI: string }) => { const TotpSetup = ({ totpURI }: { totpURI: string }) => {
return <QRCode value={totpURI} />; return <QRCode value={totpURI} />;
}; };
``` ```
@@ -78,10 +78,10 @@ Accepts codes from one period before/after current time:
```ts ```ts
const verifyTotp = async (code: string) => { const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({ const { data, error } = await authClient.twoFactor.verifyTotp({
code, code,
trustDevice: true, trustDevice: true
}); });
}; };
``` ```
@@ -89,10 +89,10 @@ const verifyTotp = async (code: string) => {
```ts ```ts
twoFactor({ twoFactor({
totpOptions: { totpOptions: {
digits: 6, // 6 or 8 digits (default: 6) 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,27 +101,27 @@ twoFactor({
### Configuring OTP Delivery ### Configuring OTP Delivery
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { twoFactor } from "better-auth/plugins"; import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from "./email"; import { sendEmail } from './email';
export const auth = betterAuth({ export const auth = betterAuth({
plugins: [ plugins: [
twoFactor({ twoFactor({
otpOptions: { otpOptions: {
sendOTP: async ({ user, otp }, ctx) => { sendOTP: async ({ user, otp }, ctx) => {
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Your verification code", subject: 'Your verification code',
text: `Your code is: ${otp}`, text: `Your code is: ${otp}`
}); });
}, },
period: 5, // Code validity in minutes (default: 3) period: 5, // Code validity in minutes (default: 3)
digits: 6, // Number of digits (default: 6) digits: 6, // Number of digits (default: 6)
allowedAttempts: 5, // Max verification attempts (default: 5) allowedAttempts: 5 // Max verification attempts (default: 5)
}, }
}), })
], ]
}); });
``` ```
@@ -135,9 +135,9 @@ Configure how OTP codes are stored in the database:
```ts ```ts
twoFactor({ twoFactor({
otpOptions: { otpOptions: {
storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed" storeOTP: 'encrypted' // Options: "plain", "encrypted", "hashed"
}, }
}); });
``` ```
@@ -145,12 +145,12 @@ For custom encryption:
```ts ```ts
twoFactor({ twoFactor({
otpOptions: { otpOptions: {
storeOTP: { storeOTP: {
encrypt: async (token) => myEncrypt(token), encrypt: async (token) => myEncrypt(token),
decrypt: async (token) => myDecrypt(token), decrypt: async (token) => myDecrypt(token)
}, }
}, }
}); });
``` ```
@@ -162,16 +162,16 @@ Generated automatically when 2FA is enabled. Each code is single-use.
```tsx ```tsx
const BackupCodes = ({ codes }: { codes: string[] }) => { const BackupCodes = ({ codes }: { codes: string[] }) => {
return ( return (
<div> <div>
<p>Save these codes in a secure location:</p> <p>Save these codes in a secure location:</p>
<ul> <ul>
{codes.map((code, i) => ( {codes.map((code, i) => (
<li key={i}>{code}</li> <li key={i}>{code}</li>
))} ))}
</ul> </ul>
</div> </div>
); );
}; };
``` ```
@@ -181,10 +181,10 @@ Invalidates all previous codes:
```ts ```ts
const regenerateBackupCodes = async (password: string) => { const regenerateBackupCodes = async (password: string) => {
const { data, error } = await authClient.twoFactor.generateBackupCodes({ const { data, error } = await authClient.twoFactor.generateBackupCodes({
password, password
}); });
// data.backupCodes contains the new codes // data.backupCodes contains the new codes
}; };
``` ```
@@ -192,10 +192,10 @@ const regenerateBackupCodes = async (password: string) => {
```ts ```ts
const verifyBackupCode = async (code: string) => { const verifyBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({ const { data, error } = await authClient.twoFactor.verifyBackupCode({
code, code,
trustDevice: true, trustDevice: true
}); });
}; };
``` ```
@@ -203,11 +203,11 @@ const verifyBackupCode = async (code: string) => {
```ts ```ts
twoFactor({ twoFactor({
backupCodeOptions: { backupCodeOptions: {
amount: 10, // Number of codes to generate (default: 10) amount: 10, // Number of codes to generate (default: 10)
length: 10, // Length of each code (default: 10) length: 10, // Length of each code (default: 10)
storeBackupCodes: "encrypted", // Options: "plain", "encrypted" storeBackupCodes: 'encrypted' // Options: "plain", "encrypted"
}, }
}); });
``` ```
@@ -225,16 +225,16 @@ Response includes `twoFactorRedirect: true` when 2FA is required:
```ts ```ts
const signIn = async (email: string, password: string) => { const signIn = async (email: string, password: string) => {
const { data, error } = await authClient.signIn.email( const { data, error } = await authClient.signIn.email(
{ email, password }, { email, password },
{ {
onSuccess(context) { onSuccess(context) {
if (context.data.twoFactorRedirect) { 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 ```ts
twoFactor({ twoFactor({
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default) twoFactorCookieMaxAge: 600 // 10 minutes in seconds (default)
}); });
``` ```
@@ -262,9 +262,9 @@ Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional at
```ts ```ts
twoFactor({ twoFactor({
otpOptions: { otpOptions: {
allowedAttempts: 5, // Max attempts per OTP code (default: 5) allowedAttempts: 5 // Max attempts per OTP code (default: 5)
}, }
}); });
``` ```
@@ -280,52 +280,52 @@ Requires password confirmation. Revokes trusted device records:
```ts ```ts
const disable2FA = async (password: string) => { const disable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.disable({ const { data, error } = await authClient.twoFactor.disable({
password, password
}); });
}; };
``` ```
## Complete Configuration Example ## Complete Configuration Example
```ts ```ts
import { betterAuth } from "better-auth"; import { betterAuth } from 'better-auth';
import { twoFactor } from "better-auth/plugins"; import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from "./email"; import { sendEmail } from './email';
export const auth = betterAuth({ export const auth = betterAuth({
appName: "My App", appName: 'My App',
plugins: [ plugins: [
twoFactor({ twoFactor({
// TOTP settings // TOTP settings
issuer: "My App", issuer: 'My App',
totpOptions: { totpOptions: {
digits: 6, digits: 6,
period: 30, period: 30
}, },
// OTP settings // OTP settings
otpOptions: { otpOptions: {
sendOTP: async ({ user, otp }) => { sendOTP: async ({ user, otp }) => {
await sendEmail({ await sendEmail({
to: user.email, to: user.email,
subject: "Your verification code", subject: 'Your verification code',
text: `Your code is: ${otp}`, text: `Your code is: ${otp}`
}); });
}, },
period: 5, period: 5,
allowedAttempts: 5, allowedAttempts: 5,
storeOTP: "encrypted", storeOTP: 'encrypted'
}, },
// Backup code settings // Backup code settings
backupCodeOptions: { backupCodeOptions: {
amount: 10, amount: 10,
length: 10, length: 10,
storeBackupCodes: "encrypted", storeBackupCodes: 'encrypted'
}, },
// Session settings // Session settings
twoFactorCookieMaxAge: 600, // 10 minutes 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", "trailingComma": "none",
"semi": true, "semi": true,
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",
+10 -10
View File
@@ -14,16 +14,16 @@ Requires `.env` with at least `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH
## Commands ## Commands
| npm run ... | What it does | | npm run ... | What it does |
|----------------|-------------| | -------------------------- | ------------------------------------------------ |
| `check` | `svelte-kit sync && svelte-check` (typecheck) | | `check` | `svelte-kit sync && svelte-check` (typecheck) |
| `lint` | `prettier --check . && eslint .` (order matters) | | `lint` | `prettier --check . && eslint .` (order matters) |
| `format` | `prettier --write .` | | `format` | `prettier --write .` |
| `test` | `vitest --run` (single run) | | `test` | `vitest --run` (single run) |
| `test:unit` | `vitest` (watch mode) | | `test:unit` | `vitest` (watch mode) |
| `migrate -- --name <name>` | Create a Prisma migration | | `migrate -- --name <name>` | Create a Prisma migration |
| `deploy` | Apply pending migrations | | `deploy` | Apply pending migrations |
| `build` | Production build (adapter-node) | | `build` | Production build (adapter-node) |
## Testing quirks ## Testing quirks
+20
View File
@@ -1 +1,21 @@
# Rhema Wave Technologies - User Portal # 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": "^5.45.6",
"svelte-check": "^4.3.4", "svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"tsx": "^4.23.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.48.1", "typescript-eslint": "^8.48.1",
"vite": "^7.2.6", "vite": "^7.2.6",
@@ -52,10 +53,15 @@
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.2.0",
"@tiptap/core": "^3.15.3", "@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", "@tiptap/starter-kit": "^3.15.3",
"better-auth": "^1.4.10", "better-auth": "^1.4.10",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"pg": "^8.16.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({ export default defineConfig({
schema: 'prisma/schema.prisma', schema: 'prisma/schema.prisma',
migrations: { migrations: {
path: 'prisma/migrations' path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts'
}, },
datasource: { datasource: {
url: process.env['DATABASE_URL'] 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;
+12 -10
View File
@@ -47,16 +47,18 @@ model Profile {
} }
model Address { model Address {
id String @id @default(uuid()) id String @id @default(uuid())
profileId String profileId String
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
street String street String
city String city String
state String state String
postalCode String postalCode String
country String country String
type String @default("Mailing") type String @default("Mailing")
isDefault Boolean @default(false) isDefaultMailing Boolean @default(false)
isDefaultBilling Boolean @default(false)
isDefaultShipping Boolean @default(false)
} }
model Session { 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());
+39 -39
View File
@@ -1,41 +1,41 @@
{ {
"version": 1, "version": 1,
"skills": { "skills": {
"better-auth-best-practices": { "better-auth-best-practices": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "better-auth/best-practices/SKILL.md", "skillPath": "better-auth/best-practices/SKILL.md",
"computedHash": "a4c830509e85557b59339d8d93a4e243e9e59c686e7678854d39230e12c2a6dc" "computedHash": "a4c830509e85557b59339d8d93a4e243e9e59c686e7678854d39230e12c2a6dc"
}, },
"better-auth-security-best-practices": { "better-auth-security-best-practices": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "security/SKILL.md", "skillPath": "security/SKILL.md",
"computedHash": "5717187bba9c22030c7174c74012f01c359f21e2ce87e0eb80e24244d8b8c915" "computedHash": "5717187bba9c22030c7174c74012f01c359f21e2ce87e0eb80e24244d8b8c915"
}, },
"create-auth-skill": { "create-auth-skill": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "better-auth/create-auth/SKILL.md", "skillPath": "better-auth/create-auth/SKILL.md",
"computedHash": "31c027d02dbf1faa37f58191f574b9443143042fb2b684fe32df53ab0270487c" "computedHash": "31c027d02dbf1faa37f58191f574b9443143042fb2b684fe32df53ab0270487c"
}, },
"email-and-password-best-practices": { "email-and-password-best-practices": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "better-auth/emailAndPassword/SKILL.md", "skillPath": "better-auth/emailAndPassword/SKILL.md",
"computedHash": "747d7f73bd9e2ec5317b8ef88dc70f97df716d00e3d02de4051feebaa48b062d" "computedHash": "747d7f73bd9e2ec5317b8ef88dc70f97df716d00e3d02de4051feebaa48b062d"
}, },
"organization-best-practices": { "organization-best-practices": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "better-auth/organization/SKILL.md", "skillPath": "better-auth/organization/SKILL.md",
"computedHash": "79a5a85b43d10e9fe37582b3506b051a33a991817b938f349099baa5ddba21aa" "computedHash": "79a5a85b43d10e9fe37582b3506b051a33a991817b938f349099baa5ddba21aa"
}, },
"two-factor-authentication-best-practices": { "two-factor-authentication-best-practices": {
"source": "better-auth/skills", "source": "better-auth/skills",
"sourceType": "github", "sourceType": "github",
"skillPath": "better-auth/twoFactor/SKILL.md", "skillPath": "better-auth/twoFactor/SKILL.md",
"computedHash": "7e297aaf887e11fdc03e52bdbf44974e161e8f7c393170ced7dc66cace4f6d46" "computedHash": "7e297aaf887e11fdc03e52bdbf44974e161e8f7c393170ced7dc66cace4f6d46"
} }
} }
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Session, User } from "better-auth"; import type { Session, User } from 'better-auth';
declare global { declare global {
namespace App { namespace App {
@@ -9,4 +9,4 @@ declare global {
} }
} }
export { }; export {};
+21 -20
View File
@@ -1,24 +1,25 @@
<!doctype html> <!doctype html>
<html lang="en"> <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)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
%sveltekit.head%
</head>
<head> <body class="m-0" data-sveltekit-preload-data="hover">
<meta charset="utf-8" /> <div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
<meta name="viewport" content="width=device-width, initial-scale=1" /> </body>
<script>
(function () {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
%sveltekit.head%
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="h-dvh w-dvw flex flex-col">%sveltekit.body%</div>
</body>
</html> </html>
+1 -3
View File
@@ -87,9 +87,7 @@ describe('hooks.server handle', () => {
const event = createMockEvent(); const event = createMockEvent();
await handle({ event, resolve: event.resolve }); await handle({ event, resolve: event.resolve });
expect(svelteKitHandler).toHaveBeenCalledWith( expect(svelteKitHandler).toHaveBeenCalledWith(expect.objectContaining({ building: false }));
expect.objectContaining({ building: false })
);
}); });
it('calls svelteKitHandler with auth instance', async () => { it('calls svelteKitHandler with auth instance', async () => {
+13 -13
View File
@@ -1,18 +1,18 @@
import { auth } from "$lib/auth"; import { auth } from '$lib/auth';
import { svelteKitHandler } from "better-auth/svelte-kit"; import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from "$app/environment"; import { building } from '$app/environment';
export async function handle({ event, resolve }) { export async function handle({ event, resolve }) {
// Fetch current session from Better Auth // Fetch current session from Better Auth
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: event.request.headers, headers: event.request.headers
}); });
// Make session and user available on server // Make session and user available on server
if (session) { if (session) {
event.locals.session = session.session; event.locals.session = session.session;
event.locals.user = session.user; event.locals.user = session.user;
} }
return svelteKitHandler({ event, resolve, auth, building }); return svelteKitHandler({ event, resolve, auth, building });
} }
+1 -1
View File
@@ -1,3 +1,3 @@
import { createAuthClient } from "better-auth/svelte" import { createAuthClient } from 'better-auth/svelte';
export const authClient = createAuthClient({}); export const authClient = createAuthClient({});
+1 -4
View File
@@ -46,9 +46,6 @@ export const auth = betterAuth({
window: 60, window: 60,
max: 100 max: 100
}, },
trustedOrigins: [ trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
'http://localhost:5173',
'https://portal.rhemawavetech.com'
],
plugins: [sveltekitCookies(getRequestEvent)] 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 * Type-safe database client for TypeScript
* @example * @example
* ``` * ```
* const prisma = new PrismaClient() * const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Users * // Fetch zero or more Users
* const users = await prisma.user.findMany() * const users = await prisma.user.findMany()
* ``` * ```
@@ -202,33 +202,6 @@ export type IntWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedIntFilter<$PrismaModel> _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> = { export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -430,20 +403,4 @@ export type NestedFloatFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatFilter<$PrismaModel> | number 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 * Prisma Client JS version: 7.8.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3 * Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/ */
export const prismaVersion: PrismaVersion = { export const prismaVersion: PrismaVersion = {
client: "7.2.0", client: "7.8.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3" engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
} }
/** /**
@@ -1436,7 +1436,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId', userId: 'userId',
firstName: 'firstName', firstName: 'firstName',
lastName: 'lastName', lastName: 'lastName',
phoneNumber: 'phoneNumber' phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const } as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum] export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -1449,7 +1450,11 @@ export const AddressScalarFieldEnum = {
city: 'city', city: 'city',
state: 'state', state: 'state',
postalCode: 'postalCode', postalCode: 'postalCode',
country: 'country' country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const } as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum] export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -1514,7 +1519,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content', content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId', categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId' subscriptionLevelId: 'subscriptionLevelId'
} as const } as const
@@ -1754,6 +1758,21 @@ export type PrismaClientOptions = ({
* ``` * ```
*/ */
comments?: runtime.SqlCommenterPlugin[] 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 = { export type GlobalOmitConfig = {
user?: Prisma.UserOmit user?: Prisma.UserOmit
@@ -72,12 +72,12 @@ export type ModelName = (typeof ModelName)[keyof typeof ModelName]
* Enums * Enums
*/ */
export const TransactionIsolationLevel = { export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted', ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted', ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead', RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable' Serializable: 'Serializable'
} as const } as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
@@ -101,7 +101,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId', userId: 'userId',
firstName: 'firstName', firstName: 'firstName',
lastName: 'lastName', lastName: 'lastName',
phoneNumber: 'phoneNumber' phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const } as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum] export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -114,7 +115,11 @@ export const AddressScalarFieldEnum = {
city: 'city', city: 'city',
state: 'state', state: 'state',
postalCode: 'postalCode', postalCode: 'postalCode',
country: 'country' country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const } as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum] export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -179,7 +184,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content', content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId', solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId', categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId' subscriptionLevelId: 'subscriptionLevelId'
} as const } as const
+6 -1
View File
@@ -214,7 +214,7 @@ export type AccountGroupByOutputType = {
_max: AccountMaxAggregateOutputType | null _max: AccountMaxAggregateOutputType | null
} }
type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise< export type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<AccountGroupByOutputType, T['by']> & Prisma.PickEnumerable<AccountGroupByOutputType, T['by']> &
{ {
@@ -1406,6 +1406,11 @@ export type AccountFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Accounts. * Skip the first `n` Accounts.
*/ */
skip?: number 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[] distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
} }
+147 -2
View File
@@ -32,6 +32,10 @@ export type AddressMinAggregateOutputType = {
state: string | null state: string | null
postalCode: string | null postalCode: string | null
country: string | null country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
} }
export type AddressMaxAggregateOutputType = { export type AddressMaxAggregateOutputType = {
@@ -42,6 +46,10 @@ export type AddressMaxAggregateOutputType = {
state: string | null state: string | null
postalCode: string | null postalCode: string | null
country: string | null country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
} }
export type AddressCountAggregateOutputType = { export type AddressCountAggregateOutputType = {
@@ -52,6 +60,10 @@ export type AddressCountAggregateOutputType = {
state: number state: number
postalCode: number postalCode: number
country: number country: number
type: number
isDefaultMailing: number
isDefaultBilling: number
isDefaultShipping: number
_all: number _all: number
} }
@@ -64,6 +76,10 @@ export type AddressMinAggregateInputType = {
state?: true state?: true
postalCode?: true postalCode?: true
country?: true country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
} }
export type AddressMaxAggregateInputType = { export type AddressMaxAggregateInputType = {
@@ -74,6 +90,10 @@ export type AddressMaxAggregateInputType = {
state?: true state?: true
postalCode?: true postalCode?: true
country?: true country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
} }
export type AddressCountAggregateInputType = { export type AddressCountAggregateInputType = {
@@ -84,6 +104,10 @@ export type AddressCountAggregateInputType = {
state?: true state?: true
postalCode?: true postalCode?: true
country?: true country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
_all?: true _all?: true
} }
@@ -167,12 +191,16 @@ export type AddressGroupByOutputType = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
_count: AddressCountAggregateOutputType | null _count: AddressCountAggregateOutputType | null
_min: AddressMinAggregateOutputType | null _min: AddressMinAggregateOutputType | null
_max: AddressMaxAggregateOutputType | null _max: AddressMaxAggregateOutputType | null
} }
type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise< export type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<AddressGroupByOutputType, T['by']> & Prisma.PickEnumerable<AddressGroupByOutputType, T['by']> &
{ {
@@ -198,6 +226,10 @@ export type AddressWhereInput = {
state?: Prisma.StringFilter<"Address"> | string state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string postalCode?: Prisma.StringFilter<"Address"> | string
country?: 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> profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
} }
@@ -209,6 +241,10 @@ export type AddressOrderByWithRelationInput = {
state?: Prisma.SortOrder state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
profile?: Prisma.ProfileOrderByWithRelationInput profile?: Prisma.ProfileOrderByWithRelationInput
} }
@@ -223,6 +259,10 @@ export type AddressWhereUniqueInput = Prisma.AtLeast<{
state?: Prisma.StringFilter<"Address"> | string state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string postalCode?: Prisma.StringFilter<"Address"> | string
country?: 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> profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
}, "id"> }, "id">
@@ -234,6 +274,10 @@ export type AddressOrderByWithAggregationInput = {
state?: Prisma.SortOrder state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
_count?: Prisma.AddressCountOrderByAggregateInput _count?: Prisma.AddressCountOrderByAggregateInput
_max?: Prisma.AddressMaxOrderByAggregateInput _max?: Prisma.AddressMaxOrderByAggregateInput
_min?: Prisma.AddressMinOrderByAggregateInput _min?: Prisma.AddressMinOrderByAggregateInput
@@ -250,6 +294,10 @@ export type AddressScalarWhereWithAggregatesInput = {
state?: Prisma.StringWithAggregatesFilter<"Address"> | string state?: Prisma.StringWithAggregatesFilter<"Address"> | string
postalCode?: Prisma.StringWithAggregatesFilter<"Address"> | string postalCode?: Prisma.StringWithAggregatesFilter<"Address"> | string
country?: 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 = { export type AddressCreateInput = {
@@ -259,6 +307,10 @@ export type AddressCreateInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile: Prisma.ProfileCreateNestedOneWithoutAddressesInput profile: Prisma.ProfileCreateNestedOneWithoutAddressesInput
} }
@@ -270,6 +322,10 @@ export type AddressUncheckedCreateInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
} }
export type AddressUpdateInput = { export type AddressUpdateInput = {
@@ -279,6 +335,10 @@ export type AddressUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 profile?: Prisma.ProfileUpdateOneRequiredWithoutAddressesNestedInput
} }
@@ -290,6 +350,10 @@ export type AddressUncheckedUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 = { export type AddressCreateManyInput = {
@@ -300,6 +364,10 @@ export type AddressCreateManyInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
} }
export type AddressUpdateManyMutationInput = { export type AddressUpdateManyMutationInput = {
@@ -309,6 +377,10 @@ export type AddressUpdateManyMutationInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 = { export type AddressUncheckedUpdateManyInput = {
@@ -319,6 +391,10 @@ export type AddressUncheckedUpdateManyInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 = { export type AddressListRelationFilter = {
@@ -339,6 +415,10 @@ export type AddressCountOrderByAggregateInput = {
state?: Prisma.SortOrder state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
} }
export type AddressMaxOrderByAggregateInput = { export type AddressMaxOrderByAggregateInput = {
@@ -349,6 +429,10 @@ export type AddressMaxOrderByAggregateInput = {
state?: Prisma.SortOrder state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
} }
export type AddressMinOrderByAggregateInput = { export type AddressMinOrderByAggregateInput = {
@@ -359,6 +443,10 @@ export type AddressMinOrderByAggregateInput = {
state?: Prisma.SortOrder state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
} }
export type AddressCreateNestedManyWithoutProfileInput = { export type AddressCreateNestedManyWithoutProfileInput = {
@@ -410,6 +498,10 @@ export type AddressCreateWithoutProfileInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
} }
export type AddressUncheckedCreateWithoutProfileInput = { export type AddressUncheckedCreateWithoutProfileInput = {
@@ -419,6 +511,10 @@ export type AddressUncheckedCreateWithoutProfileInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
} }
export type AddressCreateOrConnectWithoutProfileInput = { export type AddressCreateOrConnectWithoutProfileInput = {
@@ -458,6 +554,10 @@ export type AddressScalarWhereInput = {
state?: Prisma.StringFilter<"Address"> | string state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string postalCode?: Prisma.StringFilter<"Address"> | string
country?: 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 = { export type AddressCreateManyProfileInput = {
@@ -467,6 +567,10 @@ export type AddressCreateManyProfileInput = {
state: string state: string
postalCode: string postalCode: string
country: string country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
} }
export type AddressUpdateWithoutProfileInput = { export type AddressUpdateWithoutProfileInput = {
@@ -476,6 +580,10 @@ export type AddressUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 = { export type AddressUncheckedUpdateWithoutProfileInput = {
@@ -485,6 +593,10 @@ export type AddressUncheckedUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 = { export type AddressUncheckedUpdateManyWithoutProfileInput = {
@@ -494,6 +606,10 @@ export type AddressUncheckedUpdateManyWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: 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 state?: boolean
postalCode?: boolean postalCode?: boolean
country?: boolean country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs> profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]> }, ExtArgs["result"]["address"]>
@@ -517,6 +637,10 @@ export type AddressSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean state?: boolean
postalCode?: boolean postalCode?: boolean
country?: boolean country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs> profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]> }, ExtArgs["result"]["address"]>
@@ -528,6 +652,10 @@ export type AddressSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean state?: boolean
postalCode?: boolean postalCode?: boolean
country?: boolean country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs> profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]> }, ExtArgs["result"]["address"]>
@@ -539,9 +667,13 @@ export type AddressSelectScalar = {
state?: boolean state?: boolean
postalCode?: boolean postalCode?: boolean
country?: 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> = { export type AddressInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs> profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
} }
@@ -565,6 +697,10 @@ export type $AddressPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
state: string state: string
postalCode: string postalCode: string
country: string country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
}, ExtArgs["result"]["address"]> }, ExtArgs["result"]["address"]>
composites: {} composites: {}
} }
@@ -996,6 +1132,10 @@ export interface AddressFieldRefs {
readonly state: Prisma.FieldRef<"Address", 'String'> readonly state: Prisma.FieldRef<"Address", 'String'>
readonly postalCode: Prisma.FieldRef<"Address", 'String'> readonly postalCode: Prisma.FieldRef<"Address", 'String'>
readonly country: 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 the first `n` Addresses.
*/ */
skip?: number 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[] distinct?: Prisma.AddressScalarFieldEnum | Prisma.AddressScalarFieldEnum[]
} }
@@ -158,7 +158,7 @@ export type LicenseKeyGroupByOutputType = {
_max: LicenseKeyMaxAggregateOutputType | null _max: LicenseKeyMaxAggregateOutputType | null
} }
type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise< export type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<LicenseKeyGroupByOutputType, T['by']> & Prisma.PickEnumerable<LicenseKeyGroupByOutputType, T['by']> &
{ {
@@ -1122,6 +1122,11 @@ export type LicenseKeyFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Skip the first `n` LicenseKeys. * Skip the first `n` LicenseKeys.
*/ */
skip?: number 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[] distinct?: Prisma.LicenseKeyScalarFieldEnum | Prisma.LicenseKeyScalarFieldEnum[]
} }
@@ -29,14 +29,12 @@ export type AggregateProclamation = {
export type ProclamationAvgAggregateOutputType = { export type ProclamationAvgAggregateOutputType = {
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
price: number | null
subscriptionLevelId: number | null subscriptionLevelId: number | null
} }
export type ProclamationSumAggregateOutputType = { export type ProclamationSumAggregateOutputType = {
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
price: number | null
subscriptionLevelId: number | null subscriptionLevelId: number | null
} }
@@ -46,7 +44,6 @@ export type ProclamationMinAggregateOutputType = {
content: string | null content: string | null
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
price: number | null
subscriptionLevelId: number | null subscriptionLevelId: number | null
} }
@@ -56,7 +53,6 @@ export type ProclamationMaxAggregateOutputType = {
content: string | null content: string | null
solfeggioFrequencyId: number | null solfeggioFrequencyId: number | null
categoryId: number | null categoryId: number | null
price: number | null
subscriptionLevelId: number | null subscriptionLevelId: number | null
} }
@@ -66,7 +62,6 @@ export type ProclamationCountAggregateOutputType = {
content: number content: number
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price: number
subscriptionLevelId: number subscriptionLevelId: number
_all: number _all: number
} }
@@ -75,14 +70,12 @@ export type ProclamationCountAggregateOutputType = {
export type ProclamationAvgAggregateInputType = { export type ProclamationAvgAggregateInputType = {
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
price?: true
subscriptionLevelId?: true subscriptionLevelId?: true
} }
export type ProclamationSumAggregateInputType = { export type ProclamationSumAggregateInputType = {
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
price?: true
subscriptionLevelId?: true subscriptionLevelId?: true
} }
@@ -92,7 +85,6 @@ export type ProclamationMinAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
price?: true
subscriptionLevelId?: true subscriptionLevelId?: true
} }
@@ -102,7 +94,6 @@ export type ProclamationMaxAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
price?: true
subscriptionLevelId?: true subscriptionLevelId?: true
} }
@@ -112,7 +103,6 @@ export type ProclamationCountAggregateInputType = {
content?: true content?: true
solfeggioFrequencyId?: true solfeggioFrequencyId?: true
categoryId?: true categoryId?: true
price?: true
subscriptionLevelId?: true subscriptionLevelId?: true
_all?: true _all?: true
} }
@@ -209,7 +199,6 @@ export type ProclamationGroupByOutputType = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price: number
subscriptionLevelId: number subscriptionLevelId: number
_count: ProclamationCountAggregateOutputType | null _count: ProclamationCountAggregateOutputType | null
_avg: ProclamationAvgAggregateOutputType | null _avg: ProclamationAvgAggregateOutputType | null
@@ -218,7 +207,7 @@ export type ProclamationGroupByOutputType = {
_max: ProclamationMaxAggregateOutputType | null _max: ProclamationMaxAggregateOutputType | null
} }
type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise< export type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<ProclamationGroupByOutputType, T['by']> & Prisma.PickEnumerable<ProclamationGroupByOutputType, T['by']> &
{ {
@@ -242,7 +231,6 @@ export type ProclamationWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput> solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -256,7 +244,6 @@ export type ProclamationOrderByWithRelationInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
category?: Prisma.ProclamationCategoryOrderByWithRelationInput category?: Prisma.ProclamationCategoryOrderByWithRelationInput
@@ -273,7 +260,6 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
content?: Prisma.StringFilter<"Proclamation"> | string content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput> solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput> category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -287,7 +273,6 @@ export type ProclamationOrderByWithAggregationInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
_count?: Prisma.ProclamationCountOrderByAggregateInput _count?: Prisma.ProclamationCountOrderByAggregateInput
_avg?: Prisma.ProclamationAvgOrderByAggregateInput _avg?: Prisma.ProclamationAvgOrderByAggregateInput
@@ -305,7 +290,6 @@ export type ProclamationScalarWhereWithAggregatesInput = {
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
price?: Prisma.FloatWithAggregatesFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
} }
@@ -313,7 +297,6 @@ export type ProclamationCreateInput = {
id?: string id?: string
name: string name: string
content: string content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -326,7 +309,6 @@ export type ProclamationUncheckedCreateInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -335,7 +317,6 @@ export type ProclamationUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -348,7 +329,6 @@ export type ProclamationUncheckedUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -359,7 +339,6 @@ export type ProclamationCreateManyInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
} }
@@ -367,7 +346,6 @@ export type ProclamationUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
} }
export type ProclamationUncheckedUpdateManyInput = { export type ProclamationUncheckedUpdateManyInput = {
@@ -376,7 +354,6 @@ export type ProclamationUncheckedUpdateManyInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
} }
@@ -386,14 +363,12 @@ export type ProclamationCountOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
} }
export type ProclamationAvgOrderByAggregateInput = { export type ProclamationAvgOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
} }
@@ -403,7 +378,6 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
} }
@@ -413,14 +387,12 @@ export type ProclamationMinOrderByAggregateInput = {
content?: Prisma.SortOrder content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
} }
export type ProclamationSumOrderByAggregateInput = { export type ProclamationSumOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder subscriptionLevelId?: Prisma.SortOrder
} }
@@ -439,14 +411,6 @@ export type ProclamationOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder _count?: Prisma.SortOrder
} }
export type FloatFieldUpdateOperationsInput = {
set?: number
increment?: number
decrement?: number
multiply?: number
divide?: number
}
export type ProclamationCreateNestedOneWithoutUserProclamationsInput = { export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput> create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
@@ -591,7 +555,6 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
id?: string id?: string
name: string name: string
content: string content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -603,7 +566,6 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
} }
@@ -627,7 +589,6 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -639,7 +600,6 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
} }
@@ -647,7 +607,6 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
id?: string id?: string
name: string name: string
content: string content: string
price?: number
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -658,7 +617,6 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string name: string
content: string content: string
categoryId: number categoryId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -698,7 +656,6 @@ export type ProclamationScalarWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
} }
@@ -706,7 +663,6 @@ export type ProclamationCreateWithoutCategoryInput = {
id?: string id?: string
name: string name: string
content: string content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -717,7 +673,6 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string name: string
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -752,7 +707,6 @@ export type ProclamationCreateWithoutSubscriptionLevelInput = {
id?: string id?: string
name: string name: string
content: string content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -764,7 +718,6 @@ export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
} }
@@ -799,7 +752,6 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string name: string
content: string content: string
categoryId: number categoryId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
} }
@@ -807,7 +759,6 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -818,7 +769,6 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -828,7 +778,6 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
} }
@@ -837,7 +786,6 @@ export type ProclamationCreateManyCategoryInput = {
name: string name: string
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number subscriptionLevelId?: number
} }
@@ -845,7 +793,6 @@ export type ProclamationUpdateWithoutCategoryInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -856,7 +803,6 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -866,7 +812,6 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
} }
@@ -876,14 +821,12 @@ export type ProclamationCreateManySubscriptionLevelInput = {
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price?: number
} }
export type ProclamationUpdateWithoutSubscriptionLevelInput = { export type ProclamationUpdateWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -895,7 +838,6 @@ export type ProclamationUncheckedUpdateWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
} }
@@ -905,7 +847,6 @@ export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: 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 content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -960,7 +900,6 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -973,7 +912,6 @@ export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -986,11 +924,10 @@ export type ProclamationSelectScalar = {
content?: boolean content?: boolean
solfeggioFrequencyId?: boolean solfeggioFrequencyId?: boolean
categoryId?: boolean categoryId?: boolean
price?: boolean
subscriptionLevelId?: 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> = { export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs> solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs> category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -1023,7 +960,6 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
content: string content: string
solfeggioFrequencyId: number solfeggioFrequencyId: number
categoryId: number categoryId: number
price: number
subscriptionLevelId: number subscriptionLevelId: number
}, ExtArgs["result"]["proclamation"]> }, ExtArgs["result"]["proclamation"]>
composites: {} composites: {}
@@ -1457,7 +1393,6 @@ export interface ProclamationFieldRefs {
readonly content: Prisma.FieldRef<"Proclamation", 'String'> readonly content: Prisma.FieldRef<"Proclamation", 'String'>
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'> readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'> readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly price: Prisma.FieldRef<"Proclamation", 'Float'>
readonly subscriptionLevelId: Prisma.FieldRef<"Proclamation", 'Int'> 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 the first `n` Proclamations.
*/ */
skip?: number 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[] distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
} }
@@ -182,7 +182,7 @@ export type ProclamationCategoryGroupByOutputType = {
_max: ProclamationCategoryMaxAggregateOutputType | null _max: ProclamationCategoryMaxAggregateOutputType | null
} }
type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise< export type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<ProclamationCategoryGroupByOutputType, T['by']> & Prisma.PickEnumerable<ProclamationCategoryGroupByOutputType, T['by']> &
{ {
@@ -1297,6 +1297,11 @@ export type ProclamationCategoryFindManyArgs<ExtArgs extends runtime.Types.Exten
* Skip the first `n` ProclamationCategories. * Skip the first `n` ProclamationCategories.
*/ */
skip?: number 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[] distinct?: Prisma.ProclamationCategoryScalarFieldEnum | Prisma.ProclamationCategoryScalarFieldEnum[]
} }
+43 -2
View File
@@ -30,6 +30,7 @@ export type ProfileMinAggregateOutputType = {
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
phoneNumber: string | null phoneNumber: string | null
ghlContactId: string | null
} }
export type ProfileMaxAggregateOutputType = { export type ProfileMaxAggregateOutputType = {
@@ -38,6 +39,7 @@ export type ProfileMaxAggregateOutputType = {
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
phoneNumber: string | null phoneNumber: string | null
ghlContactId: string | null
} }
export type ProfileCountAggregateOutputType = { export type ProfileCountAggregateOutputType = {
@@ -46,6 +48,7 @@ export type ProfileCountAggregateOutputType = {
firstName: number firstName: number
lastName: number lastName: number
phoneNumber: number phoneNumber: number
ghlContactId: number
_all: number _all: number
} }
@@ -56,6 +59,7 @@ export type ProfileMinAggregateInputType = {
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
ghlContactId?: true
} }
export type ProfileMaxAggregateInputType = { export type ProfileMaxAggregateInputType = {
@@ -64,6 +68,7 @@ export type ProfileMaxAggregateInputType = {
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
ghlContactId?: true
} }
export type ProfileCountAggregateInputType = { export type ProfileCountAggregateInputType = {
@@ -72,6 +77,7 @@ export type ProfileCountAggregateInputType = {
firstName?: true firstName?: true
lastName?: true lastName?: true
phoneNumber?: true phoneNumber?: true
ghlContactId?: true
_all?: true _all?: true
} }
@@ -153,12 +159,13 @@ export type ProfileGroupByOutputType = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId: string | null
_count: ProfileCountAggregateOutputType | null _count: ProfileCountAggregateOutputType | null
_min: ProfileMinAggregateOutputType | null _min: ProfileMinAggregateOutputType | null
_max: ProfileMaxAggregateOutputType | null _max: ProfileMaxAggregateOutputType | null
} }
type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise< export type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<ProfileGroupByOutputType, T['by']> & Prisma.PickEnumerable<ProfileGroupByOutputType, T['by']> &
{ {
@@ -182,6 +189,7 @@ export type ProfileWhereInput = {
firstName?: Prisma.StringFilter<"Profile"> | string firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter addresses?: Prisma.AddressListRelationFilter
} }
@@ -192,6 +200,7 @@ export type ProfileOrderByWithRelationInput = {
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput user?: Prisma.UserOrderByWithRelationInput
addresses?: Prisma.AddressOrderByRelationAggregateInput addresses?: Prisma.AddressOrderByRelationAggregateInput
} }
@@ -205,6 +214,7 @@ export type ProfileWhereUniqueInput = Prisma.AtLeast<{
firstName?: Prisma.StringFilter<"Profile"> | string firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput> user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter addresses?: Prisma.AddressListRelationFilter
}, "id" | "userId"> }, "id" | "userId">
@@ -215,6 +225,7 @@ export type ProfileOrderByWithAggregationInput = {
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.ProfileCountOrderByAggregateInput _count?: Prisma.ProfileCountOrderByAggregateInput
_max?: Prisma.ProfileMaxOrderByAggregateInput _max?: Prisma.ProfileMaxOrderByAggregateInput
_min?: Prisma.ProfileMinOrderByAggregateInput _min?: Prisma.ProfileMinOrderByAggregateInput
@@ -229,6 +240,7 @@ export type ProfileScalarWhereWithAggregatesInput = {
firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
} }
export type ProfileCreateInput = { export type ProfileCreateInput = {
@@ -236,6 +248,7 @@ export type ProfileCreateInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput user: Prisma.UserCreateNestedOneWithoutProfileInput
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
} }
@@ -246,6 +259,7 @@ export type ProfileUncheckedCreateInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
} }
@@ -254,6 +268,7 @@ export type ProfileUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
} }
@@ -264,6 +279,7 @@ export type ProfileUncheckedUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
} }
@@ -273,6 +289,7 @@ export type ProfileCreateManyInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
} }
export type ProfileUpdateManyMutationInput = { export type ProfileUpdateManyMutationInput = {
@@ -280,6 +297,7 @@ export type ProfileUpdateManyMutationInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type ProfileUncheckedUpdateManyInput = { export type ProfileUncheckedUpdateManyInput = {
@@ -288,6 +306,7 @@ export type ProfileUncheckedUpdateManyInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type ProfileNullableScalarRelationFilter = { export type ProfileNullableScalarRelationFilter = {
@@ -301,6 +320,7 @@ export type ProfileCountOrderByAggregateInput = {
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
} }
export type ProfileMaxOrderByAggregateInput = { export type ProfileMaxOrderByAggregateInput = {
@@ -309,6 +329,7 @@ export type ProfileMaxOrderByAggregateInput = {
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
} }
export type ProfileMinOrderByAggregateInput = { export type ProfileMinOrderByAggregateInput = {
@@ -317,6 +338,7 @@ export type ProfileMinOrderByAggregateInput = {
firstName?: Prisma.SortOrder firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
} }
export type ProfileScalarRelationFilter = { export type ProfileScalarRelationFilter = {
@@ -375,6 +397,7 @@ export type ProfileCreateWithoutUserInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
} }
@@ -383,6 +406,7 @@ export type ProfileUncheckedCreateWithoutUserInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
} }
@@ -407,6 +431,7 @@ export type ProfileUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
} }
@@ -415,6 +440,7 @@ export type ProfileUncheckedUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
} }
@@ -423,6 +449,7 @@ export type ProfileCreateWithoutAddressesInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput user: Prisma.UserCreateNestedOneWithoutProfileInput
} }
@@ -432,6 +459,7 @@ export type ProfileUncheckedCreateWithoutAddressesInput = {
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId?: string | null
} }
export type ProfileCreateOrConnectWithoutAddressesInput = { export type ProfileCreateOrConnectWithoutAddressesInput = {
@@ -455,6 +483,7 @@ export type ProfileUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
} }
@@ -464,6 +493,7 @@ export type ProfileUncheckedUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: 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 firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs> addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
@@ -514,6 +545,7 @@ export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]> }, ExtArgs["result"]["profile"]>
@@ -523,6 +555,7 @@ export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: boolean phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]> }, ExtArgs["result"]["profile"]>
@@ -532,9 +565,10 @@ export type ProfileSelectScalar = {
firstName?: boolean firstName?: boolean
lastName?: boolean lastName?: boolean
phoneNumber?: 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> = { export type ProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs> addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
@@ -559,6 +593,7 @@ export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArg
firstName: string firstName: string
lastName: string lastName: string
phoneNumber: string phoneNumber: string
ghlContactId: string | null
}, ExtArgs["result"]["profile"]> }, ExtArgs["result"]["profile"]>
composites: {} composites: {}
} }
@@ -989,6 +1024,7 @@ export interface ProfileFieldRefs {
readonly firstName: Prisma.FieldRef<"Profile", 'String'> readonly firstName: Prisma.FieldRef<"Profile", 'String'>
readonly lastName: Prisma.FieldRef<"Profile", 'String'> readonly lastName: Prisma.FieldRef<"Profile", 'String'>
readonly phoneNumber: 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 the first `n` Profiles.
*/ */
skip?: number 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[] distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
} }
+6 -1
View File
@@ -179,7 +179,7 @@ export type SessionGroupByOutputType = {
_max: SessionMaxAggregateOutputType | null _max: SessionMaxAggregateOutputType | null
} }
type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise< export type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<SessionGroupByOutputType, T['by']> & Prisma.PickEnumerable<SessionGroupByOutputType, T['by']> &
{ {
@@ -1227,6 +1227,11 @@ export type SessionFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Sessions. * Skip the first `n` Sessions.
*/ */
skip?: number 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[] distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
} }
@@ -182,7 +182,7 @@ export type SolfeggioFrequencyGroupByOutputType = {
_max: SolfeggioFrequencyMaxAggregateOutputType | null _max: SolfeggioFrequencyMaxAggregateOutputType | null
} }
type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise< export type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<SolfeggioFrequencyGroupByOutputType, T['by']> & Prisma.PickEnumerable<SolfeggioFrequencyGroupByOutputType, T['by']> &
{ {
@@ -203,7 +203,7 @@ export type SolfeggioFrequencyWhereInput = {
NOT?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[] NOT?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[]
id?: Prisma.IntFilter<"SolfeggioFrequency"> | number id?: Prisma.IntFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringFilter<"SolfeggioFrequency"> | string name?: Prisma.StringFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatFilter<"SolfeggioFrequency"> | number value?: Prisma.IntFilter<"SolfeggioFrequency"> | number
proclamations?: Prisma.ProclamationListRelationFilter proclamations?: Prisma.ProclamationListRelationFilter
} }
@@ -241,7 +241,7 @@ export type SolfeggioFrequencyScalarWhereWithAggregatesInput = {
NOT?: Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput | Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput[] NOT?: Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput | Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number id?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringWithAggregatesFilter<"SolfeggioFrequency"> | string name?: Prisma.StringWithAggregatesFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatWithAggregatesFilter<"SolfeggioFrequency"> | number value?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
} }
export type SolfeggioFrequencyCreateInput = { export type SolfeggioFrequencyCreateInput = {
@@ -259,14 +259,14 @@ export type SolfeggioFrequencyUncheckedCreateInput = {
export type SolfeggioFrequencyUpdateInput = { export type SolfeggioFrequencyUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUpdateManyWithoutSolfeggioFrequencyNestedInput proclamations?: Prisma.ProclamationUpdateManyWithoutSolfeggioFrequencyNestedInput
} }
export type SolfeggioFrequencyUncheckedUpdateInput = { export type SolfeggioFrequencyUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput
} }
@@ -278,13 +278,13 @@ export type SolfeggioFrequencyCreateManyInput = {
export type SolfeggioFrequencyUpdateManyMutationInput = { export type SolfeggioFrequencyUpdateManyMutationInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number value?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type SolfeggioFrequencyUncheckedUpdateManyInput = { export type SolfeggioFrequencyUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number value?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type SolfeggioFrequencyScalarRelationFilter = { export type SolfeggioFrequencyScalarRelationFilter = {
@@ -363,13 +363,13 @@ export type SolfeggioFrequencyUpdateToOneWithWhereWithoutProclamationsInput = {
export type SolfeggioFrequencyUpdateWithoutProclamationsInput = { export type SolfeggioFrequencyUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number value?: Prisma.IntFieldUpdateOperationsInput | number
} }
export type SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput = { export type SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string 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 { export interface SolfeggioFrequencyFieldRefs {
readonly id: Prisma.FieldRef<"SolfeggioFrequency", 'Int'> readonly id: Prisma.FieldRef<"SolfeggioFrequency", 'Int'>
readonly name: Prisma.FieldRef<"SolfeggioFrequency", 'String'> 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 the first `n` SolfeggioFrequencies.
*/ */
skip?: number 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[] distinct?: Prisma.SolfeggioFrequencyScalarFieldEnum | Prisma.SolfeggioFrequencyScalarFieldEnum[]
} }
@@ -178,7 +178,7 @@ export type SubscriptionLevelGroupByOutputType = {
_max: SubscriptionLevelMaxAggregateOutputType | null _max: SubscriptionLevelMaxAggregateOutputType | null
} }
type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise< export type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<SubscriptionLevelGroupByOutputType, T['by']> & Prisma.PickEnumerable<SubscriptionLevelGroupByOutputType, T['by']> &
{ {
@@ -1063,6 +1063,11 @@ export type SubscriptionLevelFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` SubscriptionLevels. * Skip the first `n` SubscriptionLevels.
*/ */
skip?: number 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[] distinct?: Prisma.SubscriptionLevelScalarFieldEnum | Prisma.SubscriptionLevelScalarFieldEnum[]
} }
+6 -1
View File
@@ -213,7 +213,7 @@ export type UserGroupByOutputType = {
_max: UserMaxAggregateOutputType | null _max: UserMaxAggregateOutputType | null
} }
type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise< export type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<UserGroupByOutputType, T['by']> & Prisma.PickEnumerable<UserGroupByOutputType, T['by']> &
{ {
@@ -1883,6 +1883,11 @@ export type UserFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
* Skip the first `n` Users. * Skip the first `n` Users.
*/ */
skip?: number 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[] distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[]
} }
@@ -151,7 +151,7 @@ export type UserProclamationsGroupByOutputType = {
_max: UserProclamationsMaxAggregateOutputType | null _max: UserProclamationsMaxAggregateOutputType | null
} }
type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise< export type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<UserProclamationsGroupByOutputType, T['by']> & Prisma.PickEnumerable<UserProclamationsGroupByOutputType, T['by']> &
{ {
@@ -1207,6 +1207,11 @@ export type UserProclamationsFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` UserProclamations. * Skip the first `n` UserProclamations.
*/ */
skip?: number 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[] distinct?: Prisma.UserProclamationsScalarFieldEnum | Prisma.UserProclamationsScalarFieldEnum[]
} }
+6 -1
View File
@@ -171,7 +171,7 @@ export type UserRoleGroupByOutputType = {
_max: UserRoleMaxAggregateOutputType | null _max: UserRoleMaxAggregateOutputType | null
} }
type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise< export type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<UserRoleGroupByOutputType, T['by']> & Prisma.PickEnumerable<UserRoleGroupByOutputType, T['by']> &
{ {
@@ -1041,6 +1041,11 @@ export type UserRoleFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Skip the first `n` UserRoles. * Skip the first `n` UserRoles.
*/ */
skip?: number 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[] distinct?: Prisma.UserRoleScalarFieldEnum | Prisma.UserRoleScalarFieldEnum[]
} }
@@ -165,7 +165,7 @@ export type VerificationGroupByOutputType = {
_max: VerificationMaxAggregateOutputType | null _max: VerificationMaxAggregateOutputType | null
} }
type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise< export type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise<
Array< Array<
Prisma.PickEnumerable<VerificationGroupByOutputType, T['by']> & Prisma.PickEnumerable<VerificationGroupByOutputType, T['by']> &
{ {
@@ -982,6 +982,11 @@ export type VerificationFindManyArgs<ExtArgs extends runtime.Types.Extensions.In
* Skip the first `n` Verifications. * Skip the first `n` Verifications.
*/ */
skip?: number 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[] distinct?: Prisma.VerificationScalarFieldEnum | Prisma.VerificationScalarFieldEnum[]
} }
+47 -43
View File
@@ -10,62 +10,66 @@
// ✔ Delete a card // ✔ Delete a card
// ✔ Issue a refund // ✔ Issue a refund
import type { Decimal } from "@prisma/client/runtime/client"; import type { Decimal } from '@prisma/client/runtime/client';
export interface CreditDebitCard { export interface CreditDebitCard {
first_name: string, first_name: string;
last_name: string, last_name: string;
card_number: number, card_number: number;
expiration_month: number, expiration_month: number;
expiration_year: number, expiration_year: number;
security_code: number, security_code: number;
} }
export async function chargeCreditDebitCard (card: CreditDebitCard, amount: Decimal): Promise<null> { export async function chargeCreditDebitCard(card: CreditDebitCard, amount: Decimal): Promise<null> {
/** /**
* chargeCreditDebitCard attempts to request payment from a valid card, * chargeCreditDebitCard attempts to request payment from a valid card,
* for the amount specified. * for the amount specified.
*/ */
console.log(`charged card ${card} for $${amount}`); console.log(`charged card ${card} for $${amount}`);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
return null; return null;
} }
export async function chargedStoredCard(cardNumber: number, profileID: string, amount: Decimal): Promise<null> { export async function chargedStoredCard(
/** cardNumber: number,
* chargedStoredCard attempts to request payment from a valid card, profileID: string,
* for the amount specified. amount: Decimal
* The card is identified by the last four digits of the card ): Promise<null> {
* number, and the profile ID. /**
* NOTE: This should be enough to identify a saved card, but the * chargedStoredCard attempts to request payment from a valid card,
* function signature may change in the future. * for the amount specified.
*/ * The card is identified by the last four digits of the card
console.log(`charged ${profileID}'s card ${cardNumber} for ${cardNumber} $${amount}`); * number, and the profile ID.
return null; * NOTE: This should be enough to identify a saved card, but the
* function signature may change in the future.
*/
console.log(`charged ${profileID}'s card ${cardNumber} for ${cardNumber} $${amount}`);
return null;
} }
export async function deleteCardFromProfile(cardNumber: number, profileID: string): Promise<null> { export async function deleteCardFromProfile(cardNumber: number, profileID: string): Promise<null> {
/** /**
* deleteCardFromProfile deletes a stored card on a user's profile. * deleteCardFromProfile deletes a stored card on a user's profile.
*/ */
console.log(`Deleted card ${cardNumber} from ${profileID}'s profile.`); console.log(`Deleted card ${cardNumber} from ${profileID}'s profile.`);
return null; return null;
} }
export async function storeCardToProfile(card: CreditDebitCard, profileID: string): Promise<null> { export async function storeCardToProfile(card: CreditDebitCard, profileID: string): Promise<null> {
/** /**
* storeCardToProfile stores a new card to a user's profile * storeCardToProfile stores a new card to a user's profile
* for easy re-use later. * for easy re-use later.
*/ */
console.log(`Deleted card ${card.card_number} from ${profileID}'s profile.`); console.log(`Deleted card ${card.card_number} from ${profileID}'s profile.`);
return null; return null;
} }
export async function requestRefund(card: CreditDebitCard, amount: Decimal): Promise<null> { export async function requestRefund(card: CreditDebitCard, amount: Decimal): Promise<null> {
/** /**
* requestRefund attempts to process a refund for the amount to the * requestRefund attempts to process a refund for the amount to the
* provided card. * provided card.
*/ */
console.log(`Refunded ${amount} to ${card.card_number}`); console.log(`Refunded ${amount} to ${card.card_number}`);
return null; return null;
} }
+1 -4
View File
@@ -1,10 +1,7 @@
import { BETTER_AUTH_URL } from '$env/static/private'; import { BETTER_AUTH_URL } from '$env/static/private';
import { sendMail } from './resend'; import { sendMail } from './resend';
export async function sendVerificationEmail(options: { export async function sendVerificationEmail(options: { email: string; token: string }) {
email: string;
token: string;
}) {
const verificationUrl = `${BETTER_AUTH_URL}/api/auth/verify-email?token=${options.token}&callbackURL=/login?verified=true`; const verificationUrl = `${BETTER_AUTH_URL}/api/auth/verify-email?token=${options.token}&callbackURL=/login?verified=true`;
await sendMail({ await sendMail({
+18 -18
View File
@@ -31,26 +31,26 @@ export const resend = new Resend(RESEND_API_KEY);
* @throws {Error} If the Resend API call fails. * @throws {Error} If the Resend API call fails.
*/ */
export async function sendMail(options: { export async function sendMail(options: {
to: string; to: string;
subject?: string; subject?: string;
templateId?: string; templateId?: string;
data?: Record<string, string | number>; data?: Record<string, string | number>;
}) { }) {
const { to, subject, templateId, data } = options; const { to, subject, templateId, data } = options;
const result = await resend.emails.send({ const result = await resend.emails.send({
from: EMAIL_FROM, from: EMAIL_FROM,
to: [to], to: [to],
subject: subject ?? 'Template Email', subject: subject ?? 'Template Email',
template: { template: {
id: templateId ?? RESEND_TEMPLATE_ID, id: templateId ?? RESEND_TEMPLATE_ID,
variables: data ?? {} variables: data ?? {}
} }
}); });
if (result.error) { if (result.error) {
throw new Error(result.error.message); throw new Error(result.error.message);
} }
return result.data; return result.data;
} }
+102 -81
View File
@@ -1,8 +1,8 @@
# svelte-admin # svelte-admin
A config-driven admin panel framework for SvelteKit + Prisma. Define your models once in a 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 registry and get full CRUD UI with per-model static pages, role-based access control, searchable
dropdowns, and related-record panels — no per-model route files needed. tables, FK dropdowns, TipTap editor for rich text, and related-record panels.
## Architecture ## Architecture
@@ -13,27 +13,30 @@ src/lib/svelte-admin/ # Framework core
├── roles.ts # Role hierarchy and access helpers ├── roles.ts # Role hierarchy and access helpers
├── prisma-delegate.ts # Dynamic Prisma delegate lookup ├── prisma-delegate.ts # Dynamic Prisma delegate lookup
├── introspect.ts # Field label generation and dot-notation resolution ├── 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 └── 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.server.ts # Role-based access guard
├── +layout.svelte # Layout shell ├── +layout.svelte # Layout shell
├── +page.server.ts # Admin home (model grid) ├── +page.server.ts # Admin home (model grid)
├── +page.svelte # Admin home UI ├── +page.svelte # Admin home UI
├── [model]/ ├── users/ # Each model gets its own page directory
│ ├── +page.server.ts # List view + create action ├── profiles/
│ ├── +page.svelte # List view UI ├── addresses/
│ └── [id]/ ├── user-roles/
│ ├── +page.server.ts # Edit view + update/delete actions ├── proclamations/
│ └── +page.svelte # Edit view UI ├── solfeggio-frequencies/
├── proclamation-categories/
├── subscription-levels/
└── license-keys/ # listOnly — no create/edit modal
src/lib/components/admin/ # Reusable admin UI components src/lib/components/admin/ # Reusable admin UI components
├── AdminTable.svelte # Sortable, searchable data table ├── AdminTable.svelte # Sortable, searchable data table
├── AdminForm.svelte # Dynamic form with type-aware inputs ├── AdminForm.svelte # Dynamic form with type-aware inputs
├── ComboBox.svelte # Searchable dropdown for FK fields ├── AdminModal.svelte # Modal overlay for CRUD forms
├── RelatedHasOne.svelte # Display panel for 1:1 related records ├── TipTap.svelte # Rich-text editor (in components/tiptap/)
└── RelatedHasMany.svelte # Display table for 1:N related records
``` ```
## Adding a New Model ## 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, 3. **Create the page directory** `src/routes/(protected)/admin/widgets/` with:
and delete automatically. - `+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 ## 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. | | `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. | | `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. | | `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. | | `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. | | `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as `<select>` dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. | | `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. | | `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
### PrismaScalarType ### PrismaScalarType
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime'` `'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Tiptap'`
Controls both form input rendering and FormData parsing: Controls both form input rendering and FormData parsing:
| Type | HTML Input | Parse Logic | | Type | HTML Input | Parse Logic |
| ---------- | ----------------------------------- | ------------------------------ | | ---------- | ------------------------------------- | ------------------------------ |
| `String` | `<input type="text">` | `raw.toString()` | | `String` | `<input type="text">` | `raw.toString()` |
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty → `null` | | `Int` | `<input type="number" step="1">` | `parseInt()`, empty -> `null` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty → `null` | | `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty -> `null` |
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` → `true` | | `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` -> `true` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` | | `DateTime` | `<input type="datetime-local">` | `new Date()`, empty -> `null` |
| `Tiptap` | TipTap rich-text editor (hidden input)| `raw.toString()` |
### RelationOptionConfig ### 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 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`. 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 When a field also has a `relationOptions` entry, the framework renders a `<select>` dropdown
dropdown) instead of the default input for that type. 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 ## Relations
### Foreign Key Dropdowns (relationOptions) ### 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 ```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) ### 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 The `relationKey` must match a key in `include` so the data is actually loaded.
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.
## Access Control ## Access Control
@@ -228,23 +236,23 @@ Use `accessFilter` to restrict which records a viewer can see:
```ts ```ts
accessFilter: (viewerRoleId: number) => { accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= ROLES.SUPER) return undefined; // no filter if (viewerRoleId <= ROLES.SUPER) return undefined;
if (viewerRoleId <= ROLES.ADMIN) { 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`) 1. User visits `/admin/proclamations`
2. SvelteKit matches the `[model]` (and `[id]`) route params 2. SvelteKit matches the static route `/admin/proclamations/+page.server.ts`
3. `+page.server.ts` calls `getModelConfig(params.model)` to look up the config 3. `+page.server.ts` calls `getModelConfig('proclamations')` to look up the config
4. `getDelegate(config)` returns `prisma[lcfirst(config.modelName)]` (e.g., `prisma.proclamation`) 4. `loadRecords(config)` calls `prisma.proclamation.findMany()` with the config's `include` and `accessFilter`
5. The delegate runs `findMany` (list), `findUnique` (edit), `create`, `update`, or `delete` 5. For each FK field with a `relationOptions`, it also queries the related model for dropdown options
6. Data + config are passed to the Svelte page component 6. Data + config are passed to `+page.svelte`
7. Components (`AdminTable`, `AdminForm`, etc.) render based on config 7. The page renders `AdminTable` with records and `AdminModal` + `AdminForm` for create/edit
## Component API ## Component API
@@ -255,11 +263,13 @@ accessFilter: (viewerRoleId: number) => {
| `records` | `Record<string, unknown>[]` | Data rows | | `records` | `Record<string, unknown>[]` | Data rows |
| `listFields` | `string[]` | Column keys (dot-notation supported) | | `listFields` | `string[]` | Column keys (dot-notation supported) |
| `searchFields` | `string[]` | Fields for client-side search | | `searchFields` | `string[]` | Fields for client-side search |
| `modelSlug` | `string` | Used for building edit links | | `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for cell formatting |
| `listOnly` | `boolean?` | Suppress edit links | | `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, Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
decimals), filtered count display. decimals), empty state.
### AdminForm ### AdminForm
@@ -270,46 +280,31 @@ decimals), filtered count display.
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs | | `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options | | `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) | | `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
| `form` | `ActionData?` | Carries validation errors | | `readonlyFields` | `string[]?` | Read-only field names |
| `action` | `string` | Form action (e.g., `'?/create'`) | | `action` | `string` | Form action (e.g., `'?/upsert'`) |
| `submitLabel` | `string?` | Button text | | `submitLabel` | `string?` | Button text |
### ComboBox ### AdminModal
| Prop | Type | Description | | Prop | Type | Description |
| ------------- | -------------------- | ------------------ | | --------- | ----------------- | ------------------------ |
| `id` | `string` | Element ID | | `open` | `boolean` | Show/hide modal |
| `name` | `string` | Form field name | | `title` | `string` | Modal heading |
| `options` | `{ label; value }[]` | Dropdown items | | `onClose` | `() => void` | Close handler |
| `placeholder` | `string?` | Placeholder text | | `children`| `Snippet` | Modal body content |
| `value` | `string \| number?` | Pre-selected value |
Features: type-to-search filtering, keyboard navigation (ArrowUp/Down, Enter, Escape), ### TipTap
clear button, scroll-to-highlight.
### RelatedHasOne | Prop | Type | Description |
| ---------------- | -------- | --------------------------------- |
| Prop | Type | Description | | `initialContent` | `string` | Initial HTML content |
| --------------- | --------------------------------- | ------------------------------------ | | `name` | `string` | Hidden input name for form submit |
| `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 |
## Utility Functions ## Utility Functions
| Function | Module | Description | | 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 | | `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` | | `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 | | `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 | | `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` | | `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` | | `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 ## Complete Example
@@ -364,7 +385,7 @@ A hypothetical `Product` model showing all configuration features:
], ],
accessFilter: (viewerRoleId: number) => { accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= 2) return undefined; 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); data[field] = val === '' ? null : new Date(val);
break; break;
} }
case 'Tiptap': {
data[field] = raw?.toString() ?? '';
break;
}
default: { default: {
data[field] = raw?.toString() ?? ''; data[field] = raw?.toString() ?? '';
break; 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; editSlug?: string;
}; };
export type ActionResult = {
success: boolean;
error?: string;
fieldErrors?: Record<string, string>;
};
export type AdminModelConfig = { export type AdminModelConfig = {
slug: string; slug: string;
modelName: string; modelName: string;
@@ -35,4 +41,7 @@ export type AdminModelConfig = {
relationOptions?: Record<string, RelationOptionConfig>; relationOptions?: Record<string, RelationOptionConfig>;
relatedRecords?: RelatedRecordConfig[]; relatedRecords?: RelatedRecordConfig[];
accessFilter?: (viewerRoleId: number) => Record<string, unknown> | undefined; 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> <h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success} {#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. Profile updated successfully.
</div> </div>
{/if} {/if}
{#if form?.action === 'updateProfile' && form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
@@ -115,12 +119,16 @@
</div> </div>
{#if form?.action === 'addAddress' && form?.success} {#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. Address added successfully.
</div> </div>
{/if} {/if}
{#if form?.action === 'addAddress' && form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
@@ -133,7 +141,9 @@
> >
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div> <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 <input
type="text" type="text"
id="street" id="street"
@@ -144,7 +154,9 @@
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <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 <input
type="text" type="text"
id="city" id="city"
@@ -154,7 +166,9 @@
/> />
</div> </div>
<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 <input
type="text" type="text"
id="state" id="state"
@@ -166,7 +180,10 @@
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <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 Postal Code
</label> </label>
<input <input
@@ -178,7 +195,10 @@
/> />
</div> </div>
<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 <input
type="text" type="text"
id="country" id="country"
@@ -204,10 +224,13 @@
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0} {#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
{#each data.user.profile.addresses as address} {#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-900 dark:text-gray-100">{address.street}</p>
<p class="text-sm text-gray-600 dark:text-gray-400"> <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>
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p> <p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
</div> </div>
@@ -168,9 +168,9 @@ describe('(protected)/account page server', () => {
country: 'US' country: 'US'
}); });
const err = await Promise.resolve( const err = await Promise.resolve(invokeAction('addAddress', request, createLocals())).catch(
invokeAction('addAddress', request, createLocals()) (e: unknown) => e
).catch((e: unknown) => e); );
expect(isHttpError(err, 403)).toBe(true); expect(isHttpError(err, 403)).toBe(true);
}); });
@@ -201,9 +201,9 @@ describe('(protected)/account page server', () => {
}); });
const locals = createLocals(mockUser() as any); const locals = createLocals(mockUser() as any);
const err = await Promise.resolve( const err = await Promise.resolve(invokeAction('addAddress', request, locals)).catch(
invokeAction('addAddress', request, locals) (e: unknown) => e
).catch((e: unknown) => e); );
expect(isHttpError(err, 400)).toBe(true); expect(isHttpError(err, 400)).toBe(true);
}); });
@@ -1,4 +1,3 @@
<!-- <!--
There should be a form in the middle There should be a form in the middle
rounded corners rounded corners
@@ -44,17 +43,29 @@
<Window> <Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"> <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} {#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} {:else}
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div> <div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
{/if} {/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"> <div class="flex flex-col">
<input type="text" name="userId" value={data.user?.id} class="hidden"/> <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
<input type="text" name="lastName" placeholder="Last Name" class="my-2" bind:value={lastName} /> 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 <input
type="tel" type="tel"
name="phoneNumber" name="phoneNumber"
@@ -64,6 +75,8 @@
oninput={handlePhoneInput} oninput={handlePhoneInput}
/> />
</div> </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> </form>
</Window> </Window>
@@ -33,9 +33,9 @@ describe('account setup page server load', () => {
}); });
it('throws 403 when user is not logged in', async () => { it('throws 403 when user is not logged in', async () => {
const err = await Promise.resolve( const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
load({ locals: createLocals() } as any) (e: unknown) => e
).catch((e: unknown) => e); );
expect(isHttpError(err, 403)).toBe(true); expect(isHttpError(err, 403)).toBe(true);
}); });
@@ -5,7 +5,9 @@
</script> </script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100"> <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"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<a <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" 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="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> <p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
</a> </a>
@@ -12,13 +12,19 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800"> <thead class="bg-gray-50 dark:bg-gray-800">
<tr> <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 Key
</th> </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 Level
</th> </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 Status
</th> </th>
</tr> </tr>
@@ -26,15 +32,17 @@
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
{#each data.licenseKeys as key} {#each data.licenseKeys as key}
<tr> <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} {key.key}
</td> </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} {key.level}
</td> </td>
<td class="whitespace-nowrap px-6 py-4 text-sm"> <td class="px-6 py-4 text-sm whitespace-nowrap">
<span <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-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'}" : '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([ const [proclamations, categories] = await Promise.all([
prisma.proclamation.findMany({ prisma.proclamation.findMany({
where, where,
include: { solfeggioFrequency: true, category: { include: { parent: true } } }, include: { solfeggioFrequency: true, category: { include: { parent: true } }, subscriptionLevel: true },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } }) prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } })
@@ -40,7 +40,9 @@
<!-- Filters --> <!-- Filters -->
<div class="flex flex-wrap items-end gap-4"> <div class="flex flex-wrap items-end gap-4">
<div> <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 <input
type="text" type="text"
id="search" id="search"
@@ -51,7 +53,9 @@
/> />
</div> </div>
<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 <select
id="category" id="category"
bind:value={categoryId} bind:value={categoryId}
@@ -60,7 +64,7 @@
> >
<option value={null}>All Categories</option> <option value={null}>All Categories</option>
{#each data.categories as cat} {#each data.categories as cat}
<option value={cat.id}>{displayCategoryName(cat)}</option> <option value={cat.id}>{displayCategoryName(cat)}</option>
{/each} {/each}
</select> </select>
</div> </div>
@@ -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" 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> <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"> <div class="mt-auto flex items-center justify-between pt-4">
<span class="text-sm text-purple-600 dark:text-gray-400"> <span class="text-sm text-purple-600 dark:text-gray-400">
{proclamation.solfeggioFrequency.value} Hz &mdash; {proclamation.solfeggioFrequency.value} Hz &mdash;
{proclamation.solfeggioFrequency.name} {proclamation.solfeggioFrequency.name}
</span> </span>
<span class="font-semibold text-gray-900 dark:text-gray-100"> <span class="text-xs text-gray-500 dark:text-gray-400">
${proclamation.price.toFixed(2)} {proclamation.subscriptionLevel?.name ?? 'Standard'}
</span> </span>
</div> </div>
</a> </a>
@@ -14,7 +14,8 @@ export const load: PageServerLoad = async ({ params, parent }) => {
where: { id: params.id }, where: { id: params.id },
include: { include: {
solfeggioFrequency: true, solfeggioFrequency: true,
category: { include: { parent: true } } category: { include: { parent: true } },
subscriptionLevel: true
} }
}); });
@@ -19,27 +19,35 @@
</a> </a>
<div> <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> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{categoryBreadcrumb}</p>
</div> </div>
<div class="flex flex-wrap gap-4"> <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-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"> <p class="text-sm font-semibold text-purple-900 dark:text-gray-100">
{data.proclamation.solfeggioFrequency.value} Hz &mdash; {data.proclamation.solfeggioFrequency.value} Hz &mdash;
{data.proclamation.solfeggioFrequency.name} {data.proclamation.solfeggioFrequency.name}
</p> </p>
</div> </div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"> <div
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Price</p> 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"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
${data.proclamation.price.toFixed(2)} {data.proclamation.subscriptionLevel?.name ?? 'Standard'}
</p> </p>
</div> </div>
</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} {@html data.proclamation.content}
</div> </div>
</div> </div>
@@ -21,9 +21,9 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
it('throws 404 when proclamation is not found', async () => { it('throws 404 when proclamation is not found', async () => {
findUnique.mockResolvedValue(null); findUnique.mockResolvedValue(null);
const err = await Promise.resolve( const err = await Promise.resolve(load({ params: { id: 'nonexistent' } } as any)).catch(
load({ params: { id: 'nonexistent' } } as any) (e: unknown) => e
).catch((e: unknown) => e); );
expect(isHttpError(err, 404)).toBe(true); expect(isHttpError(err, 404)).toBe(true);
}); });
@@ -44,7 +44,8 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
where: { id: 'proc-1' }, where: { id: 'proc-1' },
include: { include: {
solfeggioFrequency: true, solfeggioFrequency: true,
category: { include: { parent: true } } category: { include: { parent: true } },
subscriptionLevel: true
} }
}); });
}); });
@@ -14,7 +14,7 @@ import { load } from './+page.server';
const proclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>; const proclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>; const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
describe('(protected)/admin/proclamations page server load', () => { describe('(protected)/admin/proclamations page server load', () => {
beforeEach(() => { beforeEach(() => {
proclamationFindMany.mockReset(); proclamationFindMany.mockReset();
categoryFindMany.mockReset(); categoryFindMany.mockReset();
@@ -29,10 +29,12 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
const url = createUrl('/admin/proclamations'); const url = createUrl('/admin/proclamations');
const result = (await load({ url } as any)) as any; const result = (await load({ url } as any)) as any;
expect(result.proclamations).toEqual(proclamations); expect(result.proclamations).toEqual(proclamations);
expect(result.categories[0]).toEqual(expect.objectContaining({ expect(result.categories[0]).toEqual(
...categories[0], expect.objectContaining({
displayName: categories[0].name ...categories[0],
})); displayName: categories[0].name
})
);
expect(result.search).toBe(''); expect(result.search).toBe('');
expect(result.categoryId).toBeNull(); expect(result.categoryId).toBeNull();
}); });
@@ -109,7 +111,7 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
expect(proclamationFindMany).toHaveBeenCalledWith( expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({ 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} {#each adminModels as model}
<a <a
href="/admin/{model.slug}" href="/admin/{model.slug}"
class="rounded px-3 py-1 text-sm transition-colors {isActive( class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
'/admin/' + model.slug
)
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100' ? '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'}" : '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)) { 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'); 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 type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry'; 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 { prisma } from '$lib/db';
import { error } from '@sveltejs/kit'; 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')!; const config = getModelConfig('profiles')!;
@@ -12,67 +20,73 @@ export const load: PageServerLoad = async ({ locals }) => {
export const actions = { export const actions = {
upsert: async ({ request, locals }) => { upsert: async ({ request, locals }) => {
const formData = await request.formData(); return upsertRecord(request, config, locals);
return upsertRecord(formData, config, locals);
}, },
delete: async ({ request, locals }) => { delete: async ({ request, locals }) => {
const formData = await request.formData(); return deleteRecord(request, config, locals);
return deleteRecord(formData, config, locals);
}, },
upsertAddress: async ({ request, locals }) => { upsertAddress: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized'); if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData(); const form = await superValidate(request, zod4(Schemas.Address));
const id = formData.get('id')?.toString(); if (!form.valid) {
const profileId = formData.get('profileId')?.toString(); 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) { if (!profileId) {
return { success: false, addressError: 'Missing profile ID' }; return { success: false, addressError: 'Missing profile ID' };
} }
const type = formData.get('type')?.toString() || 'Mailing'; // Unique-default enforcement: clear defaults of same type before setting
const street = formData.get('street')?.toString() || ''; if (isDefaultMailing) {
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) {
await prisma.address.updateMany({ await prisma.address.updateMany({
where: { profileId, type, isDefault: true }, where: { profileId, isDefaultMailing: true },
data: { isDefault: false } 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 payload = {
const updated = await prisma.address.update({ ...addressData,
where: { id }, profileId,
data: { type, street, city, state, postalCode, country, isDefault } isDefaultMailing,
}); isDefaultBilling,
return { success: true, address: updated }; isDefaultShipping
} };
const created = await prisma.address.create({ const created = await prisma.address.create({ data: payload });
data: { profileId, type, street, city, state, postalCode, country, isDefault }
});
return { success: true, address: created }; return { success: true, address: created };
}, },
updateUser: async ({ request, locals }) => { updateUser: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized'); if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData(); const form = await superValidate(request, zod4(Schemas.UserUpdate));
const userId = formData.get('id')?.toString(); if (!form.valid) {
const email = formData.get('email')?.toString(); const { error: userErr, fieldErrors } = superFormErrorsToFieldErrors(
const name = formData.get('name')?.toString(); 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' }; return { success: false, userError: 'Missing user ID' };
} }
@@ -83,7 +97,7 @@ export const actions = {
return { success: false, userError: 'No fields to update' }; 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 }; return { success: true };
} }
}; };
@@ -30,8 +30,9 @@
state: '', state: '',
postalCode: '', postalCode: '',
country: '', country: '',
type: 'Mailing', isDefaultMailing: 'false',
isDefault: 'false' isDefaultBilling: 'false',
isDefaultShipping: 'false'
}); });
let userEmail = $state(''); let userEmail = $state('');
@@ -44,11 +45,9 @@
} else if ((form as any).userError) { } else if ((form as any).userError) {
userError = (form as any).userError; userError = (form as any).userError;
} else if (form.success) { } else if (form.success) {
toastMessage = (form as any).addressError
? 'Address saved'
: 'Saved successfully';
toastType = 'success'; toastType = 'success';
if ((form as any).address) { if ((form as any).address) {
toastMessage = 'Address saved';
const addr = (form as any).address as Record<string, unknown>; const addr = (form as any).address as Record<string, unknown>;
if (editingRecord) { if (editingRecord) {
const addrs = (editingRecord.addresses as Record<string, unknown>[]) ?? []; const addrs = (editingRecord.addresses as Record<string, unknown>[]) ?? [];
@@ -59,7 +58,8 @@
} }
addressFormOpen = false; addressFormOpen = false;
addressError = ''; addressError = '';
} else if (!(form as any).addressError && !(form as any).userError) { } else {
toastMessage = 'Saved successfully';
closeModal(); closeModal();
} }
} else if (!(form as any).addressError && !(form as any).userError) { } else if (!(form as any).addressError && !(form as any).userError) {
@@ -86,8 +86,8 @@
userFormOpen = false; userFormOpen = false;
addressError = ''; addressError = '';
userError = ''; userError = '';
userEmail = (record.user as Record<string, unknown>)?.email as string ?? ''; userEmail = ((record.user as Record<string, unknown>)?.email as string) ?? '';
userName = (record.user as Record<string, unknown>)?.name as string ?? ''; userName = ((record.user as Record<string, unknown>)?.name as string) ?? '';
modalOpen = true; modalOpen = true;
} }
@@ -162,11 +162,16 @@
</button> </button>
</div> </div>
<p class="mb-1 text-sm text-gray-600 dark:text-gray-400"> <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> </p>
{#if userFormOpen} {#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 ?? '')} /> <input type="hidden" name="id" value={String(user?.id ?? '')} />
{#if userError} {#if userError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700"> <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} {/if}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div> <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 Name
</label> </label>
<input <input
@@ -187,7 +195,10 @@
/> />
</div> </div>
<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 Email
</label> </label>
<input <input
@@ -227,7 +238,12 @@
</div> </div>
{#if addressFormOpen} {#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)} /> <input type="hidden" name="profileId" value={String(editingRecord.id)} />
{#if addressError} {#if addressError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700"> <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} {/if}
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div> <div>
<label for="addr-type" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"> <label
Type for="addr-street"
</label> class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
<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"
> >
<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 Street
</label> </label>
<input <input
@@ -262,7 +267,10 @@
/> />
</div> </div>
<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 City
</label> </label>
<input <input
@@ -274,7 +282,10 @@
/> />
</div> </div>
<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 State
</label> </label>
<input <input
@@ -286,7 +297,10 @@
/> />
</div> </div>
<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 Postal Code
</label> </label>
<input <input
@@ -298,7 +312,10 @@
/> />
</div> </div>
<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 Country
</label> </label>
<input <input
@@ -310,10 +327,20 @@
/> />
</div> </div>
</div> </div>
<label class="mt-2 flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400"> <div class="mt-2 flex flex-wrap gap-4">
<input type="checkbox" name="isDefault" class="h-3.5 w-3.5" /> <label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
Set as default for this type <input type="checkbox" name="isDefaultMailing" class="h-3.5 w-3.5" />
</label> 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"> <div class="mt-2 flex justify-end">
<button <button
type="submit" type="submit"
@@ -329,25 +356,30 @@
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead> <thead>
<tr class="border-b border-gray-200 dark:border-gray-700"> <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">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">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">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"
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Default</th> >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> </tr>
</thead> </thead>
<tbody> <tbody>
{#each addresses as addr} {#each addresses as addr}
<tr class="border-b border-gray-100 dark:border-gray-800"> <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.street as string}</td>
<td class="px-2 py-1">{addr.city 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.state as string}</td>
<td class="px-2 py-1">{addr.postalCode 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.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> </tr>
{/each} {/each}
</tbody> </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 () => { it('throws 403 when locals.user is undefined', async () => {
findUnique.mockResolvedValue(null); 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); expect(isHttpError(err, 403)).toBe(true);
}); });
+6 -3
View File
@@ -6,8 +6,11 @@
<Window> <Window>
{#if $page.status === 403} {#if $page.status === 403}
<h1 class="dark:text-gray-100">Not Authorized</h1> <h1 class="dark:text-gray-100">Not Authorized</h1>
<p class="dark:text-gray-300"> <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
</p> href="/"
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
>.
</p>
{/if} {/if}
</Window> </Window>
@@ -1,21 +1,21 @@
import type { RequestHandler } from "./$types"; import type { RequestHandler } from './$types';
export const POST: RequestHandler = async () => { export const POST: RequestHandler = async () => {
// Request validation (400) // Request validation (400)
// 1. Validate and extract information // 1. Validate and extract information
// Business rule validation (409) // Business rule validation (409)
// 2. Check for existing entries // 2. Check for existing entries
// Resource creation (500 on unexpected failures) // Resource creation (500 on unexpected failures)
// 3. Create new entries // 3. Create new entries
// 5. Create license key // 5. Create license key
// External operations (502/503/504 or 500, depending on your API) // External operations (502/503/504 or 500, depending on your API)
// 4. Send profile setup email // 4. Send profile setup email
// 6. Send license key email // 6. Send license key email
// Success (200/201) // Success (200/201)
// 7. Send response // 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'; import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => { export const GET: RequestHandler = async () => {
const categories = await prisma.proclamationCategory.findMany() const categories = await prisma.proclamationCategory.findMany();
return new Response(JSON.stringify(categories)) return new Response(JSON.stringify(categories));
} };
+3 -3
View File
@@ -2,6 +2,6 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/db'; import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => { export const GET: RequestHandler = async () => {
const frequencies = await prisma.solfeggioFrequency.findMany() const frequencies = await prisma.solfeggioFrequency.findMany();
return new Response(JSON.stringify(frequencies)) 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> <h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Forgot Password</h1>
{#if form?.success} {#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. If an account with that email exists, we've sent a password reset link.
</div> </div>
{/if} {/if}
{#if form?.error && !form?.success} {#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} {form.error}
</div> </div>
{/if} {/if}
@@ -36,7 +40,10 @@
</button> </button>
</form> </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 Back to Login
</a> </a>
</div> </div>
+65 -65
View File
@@ -10,68 +10,68 @@
/* Use var(--color-gray-N) so dark-mode inversion flows through */ /* Use var(--color-gray-N) so dark-mode inversion flows through */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@theme { @theme {
--color-blue-50: var(--color-gray-100); --color-blue-50: var(--color-gray-100);
--color-blue-100: var(--color-gray-200); --color-blue-100: var(--color-gray-200);
--color-blue-200: var(--color-gray-300); --color-blue-200: var(--color-gray-300);
--color-blue-300: var(--color-gray-400); --color-blue-300: var(--color-gray-400);
--color-blue-400: var(--color-gray-500); --color-blue-400: var(--color-gray-500);
--color-blue-500: var(--color-gray-500); --color-blue-500: var(--color-gray-500);
--color-blue-600: var(--color-gray-600); --color-blue-600: var(--color-gray-600);
--color-blue-700: var(--color-gray-700); --color-blue-700: var(--color-gray-700);
--color-blue-800: var(--color-gray-800); --color-blue-800: var(--color-gray-800);
--color-green-50: var(--color-gray-100); --color-green-50: var(--color-gray-100);
--color-green-100: var(--color-gray-200); --color-green-100: var(--color-gray-200);
--color-green-200: var(--color-gray-300); --color-green-200: var(--color-gray-300);
--color-green-300: var(--color-gray-400); --color-green-300: var(--color-gray-400);
--color-green-400: var(--color-gray-500); --color-green-400: var(--color-gray-500);
--color-green-500: var(--color-gray-500); --color-green-500: var(--color-gray-500);
--color-green-600: var(--color-gray-600); --color-green-600: var(--color-gray-600);
--color-green-700: var(--color-gray-700); --color-green-700: var(--color-gray-700);
--color-green-800: var(--color-gray-800); --color-green-800: var(--color-gray-800);
--color-red-50: var(--color-gray-100); --color-red-50: var(--color-gray-100);
--color-red-100: var(--color-gray-200); --color-red-100: var(--color-gray-200);
--color-red-200: var(--color-gray-300); --color-red-200: var(--color-gray-300);
--color-red-300: var(--color-gray-400); --color-red-300: var(--color-gray-400);
--color-red-400: var(--color-gray-500); --color-red-400: var(--color-gray-500);
--color-red-500: var(--color-gray-500); --color-red-500: var(--color-gray-500);
--color-red-600: var(--color-gray-600); --color-red-600: var(--color-gray-600);
--color-red-700: var(--color-gray-700); --color-red-700: var(--color-gray-700);
--color-red-800: var(--color-gray-800); --color-red-800: var(--color-gray-800);
--color-purple-50: var(--color-gray-100); --color-purple-50: var(--color-gray-100);
--color-purple-100: var(--color-gray-200); --color-purple-100: var(--color-gray-200);
--color-purple-200: var(--color-gray-300); --color-purple-200: var(--color-gray-300);
--color-purple-300: var(--color-gray-400); --color-purple-300: var(--color-gray-400);
--color-purple-400: var(--color-gray-500); --color-purple-400: var(--color-gray-500);
--color-purple-500: var(--color-gray-500); --color-purple-500: var(--color-gray-500);
--color-purple-600: var(--color-gray-600); --color-purple-600: var(--color-gray-600);
--color-purple-700: var(--color-gray-700); --color-purple-700: var(--color-gray-700);
--color-purple-800: var(--color-gray-800); --color-purple-800: var(--color-gray-800);
--color-purple-900: var(--color-gray-900); --color-purple-900: var(--color-gray-900);
--color-slate-50: var(--color-gray-50); --color-slate-50: var(--color-gray-50);
--color-slate-100: var(--color-gray-100); --color-slate-100: var(--color-gray-100);
--color-slate-200: var(--color-gray-200); --color-slate-200: var(--color-gray-200);
--color-slate-300: var(--color-gray-300); --color-slate-300: var(--color-gray-300);
--color-slate-400: var(--color-gray-400); --color-slate-400: var(--color-gray-400);
--color-slate-500: var(--color-gray-500); --color-slate-500: var(--color-gray-500);
--color-slate-600: var(--color-gray-600); --color-slate-600: var(--color-gray-600);
--color-slate-700: var(--color-gray-700); --color-slate-700: var(--color-gray-700);
--color-slate-800: var(--color-gray-800); --color-slate-800: var(--color-gray-800);
--color-slate-900: var(--color-gray-900); --color-slate-900: var(--color-gray-900);
--radius: 0px; --radius: 0px;
--radius-sm: 0px; --radius-sm: 0px;
--radius-md: 0px; --radius-md: 0px;
--radius-lg: 0px; --radius-lg: 0px;
--radius-xl: 0px; --radius-xl: 0px;
--radius-2xl: 0px; --radius-2xl: 0px;
--radius-3xl: 0px; --radius-3xl: 0px;
--shadow-sm: 0 0 #0000; --shadow-sm: 0 0 #0000;
--shadow: 0 0 #0000; --shadow: 0 0 #0000;
--shadow-md: 0 0 #0000; --shadow-md: 0 0 #0000;
--shadow-lg: 0 0 #0000; --shadow-lg: 0 0 #0000;
--shadow-xl: 0 0 #0000; --shadow-xl: 0 0 #0000;
--shadow-2xl: 0 0 #0000; --shadow-2xl: 0 0 #0000;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -80,9 +80,9 @@
.dark input, .dark input,
.dark select, .dark select,
.dark textarea { .dark textarea {
background-color: #1f2937; background-color: #1f2937;
color: #f9fafb; color: #f9fafb;
border-color: #374151; border-color: #374151;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -91,6 +91,6 @@
*, *,
::before, ::before,
::after { ::after {
transition: none !important; transition: none !important;
animation: none !important; animation: none !important;
} }
+11 -7
View File
@@ -37,20 +37,24 @@
{#if error} {#if error}
<span class="text-red-600 dark:text-gray-300">{error}</span> <span class="text-red-600 dark:text-gray-300">{error}</span>
{/if} {/if}
<form <form onsubmit={handleSubmit} class="my-6 flex flex-col items-center justify-center">
onsubmit={handleSubmit}
class="my-6 flex flex-col items-center justify-center"
>
<p>Login</p> <p>Login</p>
<input type="email" placeholder="Email Address" bind:value={email} required class="my-2" /> <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" /> <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> </form>
<div class="mt-2 flex flex-col items-center gap-2"> <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 >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 >Don't have an account? Register</a
> >
</div> </div>
+13 -3
View File
@@ -23,7 +23,9 @@
{:else} {:else}
<div class="flex flex-col items-start justify-center"> <div class="flex flex-col items-start justify-center">
{#if form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
@@ -32,8 +34,16 @@
<input type="text" name="lastName" placeholder="Last Name" required class="mb-2" /> <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="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="password" placeholder="Password" required class="mb-2" />
<input type="password" name="password2" placeholder="Verify Password" required class="mb-2" /> <input
<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" 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 >Submit</button
> >
</form> </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> <h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Reset Password</h1>
{#if form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
+3 -10
View File
@@ -89,8 +89,7 @@ describe('reset-password page server action', () => {
resetPassword.mockReset(); resetPassword.mockReset();
}); });
const invokeDefault = (url: URL, request: Request) => const invokeDefault = (url: URL, request: Request) => actions.default({ request, url } as any);
actions.default({ request, url } as any);
it('returns fail(400) when token is missing', async () => { it('returns fail(400) when token is missing', async () => {
const url = createUrl('/reset-password'); const url = createUrl('/reset-password');
@@ -120,10 +119,7 @@ describe('reset-password page server action', () => {
it('returns fail(400) when password is empty', async () => { it('returns fail(400) when password is empty', async () => {
const url = createUrl('/reset-password', { token: 'token-1' }); const url = createUrl('/reset-password', { token: 'token-1' });
const result = await invokeDefault( const result = await invokeDefault(url, createFormRequest({ password: '', password2: '' }));
url,
createFormRequest({ password: '', password2: '' })
);
expect(isActionFailure(result)).toBe(true); expect(isActionFailure(result)).toBe(true);
if (isActionFailure(result)) { if (isActionFailure(result)) {
expect(result.data).toEqual({ error: 'Password must be at least 8 characters.' }); 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 url = createUrl('/reset-password', { token: 'token-1' });
const err = await Promise.resolve( const err = await Promise.resolve(
invokeDefault( invokeDefault(url, createFormRequest({ password: 'longenough1', password2: 'longenough1' }))
url,
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
)
).catch((e: unknown) => e); ).catch((e: unknown) => e);
expect(isRedirect(err)).toBe(true); expect(isRedirect(err)).toBe(true);
if (isRedirect(err)) { if (isRedirect(err)) {
+7 -7
View File
@@ -14,12 +14,12 @@ import { sendMail } from '$lib/server/resend';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => { export const POST: RequestHandler = async ({ request }) => {
const { to, subject, templateId, data } = await request.json(); const { to, subject, templateId, data } = await request.json();
try { try {
const result = await sendMail({ to, subject, templateId, data }); const result = await sendMail({ to, subject, templateId, data });
return json(result); return json(result);
} catch (e) { } catch (e) {
return json({ error: (e as Error).message }, { status: 400 }); return json({ error: (e as Error).message }, { status: 400 });
} }
}; };