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