create-z3 0.0.47 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.js +17 -18
- package/package.json +11 -11
- package/templates/nextjs/convex/auth/adapter/index.ts +7 -22
- package/templates/nextjs/convex/auth/index.ts +1 -1
- package/templates/nextjs/convex/auth/plugins/index.ts +6 -2
- package/templates/nextjs/convex/auth/sessions.ts +1 -1
- package/templates/nextjs/convex/auth.config.ts +7 -0
- package/templates/nextjs/convex/schema.ts +30 -2
- package/templates/nextjs/package.json +3 -2
- package/templates/nextjs/src/auth/client.tsx +2 -4
- package/templates/nextjs/src/auth/permissions.ts +2 -2
- package/templates/nextjs/src/db/constants/index.ts +1 -0
- package/templates/tanstack-start/convex/auth/adapter/index.ts +7 -22
- package/templates/tanstack-start/convex/auth/index.ts +1 -1
- package/templates/tanstack-start/convex/auth/plugins/index.ts +5 -1
- package/templates/tanstack-start/convex/auth/sessions.ts +1 -1
- package/templates/tanstack-start/convex/auth.config.ts +7 -0
- package/templates/tanstack-start/convex/schema.ts +37 -2
- package/templates/tanstack-start/package.json +3 -2
- package/templates/tanstack-start/src/components/component-example.tsx +42 -461
- package/templates/tanstack-start/src/db/constants/index.ts +1 -0
- package/templates/tanstack-start/src/lib/auth/client.ts +2 -1
- package/templates/tanstack-start/src/lib/auth/permissions.ts +2 -2
- package/templates/tanstack-start/src/routes/auth/$authView.tsx +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Isaiah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ async function isDirectoryEmpty(dirPath) {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function resolveProjectName(input2,
|
|
40
|
+
function resolveProjectName(input2, _cwd) {
|
|
41
41
|
return input2;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -1798,9 +1798,11 @@ function generateCredentialsValue(enabled) {
|
|
|
1798
1798
|
}
|
|
1799
1799
|
function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
|
|
1800
1800
|
const parts = [];
|
|
1801
|
-
|
|
1802
|
-
|
|
1801
|
+
if (emailPasswordEnabled) {
|
|
1802
|
+
parts.push(`emailAndPassword: {
|
|
1803
|
+
enabled: true
|
|
1803
1804
|
},`);
|
|
1805
|
+
}
|
|
1804
1806
|
if (oauthProviders.length > 0) {
|
|
1805
1807
|
const providersObject = oauthProviders.map((providerId) => {
|
|
1806
1808
|
const provider = getProvider(providerId);
|
|
@@ -2300,13 +2302,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
|
|
|
2300
2302
|
async updateEnvExample(selectedProviders) {
|
|
2301
2303
|
const envFilePath = join2(this.targetPath, ".env.example");
|
|
2302
2304
|
const envVarsBlock = generateEnvVarsBlock(selectedProviders, "tanstack");
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
}
|
|
2305
|
+
await replacePlaceholder(
|
|
2306
|
+
envFilePath,
|
|
2307
|
+
"# {{ENV_OAUTH_VARS}}",
|
|
2308
|
+
envVarsBlock,
|
|
2309
|
+
{ graceful: true }
|
|
2310
|
+
);
|
|
2310
2311
|
}
|
|
2311
2312
|
/**
|
|
2312
2313
|
* Update README with OAuth provider setup guides
|
|
@@ -2319,14 +2320,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
|
|
|
2319
2320
|
async updateReadme(selectedProviders) {
|
|
2320
2321
|
const readmeFilePath = join2(this.targetPath, "README.md");
|
|
2321
2322
|
const readmeSection = generateReadmeSection(selectedProviders);
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
);
|
|
2329
|
-
}
|
|
2323
|
+
await replacePlaceholder(
|
|
2324
|
+
readmeFilePath,
|
|
2325
|
+
"<!-- {{OAUTH_SETUP_GUIDE}} -->",
|
|
2326
|
+
readmeSection,
|
|
2327
|
+
{ graceful: true }
|
|
2328
|
+
);
|
|
2330
2329
|
}
|
|
2331
2330
|
/**
|
|
2332
2331
|
* Apply TweakCN theme to global CSS file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-z3",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for scaffolding Z3 Stack applications (TanStack/Next.js + Convex + Better Auth)",
|
|
6
6
|
"bin": {
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"templates"
|
|
12
12
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
15
|
-
"dev": "tsup src/index.ts --format esm --watch",
|
|
16
|
-
"typecheck": "tsc --noEmit",
|
|
17
|
-
"test": "vitest run",
|
|
18
|
-
"test:watch": "vitest"
|
|
19
|
-
},
|
|
20
13
|
"dependencies": {
|
|
21
14
|
"commander": "^12.0.0",
|
|
22
15
|
"@inquirer/prompts": "^7.2.0",
|
|
@@ -55,7 +48,14 @@
|
|
|
55
48
|
],
|
|
56
49
|
"repository": {
|
|
57
50
|
"type": "git",
|
|
58
|
-
"url": "https://github.com/
|
|
51
|
+
"url": "https://github.com/ianyimi/create-z3-app"
|
|
59
52
|
},
|
|
60
|
-
"license": "MIT"
|
|
61
|
-
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
56
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:watch": "vitest"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -50,6 +50,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
50
50
|
|
|
51
51
|
return {
|
|
52
52
|
id: "convex",
|
|
53
|
+
// Tell the convex() plugin this context supports mutations. It checks
|
|
54
|
+
// ctx.context.adapter.options?.isRunMutationCtx — if falsy it replaces all
|
|
55
|
+
// adapter writes with silent no-ops. HTTP actions always have runMutation.
|
|
56
|
+
options: {
|
|
57
|
+
isRunMutationCtx: "runMutation" in ctx,
|
|
58
|
+
},
|
|
53
59
|
|
|
54
60
|
create: async ({ data, model, select }): Promise<any> => {
|
|
55
61
|
return await ctx.runMutation(internal.auth.db.dbCreate, {
|
|
@@ -189,34 +195,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
189
195
|
if (data && fieldAttributes.type === "date") {
|
|
190
196
|
return new Date(data).getTime();
|
|
191
197
|
}
|
|
192
|
-
// Handle array fields - Better Auth may send single values or arrays
|
|
193
|
-
if ((fieldAttributes.type as string)?.endsWith("[]")) {
|
|
194
|
-
// If already an array, return as-is
|
|
195
|
-
if (Array.isArray(data)) {
|
|
196
|
-
return data
|
|
197
|
-
}
|
|
198
|
-
// If it's a string that looks like JSON array, parse it
|
|
199
|
-
if (typeof data === "string") {
|
|
200
|
-
try {
|
|
201
|
-
const parsed = JSON.parse(data)
|
|
202
|
-
return Array.isArray(parsed) ? parsed : [data]
|
|
203
|
-
} catch {
|
|
204
|
-
// If parsing fails, wrap the string in an array
|
|
205
|
-
return [data]
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// For any other value type, wrap in array
|
|
209
|
-
if (data !== null && data !== undefined) {
|
|
210
|
-
return [data]
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
198
|
return data;
|
|
214
199
|
},
|
|
215
200
|
customTransformOutput: ({ data, fieldAttributes }) => {
|
|
216
201
|
if (data && fieldAttributes.type === "date") {
|
|
217
202
|
return new Date(data).getTime();
|
|
218
203
|
}
|
|
219
|
-
// Arrays are stored natively in Convex, no transformation needed for output
|
|
220
204
|
return data;
|
|
221
205
|
},
|
|
222
206
|
debugLogs: config.debugLogs ?? false,
|
|
@@ -230,6 +214,7 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
230
214
|
supportsDates: false,
|
|
231
215
|
supportsJSON: true,
|
|
232
216
|
supportsNumericIds: false,
|
|
217
|
+
supportsArrays: true,
|
|
233
218
|
transaction: false,
|
|
234
219
|
usePlural: false,
|
|
235
220
|
},
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { nextCookies } from "better-auth/next-js"
|
|
2
|
-
import { admin
|
|
2
|
+
import { admin } from "better-auth/plugins"
|
|
3
|
+
import { apiKey } from "@better-auth/api-key"
|
|
4
|
+
import { convex } from "@convex-dev/better-auth/plugins"
|
|
3
5
|
import { USER_ROLES } from "~/db/constants"
|
|
6
|
+
import authConfig from "@convex/auth.config"
|
|
4
7
|
|
|
5
8
|
const plugins = [
|
|
6
9
|
admin({
|
|
7
10
|
adminRoles: [USER_ROLES.admin],
|
|
8
|
-
defaultRole: USER_ROLES.user
|
|
11
|
+
defaultRole: USER_ROLES.user,
|
|
9
12
|
}),
|
|
10
13
|
apiKey(),
|
|
11
14
|
nextCookies(),
|
|
15
|
+
convex({ authConfig }),
|
|
12
16
|
]
|
|
13
17
|
|
|
14
18
|
export default plugins
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values"
|
|
3
3
|
|
|
4
|
-
import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
|
|
4
|
+
import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_API_KEYS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
|
|
5
5
|
|
|
6
6
|
export default defineSchema({
|
|
7
7
|
// Better Auth component tables (type definitions only - actual tables are in component)
|
|
@@ -19,7 +19,8 @@ export default defineSchema({
|
|
|
19
19
|
isAnonymous: v.optional(v.union(v.null(), v.boolean())),
|
|
20
20
|
phoneNumber: v.optional(v.union(v.null(), v.string())),
|
|
21
21
|
phoneNumberVerified: v.optional(v.union(v.null(), v.boolean())),
|
|
22
|
-
role: v.
|
|
22
|
+
role: v.optional(v.string()), // admin plugin — single string in BA 1.5
|
|
23
|
+
roles: v.array(v.string()), // our multi-role field via additionalFields
|
|
23
24
|
twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())),
|
|
24
25
|
updatedAt: v.number(),
|
|
25
26
|
userId: v.optional(v.union(v.null(), v.string()))
|
|
@@ -70,4 +71,31 @@ export default defineSchema({
|
|
|
70
71
|
privateKey: v.optional(v.string()),
|
|
71
72
|
publicKey: v.string(),
|
|
72
73
|
}),
|
|
74
|
+
|
|
75
|
+
// Better Auth 1.5 — apiKey plugin (@better-auth/api-key)
|
|
76
|
+
[TABLE_SLUG_API_KEYS]: defineTable({
|
|
77
|
+
configId: v.string(),
|
|
78
|
+
name: v.optional(v.string()),
|
|
79
|
+
start: v.optional(v.string()),
|
|
80
|
+
referenceId: v.string(),
|
|
81
|
+
prefix: v.optional(v.string()),
|
|
82
|
+
key: v.string(),
|
|
83
|
+
refillInterval: v.optional(v.number()),
|
|
84
|
+
refillAmount: v.optional(v.number()),
|
|
85
|
+
lastRefillAt: v.optional(v.number()),
|
|
86
|
+
enabled: v.optional(v.boolean()),
|
|
87
|
+
rateLimitEnabled: v.optional(v.boolean()),
|
|
88
|
+
rateLimitTimeWindow: v.optional(v.number()),
|
|
89
|
+
rateLimitMax: v.optional(v.number()),
|
|
90
|
+
requestCount: v.optional(v.number()),
|
|
91
|
+
remaining: v.optional(v.number()),
|
|
92
|
+
lastRequest: v.optional(v.number()),
|
|
93
|
+
expiresAt: v.optional(v.number()),
|
|
94
|
+
createdAt: v.number(),
|
|
95
|
+
updatedAt: v.number(),
|
|
96
|
+
permissions: v.optional(v.string()),
|
|
97
|
+
metadata: v.optional(v.string()),
|
|
98
|
+
})
|
|
99
|
+
.index("by_referenceId", ["referenceId"])
|
|
100
|
+
.index("by_key", ["key"]),
|
|
73
101
|
})
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@base-ui/react": "^1.1.0",
|
|
16
|
-
"@
|
|
16
|
+
"@better-auth/api-key": "^1.0.0",
|
|
17
|
+
"@convex-dev/better-auth": "^0.11.0",
|
|
17
18
|
"@convex-dev/react-query": "^0.1.0",
|
|
18
19
|
"@daveyplate/better-auth-ui": "^3.3.15",
|
|
19
20
|
"@t3-oss/env-nextjs": "^0.13.10",
|
|
20
21
|
"@tanstack/react-form": "^1.27.7",
|
|
21
22
|
"@tanstack/react-query": "^5.90.17",
|
|
22
|
-
"better-auth": "^1.
|
|
23
|
+
"better-auth": "^1.5.0",
|
|
23
24
|
"class-variance-authority": "^0.7.1",
|
|
24
25
|
"clsx": "^2.1.1",
|
|
25
26
|
"convex": "^1.31.5",
|
|
@@ -2,10 +2,8 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { convexClient } from "@convex-dev/better-auth/client/plugins";
|
|
4
4
|
import { AuthUIProvider } from "@daveyplate/better-auth-ui";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
apiKeyClient,
|
|
8
|
-
} from "better-auth/client/plugins";
|
|
5
|
+
import { apiKeyClient } from "@better-auth/api-key/client";
|
|
6
|
+
import { adminClient } from "better-auth/client/plugins";
|
|
9
7
|
import { createAuthClient } from 'better-auth/react'
|
|
10
8
|
import Link from "next/link";
|
|
11
9
|
import { useRouter } from "next/navigation";
|
|
@@ -80,9 +80,9 @@ export function hasPermission<Resource extends keyof Permissions>({
|
|
|
80
80
|
resource: Resource;
|
|
81
81
|
user?: null | User;
|
|
82
82
|
}): boolean {
|
|
83
|
-
if (!user?.
|
|
83
|
+
if (!user?.roles) { return false; }
|
|
84
84
|
|
|
85
|
-
return user.
|
|
85
|
+
return user.roles.some((role) => {
|
|
86
86
|
const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
|
|
87
87
|
|
|
88
88
|
if (!permission) { return false }
|
|
@@ -6,6 +6,7 @@ export const TABLE_SLUG_ACCOUNTS = "account" as const;
|
|
|
6
6
|
export const TABLE_SLUG_SESSIONS = "session" as const;
|
|
7
7
|
export const TABLE_SLUG_VERIFICATIONS = "verification" as const;
|
|
8
8
|
export const TABLE_SLUG_JWKS = "jwks" as const;
|
|
9
|
+
export const TABLE_SLUG_API_KEYS = "apikey" as const;
|
|
9
10
|
|
|
10
11
|
export const COLLECTION_SLUG_MEDIA = "media" as const;
|
|
11
12
|
|
|
@@ -46,6 +46,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
46
46
|
|
|
47
47
|
return {
|
|
48
48
|
id: "convex",
|
|
49
|
+
// Tell the convex() plugin this context supports mutations. It checks
|
|
50
|
+
// ctx.context.adapter.options?.isRunMutationCtx — if falsy it replaces all
|
|
51
|
+
// adapter writes with silent no-ops. HTTP actions always have runMutation.
|
|
52
|
+
options: {
|
|
53
|
+
isRunMutationCtx: "runMutation" in ctx,
|
|
54
|
+
},
|
|
49
55
|
|
|
50
56
|
create: async ({ data, model, select }): Promise<any> => {
|
|
51
57
|
return await ctx.runMutation(internal.auth.db.dbCreate, {
|
|
@@ -185,34 +191,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
185
191
|
if (data && fieldAttributes.type === "date") {
|
|
186
192
|
return new Date(data).getTime();
|
|
187
193
|
}
|
|
188
|
-
// Handle array fields - Better Auth may send single values or arrays
|
|
189
|
-
if ((fieldAttributes.type as string)?.endsWith("[]")) {
|
|
190
|
-
// If already an array, return as-is
|
|
191
|
-
if (Array.isArray(data)) {
|
|
192
|
-
return data
|
|
193
|
-
}
|
|
194
|
-
// If it's a string that looks like JSON array, parse it
|
|
195
|
-
if (typeof data === "string") {
|
|
196
|
-
try {
|
|
197
|
-
const parsed = JSON.parse(data)
|
|
198
|
-
return Array.isArray(parsed) ? parsed : [data]
|
|
199
|
-
} catch {
|
|
200
|
-
// If parsing fails, wrap the string in an array
|
|
201
|
-
return [data]
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// For any other value type, wrap in array
|
|
205
|
-
if (data !== null && data !== undefined) {
|
|
206
|
-
return [data]
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
194
|
return data;
|
|
210
195
|
},
|
|
211
196
|
customTransformOutput: ({ data, fieldAttributes }) => {
|
|
212
197
|
if (data && fieldAttributes.type === "date") {
|
|
213
198
|
return new Date(data).getTime();
|
|
214
199
|
}
|
|
215
|
-
// Arrays are stored natively in Convex, no transformation needed for output
|
|
216
200
|
return data;
|
|
217
201
|
},
|
|
218
202
|
debugLogs: config.debugLogs ?? false,
|
|
@@ -226,6 +210,7 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
|
|
|
226
210
|
supportsDates: false,
|
|
227
211
|
supportsJSON: true,
|
|
228
212
|
supportsNumericIds: false,
|
|
213
|
+
supportsArrays: true,
|
|
229
214
|
transaction: false,
|
|
230
215
|
usePlural: false,
|
|
231
216
|
},
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { admin
|
|
1
|
+
import { admin } from "better-auth/plugins"
|
|
2
|
+
import { apiKey } from "@better-auth/api-key"
|
|
3
|
+
import { convex } from "@convex-dev/better-auth/plugins"
|
|
4
|
+
import authConfig from "@convex/auth.config"
|
|
2
5
|
import { USER_ROLES } from "~/db/constants"
|
|
3
6
|
|
|
4
7
|
const plugins = [
|
|
@@ -7,6 +10,7 @@ const plugins = [
|
|
|
7
10
|
defaultRole: USER_ROLES.user
|
|
8
11
|
}),
|
|
9
12
|
apiKey(),
|
|
13
|
+
convex({ authConfig }),
|
|
10
14
|
]
|
|
11
15
|
|
|
12
16
|
export default plugins
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values"
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TABLE_SLUG_ACCOUNTS,
|
|
6
|
+
TABLE_SLUG_API_KEYS,
|
|
7
|
+
TABLE_SLUG_JWKS,
|
|
8
|
+
TABLE_SLUG_SESSIONS,
|
|
9
|
+
TABLE_SLUG_USERS,
|
|
10
|
+
TABLE_SLUG_VERIFICATIONS,
|
|
11
|
+
} from "~/db/constants";
|
|
5
12
|
|
|
6
13
|
export default defineSchema({
|
|
7
14
|
// Better Auth component tables (type definitions only - actual tables are in component)
|
|
@@ -22,7 +29,8 @@ export default defineSchema({
|
|
|
22
29
|
banExpires: v.optional(v.number()), // admin plugin
|
|
23
30
|
banned: v.optional(v.boolean()), // admin plugin
|
|
24
31
|
banReason: v.optional(v.string()), // admin plugin
|
|
25
|
-
role: v.
|
|
32
|
+
role: v.optional(v.string()), // admin plugin — single string in BA 1.5
|
|
33
|
+
roles: v.array(v.string()), // our multi-role field via additionalFields
|
|
26
34
|
})
|
|
27
35
|
.index("by_email", ["email"]),
|
|
28
36
|
|
|
@@ -70,4 +78,31 @@ export default defineSchema({
|
|
|
70
78
|
privateKey: v.optional(v.string()),
|
|
71
79
|
publicKey: v.string(),
|
|
72
80
|
}),
|
|
81
|
+
|
|
82
|
+
// Better Auth 1.5 — apiKey plugin (@better-auth/api-key)
|
|
83
|
+
[TABLE_SLUG_API_KEYS]: defineTable({
|
|
84
|
+
configId: v.string(), // new in 1.5, default "default"
|
|
85
|
+
name: v.optional(v.string()),
|
|
86
|
+
start: v.optional(v.string()),
|
|
87
|
+
referenceId: v.string(), // replaces userId from 1.4
|
|
88
|
+
prefix: v.optional(v.string()),
|
|
89
|
+
key: v.string(),
|
|
90
|
+
refillInterval: v.optional(v.number()),
|
|
91
|
+
refillAmount: v.optional(v.number()),
|
|
92
|
+
lastRefillAt: v.optional(v.number()),
|
|
93
|
+
enabled: v.optional(v.boolean()),
|
|
94
|
+
rateLimitEnabled: v.optional(v.boolean()),
|
|
95
|
+
rateLimitTimeWindow: v.optional(v.number()),
|
|
96
|
+
rateLimitMax: v.optional(v.number()),
|
|
97
|
+
requestCount: v.optional(v.number()),
|
|
98
|
+
remaining: v.optional(v.number()),
|
|
99
|
+
lastRequest: v.optional(v.number()),
|
|
100
|
+
expiresAt: v.optional(v.number()),
|
|
101
|
+
createdAt: v.number(),
|
|
102
|
+
updatedAt: v.number(),
|
|
103
|
+
permissions: v.optional(v.string()),
|
|
104
|
+
metadata: v.optional(v.string()),
|
|
105
|
+
})
|
|
106
|
+
.index("by_referenceId", ["referenceId"])
|
|
107
|
+
.index("by_key", ["key"]),
|
|
73
108
|
})
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@base-ui/react": "^1.0.0",
|
|
20
|
-
"@
|
|
20
|
+
"@better-auth/api-key": "^1.0.0",
|
|
21
|
+
"@convex-dev/better-auth": "^0.11.0",
|
|
21
22
|
"@convex-dev/react-query": "^0.1.0",
|
|
22
23
|
"@daveyplate/better-auth-tanstack": "^1.3.6",
|
|
23
24
|
"@daveyplate/better-auth-ui": "^3.3.10",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"@tanstack/react-router-with-query": "^1.130.17",
|
|
33
34
|
"@tanstack/react-start": "^1.132.0",
|
|
34
35
|
"@tanstack/router-plugin": "^1.132.0",
|
|
35
|
-
"better-auth": "^1.
|
|
36
|
+
"better-auth": "^1.5.0",
|
|
36
37
|
"class-variance-authority": "^0.7.1",
|
|
37
38
|
"clsx": "^2.1.1",
|
|
38
39
|
"convex": "^1.31.2",
|
|
@@ -1,472 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
BellIcon,
|
|
4
|
-
BluetoothIcon,
|
|
5
|
-
CreditCardIcon,
|
|
6
|
-
DownloadIcon,
|
|
7
|
-
EyeIcon,
|
|
8
|
-
FileCodeIcon,
|
|
9
|
-
FileIcon,
|
|
10
|
-
FileTextIcon,
|
|
11
|
-
FolderIcon,
|
|
12
|
-
FolderOpenIcon,
|
|
13
|
-
FolderSearchIcon,
|
|
14
|
-
HelpCircleIcon,
|
|
15
|
-
KeyboardIcon,
|
|
16
|
-
LanguagesIcon,
|
|
17
|
-
LayoutIcon,
|
|
18
|
-
LogOutIcon,
|
|
19
|
-
MailIcon,
|
|
20
|
-
MonitorIcon,
|
|
21
|
-
MoonIcon,
|
|
22
|
-
MoreHorizontalIcon,
|
|
23
|
-
MoreVerticalIcon,
|
|
24
|
-
PaletteIcon,
|
|
25
|
-
PlusIcon,
|
|
26
|
-
SaveIcon,
|
|
27
|
-
SettingsIcon,
|
|
28
|
-
ShieldIcon,
|
|
29
|
-
SunIcon,
|
|
30
|
-
UserIcon,
|
|
31
|
-
} from 'lucide-react'
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { TerminalIcon, UserIcon } from 'lucide-react'
|
|
32
3
|
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
AlertDialog,
|
|
36
|
-
AlertDialogAction,
|
|
37
|
-
AlertDialogCancel,
|
|
38
|
-
AlertDialogContent,
|
|
39
|
-
AlertDialogDescription,
|
|
40
|
-
AlertDialogFooter,
|
|
41
|
-
AlertDialogHeader,
|
|
42
|
-
AlertDialogMedia,
|
|
43
|
-
AlertDialogTitle,
|
|
44
|
-
AlertDialogTrigger,
|
|
45
|
-
} from '~/components/ui/alert-dialog'
|
|
46
|
-
import { Badge } from '~/components/ui/badge'
|
|
4
|
+
import { signOut, useSession } from '~/lib/auth/client'
|
|
47
5
|
import { Button } from '~/components/ui/button'
|
|
48
|
-
import {
|
|
49
|
-
Card,
|
|
50
|
-
CardAction,
|
|
51
|
-
CardContent,
|
|
52
|
-
CardDescription,
|
|
53
|
-
CardFooter,
|
|
54
|
-
CardHeader,
|
|
55
|
-
CardTitle,
|
|
56
|
-
} from '~/components/ui/card'
|
|
57
|
-
import {
|
|
58
|
-
Combobox,
|
|
59
|
-
ComboboxContent,
|
|
60
|
-
ComboboxEmpty,
|
|
61
|
-
ComboboxInput,
|
|
62
|
-
ComboboxItem,
|
|
63
|
-
ComboboxList,
|
|
64
|
-
} from '~/components/ui/combobox'
|
|
65
|
-
import {
|
|
66
|
-
DropdownMenu,
|
|
67
|
-
DropdownMenuCheckboxItem,
|
|
68
|
-
DropdownMenuContent,
|
|
69
|
-
DropdownMenuGroup,
|
|
70
|
-
DropdownMenuItem,
|
|
71
|
-
DropdownMenuLabel,
|
|
72
|
-
DropdownMenuPortal,
|
|
73
|
-
DropdownMenuRadioGroup,
|
|
74
|
-
DropdownMenuRadioItem,
|
|
75
|
-
DropdownMenuSeparator,
|
|
76
|
-
DropdownMenuShortcut,
|
|
77
|
-
DropdownMenuSub,
|
|
78
|
-
DropdownMenuSubContent,
|
|
79
|
-
DropdownMenuSubTrigger,
|
|
80
|
-
DropdownMenuTrigger,
|
|
81
|
-
} from '~/components/ui/dropdown-menu'
|
|
82
|
-
import { Field, FieldGroup, FieldLabel } from '~/components/ui/field'
|
|
83
|
-
import { Input } from '~/components/ui/input'
|
|
84
|
-
import {
|
|
85
|
-
Select,
|
|
86
|
-
SelectContent,
|
|
87
|
-
SelectGroup,
|
|
88
|
-
SelectItem,
|
|
89
|
-
SelectTrigger,
|
|
90
|
-
SelectValue,
|
|
91
|
-
} from '~/components/ui/select'
|
|
92
|
-
import { Textarea } from '~/components/ui/textarea'
|
|
93
|
-
import { ThemeToggle } from '~/components/ui/theme-toggle'
|
|
94
6
|
|
|
95
7
|
export function ComponentExample() {
|
|
96
|
-
|
|
97
|
-
<ExampleWrapper>
|
|
98
|
-
<ThemeToggle className="absolute w-md:top-10 right-10 top-2" />
|
|
99
|
-
<CardExample />
|
|
100
|
-
<FormExample />
|
|
101
|
-
</ExampleWrapper>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
8
|
+
const { data: session } = useSession()
|
|
104
9
|
|
|
105
|
-
function CardExample() {
|
|
106
10
|
return (
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
<div className="
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<CardHeader>
|
|
117
|
-
<CardTitle>Observability Plus is replacing Monitoring</CardTitle>
|
|
118
|
-
<CardDescription>
|
|
119
|
-
Switch to the improved way to explore your data, with natural language. Monitoring will
|
|
120
|
-
no longer be available on the Pro plan in November, 2025
|
|
121
|
-
</CardDescription>
|
|
122
|
-
</CardHeader>
|
|
123
|
-
<CardFooter>
|
|
124
|
-
<AlertDialog>
|
|
125
|
-
<AlertDialogTrigger render={<Button />}>
|
|
126
|
-
<PlusIcon data-icon="inline-start" />
|
|
127
|
-
Show Dialog
|
|
128
|
-
</AlertDialogTrigger>
|
|
129
|
-
<AlertDialogContent size="sm">
|
|
130
|
-
<AlertDialogHeader>
|
|
131
|
-
<AlertDialogMedia>
|
|
132
|
-
<BluetoothIcon />
|
|
133
|
-
</AlertDialogMedia>
|
|
134
|
-
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
|
135
|
-
<AlertDialogDescription>
|
|
136
|
-
Do you want to allow the USB accessory to connect to this device?
|
|
137
|
-
</AlertDialogDescription>
|
|
138
|
-
</AlertDialogHeader>
|
|
139
|
-
<AlertDialogFooter>
|
|
140
|
-
<AlertDialogCancel>Don't allow</AlertDialogCancel>
|
|
141
|
-
<AlertDialogAction>Allow</AlertDialogAction>
|
|
142
|
-
</AlertDialogFooter>
|
|
143
|
-
</AlertDialogContent>
|
|
144
|
-
</AlertDialog>
|
|
145
|
-
<Badge variant="secondary" className="ml-auto">
|
|
146
|
-
Warning
|
|
147
|
-
</Badge>
|
|
148
|
-
</CardFooter>
|
|
149
|
-
</Card>
|
|
150
|
-
</Example>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const frameworks = ['Next.js', 'SvelteKit', 'Nuxt.js', 'Remix', 'Astro'] as const
|
|
11
|
+
<div className="bg-background flex min-h-screen flex-col items-center justify-center gap-8 p-6 text-center">
|
|
12
|
+
{session?.user && (
|
|
13
|
+
<div className="absolute top-4 right-4 flex items-center gap-2">
|
|
14
|
+
<div className="bg-muted flex size-8 items-center justify-center rounded-full">
|
|
15
|
+
<UserIcon className="text-muted-foreground size-4" />
|
|
16
|
+
</div>
|
|
17
|
+
<span className="text-sm font-medium">{session.user.name}</span>
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
155
20
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
21
|
+
<div className="flex flex-col items-center gap-4">
|
|
22
|
+
<div className="bg-foreground text-background flex size-14 items-center justify-center rounded-2xl">
|
|
23
|
+
<TerminalIcon className="size-7" />
|
|
24
|
+
</div>
|
|
25
|
+
<div className="flex flex-col gap-1">
|
|
26
|
+
<h1 className="text-3xl font-bold tracking-tight">create-z3-app</h1>
|
|
27
|
+
<p className="text-muted-foreground max-w-sm text-base">
|
|
28
|
+
A full-stack starter with TanStack Start, Convex, and Better Auth — ready to ship.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
162
32
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
33
|
+
<div className="flex gap-3">
|
|
34
|
+
{session?.user ? (
|
|
35
|
+
<Button variant="outline" onClick={() => signOut()}>Sign out</Button>
|
|
36
|
+
) : (
|
|
37
|
+
<>
|
|
38
|
+
<Button asChild>
|
|
39
|
+
<Link to="/auth/$authView" params={{ authView: 'sign-up' }}>Sign up</Link>
|
|
40
|
+
</Button>
|
|
41
|
+
<Button variant="outline" asChild>
|
|
42
|
+
<Link to="/auth/$authView" params={{ authView: 'sign-in' }}>Sign in</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
170
47
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<CardTitle>User Information</CardTitle>
|
|
176
|
-
<CardDescription>Please fill in your details below</CardDescription>
|
|
177
|
-
<CardAction>
|
|
178
|
-
<DropdownMenu>
|
|
179
|
-
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
|
|
180
|
-
<MoreVerticalIcon />
|
|
181
|
-
<span className="sr-only">More options</span>
|
|
182
|
-
</DropdownMenuTrigger>
|
|
183
|
-
<DropdownMenuContent align="end" className="w-56">
|
|
184
|
-
<DropdownMenuGroup>
|
|
185
|
-
<DropdownMenuLabel>File</DropdownMenuLabel>
|
|
186
|
-
<DropdownMenuItem>
|
|
187
|
-
<FileIcon />
|
|
188
|
-
New File
|
|
189
|
-
<DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
|
|
190
|
-
</DropdownMenuItem>
|
|
191
|
-
<DropdownMenuItem>
|
|
192
|
-
<FolderIcon />
|
|
193
|
-
New Folder
|
|
194
|
-
<DropdownMenuShortcut>⇧⌘N</DropdownMenuShortcut>
|
|
195
|
-
</DropdownMenuItem>
|
|
196
|
-
<DropdownMenuSub>
|
|
197
|
-
<DropdownMenuSubTrigger>
|
|
198
|
-
<FolderOpenIcon />
|
|
199
|
-
Open Recent
|
|
200
|
-
</DropdownMenuSubTrigger>
|
|
201
|
-
<DropdownMenuPortal>
|
|
202
|
-
<DropdownMenuSubContent>
|
|
203
|
-
<DropdownMenuGroup>
|
|
204
|
-
<DropdownMenuLabel>Recent Projects</DropdownMenuLabel>
|
|
205
|
-
<DropdownMenuItem>
|
|
206
|
-
<FileCodeIcon />
|
|
207
|
-
Project Alpha
|
|
208
|
-
</DropdownMenuItem>
|
|
209
|
-
<DropdownMenuItem>
|
|
210
|
-
<FileCodeIcon />
|
|
211
|
-
Project Beta
|
|
212
|
-
</DropdownMenuItem>
|
|
213
|
-
<DropdownMenuSub>
|
|
214
|
-
<DropdownMenuSubTrigger>
|
|
215
|
-
<MoreHorizontalIcon />
|
|
216
|
-
More Projects
|
|
217
|
-
</DropdownMenuSubTrigger>
|
|
218
|
-
<DropdownMenuPortal>
|
|
219
|
-
<DropdownMenuSubContent>
|
|
220
|
-
<DropdownMenuItem>
|
|
221
|
-
<FileCodeIcon />
|
|
222
|
-
Project Gamma
|
|
223
|
-
</DropdownMenuItem>
|
|
224
|
-
<DropdownMenuItem>
|
|
225
|
-
<FileCodeIcon />
|
|
226
|
-
Project Delta
|
|
227
|
-
</DropdownMenuItem>
|
|
228
|
-
</DropdownMenuSubContent>
|
|
229
|
-
</DropdownMenuPortal>
|
|
230
|
-
</DropdownMenuSub>
|
|
231
|
-
</DropdownMenuGroup>
|
|
232
|
-
<DropdownMenuSeparator />
|
|
233
|
-
<DropdownMenuGroup>
|
|
234
|
-
<DropdownMenuItem>
|
|
235
|
-
<FolderSearchIcon />
|
|
236
|
-
Browse...
|
|
237
|
-
</DropdownMenuItem>
|
|
238
|
-
</DropdownMenuGroup>
|
|
239
|
-
</DropdownMenuSubContent>
|
|
240
|
-
</DropdownMenuPortal>
|
|
241
|
-
</DropdownMenuSub>
|
|
242
|
-
<DropdownMenuSeparator />
|
|
243
|
-
<DropdownMenuItem>
|
|
244
|
-
<SaveIcon />
|
|
245
|
-
Save
|
|
246
|
-
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
247
|
-
</DropdownMenuItem>
|
|
248
|
-
<DropdownMenuItem>
|
|
249
|
-
<DownloadIcon />
|
|
250
|
-
Export
|
|
251
|
-
<DropdownMenuShortcut>⇧⌘E</DropdownMenuShortcut>
|
|
252
|
-
</DropdownMenuItem>
|
|
253
|
-
</DropdownMenuGroup>
|
|
254
|
-
<DropdownMenuSeparator />
|
|
255
|
-
<DropdownMenuGroup>
|
|
256
|
-
<DropdownMenuLabel>View</DropdownMenuLabel>
|
|
257
|
-
<DropdownMenuCheckboxItem
|
|
258
|
-
checked={notifications.email}
|
|
259
|
-
onCheckedChange={(checked) =>
|
|
260
|
-
setNotifications({
|
|
261
|
-
...notifications,
|
|
262
|
-
email: checked === true,
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
>
|
|
266
|
-
<EyeIcon />
|
|
267
|
-
Show Sidebar
|
|
268
|
-
</DropdownMenuCheckboxItem>
|
|
269
|
-
<DropdownMenuCheckboxItem
|
|
270
|
-
checked={notifications.sms}
|
|
271
|
-
onCheckedChange={(checked) =>
|
|
272
|
-
setNotifications({
|
|
273
|
-
...notifications,
|
|
274
|
-
sms: checked === true,
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
>
|
|
278
|
-
<LayoutIcon />
|
|
279
|
-
Show Status Bar
|
|
280
|
-
</DropdownMenuCheckboxItem>
|
|
281
|
-
<DropdownMenuSub>
|
|
282
|
-
<DropdownMenuSubTrigger>
|
|
283
|
-
<PaletteIcon />
|
|
284
|
-
Theme
|
|
285
|
-
</DropdownMenuSubTrigger>
|
|
286
|
-
<DropdownMenuPortal>
|
|
287
|
-
<DropdownMenuSubContent>
|
|
288
|
-
<DropdownMenuGroup>
|
|
289
|
-
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
|
|
290
|
-
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
|
291
|
-
<DropdownMenuRadioItem value="light">
|
|
292
|
-
<SunIcon />
|
|
293
|
-
Light
|
|
294
|
-
</DropdownMenuRadioItem>
|
|
295
|
-
<DropdownMenuRadioItem value="dark">
|
|
296
|
-
<MoonIcon />
|
|
297
|
-
Dark
|
|
298
|
-
</DropdownMenuRadioItem>
|
|
299
|
-
<DropdownMenuRadioItem value="system">
|
|
300
|
-
<MonitorIcon />
|
|
301
|
-
System
|
|
302
|
-
</DropdownMenuRadioItem>
|
|
303
|
-
</DropdownMenuRadioGroup>
|
|
304
|
-
</DropdownMenuGroup>
|
|
305
|
-
</DropdownMenuSubContent>
|
|
306
|
-
</DropdownMenuPortal>
|
|
307
|
-
</DropdownMenuSub>
|
|
308
|
-
</DropdownMenuGroup>
|
|
309
|
-
<DropdownMenuSeparator />
|
|
310
|
-
<DropdownMenuGroup>
|
|
311
|
-
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
|
312
|
-
<DropdownMenuItem>
|
|
313
|
-
<UserIcon />
|
|
314
|
-
Profile
|
|
315
|
-
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
|
316
|
-
</DropdownMenuItem>
|
|
317
|
-
<DropdownMenuItem>
|
|
318
|
-
<CreditCardIcon />
|
|
319
|
-
Billing
|
|
320
|
-
</DropdownMenuItem>
|
|
321
|
-
<DropdownMenuSub>
|
|
322
|
-
<DropdownMenuSubTrigger>
|
|
323
|
-
<SettingsIcon />
|
|
324
|
-
Settings
|
|
325
|
-
</DropdownMenuSubTrigger>
|
|
326
|
-
<DropdownMenuPortal>
|
|
327
|
-
<DropdownMenuSubContent>
|
|
328
|
-
<DropdownMenuGroup>
|
|
329
|
-
<DropdownMenuLabel>Preferences</DropdownMenuLabel>
|
|
330
|
-
<DropdownMenuItem>
|
|
331
|
-
<KeyboardIcon />
|
|
332
|
-
Keyboard Shortcuts
|
|
333
|
-
</DropdownMenuItem>
|
|
334
|
-
<DropdownMenuItem>
|
|
335
|
-
<LanguagesIcon />
|
|
336
|
-
Language
|
|
337
|
-
</DropdownMenuItem>
|
|
338
|
-
<DropdownMenuSub>
|
|
339
|
-
<DropdownMenuSubTrigger>
|
|
340
|
-
<BellIcon />
|
|
341
|
-
Notifications
|
|
342
|
-
</DropdownMenuSubTrigger>
|
|
343
|
-
<DropdownMenuPortal>
|
|
344
|
-
<DropdownMenuSubContent>
|
|
345
|
-
<DropdownMenuGroup>
|
|
346
|
-
<DropdownMenuLabel>Notification Types</DropdownMenuLabel>
|
|
347
|
-
<DropdownMenuCheckboxItem
|
|
348
|
-
checked={notifications.push}
|
|
349
|
-
onCheckedChange={(checked) =>
|
|
350
|
-
setNotifications({
|
|
351
|
-
...notifications,
|
|
352
|
-
push: checked === true,
|
|
353
|
-
})
|
|
354
|
-
}
|
|
355
|
-
>
|
|
356
|
-
<BellIcon />
|
|
357
|
-
Push Notifications
|
|
358
|
-
</DropdownMenuCheckboxItem>
|
|
359
|
-
<DropdownMenuCheckboxItem
|
|
360
|
-
checked={notifications.email}
|
|
361
|
-
onCheckedChange={(checked) =>
|
|
362
|
-
setNotifications({
|
|
363
|
-
...notifications,
|
|
364
|
-
email: checked === true,
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
>
|
|
368
|
-
<MailIcon />
|
|
369
|
-
Email Notifications
|
|
370
|
-
</DropdownMenuCheckboxItem>
|
|
371
|
-
</DropdownMenuGroup>
|
|
372
|
-
</DropdownMenuSubContent>
|
|
373
|
-
</DropdownMenuPortal>
|
|
374
|
-
</DropdownMenuSub>
|
|
375
|
-
</DropdownMenuGroup>
|
|
376
|
-
<DropdownMenuSeparator />
|
|
377
|
-
<DropdownMenuGroup>
|
|
378
|
-
<DropdownMenuItem>
|
|
379
|
-
<ShieldIcon />
|
|
380
|
-
Privacy & Security
|
|
381
|
-
</DropdownMenuItem>
|
|
382
|
-
</DropdownMenuGroup>
|
|
383
|
-
</DropdownMenuSubContent>
|
|
384
|
-
</DropdownMenuPortal>
|
|
385
|
-
</DropdownMenuSub>
|
|
386
|
-
</DropdownMenuGroup>
|
|
387
|
-
<DropdownMenuSeparator />
|
|
388
|
-
<DropdownMenuGroup>
|
|
389
|
-
<DropdownMenuItem>
|
|
390
|
-
<HelpCircleIcon />
|
|
391
|
-
Help & Support
|
|
392
|
-
</DropdownMenuItem>
|
|
393
|
-
<DropdownMenuItem>
|
|
394
|
-
<FileTextIcon />
|
|
395
|
-
Documentation
|
|
396
|
-
</DropdownMenuItem>
|
|
397
|
-
</DropdownMenuGroup>
|
|
398
|
-
<DropdownMenuSeparator />
|
|
399
|
-
<DropdownMenuGroup>
|
|
400
|
-
<DropdownMenuItem variant="destructive">
|
|
401
|
-
<LogOutIcon />
|
|
402
|
-
Sign Out
|
|
403
|
-
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
|
404
|
-
</DropdownMenuItem>
|
|
405
|
-
</DropdownMenuGroup>
|
|
406
|
-
</DropdownMenuContent>
|
|
407
|
-
</DropdownMenu>
|
|
408
|
-
</CardAction>
|
|
409
|
-
</CardHeader>
|
|
410
|
-
<CardContent>
|
|
411
|
-
<form>
|
|
412
|
-
<FieldGroup>
|
|
413
|
-
<div className="grid grid-cols-2 gap-4">
|
|
414
|
-
<Field>
|
|
415
|
-
<FieldLabel htmlFor="small-form-name">Name</FieldLabel>
|
|
416
|
-
<Input id="small-form-name" placeholder="Enter your name" required />
|
|
417
|
-
</Field>
|
|
418
|
-
<Field>
|
|
419
|
-
<FieldLabel htmlFor="small-form-role">Role</FieldLabel>
|
|
420
|
-
<Select items={roleItems} defaultValue={null}>
|
|
421
|
-
<SelectTrigger id="small-form-role">
|
|
422
|
-
<SelectValue />
|
|
423
|
-
</SelectTrigger>
|
|
424
|
-
<SelectContent>
|
|
425
|
-
<SelectGroup>
|
|
426
|
-
{roleItems.map((item) => (
|
|
427
|
-
<SelectItem key={item.value} value={item.value}>
|
|
428
|
-
{item.label}
|
|
429
|
-
</SelectItem>
|
|
430
|
-
))}
|
|
431
|
-
</SelectGroup>
|
|
432
|
-
</SelectContent>
|
|
433
|
-
</Select>
|
|
434
|
-
</Field>
|
|
435
|
-
</div>
|
|
436
|
-
<Field>
|
|
437
|
-
<FieldLabel htmlFor="small-form-framework">Framework</FieldLabel>
|
|
438
|
-
<Combobox items={frameworks}>
|
|
439
|
-
<ComboboxInput
|
|
440
|
-
id="small-form-framework"
|
|
441
|
-
placeholder="Select a framework"
|
|
442
|
-
required
|
|
443
|
-
/>
|
|
444
|
-
<ComboboxContent>
|
|
445
|
-
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
|
|
446
|
-
<ComboboxList>
|
|
447
|
-
{(item) => (
|
|
448
|
-
<ComboboxItem key={item} value={item}>
|
|
449
|
-
{item}
|
|
450
|
-
</ComboboxItem>
|
|
451
|
-
)}
|
|
452
|
-
</ComboboxList>
|
|
453
|
-
</ComboboxContent>
|
|
454
|
-
</Combobox>
|
|
455
|
-
</Field>
|
|
456
|
-
<Field>
|
|
457
|
-
<FieldLabel htmlFor="small-form-comments">Comments</FieldLabel>
|
|
458
|
-
<Textarea id="small-form-comments" placeholder="Add any additional comments" />
|
|
459
|
-
</Field>
|
|
460
|
-
<Field orientation="horizontal">
|
|
461
|
-
<Button type="submit">Submit</Button>
|
|
462
|
-
<Button variant="outline" type="button">
|
|
463
|
-
Cancel
|
|
464
|
-
</Button>
|
|
465
|
-
</Field>
|
|
466
|
-
</FieldGroup>
|
|
467
|
-
</form>
|
|
468
|
-
</CardContent>
|
|
469
|
-
</Card>
|
|
470
|
-
</Example>
|
|
48
|
+
<code className="bg-muted text-muted-foreground rounded-lg px-4 py-2 text-sm font-mono">
|
|
49
|
+
npm create z3-app@latest
|
|
50
|
+
</code>
|
|
51
|
+
</div>
|
|
471
52
|
)
|
|
472
53
|
}
|
|
@@ -6,3 +6,4 @@ export const TABLE_SLUG_ACCOUNTS = "account" as const;
|
|
|
6
6
|
export const TABLE_SLUG_SESSIONS = "session" as const;
|
|
7
7
|
export const TABLE_SLUG_VERIFICATIONS = "verification" as const;
|
|
8
8
|
export const TABLE_SLUG_JWKS = "jwks" as const;
|
|
9
|
+
export const TABLE_SLUG_API_KEYS = "apikey" as const;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAuthClient } from "better-auth/react"
|
|
2
|
-
import { adminClient
|
|
2
|
+
import { adminClient } from "better-auth/client/plugins"
|
|
3
|
+
import { apiKeyClient } from "@better-auth/api-key/client"
|
|
3
4
|
import { env } from '~/env';
|
|
4
5
|
|
|
5
6
|
export const authClient = createAuthClient({
|
|
@@ -80,9 +80,9 @@ export function hasPermission<Resource extends keyof Permissions>({
|
|
|
80
80
|
resource: Resource;
|
|
81
81
|
user?: null | User;
|
|
82
82
|
}): boolean {
|
|
83
|
-
if (!user?.
|
|
83
|
+
if (!user?.roles) { return false; }
|
|
84
84
|
|
|
85
|
-
return user.
|
|
85
|
+
return user.roles.some((role) => {
|
|
86
86
|
const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
|
|
87
87
|
|
|
88
88
|
if (!permission) { return false }
|
|
@@ -8,7 +8,7 @@ export const Route = createFileRoute('/auth/$authView')({
|
|
|
8
8
|
function RouteComponent() {
|
|
9
9
|
const { authView } = Route.useParams()
|
|
10
10
|
return (
|
|
11
|
-
<main className="
|
|
11
|
+
<main className="mx-auto flex h-[100svh] grow flex-col items-center justify-center gap-3 self-center p-4 md:p-6">
|
|
12
12
|
<AuthView pathname={authView} />
|
|
13
13
|
</main>
|
|
14
14
|
)
|