@ramonclaudio/create-vexpo 0.1.0
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/README.md +50 -0
- package/dist/index.js +183 -0
- package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
- package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
- package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
- package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
- package/dist/templates/default/.eas/workflows/release.yml +44 -0
- package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
- package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
- package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
- package/dist/templates/default/.github/workflows/check.yml +28 -0
- package/dist/templates/default/.maestro/launch.yaml +18 -0
- package/dist/templates/default/AGENTS.md +79 -0
- package/dist/templates/default/DESIGN.md +331 -0
- package/dist/templates/default/LICENSE +21 -0
- package/dist/templates/default/README.md +153 -0
- package/dist/templates/default/SETUP.md +618 -0
- package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
- package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
- package/dist/templates/default/_easignore +22 -0
- package/dist/templates/default/_editorconfig +9 -0
- package/dist/templates/default/_env.example +34 -0
- package/dist/templates/default/_fingerprintignore +24 -0
- package/dist/templates/default/_gitattributes +7 -0
- package/dist/templates/default/_gitignore +69 -0
- package/dist/templates/default/_oxfmtrc.json +3 -0
- package/dist/templates/default/_oxlintrc.json +34 -0
- package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
- package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
- package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
- package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
- package/dist/templates/default/app/(app)/_layout.tsx +73 -0
- package/dist/templates/default/app/(app)/debug.tsx +389 -0
- package/dist/templates/default/app/(app)/help.tsx +254 -0
- package/dist/templates/default/app/(app)/linked.tsx +116 -0
- package/dist/templates/default/app/(app)/privacy.tsx +159 -0
- package/dist/templates/default/app/(app)/profile.tsx +915 -0
- package/dist/templates/default/app/(app)/sessions.tsx +191 -0
- package/dist/templates/default/app/(app)/welcome.tsx +140 -0
- package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
- package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
- package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
- package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
- package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
- package/dist/templates/default/app/+native-intent.tsx +14 -0
- package/dist/templates/default/app/+not-found.tsx +51 -0
- package/dist/templates/default/app/_layout.tsx +102 -0
- package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
- package/dist/templates/default/app-store/screenshots/README.md +13 -0
- package/dist/templates/default/app.config.ts +201 -0
- package/dist/templates/default/app.json +11 -0
- package/dist/templates/default/assets/brand-icon-dark.png +0 -0
- package/dist/templates/default/assets/brand-icon-light.png +0 -0
- package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
- package/dist/templates/default/assets/icon.png +0 -0
- package/dist/templates/default/assets/sounds/notification.wav +0 -0
- package/dist/templates/default/assets/splash-image-dark.png +0 -0
- package/dist/templates/default/assets/splash-image-light.png +0 -0
- package/dist/templates/default/bun.lock +1860 -0
- package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
- package/dist/templates/default/components/auth/password-field.tsx +121 -0
- package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
- package/dist/templates/default/components/ui/convex-error.tsx +32 -0
- package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
- package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
- package/dist/templates/default/components/ui/material.tsx +94 -0
- package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
- package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
- package/dist/templates/default/components/ui/skeleton.tsx +107 -0
- package/dist/templates/default/components/ui/status-text.tsx +49 -0
- package/dist/templates/default/components/ui/update-banner.tsx +82 -0
- package/dist/templates/default/constants/layout.ts +102 -0
- package/dist/templates/default/constants/theme.ts +401 -0
- package/dist/templates/default/constants/ui.ts +77 -0
- package/dist/templates/default/convex/_generated/api.d.ts +77 -0
- package/dist/templates/default/convex/_generated/api.js +23 -0
- package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
- package/dist/templates/default/convex/_generated/server.d.ts +143 -0
- package/dist/templates/default/convex/_generated/server.js +93 -0
- package/dist/templates/default/convex/admin.ts +102 -0
- package/dist/templates/default/convex/auth.config.ts +6 -0
- package/dist/templates/default/convex/auth.ts +335 -0
- package/dist/templates/default/convex/constants.ts +46 -0
- package/dist/templates/default/convex/convex.config.ts +11 -0
- package/dist/templates/default/convex/crons.ts +42 -0
- package/dist/templates/default/convex/email.ts +109 -0
- package/dist/templates/default/convex/env.ts +31 -0
- package/dist/templates/default/convex/errors.ts +33 -0
- package/dist/templates/default/convex/functions.ts +54 -0
- package/dist/templates/default/convex/http.ts +176 -0
- package/dist/templates/default/convex/log.ts +81 -0
- package/dist/templates/default/convex/pushTokens.ts +114 -0
- package/dist/templates/default/convex/rateLimit.ts +92 -0
- package/dist/templates/default/convex/schema.ts +28 -0
- package/dist/templates/default/convex/tsconfig.json +18 -0
- package/dist/templates/default/convex/users.ts +279 -0
- package/dist/templates/default/convex/validators.ts +74 -0
- package/dist/templates/default/convex/webhook.ts +193 -0
- package/dist/templates/default/convex.json +6 -0
- package/dist/templates/default/eas.json +56 -0
- package/dist/templates/default/fingerprint.config.js +9 -0
- package/dist/templates/default/hooks/use-debounce.ts +20 -0
- package/dist/templates/default/hooks/use-deep-link.ts +43 -0
- package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
- package/dist/templates/default/hooks/use-network.ts +11 -0
- package/dist/templates/default/hooks/use-notifications.ts +107 -0
- package/dist/templates/default/hooks/use-onboarding.ts +15 -0
- package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
- package/dist/templates/default/hooks/use-theme.ts +53 -0
- package/dist/templates/default/hooks/use-updates.ts +86 -0
- package/dist/templates/default/lib/a11y.ts +5 -0
- package/dist/templates/default/lib/app.ts +14 -0
- package/dist/templates/default/lib/assets.ts +17 -0
- package/dist/templates/default/lib/auth-client.ts +21 -0
- package/dist/templates/default/lib/convex-auth.tsx +79 -0
- package/dist/templates/default/lib/deep-link.ts +71 -0
- package/dist/templates/default/lib/dev-menu.ts +119 -0
- package/dist/templates/default/lib/device.ts +40 -0
- package/dist/templates/default/lib/dynamic-font.ts +49 -0
- package/dist/templates/default/lib/env.ts +10 -0
- package/dist/templates/default/lib/haptics.ts +24 -0
- package/dist/templates/default/lib/notifications.ts +276 -0
- package/dist/templates/default/lib/preferences.ts +45 -0
- package/dist/templates/default/lib/schemas.ts +137 -0
- package/dist/templates/default/lib/storage.ts +47 -0
- package/dist/templates/default/lib/updates.ts +107 -0
- package/dist/templates/default/metro.config.js +14 -0
- package/dist/templates/default/package.json +129 -0
- package/dist/templates/default/patches/PR-368.patch +91 -0
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- package/dist/templates/default/plugins/README.md +9 -0
- package/dist/templates/default/plugins/with-auto-signing.js +45 -0
- package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
- package/dist/templates/default/scripts/README.md +36 -0
- package/dist/templates/default/scripts/_run.mjs +77 -0
- package/dist/templates/default/scripts/clean.ts +543 -0
- package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
- package/dist/templates/default/store.config.json +58 -0
- package/dist/templates/default/tsconfig.json +13 -0
- package/dist/templates/default/vitest.config.ts +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ActionBuilder,
|
|
13
|
+
HttpActionBuilder,
|
|
14
|
+
MutationBuilder,
|
|
15
|
+
QueryBuilder,
|
|
16
|
+
GenericActionCtx,
|
|
17
|
+
GenericMutationCtx,
|
|
18
|
+
GenericQueryCtx,
|
|
19
|
+
GenericDatabaseReader,
|
|
20
|
+
GenericDatabaseWriter,
|
|
21
|
+
} from "convex/server";
|
|
22
|
+
import type { DataModel } from "./dataModel.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Define a query in this Convex app's public API.
|
|
26
|
+
*
|
|
27
|
+
* This function will be allowed to read your Convex database and will be accessible from the client.
|
|
28
|
+
*
|
|
29
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
30
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
31
|
+
*/
|
|
32
|
+
export declare const query: QueryBuilder<DataModel, "public">;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Define a query that is only accessible from other Convex functions (but not from the client).
|
|
36
|
+
*
|
|
37
|
+
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
|
38
|
+
*
|
|
39
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
40
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
41
|
+
*/
|
|
42
|
+
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Define a mutation in this Convex app's public API.
|
|
46
|
+
*
|
|
47
|
+
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
|
48
|
+
*
|
|
49
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
50
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
51
|
+
*/
|
|
52
|
+
export declare const mutation: MutationBuilder<DataModel, "public">;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
|
56
|
+
*
|
|
57
|
+
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
|
58
|
+
*
|
|
59
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
60
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
61
|
+
*/
|
|
62
|
+
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Define an action in this Convex app's public API.
|
|
66
|
+
*
|
|
67
|
+
* An action is a function which can execute any JavaScript code, including non-deterministic
|
|
68
|
+
* code and code with side-effects, like calling third-party services.
|
|
69
|
+
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
|
70
|
+
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
|
71
|
+
*
|
|
72
|
+
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
|
73
|
+
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
|
74
|
+
*/
|
|
75
|
+
export declare const action: ActionBuilder<DataModel, "public">;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Define an action that is only accessible from other Convex functions (but not from the client).
|
|
79
|
+
*
|
|
80
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
|
81
|
+
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
|
82
|
+
*/
|
|
83
|
+
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Define an HTTP action.
|
|
87
|
+
*
|
|
88
|
+
* The wrapped function will be used to respond to HTTP requests received
|
|
89
|
+
* by a Convex deployment if the requests matches the path and method where
|
|
90
|
+
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
|
91
|
+
*
|
|
92
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
|
93
|
+
* and a Fetch API `Request` object as its second.
|
|
94
|
+
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
|
95
|
+
*/
|
|
96
|
+
export declare const httpAction: HttpActionBuilder;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A set of services for use within Convex query functions.
|
|
100
|
+
*
|
|
101
|
+
* The query context is passed as the first argument to any Convex query
|
|
102
|
+
* function run on the server.
|
|
103
|
+
*
|
|
104
|
+
* This differs from the {@link MutationCtx} because all of the services are
|
|
105
|
+
* read-only.
|
|
106
|
+
*/
|
|
107
|
+
export type QueryCtx = GenericQueryCtx<DataModel>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A set of services for use within Convex mutation functions.
|
|
111
|
+
*
|
|
112
|
+
* The mutation context is passed as the first argument to any Convex mutation
|
|
113
|
+
* function run on the server.
|
|
114
|
+
*/
|
|
115
|
+
export type MutationCtx = GenericMutationCtx<DataModel>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A set of services for use within Convex action functions.
|
|
119
|
+
*
|
|
120
|
+
* The action context is passed as the first argument to any Convex action
|
|
121
|
+
* function run on the server.
|
|
122
|
+
*/
|
|
123
|
+
export type ActionCtx = GenericActionCtx<DataModel>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* An interface to read from the database within Convex query functions.
|
|
127
|
+
*
|
|
128
|
+
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
|
129
|
+
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
|
130
|
+
* building a query.
|
|
131
|
+
*/
|
|
132
|
+
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An interface to read from and write to the database within Convex mutation
|
|
136
|
+
* functions.
|
|
137
|
+
*
|
|
138
|
+
* Convex guarantees that all writes within a single mutation are
|
|
139
|
+
* executed atomically, so you never have to worry about partial writes leaving
|
|
140
|
+
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
|
141
|
+
* for the guarantees Convex provides your functions.
|
|
142
|
+
*/
|
|
143
|
+
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
actionGeneric,
|
|
13
|
+
httpActionGeneric,
|
|
14
|
+
queryGeneric,
|
|
15
|
+
mutationGeneric,
|
|
16
|
+
internalActionGeneric,
|
|
17
|
+
internalMutationGeneric,
|
|
18
|
+
internalQueryGeneric,
|
|
19
|
+
} from "convex/server";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Define a query in this Convex app's public API.
|
|
23
|
+
*
|
|
24
|
+
* This function will be allowed to read your Convex database and will be accessible from the client.
|
|
25
|
+
*
|
|
26
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
27
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
28
|
+
*/
|
|
29
|
+
export const query = queryGeneric;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Define a query that is only accessible from other Convex functions (but not from the client).
|
|
33
|
+
*
|
|
34
|
+
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
|
35
|
+
*
|
|
36
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
37
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
38
|
+
*/
|
|
39
|
+
export const internalQuery = internalQueryGeneric;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Define a mutation in this Convex app's public API.
|
|
43
|
+
*
|
|
44
|
+
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
|
45
|
+
*
|
|
46
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
47
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
48
|
+
*/
|
|
49
|
+
export const mutation = mutationGeneric;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
|
53
|
+
*
|
|
54
|
+
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
|
55
|
+
*
|
|
56
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
57
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
58
|
+
*/
|
|
59
|
+
export const internalMutation = internalMutationGeneric;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Define an action in this Convex app's public API.
|
|
63
|
+
*
|
|
64
|
+
* An action is a function which can execute any JavaScript code, including non-deterministic
|
|
65
|
+
* code and code with side-effects, like calling third-party services.
|
|
66
|
+
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
|
67
|
+
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
|
68
|
+
*
|
|
69
|
+
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
|
70
|
+
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
|
71
|
+
*/
|
|
72
|
+
export const action = actionGeneric;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Define an action that is only accessible from other Convex functions (but not from the client).
|
|
76
|
+
*
|
|
77
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
|
78
|
+
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
|
79
|
+
*/
|
|
80
|
+
export const internalAction = internalActionGeneric;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Define an HTTP action.
|
|
84
|
+
*
|
|
85
|
+
* The wrapped function will be used to respond to HTTP requests received
|
|
86
|
+
* by a Convex deployment if the requests matches the path and method where
|
|
87
|
+
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
|
88
|
+
*
|
|
89
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
|
90
|
+
* and a Fetch API `Request` object as its second.
|
|
91
|
+
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
|
92
|
+
*/
|
|
93
|
+
export const httpAction = httpActionGeneric;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin actions for fixture / review accounts. Internal-only, never exposed
|
|
3
|
+
* to client code. Run via `bunx convex run admin:<fn>` (or your PM's dlx).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { v } from "convex/values";
|
|
7
|
+
|
|
8
|
+
import { components } from "./_generated/api";
|
|
9
|
+
import { internalAction, internalMutation } from "./_generated/server";
|
|
10
|
+
import { createAuth } from "./auth";
|
|
11
|
+
import { rateLimiter, type RateLimitName } from "./rateLimit";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a fully verified review account. Used by `setup:review-account`
|
|
15
|
+
* to seed Apple's App Review with a working sign-in.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: if the user already exists, just re-asserts emailVerified=true.
|
|
18
|
+
* Does NOT rotate the password on re-run, delete the user from the dashboard
|
|
19
|
+
* first if you need a fresh password.
|
|
20
|
+
*
|
|
21
|
+
* Side effect: triggers a verification OTP email on first run via the normal
|
|
22
|
+
* sign-up flow. The OTP is unused (we flip emailVerified directly via the
|
|
23
|
+
* adapter) and lands in the configured inbox.
|
|
24
|
+
*/
|
|
25
|
+
export const createReviewAccount = internalAction({
|
|
26
|
+
args: {
|
|
27
|
+
email: v.string(),
|
|
28
|
+
password: v.string(),
|
|
29
|
+
name: v.string(),
|
|
30
|
+
username: v.optional(v.string()),
|
|
31
|
+
},
|
|
32
|
+
returns: v.object({
|
|
33
|
+
userId: v.string(),
|
|
34
|
+
email: v.string(),
|
|
35
|
+
created: v.boolean(),
|
|
36
|
+
verified: v.boolean(),
|
|
37
|
+
name: v.string(),
|
|
38
|
+
}),
|
|
39
|
+
handler: async (ctx, { email, password, name, username }) => {
|
|
40
|
+
const auth = createAuth(ctx);
|
|
41
|
+
|
|
42
|
+
type User = { _id?: string; id?: string; email: string; emailVerified: boolean };
|
|
43
|
+
|
|
44
|
+
const lookup = async (): Promise<User | null> =>
|
|
45
|
+
(await ctx.runQuery(components.betterAuth.adapter.findOne, {
|
|
46
|
+
model: "user",
|
|
47
|
+
where: [{ field: "email", value: email }],
|
|
48
|
+
} as never)) as User | null;
|
|
49
|
+
|
|
50
|
+
let user = await lookup();
|
|
51
|
+
let created = false;
|
|
52
|
+
|
|
53
|
+
if (!user) {
|
|
54
|
+
const body: Record<string, string> = { email, password, name };
|
|
55
|
+
if (username) body.username = username;
|
|
56
|
+
await auth.api.signUpEmail({
|
|
57
|
+
body: body as { email: string; password: string; name: string },
|
|
58
|
+
asResponse: false,
|
|
59
|
+
});
|
|
60
|
+
user = await lookup();
|
|
61
|
+
if (!user) throw new Error("user not found after signUpEmail");
|
|
62
|
+
created = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const docId = user._id ?? user.id;
|
|
66
|
+
if (!docId) throw new Error("user document is missing both _id and id");
|
|
67
|
+
|
|
68
|
+
await ctx.runMutation(components.betterAuth.adapter.updateOne, {
|
|
69
|
+
input: {
|
|
70
|
+
model: "user",
|
|
71
|
+
where: [{ field: "_id", value: docId }],
|
|
72
|
+
update: { emailVerified: true },
|
|
73
|
+
},
|
|
74
|
+
} as never);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
userId: docId,
|
|
78
|
+
email,
|
|
79
|
+
created,
|
|
80
|
+
verified: true,
|
|
81
|
+
name,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reset a rate-limit bucket. Run from the dashboard:
|
|
88
|
+
* `bunx convex run admin:resetRateLimit '{"name":"avatarUpload","key":"<userId>"}'`
|
|
89
|
+
* Omit `key` to reset the shared bucket.
|
|
90
|
+
*/
|
|
91
|
+
export const resetRateLimit = internalMutation({
|
|
92
|
+
args: { name: v.string(), key: v.optional(v.string()) },
|
|
93
|
+
returns: v.object({
|
|
94
|
+
reset: v.boolean(),
|
|
95
|
+
name: v.string(),
|
|
96
|
+
key: v.union(v.string(), v.null()),
|
|
97
|
+
}),
|
|
98
|
+
handler: async (ctx, { name, key }) => {
|
|
99
|
+
await rateLimiter.reset(ctx, name as RateLimitName, key ? { key } : undefined);
|
|
100
|
+
return { reset: true, name, key: key ?? null };
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { expo } from "@better-auth/expo";
|
|
2
|
+
import { createClient } from "@convex-dev/better-auth";
|
|
3
|
+
import type { AuthFunctions, GenericCtx } from "@convex-dev/better-auth";
|
|
4
|
+
import { convex } from "@convex-dev/better-auth/plugins";
|
|
5
|
+
import type { BetterAuthOptions } from "better-auth";
|
|
6
|
+
import { betterAuth } from "better-auth/minimal";
|
|
7
|
+
import { emailOTP, username } from "better-auth/plugins";
|
|
8
|
+
import { v } from "convex/values";
|
|
9
|
+
|
|
10
|
+
import { components, internal } from "./_generated/api";
|
|
11
|
+
import type { DataModel, Doc } from "./_generated/dataModel";
|
|
12
|
+
import { internalAction, query } from "./_generated/server";
|
|
13
|
+
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|
14
|
+
import authConfig from "./auth.config";
|
|
15
|
+
import {
|
|
16
|
+
USERNAME_FORMAT_REGEX,
|
|
17
|
+
USERNAME_MAX_LENGTH,
|
|
18
|
+
USERNAME_MIN_LENGTH,
|
|
19
|
+
isReservedUsername,
|
|
20
|
+
} from "./constants";
|
|
21
|
+
import { sendAuthOTP } from "./email";
|
|
22
|
+
import { env } from "./env";
|
|
23
|
+
import { authenticationRequired } from "./errors";
|
|
24
|
+
|
|
25
|
+
const ONE_MINUTE = 60;
|
|
26
|
+
const ONE_HOUR = 60 * ONE_MINUTE;
|
|
27
|
+
const ONE_DAY = 24 * ONE_HOUR;
|
|
28
|
+
const SEVEN_DAYS = 7 * ONE_DAY;
|
|
29
|
+
const TEN_MINUTES = 10 * ONE_MINUTE;
|
|
30
|
+
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
|
31
|
+
|
|
32
|
+
const authFunctions: AuthFunctions = internal.auth;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the app user doc by Better Auth id, using the indexed lookup.
|
|
36
|
+
*/
|
|
37
|
+
export async function getUserByAuthId(
|
|
38
|
+
ctx: QueryCtx | MutationCtx,
|
|
39
|
+
authId: string,
|
|
40
|
+
): Promise<Doc<"users"> | null> {
|
|
41
|
+
return await ctx.db
|
|
42
|
+
.query("users")
|
|
43
|
+
.withIndex("authId", (q) => q.eq("authId", authId))
|
|
44
|
+
.unique();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Merged representation of the authenticated user.
|
|
49
|
+
*
|
|
50
|
+
* Identity fields (email, name, username, image, emailVerified) come from the
|
|
51
|
+
* Better Auth user. App-specific fields (_id, bio, avatar, timestamps) come
|
|
52
|
+
* from our users table. `avatarUrl` is resolved: user-uploaded storage id
|
|
53
|
+
* takes precedence, otherwise falls back to Better Auth's `image` (e.g. OAuth
|
|
54
|
+
* provider avatar).
|
|
55
|
+
*/
|
|
56
|
+
export type AuthUser = Doc<"users"> & {
|
|
57
|
+
authUserId: string;
|
|
58
|
+
email: string;
|
|
59
|
+
name: string;
|
|
60
|
+
emailVerified: boolean;
|
|
61
|
+
image: string | null;
|
|
62
|
+
username: string | null;
|
|
63
|
+
displayUsername: string | null;
|
|
64
|
+
avatarUrl: string | null;
|
|
65
|
+
hasUploadedAvatar: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The component client has methods needed for integrating Convex with Better
|
|
69
|
+
// Auth, plus helper methods for general use.
|
|
70
|
+
export const authComponent = createClient<DataModel>(components.betterAuth, {
|
|
71
|
+
authFunctions,
|
|
72
|
+
triggers: {
|
|
73
|
+
user: {
|
|
74
|
+
onCreate: async (ctx, authUser) => {
|
|
75
|
+
// Create the app user row with defaults. Identity fields live on the
|
|
76
|
+
// Better Auth user record, not here.
|
|
77
|
+
await ctx.db.insert("users", {
|
|
78
|
+
authId: authUser._id,
|
|
79
|
+
createdAt: Date.now(),
|
|
80
|
+
updatedAt: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
onDelete: async (ctx, authUser) => {
|
|
84
|
+
const user = await getUserByAuthId(ctx, authUser._id);
|
|
85
|
+
if (!user) return;
|
|
86
|
+
// Free the avatar blob before dropping the row so we don't leak storage.
|
|
87
|
+
if (user.avatar) await ctx.storage.delete(user.avatar);
|
|
88
|
+
await ctx.db.delete(user._id);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Export trigger handlers - these become available at internal.auth
|
|
95
|
+
export const { onCreate, onDelete } = authComponent.triggersApi();
|
|
96
|
+
|
|
97
|
+
// Export client API for AuthBoundary and other client-side auth checks
|
|
98
|
+
export const { getAuthUser } = authComponent.clientApi();
|
|
99
|
+
|
|
100
|
+
export const createAuth = (ctx: GenericCtx<DataModel>) =>
|
|
101
|
+
betterAuth({
|
|
102
|
+
baseURL: env.convexSiteUrl,
|
|
103
|
+
trustedOrigins: [
|
|
104
|
+
"vexpo://",
|
|
105
|
+
env.siteUrl,
|
|
106
|
+
// In dev, Expo Go uses `exp://<lan-ip>:<port>` and the dev client uses
|
|
107
|
+
// `exp+<scheme>://`. Wildcards match the host/port suffix that Better
|
|
108
|
+
// Auth sees in the request origin. Production builds drop these.
|
|
109
|
+
...(process.env.NODE_ENV === "development"
|
|
110
|
+
? ["exp://*", "exp://**", "http://localhost:8081"]
|
|
111
|
+
: []),
|
|
112
|
+
],
|
|
113
|
+
database: authComponent.adapter(ctx),
|
|
114
|
+
emailAndPassword: {
|
|
115
|
+
enabled: true,
|
|
116
|
+
// Email verification is gated on the `REQUIRE_EMAIL_VERIFICATION`
|
|
117
|
+
// Convex env var. The lite-mode setup (`bunx vexpo lite`) leaves it
|
|
118
|
+
// unset (default `false`) so sign-up creates verified accounts
|
|
119
|
+
// immediately and the user can sign in without an OTP. No Resend
|
|
120
|
+
// configuration needed to get up and running on the iOS Simulator.
|
|
121
|
+
// `bunx vexpo full` flips this to `true` when it provisions Resend.
|
|
122
|
+
// Production runs with verification on.
|
|
123
|
+
requireEmailVerification: env.requireEmailVerification,
|
|
124
|
+
minPasswordLength: 10,
|
|
125
|
+
maxPasswordLength: 128,
|
|
126
|
+
// When verification is off, accounts land verified-on-create so
|
|
127
|
+
// password sign-in works without the email round-trip.
|
|
128
|
+
autoSignIn: !env.requireEmailVerification,
|
|
129
|
+
},
|
|
130
|
+
emailVerification: {
|
|
131
|
+
// When `emailOtp.verifyEmail` succeeds, Better Auth creates a session and
|
|
132
|
+
// sets the cookie inline instead of returning { token: null } and forcing
|
|
133
|
+
// the user to sign in manually.
|
|
134
|
+
autoSignInAfterVerification: true,
|
|
135
|
+
},
|
|
136
|
+
// Only register the Apple provider when its credentials are present.
|
|
137
|
+
// Better Auth logs a warning on every request otherwise, and the client
|
|
138
|
+
// hides the button via `getEnabledProviders` when the env vars are unset,
|
|
139
|
+
// so registering an empty provider serves no purpose.
|
|
140
|
+
socialProviders:
|
|
141
|
+
process.env.APPLE_CLIENT_ID && process.env.APPLE_CLIENT_SECRET
|
|
142
|
+
? {
|
|
143
|
+
apple: {
|
|
144
|
+
clientId: process.env.APPLE_CLIENT_ID,
|
|
145
|
+
clientSecret: process.env.APPLE_CLIENT_SECRET,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {},
|
|
149
|
+
session: {
|
|
150
|
+
expiresIn: SEVEN_DAYS,
|
|
151
|
+
updateAge: ONE_DAY,
|
|
152
|
+
freshAge: TEN_MINUTES,
|
|
153
|
+
cookieCache: { enabled: true, maxAge: FIVE_MINUTES },
|
|
154
|
+
},
|
|
155
|
+
// Better Auth handles HTTP-level rate limiting for all auth endpoints.
|
|
156
|
+
// Custom rules use EXACT match unless the key contains "*" (wildcard).
|
|
157
|
+
// Paths here are the post-basePath form (Better Auth strips /api/auth).
|
|
158
|
+
// This app uses the email-OTP flow exclusively, so password reset and
|
|
159
|
+
// verification hit /email-otp/* rather than the link-based endpoints.
|
|
160
|
+
rateLimit: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
window: ONE_MINUTE,
|
|
163
|
+
max: 100,
|
|
164
|
+
customRules: {
|
|
165
|
+
"/sign-in/*": { window: ONE_MINUTE, max: 5 },
|
|
166
|
+
"/sign-up/*": { window: ONE_MINUTE, max: 3 },
|
|
167
|
+
"/email-otp/request-password-reset": { window: ONE_HOUR, max: 3 },
|
|
168
|
+
"/email-otp/reset-password": { window: ONE_MINUTE, max: 3 },
|
|
169
|
+
"/email-otp/send-verification-otp": { window: ONE_MINUTE, max: 3 },
|
|
170
|
+
"/list-sessions": { window: ONE_MINUTE, max: 30 },
|
|
171
|
+
"/get-session": { window: ONE_MINUTE, max: 60 },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
advanced: {
|
|
175
|
+
ipAddress: {
|
|
176
|
+
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
plugins: [
|
|
180
|
+
convex({ authConfig }),
|
|
181
|
+
// Email OTP for sign-in, verification, password reset, and change-email.
|
|
182
|
+
emailOTP({
|
|
183
|
+
otpLength: 6,
|
|
184
|
+
expiresIn: FIVE_MINUTES,
|
|
185
|
+
overrideDefaultEmailVerification: true,
|
|
186
|
+
// Only send a verification OTP on sign-up when verification is
|
|
187
|
+
// actually required. Minimal-tier setup short-circuits the OTP
|
|
188
|
+
// so users can sign up without ever opening their email.
|
|
189
|
+
sendVerificationOnSignUp: env.requireEmailVerification,
|
|
190
|
+
changeEmail: {
|
|
191
|
+
enabled: true,
|
|
192
|
+
verifyCurrentEmail: true,
|
|
193
|
+
},
|
|
194
|
+
sendVerificationOTP: async ({ email, otp, type }) => {
|
|
195
|
+
await sendAuthOTP(ctx, { email, otp, type });
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
username({
|
|
199
|
+
minUsernameLength: USERNAME_MIN_LENGTH,
|
|
200
|
+
maxUsernameLength: USERNAME_MAX_LENGTH,
|
|
201
|
+
validationOrder: { username: "post-normalization" },
|
|
202
|
+
usernameValidator: (normalized) => {
|
|
203
|
+
if (isReservedUsername(normalized)) return false;
|
|
204
|
+
return USERNAME_FORMAT_REGEX.test(normalized);
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
expo(),
|
|
208
|
+
],
|
|
209
|
+
} satisfies BetterAuthOptions);
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Safely get the current authenticated user. Returns undefined if not
|
|
213
|
+
* authenticated or if the app user row is missing (shouldn't happen in
|
|
214
|
+
* practice, but we handle it gracefully).
|
|
215
|
+
*/
|
|
216
|
+
export async function safeGetAuthenticatedUser(
|
|
217
|
+
ctx: QueryCtx | MutationCtx,
|
|
218
|
+
): Promise<AuthUser | undefined> {
|
|
219
|
+
const authUser = await authComponent.safeGetAuthUser(ctx);
|
|
220
|
+
if (!authUser) return undefined;
|
|
221
|
+
|
|
222
|
+
const user = await getUserByAuthId(ctx, authUser._id);
|
|
223
|
+
if (!user) return undefined;
|
|
224
|
+
|
|
225
|
+
// Resolve avatar: user upload takes precedence over Better Auth image.
|
|
226
|
+
const hasUploadedAvatar = !!user.avatar;
|
|
227
|
+
const avatarUrl = hasUploadedAvatar
|
|
228
|
+
? await ctx.storage.getUrl(user.avatar!)
|
|
229
|
+
: (authUser.image ?? null);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
...user,
|
|
233
|
+
authUserId: authUser._id,
|
|
234
|
+
email: authUser.email,
|
|
235
|
+
name: authUser.name,
|
|
236
|
+
emailVerified: authUser.emailVerified,
|
|
237
|
+
image: authUser.image ?? null,
|
|
238
|
+
username: (authUser as { username?: string | null }).username ?? null,
|
|
239
|
+
displayUsername: (authUser as { displayUsername?: string | null }).displayUsername ?? null,
|
|
240
|
+
avatarUrl,
|
|
241
|
+
hasUploadedAvatar,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get the current authenticated user, throwing if not authenticated.
|
|
247
|
+
*/
|
|
248
|
+
export async function requireAuthenticatedUser(ctx: QueryCtx | MutationCtx): Promise<AuthUser> {
|
|
249
|
+
const user = await safeGetAuthenticatedUser(ctx);
|
|
250
|
+
if (!user) throw authenticationRequired();
|
|
251
|
+
return user;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Validator for AuthUser return type.
|
|
256
|
+
*/
|
|
257
|
+
export const authUserValidator = v.object({
|
|
258
|
+
_id: v.id("users"),
|
|
259
|
+
_creationTime: v.number(),
|
|
260
|
+
authId: v.string(),
|
|
261
|
+
bio: v.optional(v.string()),
|
|
262
|
+
avatar: v.optional(v.id("_storage")),
|
|
263
|
+
createdAt: v.number(),
|
|
264
|
+
updatedAt: v.number(),
|
|
265
|
+
authUserId: v.string(),
|
|
266
|
+
email: v.string(),
|
|
267
|
+
name: v.string(),
|
|
268
|
+
emailVerified: v.boolean(),
|
|
269
|
+
image: v.union(v.string(), v.null()),
|
|
270
|
+
username: v.union(v.string(), v.null()),
|
|
271
|
+
displayUsername: v.union(v.string(), v.null()),
|
|
272
|
+
avatarUrl: v.union(v.string(), v.null()),
|
|
273
|
+
hasUploadedAvatar: v.boolean(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Queries
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// These use the raw `query` builder because this file IS the auth primitive
|
|
280
|
+
// that functions.ts depends on. Importing wrappers from ./functions would
|
|
281
|
+
// create a circular dependency.
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if the current user has a password-based account.
|
|
285
|
+
* Useful for detecting social-only accounts that need password setup.
|
|
286
|
+
* Returns false if not authenticated.
|
|
287
|
+
*/
|
|
288
|
+
export const hasPassword = query({
|
|
289
|
+
args: {},
|
|
290
|
+
returns: v.boolean(),
|
|
291
|
+
handler: async (ctx) => {
|
|
292
|
+
const user = await safeGetAuthenticatedUser(ctx);
|
|
293
|
+
if (!user) return false;
|
|
294
|
+
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
|
|
295
|
+
const accounts = await auth.api.listUserAccounts({ headers });
|
|
296
|
+
return accounts.some((account) => account.providerId === "credential");
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Public read of which auth features are configured server-side. Lets the
|
|
302
|
+
* client hide buttons that would fail at submit (e.g. Apple Sign In with empty
|
|
303
|
+
* `APPLE_CLIENT_ID`, OTP sign-in or password reset with `REQUIRE_EMAIL_VERIFICATION`
|
|
304
|
+
* unset). Returns booleans only, never leaks the credentials.
|
|
305
|
+
*
|
|
306
|
+
* `emailFeatures` is true when `REQUIRE_EMAIL_VERIFICATION` is set on the
|
|
307
|
+
* Convex deployment env (testflight tier setup or later). When false, the
|
|
308
|
+
* client hides OTP sign-in, password reset, change-email. the only working
|
|
309
|
+
* flow is email + password sign-up/sign-in. This is the minimal-tier path:
|
|
310
|
+
* users get into the app without configuring Resend or any DNS.
|
|
311
|
+
*/
|
|
312
|
+
export const getEnabledProviders = query({
|
|
313
|
+
args: {},
|
|
314
|
+
returns: v.object({ apple: v.boolean(), emailFeatures: v.boolean() }),
|
|
315
|
+
handler: async () => {
|
|
316
|
+
const apple = !!process.env.APPLE_CLIENT_ID && !!process.env.APPLE_CLIENT_SECRET;
|
|
317
|
+
const emailFeatures = env.requireEmailVerification;
|
|
318
|
+
return { apple, emailFeatures };
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Rotate JWKS keys for JWT signing.
|
|
324
|
+
* Run with: bunx convex run auth:rotateKeys
|
|
325
|
+
*/
|
|
326
|
+
export const rotateKeys = internalAction({
|
|
327
|
+
args: {},
|
|
328
|
+
// Better Auth's `rotateKeys()` returns implementation-specific JWKS metadata
|
|
329
|
+
// that we don't constrain here. `v.any()` documents the upstream contract.
|
|
330
|
+
returns: v.any(),
|
|
331
|
+
handler: async (ctx) => {
|
|
332
|
+
const auth = createAuth(ctx);
|
|
333
|
+
return auth.api.rotateKeys();
|
|
334
|
+
},
|
|
335
|
+
});
|