convex-zen 0.0.1
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/dist/cli/generate.d.ts +14 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +297 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +111 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.d.ts +300 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +434 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/plugins/admin.d.ts +92 -0
- package/dist/client/plugins/admin.d.ts.map +1 -0
- package/dist/client/plugins/admin.js +165 -0
- package/dist/client/plugins/admin.js.map +1 -0
- package/dist/client/primitives.d.ts +57 -0
- package/dist/client/primitives.d.ts.map +1 -0
- package/dist/client/primitives.js +64 -0
- package/dist/client/primitives.js.map +1 -0
- package/dist/client/providers.d.ts +14 -0
- package/dist/client/providers.d.ts.map +1 -0
- package/dist/client/providers.js +25 -0
- package/dist/client/providers.js.map +1 -0
- package/dist/client/react.d.ts +23 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +48 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/tanstack-start-client-plugins.d.ts +34 -0
- package/dist/client/tanstack-start-client-plugins.d.ts.map +1 -0
- package/dist/client/tanstack-start-client-plugins.js +32 -0
- package/dist/client/tanstack-start-client-plugins.js.map +1 -0
- package/dist/client/tanstack-start-client.d.ts +52 -0
- package/dist/client/tanstack-start-client.d.ts.map +1 -0
- package/dist/client/tanstack-start-client.js +130 -0
- package/dist/client/tanstack-start-client.js.map +1 -0
- package/dist/client/tanstack-start-plugins.d.ts +27 -0
- package/dist/client/tanstack-start-plugins.d.ts.map +1 -0
- package/dist/client/tanstack-start-plugins.js +145 -0
- package/dist/client/tanstack-start-plugins.js.map +1 -0
- package/dist/client/tanstack-start.d.ts +130 -0
- package/dist/client/tanstack-start.d.ts.map +1 -0
- package/dist/client/tanstack-start.js +331 -0
- package/dist/client/tanstack-start.js.map +1 -0
- package/dist/component/_generated/api.d.ts +50 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +92 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/core/sessions.d.ts +33 -0
- package/dist/component/core/sessions.d.ts.map +1 -0
- package/dist/component/core/sessions.js +186 -0
- package/dist/component/core/sessions.js.map +1 -0
- package/dist/component/core/users.d.ts +19 -0
- package/dist/component/core/users.d.ts.map +1 -0
- package/dist/component/core/users.js +154 -0
- package/dist/component/core/users.js.map +1 -0
- package/dist/component/core/verifications.d.ts +34 -0
- package/dist/component/core/verifications.d.ts.map +1 -0
- package/dist/component/core/verifications.js +135 -0
- package/dist/component/core/verifications.js.map +1 -0
- package/dist/component/gateway.d.ts +16 -0
- package/dist/component/gateway.d.ts.map +1 -0
- package/dist/component/gateway.js +229 -0
- package/dist/component/gateway.js.map +1 -0
- package/dist/component/lib/crypto.d.ts +24 -0
- package/dist/component/lib/crypto.d.ts.map +1 -0
- package/dist/component/lib/crypto.js +57 -0
- package/dist/component/lib/crypto.js.map +1 -0
- package/dist/component/lib/rateLimit.d.ts +26 -0
- package/dist/component/lib/rateLimit.d.ts.map +1 -0
- package/dist/component/lib/rateLimit.js +96 -0
- package/dist/component/lib/rateLimit.js.map +1 -0
- package/dist/component/lib/validators.d.ts +19 -0
- package/dist/component/lib/validators.d.ts.map +1 -0
- package/dist/component/lib/validators.js +12 -0
- package/dist/component/lib/validators.js.map +1 -0
- package/dist/component/plugins/admin.d.ts +72 -0
- package/dist/component/plugins/admin.d.ts.map +1 -0
- package/dist/component/plugins/admin.js +152 -0
- package/dist/component/plugins/admin.js.map +1 -0
- package/dist/component/providers/emailPassword.d.ts +49 -0
- package/dist/component/providers/emailPassword.d.ts.map +1 -0
- package/dist/component/providers/emailPassword.js +316 -0
- package/dist/component/providers/emailPassword.js.map +1 -0
- package/dist/component/providers/oauth.d.ts +33 -0
- package/dist/component/providers/oauth.d.ts.map +1 -0
- package/dist/component/providers/oauth.js +256 -0
- package/dist/component/providers/oauth.js.map +1 -0
- package/dist/component/schema.d.ts +132 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +82 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +121 -0
- package/src/cli/generate.ts +360 -0
- package/src/cli/index.ts +133 -0
- package/src/client/index.ts +707 -0
- package/src/client/plugins/admin.ts +205 -0
- package/src/client/primitives.ts +100 -0
- package/src/client/providers.ts +35 -0
- package/src/client/react.ts +97 -0
- package/src/client/tanstack-start-client-plugins.ts +113 -0
- package/src/client/tanstack-start-client.ts +259 -0
- package/src/client/tanstack-start-plugins.ts +203 -0
- package/src/client/tanstack-start.ts +535 -0
- package/src/component/_generated/api.ts +70 -0
- package/src/component/_generated/component.ts +184 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/core/sessions.ts +228 -0
- package/src/component/core/users.ts +199 -0
- package/src/component/core/verifications.ts +173 -0
- package/src/component/gateway.ts +321 -0
- package/src/component/lib/crypto.ts +63 -0
- package/src/component/lib/internalApi.ts +66 -0
- package/src/component/lib/rateLimit.ts +111 -0
- package/src/component/lib/validators.ts +12 -0
- package/src/component/plugins/admin.ts +178 -0
- package/src/component/providers/emailPassword.ts +374 -0
- package/src/component/providers/oauth.ts +324 -0
- package/src/component/schema.ts +88 -0
- package/src/types.ts +68 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { AdminPluginConfig } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create an admin plugin configuration.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { adminPlugin } from "convex-zen/plugins/admin";
|
|
9
|
+
*
|
|
10
|
+
* export const auth = new ConvexAuth(components.convexAuth, {
|
|
11
|
+
* plugins: [adminPlugin({ defaultRole: "user", adminRole: "admin" })],
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function adminPlugin(config?: {
|
|
16
|
+
defaultRole?: string;
|
|
17
|
+
adminRole?: string;
|
|
18
|
+
}): AdminPluginConfig {
|
|
19
|
+
const plugin: AdminPluginConfig = {
|
|
20
|
+
id: "admin",
|
|
21
|
+
};
|
|
22
|
+
if (config?.defaultRole !== undefined) {
|
|
23
|
+
plugin.defaultRole = config.defaultRole;
|
|
24
|
+
}
|
|
25
|
+
if (config?.adminRole !== undefined) {
|
|
26
|
+
plugin.adminRole = config.adminRole;
|
|
27
|
+
}
|
|
28
|
+
return plugin;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* AdminPlugin client class — exposes admin operations as typed methods.
|
|
33
|
+
* Obtained via `auth.plugins.admin` after ConvexAuth is initialized with adminPlugin.
|
|
34
|
+
*/
|
|
35
|
+
export class AdminPlugin {
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly componentApi: Record<string, unknown>,
|
|
38
|
+
private readonly config: AdminPluginConfig
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a nested component function reference from a path string.
|
|
43
|
+
* e.g. "plugins/admin:listUsers" → component.plugins.admin.listUsers
|
|
44
|
+
*/
|
|
45
|
+
private fn(path: string): unknown {
|
|
46
|
+
const [modulePath, funcName] = path.split(":");
|
|
47
|
+
if (!modulePath || !funcName) {
|
|
48
|
+
throw new Error(`Invalid function path: ${path}`);
|
|
49
|
+
}
|
|
50
|
+
const parts = modulePath.split("/");
|
|
51
|
+
let ref: Record<string, unknown> = this.componentApi;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
const next = ref[part];
|
|
54
|
+
if (
|
|
55
|
+
!next ||
|
|
56
|
+
typeof next !== "object" ||
|
|
57
|
+
Array.isArray(next)
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(`Invalid function path segment: ${part}`);
|
|
60
|
+
}
|
|
61
|
+
ref = next as Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
const resolved = ref[funcName];
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
throw new Error(`Function not found: ${path}`);
|
|
66
|
+
}
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private getAdminToken(args: { adminToken?: string; token?: string }): string {
|
|
71
|
+
const token = args.adminToken ?? args.token;
|
|
72
|
+
if (!token) {
|
|
73
|
+
throw new Error("adminToken is required");
|
|
74
|
+
}
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private stringifyError(error: unknown): string {
|
|
79
|
+
const errorObj = error as { message?: unknown; data?: unknown } | null;
|
|
80
|
+
return [
|
|
81
|
+
error instanceof Error ? error.message : "",
|
|
82
|
+
typeof errorObj?.message === "string" ? errorObj.message : "",
|
|
83
|
+
typeof errorObj?.data === "string"
|
|
84
|
+
? errorObj.data
|
|
85
|
+
: errorObj?.data
|
|
86
|
+
? JSON.stringify(errorObj.data)
|
|
87
|
+
: "",
|
|
88
|
+
String(error),
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" | ");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private isRetryableGatewayShapeError(error: unknown): boolean {
|
|
95
|
+
const details = this.stringifyError(error);
|
|
96
|
+
return (
|
|
97
|
+
details.includes("Object contains extra field") ||
|
|
98
|
+
details.includes("ArgumentValidationError") ||
|
|
99
|
+
details.includes("Server Error")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async runAdminGatewayAction(
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
106
|
+
path: string,
|
|
107
|
+
args: Record<string, unknown> & { adminToken?: string; token?: string }
|
|
108
|
+
) {
|
|
109
|
+
const adminToken = this.getAdminToken(args);
|
|
110
|
+
const payload = { ...args };
|
|
111
|
+
delete payload.adminToken;
|
|
112
|
+
delete payload.token;
|
|
113
|
+
|
|
114
|
+
const attempts = [
|
|
115
|
+
{ ...payload, adminToken },
|
|
116
|
+
payload,
|
|
117
|
+
{ ...payload, token: adminToken },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
let lastError: unknown = null;
|
|
121
|
+
for (let i = 0; i < attempts.length; i += 1) {
|
|
122
|
+
const attempt = attempts[i];
|
|
123
|
+
try {
|
|
124
|
+
return await ctx.runAction(this.fn(path), attempt);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const isLastAttempt = i === attempts.length - 1;
|
|
127
|
+
if (isLastAttempt || !this.isRetryableGatewayShapeError(error)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
lastError = error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw lastError ?? new Error(`Failed to call admin action: ${path}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* List users with pagination.
|
|
139
|
+
*/
|
|
140
|
+
async listUsers(
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
143
|
+
args: { adminToken?: string; token?: string; limit?: number; cursor?: string }
|
|
144
|
+
) {
|
|
145
|
+
return this.runAdminGatewayAction(ctx, "gateway:adminListUsers", args);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Ban a user, invalidating all their sessions.
|
|
150
|
+
*/
|
|
151
|
+
async banUser(
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
154
|
+
args: {
|
|
155
|
+
adminToken?: string;
|
|
156
|
+
token?: string;
|
|
157
|
+
userId: string;
|
|
158
|
+
reason?: string;
|
|
159
|
+
expiresAt?: number;
|
|
160
|
+
}
|
|
161
|
+
) {
|
|
162
|
+
return this.runAdminGatewayAction(ctx, "gateway:adminBanUser", args);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Unban a user.
|
|
167
|
+
*/
|
|
168
|
+
async unbanUser(
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
171
|
+
args: { adminToken?: string; token?: string; userId: string }
|
|
172
|
+
) {
|
|
173
|
+
return this.runAdminGatewayAction(ctx, "gateway:adminUnbanUser", args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Set a user's role.
|
|
178
|
+
*/
|
|
179
|
+
async setRole(
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
182
|
+
args: { adminToken?: string; token?: string; userId: string; role: string }
|
|
183
|
+
) {
|
|
184
|
+
return this.runAdminGatewayAction(ctx, "gateway:adminSetRole", args);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Permanently delete a user and all associated data.
|
|
189
|
+
*/
|
|
190
|
+
async deleteUser(
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
192
|
+
ctx: { runAction: (fn: any, args: any) => Promise<any> },
|
|
193
|
+
args: { adminToken?: string; token?: string; userId: string }
|
|
194
|
+
) {
|
|
195
|
+
return this.runAdminGatewayAction(ctx, "gateway:adminDeleteUser", args);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get defaultRole() {
|
|
199
|
+
return this.config.defaultRole;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get adminRole() {
|
|
203
|
+
return this.config.adminRole;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic auth/session primitives.
|
|
3
|
+
*
|
|
4
|
+
* These helpers keep auth flow logic in one place so framework adapters
|
|
5
|
+
* (TanStack Start, Next.js, etc.) can stay thin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SessionInfo {
|
|
9
|
+
userId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SignInInput {
|
|
14
|
+
email: string;
|
|
15
|
+
password: string;
|
|
16
|
+
ipAddress?: string;
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SignInOutput {
|
|
21
|
+
sessionToken: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionTransport {
|
|
26
|
+
signIn: (input: SignInInput) => Promise<SignInOutput>;
|
|
27
|
+
validateSession: (token: string) => Promise<SessionInfo | null>;
|
|
28
|
+
signOut: (token: string) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EstablishedSession {
|
|
32
|
+
sessionToken: string;
|
|
33
|
+
session: SessionInfo;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shared auth/session operations that can be reused by all framework adapters.
|
|
38
|
+
*/
|
|
39
|
+
export class SessionPrimitives {
|
|
40
|
+
constructor(private readonly transport: SessionTransport) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve session details from a raw token.
|
|
44
|
+
*/
|
|
45
|
+
async getSessionFromToken(
|
|
46
|
+
token: string | null | undefined
|
|
47
|
+
): Promise<SessionInfo | null> {
|
|
48
|
+
if (!token) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return this.transport.validateSession(token);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve and require a valid session from a raw token.
|
|
56
|
+
* Throws "Unauthorized" if missing/invalid.
|
|
57
|
+
*/
|
|
58
|
+
async requireSessionFromToken(token: string | null | undefined) {
|
|
59
|
+
const session = await this.getSessionFromToken(token);
|
|
60
|
+
if (!session) {
|
|
61
|
+
throw new Error("Unauthorized");
|
|
62
|
+
}
|
|
63
|
+
return session;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sign in and immediately verify the returned token.
|
|
68
|
+
* This mirrors common auth library behavior where sign-in returns a token,
|
|
69
|
+
* and the framework layer then stores the token in a cookie.
|
|
70
|
+
*/
|
|
71
|
+
async signInAndResolveSession(
|
|
72
|
+
input: SignInInput
|
|
73
|
+
): Promise<EstablishedSession> {
|
|
74
|
+
const result = await this.transport.signIn(input);
|
|
75
|
+
const session = await this.transport.validateSession(result.sessionToken);
|
|
76
|
+
|
|
77
|
+
if (!session) {
|
|
78
|
+
throw new Error("Could not validate newly created session");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
sessionToken: result.sessionToken,
|
|
83
|
+
session,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Best-effort sign out by token.
|
|
89
|
+
*/
|
|
90
|
+
async signOutByToken(token: string | null | undefined): Promise<void> {
|
|
91
|
+
if (!token) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await this.transport.signOut(token);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createSessionPrimitives(transport: SessionTransport) {
|
|
99
|
+
return new SessionPrimitives(transport);
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OAuthProviderConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Create a Google OAuth provider configuration. */
|
|
4
|
+
export function googleProvider(config: {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
scopes?: string[];
|
|
8
|
+
}): OAuthProviderConfig {
|
|
9
|
+
return {
|
|
10
|
+
id: "google",
|
|
11
|
+
clientId: config.clientId,
|
|
12
|
+
clientSecret: config.clientSecret,
|
|
13
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
14
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
15
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
16
|
+
scopes: config.scopes ?? ["openid", "email", "profile"],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Create a GitHub OAuth provider configuration. */
|
|
21
|
+
export function githubProvider(config: {
|
|
22
|
+
clientId: string;
|
|
23
|
+
clientSecret: string;
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
}): OAuthProviderConfig {
|
|
26
|
+
return {
|
|
27
|
+
id: "github",
|
|
28
|
+
clientId: config.clientId,
|
|
29
|
+
clientSecret: config.clientSecret,
|
|
30
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
31
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
32
|
+
userInfoUrl: "https://api.github.com/user",
|
|
33
|
+
scopes: config.scopes ?? ["read:user", "user:email"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createElement,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import type { SessionInfo } from "./primitives";
|
|
12
|
+
|
|
13
|
+
export type AuthStatus = "loading" | "authenticated" | "unauthenticated";
|
|
14
|
+
|
|
15
|
+
export type AuthSession = SessionInfo;
|
|
16
|
+
|
|
17
|
+
export interface ReactAuthClient {
|
|
18
|
+
getSession: () => Promise<AuthSession | null>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ConvexZenAuthContextValue {
|
|
22
|
+
status: AuthStatus;
|
|
23
|
+
session: AuthSession | null;
|
|
24
|
+
isAuthenticated: boolean;
|
|
25
|
+
refresh: () => Promise<AuthSession | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConvexZenAuthProviderProps {
|
|
29
|
+
client: ReactAuthClient;
|
|
30
|
+
initialSession?: AuthSession | null;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AuthContext = createContext<ConvexZenAuthContextValue | null>(null);
|
|
35
|
+
|
|
36
|
+
export function ConvexZenAuthProvider({
|
|
37
|
+
client,
|
|
38
|
+
initialSession,
|
|
39
|
+
children,
|
|
40
|
+
}: ConvexZenAuthProviderProps) {
|
|
41
|
+
const [session, setSession] = useState<AuthSession | null>(
|
|
42
|
+
initialSession ?? null
|
|
43
|
+
);
|
|
44
|
+
const [status, setStatus] = useState<AuthStatus>(() => {
|
|
45
|
+
if (initialSession === undefined) {
|
|
46
|
+
return "loading";
|
|
47
|
+
}
|
|
48
|
+
return initialSession ? "authenticated" : "unauthenticated";
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const refresh = useCallback(async () => {
|
|
52
|
+
setStatus("loading");
|
|
53
|
+
const next = await client.getSession();
|
|
54
|
+
setSession(next);
|
|
55
|
+
setStatus(next ? "authenticated" : "unauthenticated");
|
|
56
|
+
return next;
|
|
57
|
+
}, [client]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (initialSession !== undefined) {
|
|
61
|
+
setSession(initialSession);
|
|
62
|
+
setStatus(initialSession ? "authenticated" : "unauthenticated");
|
|
63
|
+
}
|
|
64
|
+
}, [initialSession]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (initialSession === undefined) {
|
|
68
|
+
void refresh();
|
|
69
|
+
}
|
|
70
|
+
}, [initialSession, refresh]);
|
|
71
|
+
|
|
72
|
+
const value = useMemo<ConvexZenAuthContextValue>(
|
|
73
|
+
() => ({
|
|
74
|
+
status,
|
|
75
|
+
session,
|
|
76
|
+
isAuthenticated: session !== null,
|
|
77
|
+
refresh,
|
|
78
|
+
}),
|
|
79
|
+
[status, session, refresh]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return createElement(AuthContext.Provider, { value }, children);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useConvexZenAuth(): ConvexZenAuthContextValue {
|
|
86
|
+
const context = useContext(AuthContext);
|
|
87
|
+
if (!context) {
|
|
88
|
+
throw new Error("useConvexZenAuth must be used within ConvexZenAuthProvider");
|
|
89
|
+
}
|
|
90
|
+
return context;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const useAuth = useConvexZenAuth;
|
|
94
|
+
|
|
95
|
+
export function useSession(): AuthSession | null {
|
|
96
|
+
return useConvexZenAuth().session;
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { TanStackStartAuthApiClientPlugin } from "./tanstack-start-client";
|
|
2
|
+
|
|
3
|
+
function normalizeRoutePrefix(prefix: string): string {
|
|
4
|
+
const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
|
|
5
|
+
return trimmed.length > 0 ? trimmed : "admin";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TanStackStartAdminListUsersInput {
|
|
9
|
+
limit?: number;
|
|
10
|
+
cursor?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TanStackStartAdminBanUserInput {
|
|
14
|
+
userId: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
expiresAt?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TanStackStartAdminSetRoleInput {
|
|
20
|
+
userId: string;
|
|
21
|
+
role: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TanStackStartAdminUserIdInput {
|
|
25
|
+
userId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TanStackStartAdminClientPluginShape<
|
|
29
|
+
TListUsersOutput = unknown,
|
|
30
|
+
TBanUserOutput = unknown,
|
|
31
|
+
TSetRoleOutput = unknown,
|
|
32
|
+
TUnbanUserOutput = unknown,
|
|
33
|
+
TDeleteUserOutput = unknown,
|
|
34
|
+
> {
|
|
35
|
+
admin: {
|
|
36
|
+
listUsers: (
|
|
37
|
+
input?: TanStackStartAdminListUsersInput
|
|
38
|
+
) => Promise<TListUsersOutput>;
|
|
39
|
+
banUser: (input: TanStackStartAdminBanUserInput) => Promise<TBanUserOutput>;
|
|
40
|
+
setRole: (input: TanStackStartAdminSetRoleInput) => Promise<TSetRoleOutput>;
|
|
41
|
+
unbanUser: (input: TanStackStartAdminUserIdInput) => Promise<TUnbanUserOutput>;
|
|
42
|
+
deleteUser: (
|
|
43
|
+
input: TanStackStartAdminUserIdInput
|
|
44
|
+
) => Promise<TDeleteUserOutput>;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TanStackStartAdminClientPluginOptions {
|
|
49
|
+
routePrefix?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Adds `authClient.admin.*` methods to the TanStack auth API client.
|
|
54
|
+
*/
|
|
55
|
+
export function adminClient<
|
|
56
|
+
TListUsersOutput = unknown,
|
|
57
|
+
TBanUserOutput = unknown,
|
|
58
|
+
TSetRoleOutput = unknown,
|
|
59
|
+
TUnbanUserOutput = unknown,
|
|
60
|
+
TDeleteUserOutput = unknown,
|
|
61
|
+
>(
|
|
62
|
+
options: TanStackStartAdminClientPluginOptions = {}
|
|
63
|
+
): TanStackStartAuthApiClientPlugin<
|
|
64
|
+
TanStackStartAdminClientPluginShape<
|
|
65
|
+
TListUsersOutput,
|
|
66
|
+
TBanUserOutput,
|
|
67
|
+
TSetRoleOutput,
|
|
68
|
+
TUnbanUserOutput,
|
|
69
|
+
TDeleteUserOutput
|
|
70
|
+
>
|
|
71
|
+
> {
|
|
72
|
+
const routePrefix = normalizeRoutePrefix(options.routePrefix ?? "admin");
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: "admin",
|
|
76
|
+
create: (context) => {
|
|
77
|
+
const post = async <T>(path: string, input: unknown, fallback: string) => {
|
|
78
|
+
return context.requestJson<T>(
|
|
79
|
+
`${routePrefix}/${path}`,
|
|
80
|
+
{
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify(input),
|
|
84
|
+
},
|
|
85
|
+
{ fallback }
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
admin: {
|
|
91
|
+
listUsers: async (input) =>
|
|
92
|
+
post<TListUsersOutput>(
|
|
93
|
+
"list-users",
|
|
94
|
+
input ?? {},
|
|
95
|
+
"Could not list users"
|
|
96
|
+
),
|
|
97
|
+
banUser: async (input) =>
|
|
98
|
+
post<TBanUserOutput>("ban-user", input, "Could not ban user"),
|
|
99
|
+
setRole: async (input) =>
|
|
100
|
+
post<TSetRoleOutput>("set-role", input, "Could not set role"),
|
|
101
|
+
unbanUser: async (input) =>
|
|
102
|
+
post<TUnbanUserOutput>("unban-user", input, "Could not unban user"),
|
|
103
|
+
deleteUser: async (input) =>
|
|
104
|
+
post<TDeleteUserOutput>(
|
|
105
|
+
"delete-user",
|
|
106
|
+
input,
|
|
107
|
+
"Could not delete user"
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|