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.
Files changed (142) hide show
  1. package/dist/cli/generate.d.ts +14 -0
  2. package/dist/cli/generate.d.ts.map +1 -0
  3. package/dist/cli/generate.js +297 -0
  4. package/dist/cli/generate.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +111 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/client/index.d.ts +300 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +434 -0
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/client/plugins/admin.d.ts +92 -0
  14. package/dist/client/plugins/admin.d.ts.map +1 -0
  15. package/dist/client/plugins/admin.js +165 -0
  16. package/dist/client/plugins/admin.js.map +1 -0
  17. package/dist/client/primitives.d.ts +57 -0
  18. package/dist/client/primitives.d.ts.map +1 -0
  19. package/dist/client/primitives.js +64 -0
  20. package/dist/client/primitives.js.map +1 -0
  21. package/dist/client/providers.d.ts +14 -0
  22. package/dist/client/providers.d.ts.map +1 -0
  23. package/dist/client/providers.js +25 -0
  24. package/dist/client/providers.js.map +1 -0
  25. package/dist/client/react.d.ts +23 -0
  26. package/dist/client/react.d.ts.map +1 -0
  27. package/dist/client/react.js +48 -0
  28. package/dist/client/react.js.map +1 -0
  29. package/dist/client/tanstack-start-client-plugins.d.ts +34 -0
  30. package/dist/client/tanstack-start-client-plugins.d.ts.map +1 -0
  31. package/dist/client/tanstack-start-client-plugins.js +32 -0
  32. package/dist/client/tanstack-start-client-plugins.js.map +1 -0
  33. package/dist/client/tanstack-start-client.d.ts +52 -0
  34. package/dist/client/tanstack-start-client.d.ts.map +1 -0
  35. package/dist/client/tanstack-start-client.js +130 -0
  36. package/dist/client/tanstack-start-client.js.map +1 -0
  37. package/dist/client/tanstack-start-plugins.d.ts +27 -0
  38. package/dist/client/tanstack-start-plugins.d.ts.map +1 -0
  39. package/dist/client/tanstack-start-plugins.js +145 -0
  40. package/dist/client/tanstack-start-plugins.js.map +1 -0
  41. package/dist/client/tanstack-start.d.ts +130 -0
  42. package/dist/client/tanstack-start.d.ts.map +1 -0
  43. package/dist/client/tanstack-start.js +331 -0
  44. package/dist/client/tanstack-start.js.map +1 -0
  45. package/dist/component/_generated/api.d.ts +50 -0
  46. package/dist/component/_generated/api.d.ts.map +1 -0
  47. package/dist/component/_generated/api.js +31 -0
  48. package/dist/component/_generated/api.js.map +1 -0
  49. package/dist/component/_generated/component.d.ts +92 -0
  50. package/dist/component/_generated/component.d.ts.map +1 -0
  51. package/dist/component/_generated/component.js +11 -0
  52. package/dist/component/_generated/component.js.map +1 -0
  53. package/dist/component/_generated/dataModel.d.ts +46 -0
  54. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  55. package/dist/component/_generated/dataModel.js +11 -0
  56. package/dist/component/_generated/dataModel.js.map +1 -0
  57. package/dist/component/_generated/server.d.ts +121 -0
  58. package/dist/component/_generated/server.d.ts.map +1 -0
  59. package/dist/component/_generated/server.js +78 -0
  60. package/dist/component/_generated/server.js.map +1 -0
  61. package/dist/component/convex.config.d.ts +3 -0
  62. package/dist/component/convex.config.d.ts.map +1 -0
  63. package/dist/component/convex.config.js +4 -0
  64. package/dist/component/convex.config.js.map +1 -0
  65. package/dist/component/core/sessions.d.ts +33 -0
  66. package/dist/component/core/sessions.d.ts.map +1 -0
  67. package/dist/component/core/sessions.js +186 -0
  68. package/dist/component/core/sessions.js.map +1 -0
  69. package/dist/component/core/users.d.ts +19 -0
  70. package/dist/component/core/users.d.ts.map +1 -0
  71. package/dist/component/core/users.js +154 -0
  72. package/dist/component/core/users.js.map +1 -0
  73. package/dist/component/core/verifications.d.ts +34 -0
  74. package/dist/component/core/verifications.d.ts.map +1 -0
  75. package/dist/component/core/verifications.js +135 -0
  76. package/dist/component/core/verifications.js.map +1 -0
  77. package/dist/component/gateway.d.ts +16 -0
  78. package/dist/component/gateway.d.ts.map +1 -0
  79. package/dist/component/gateway.js +229 -0
  80. package/dist/component/gateway.js.map +1 -0
  81. package/dist/component/lib/crypto.d.ts +24 -0
  82. package/dist/component/lib/crypto.d.ts.map +1 -0
  83. package/dist/component/lib/crypto.js +57 -0
  84. package/dist/component/lib/crypto.js.map +1 -0
  85. package/dist/component/lib/rateLimit.d.ts +26 -0
  86. package/dist/component/lib/rateLimit.d.ts.map +1 -0
  87. package/dist/component/lib/rateLimit.js +96 -0
  88. package/dist/component/lib/rateLimit.js.map +1 -0
  89. package/dist/component/lib/validators.d.ts +19 -0
  90. package/dist/component/lib/validators.d.ts.map +1 -0
  91. package/dist/component/lib/validators.js +12 -0
  92. package/dist/component/lib/validators.js.map +1 -0
  93. package/dist/component/plugins/admin.d.ts +72 -0
  94. package/dist/component/plugins/admin.d.ts.map +1 -0
  95. package/dist/component/plugins/admin.js +152 -0
  96. package/dist/component/plugins/admin.js.map +1 -0
  97. package/dist/component/providers/emailPassword.d.ts +49 -0
  98. package/dist/component/providers/emailPassword.d.ts.map +1 -0
  99. package/dist/component/providers/emailPassword.js +316 -0
  100. package/dist/component/providers/emailPassword.js.map +1 -0
  101. package/dist/component/providers/oauth.d.ts +33 -0
  102. package/dist/component/providers/oauth.d.ts.map +1 -0
  103. package/dist/component/providers/oauth.js +256 -0
  104. package/dist/component/providers/oauth.js.map +1 -0
  105. package/dist/component/schema.d.ts +132 -0
  106. package/dist/component/schema.d.ts.map +1 -0
  107. package/dist/component/schema.js +82 -0
  108. package/dist/component/schema.js.map +1 -0
  109. package/dist/types.d.ts +67 -0
  110. package/dist/types.d.ts.map +1 -0
  111. package/dist/types.js +5 -0
  112. package/dist/types.js.map +1 -0
  113. package/package.json +121 -0
  114. package/src/cli/generate.ts +360 -0
  115. package/src/cli/index.ts +133 -0
  116. package/src/client/index.ts +707 -0
  117. package/src/client/plugins/admin.ts +205 -0
  118. package/src/client/primitives.ts +100 -0
  119. package/src/client/providers.ts +35 -0
  120. package/src/client/react.ts +97 -0
  121. package/src/client/tanstack-start-client-plugins.ts +113 -0
  122. package/src/client/tanstack-start-client.ts +259 -0
  123. package/src/client/tanstack-start-plugins.ts +203 -0
  124. package/src/client/tanstack-start.ts +535 -0
  125. package/src/component/_generated/api.ts +70 -0
  126. package/src/component/_generated/component.ts +184 -0
  127. package/src/component/_generated/dataModel.ts +60 -0
  128. package/src/component/_generated/server.ts +156 -0
  129. package/src/component/convex.config.ts +5 -0
  130. package/src/component/core/sessions.ts +228 -0
  131. package/src/component/core/users.ts +199 -0
  132. package/src/component/core/verifications.ts +173 -0
  133. package/src/component/gateway.ts +321 -0
  134. package/src/component/lib/crypto.ts +63 -0
  135. package/src/component/lib/internalApi.ts +66 -0
  136. package/src/component/lib/rateLimit.ts +111 -0
  137. package/src/component/lib/validators.ts +12 -0
  138. package/src/component/plugins/admin.ts +178 -0
  139. package/src/component/providers/emailPassword.ts +374 -0
  140. package/src/component/providers/oauth.ts +324 -0
  141. package/src/component/schema.ts +88 -0
  142. package/src/types.ts +68 -0
@@ -0,0 +1,199 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation, internalQuery } from "../_generated/server";
3
+
4
+ /** Create a new user record. Returns the new user's ID. */
5
+ export const create = internalMutation({
6
+ args: {
7
+ email: v.string(),
8
+ emailVerified: v.boolean(),
9
+ name: v.optional(v.string()),
10
+ image: v.optional(v.string()),
11
+ },
12
+ handler: async (ctx, args) => {
13
+ const now = Date.now();
14
+ const userDoc: {
15
+ email: string;
16
+ emailVerified: boolean;
17
+ createdAt: number;
18
+ updatedAt: number;
19
+ name?: string;
20
+ image?: string;
21
+ } = {
22
+ email: args.email,
23
+ emailVerified: args.emailVerified,
24
+ createdAt: now,
25
+ updatedAt: now,
26
+ };
27
+ if (args.name !== undefined) {
28
+ userDoc.name = args.name;
29
+ }
30
+ if (args.image !== undefined) {
31
+ userDoc.image = args.image;
32
+ }
33
+
34
+ return await ctx.db.insert("users", userDoc);
35
+ },
36
+ });
37
+
38
+ /** Get a user by email address. Returns null if not found. */
39
+ export const getByEmail = internalQuery({
40
+ args: { email: v.string() },
41
+ handler: async (ctx, { email }) => {
42
+ return await ctx.db
43
+ .query("users")
44
+ .withIndex("by_email", (q) => q.eq("email", email))
45
+ .unique();
46
+ },
47
+ });
48
+
49
+ /** Get a user by ID. Returns null if not found. */
50
+ export const getById = internalQuery({
51
+ args: { userId: v.id("users") },
52
+ handler: async (ctx, { userId }) => {
53
+ return await ctx.db.get(userId);
54
+ },
55
+ });
56
+
57
+ /** Update mutable fields on a user. */
58
+ export const update = internalMutation({
59
+ args: {
60
+ userId: v.id("users"),
61
+ email: v.optional(v.string()),
62
+ emailVerified: v.optional(v.boolean()),
63
+ name: v.optional(v.string()),
64
+ image: v.optional(v.string()),
65
+ role: v.optional(v.string()),
66
+ banned: v.optional(v.boolean()),
67
+ banReason: v.optional(v.string()),
68
+ banExpires: v.optional(v.number()),
69
+ },
70
+ handler: async (ctx, { userId, ...fields }) => {
71
+ const patch: Record<string, unknown> = { updatedAt: Date.now() };
72
+ for (const [key, value] of Object.entries(fields)) {
73
+ if (value !== undefined) {
74
+ patch[key] = value;
75
+ }
76
+ }
77
+ await ctx.db.patch(userId, patch);
78
+ },
79
+ });
80
+
81
+ /** Delete a user and all associated accounts and sessions. */
82
+ export const remove = internalMutation({
83
+ args: { userId: v.id("users") },
84
+ handler: async (ctx, { userId }) => {
85
+ // Delete accounts
86
+ const accounts = await ctx.db
87
+ .query("accounts")
88
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
89
+ .collect();
90
+ for (const account of accounts) {
91
+ await ctx.db.delete(account._id);
92
+ }
93
+
94
+ // Delete sessions
95
+ const sessions = await ctx.db
96
+ .query("sessions")
97
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
98
+ .collect();
99
+ for (const session of sessions) {
100
+ await ctx.db.delete(session._id);
101
+ }
102
+
103
+ // Delete user
104
+ await ctx.db.delete(userId);
105
+ },
106
+ });
107
+
108
+ /** Create an account entry for a user. */
109
+ export const createAccount = internalMutation({
110
+ args: {
111
+ userId: v.id("users"),
112
+ providerId: v.string(),
113
+ accountId: v.string(),
114
+ passwordHash: v.optional(v.string()),
115
+ accessToken: v.optional(v.string()),
116
+ refreshToken: v.optional(v.string()),
117
+ accessTokenExpiresAt: v.optional(v.number()),
118
+ },
119
+ handler: async (ctx, args) => {
120
+ const now = Date.now();
121
+ const accountDoc: {
122
+ userId: typeof args.userId;
123
+ providerId: string;
124
+ accountId: string;
125
+ createdAt: number;
126
+ updatedAt: number;
127
+ passwordHash?: string;
128
+ accessToken?: string;
129
+ refreshToken?: string;
130
+ accessTokenExpiresAt?: number;
131
+ } = {
132
+ userId: args.userId,
133
+ providerId: args.providerId,
134
+ accountId: args.accountId,
135
+ createdAt: now,
136
+ updatedAt: now,
137
+ };
138
+ if (args.passwordHash !== undefined) {
139
+ accountDoc.passwordHash = args.passwordHash;
140
+ }
141
+ if (args.accessToken !== undefined) {
142
+ accountDoc.accessToken = args.accessToken;
143
+ }
144
+ if (args.refreshToken !== undefined) {
145
+ accountDoc.refreshToken = args.refreshToken;
146
+ }
147
+ if (args.accessTokenExpiresAt !== undefined) {
148
+ accountDoc.accessTokenExpiresAt = args.accessTokenExpiresAt;
149
+ }
150
+
151
+ return await ctx.db.insert("accounts", accountDoc);
152
+ },
153
+ });
154
+
155
+ /** Get an account by provider + accountId. */
156
+ export const getAccount = internalQuery({
157
+ args: {
158
+ providerId: v.string(),
159
+ accountId: v.string(),
160
+ },
161
+ handler: async (ctx, { providerId, accountId }) => {
162
+ return await ctx.db
163
+ .query("accounts")
164
+ .withIndex("by_provider_accountId", (q) =>
165
+ q.eq("providerId", providerId).eq("accountId", accountId)
166
+ )
167
+ .unique();
168
+ },
169
+ });
170
+
171
+ /** Get all accounts for a user. */
172
+ export const getAccountsByUserId = internalQuery({
173
+ args: { userId: v.id("users") },
174
+ handler: async (ctx, { userId }) => {
175
+ return await ctx.db
176
+ .query("accounts")
177
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
178
+ .collect();
179
+ },
180
+ });
181
+
182
+ /** Update an account's tokens. */
183
+ export const updateAccount = internalMutation({
184
+ args: {
185
+ accountId: v.id("accounts"),
186
+ accessToken: v.optional(v.string()),
187
+ refreshToken: v.optional(v.string()),
188
+ accessTokenExpiresAt: v.optional(v.number()),
189
+ },
190
+ handler: async (ctx, { accountId, ...fields }) => {
191
+ const patch: Record<string, unknown> = { updatedAt: Date.now() };
192
+ for (const [key, value] of Object.entries(fields)) {
193
+ if (value !== undefined) {
194
+ patch[key] = value;
195
+ }
196
+ }
197
+ await ctx.db.patch(accountId, patch);
198
+ },
199
+ });
@@ -0,0 +1,173 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation, internalQuery } from "../_generated/server";
3
+ import { generateCode, hashToken } from "../lib/crypto";
4
+
5
+ /** Max attempts before a verification code is locked. */
6
+ const MAX_ATTEMPTS = 10;
7
+
8
+ /** Email verification TTL: 60 minutes. */
9
+ const EMAIL_VERIFICATION_TTL_MS = 60 * 60 * 1000;
10
+
11
+ /** Password reset TTL: 15 minutes. */
12
+ const PASSWORD_RESET_TTL_MS = 15 * 60 * 1000;
13
+
14
+ /**
15
+ * Create a verification code for a given identifier and type.
16
+ * Any previous verification of the same type is replaced.
17
+ * Returns the raw code (to be sent to the user) and the expiry.
18
+ */
19
+ export const create = internalMutation({
20
+ args: {
21
+ identifier: v.string(),
22
+ type: v.union(
23
+ v.literal("email-verification"),
24
+ v.literal("password-reset")
25
+ ),
26
+ },
27
+ handler: async (ctx, { identifier, type }) => {
28
+ const now = Date.now();
29
+ const code = generateCode();
30
+ const codeHash = await hashToken(code);
31
+ const ttl =
32
+ type === "email-verification"
33
+ ? EMAIL_VERIFICATION_TTL_MS
34
+ : PASSWORD_RESET_TTL_MS;
35
+ const expiresAt = now + ttl;
36
+
37
+ // Remove any existing verification of this type for this identifier
38
+ const existing = await ctx.db
39
+ .query("verifications")
40
+ .withIndex("by_identifier_type", (q) =>
41
+ q.eq("identifier", identifier).eq("type", type)
42
+ )
43
+ .unique();
44
+ if (existing) {
45
+ await ctx.db.delete(existing._id);
46
+ }
47
+
48
+ await ctx.db.insert("verifications", {
49
+ identifier,
50
+ type,
51
+ codeHash,
52
+ expiresAt,
53
+ attempts: 0,
54
+ createdAt: now,
55
+ });
56
+
57
+ return { code, expiresAt };
58
+ },
59
+ });
60
+
61
+ /**
62
+ * Get verification record without consuming it.
63
+ */
64
+ export const getByIdentifierType = internalQuery({
65
+ args: {
66
+ identifier: v.string(),
67
+ type: v.union(
68
+ v.literal("email-verification"),
69
+ v.literal("password-reset")
70
+ ),
71
+ },
72
+ handler: async (ctx, { identifier, type }) => {
73
+ return await ctx.db
74
+ .query("verifications")
75
+ .withIndex("by_identifier_type", (q) =>
76
+ q.eq("identifier", identifier).eq("type", type)
77
+ )
78
+ .unique();
79
+ },
80
+ });
81
+
82
+ /**
83
+ * Verify a code for an identifier and type.
84
+ * Increments attempt count; invalidates after MAX_ATTEMPTS.
85
+ * Returns status: "valid" | "invalid" | "expired" | "too_many_attempts"
86
+ */
87
+ export const verify = internalMutation({
88
+ args: {
89
+ identifier: v.string(),
90
+ type: v.union(
91
+ v.literal("email-verification"),
92
+ v.literal("password-reset")
93
+ ),
94
+ code: v.string(),
95
+ },
96
+ handler: async (ctx, { identifier, type, code }) => {
97
+ const record = await ctx.db
98
+ .query("verifications")
99
+ .withIndex("by_identifier_type", (q) =>
100
+ q.eq("identifier", identifier).eq("type", type)
101
+ )
102
+ .unique();
103
+
104
+ if (!record) {
105
+ return { status: "invalid" as const };
106
+ }
107
+
108
+ const now = Date.now();
109
+
110
+ if (record.expiresAt < now) {
111
+ await ctx.db.delete(record._id);
112
+ return { status: "expired" as const };
113
+ }
114
+
115
+ if (record.attempts >= MAX_ATTEMPTS) {
116
+ return { status: "too_many_attempts" as const };
117
+ }
118
+
119
+ const codeHash = await hashToken(code);
120
+
121
+ if (codeHash !== record.codeHash) {
122
+ // Increment attempts
123
+ await ctx.db.patch(record._id, { attempts: record.attempts + 1 });
124
+ if (record.attempts + 1 >= MAX_ATTEMPTS) {
125
+ return { status: "too_many_attempts" as const };
126
+ }
127
+ return { status: "invalid" as const };
128
+ }
129
+
130
+ // Valid — delete the verification (single-use)
131
+ await ctx.db.delete(record._id);
132
+ return { status: "valid" as const };
133
+ },
134
+ });
135
+
136
+ /** Delete a verification record (e.g., after successful use or on cancel). */
137
+ export const invalidate = internalMutation({
138
+ args: {
139
+ identifier: v.string(),
140
+ type: v.union(
141
+ v.literal("email-verification"),
142
+ v.literal("password-reset")
143
+ ),
144
+ },
145
+ handler: async (ctx, { identifier, type }) => {
146
+ const record = await ctx.db
147
+ .query("verifications")
148
+ .withIndex("by_identifier_type", (q) =>
149
+ q.eq("identifier", identifier).eq("type", type)
150
+ )
151
+ .unique();
152
+ if (record) {
153
+ await ctx.db.delete(record._id);
154
+ }
155
+ },
156
+ });
157
+
158
+ /**
159
+ * Cleanup expired verifications (intended to be scheduled).
160
+ */
161
+ export const cleanup = internalMutation({
162
+ args: {},
163
+ handler: async (ctx) => {
164
+ const now = Date.now();
165
+ // Collect and delete expired records
166
+ const allVerifications = await ctx.db.query("verifications").collect();
167
+ for (const v of allVerifications) {
168
+ if (v.expiresAt < now) {
169
+ await ctx.db.delete(v._id);
170
+ }
171
+ }
172
+ },
173
+ });
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Public gateway — the only functions callable from the host app.
3
+ *
4
+ * All functions are `action` (not `internalAction`) so they appear in the
5
+ * component's public API and can be reached via ctx.runAction / ctx.runQuery
6
+ * from the parent Convex backend. They simply delegate to the corresponding
7
+ * internal functions.
8
+ *
9
+ * Internal functions remain `internalAction`/`internalMutation`/`internalQuery`
10
+ * and are only callable from within this component.
11
+ */
12
+ import { v } from "convex/values";
13
+ import { action } from "./_generated/server";
14
+ import type { Id } from "./_generated/dataModel";
15
+ import { internal } from "./lib/internalApi";
16
+ import { oauthProviderConfigValidator } from "./lib/validators";
17
+
18
+ // ─── Email / Password ──────────────────────────────────────────────────────
19
+
20
+ export const signUp = action({
21
+ args: {
22
+ email: v.string(),
23
+ password: v.string(),
24
+ name: v.optional(v.string()),
25
+ ipAddress: v.optional(v.string()),
26
+ },
27
+ handler: (ctx, args) =>
28
+ ctx.runAction(internal.providers.emailPassword.signUp, args),
29
+ });
30
+
31
+ export const signIn = action({
32
+ args: {
33
+ email: v.string(),
34
+ password: v.string(),
35
+ ipAddress: v.optional(v.string()),
36
+ userAgent: v.optional(v.string()),
37
+ requireEmailVerified: v.optional(v.boolean()),
38
+ },
39
+ handler: (ctx, args) =>
40
+ ctx.runAction(internal.providers.emailPassword.signIn, args),
41
+ });
42
+
43
+ export const verifyEmail = action({
44
+ args: {
45
+ email: v.string(),
46
+ code: v.string(),
47
+ },
48
+ handler: (ctx, args) =>
49
+ ctx.runAction(internal.providers.emailPassword.verifyEmail, args),
50
+ });
51
+
52
+ export const requestPasswordReset = action({
53
+ args: {
54
+ email: v.string(),
55
+ ipAddress: v.optional(v.string()),
56
+ },
57
+ handler: (ctx, args) =>
58
+ ctx.runAction(internal.providers.emailPassword.requestPasswordReset, args),
59
+ });
60
+
61
+ export const resetPassword = action({
62
+ args: {
63
+ email: v.string(),
64
+ code: v.string(),
65
+ newPassword: v.string(),
66
+ },
67
+ handler: (ctx, args) =>
68
+ ctx.runAction(internal.providers.emailPassword.resetPassword, args),
69
+ });
70
+
71
+ // ─── Sessions ──────────────────────────────────────────────────────────────
72
+
73
+ export const validateSession = action({
74
+ args: {
75
+ token: v.string(),
76
+ checkBanned: v.optional(v.boolean()),
77
+ },
78
+ handler: (ctx, args) =>
79
+ ctx.runAction(internal.core.sessions.validate, args),
80
+ });
81
+
82
+ export const getCurrentUser = action({
83
+ args: {
84
+ token: v.string(),
85
+ checkBanned: v.optional(v.boolean()),
86
+ },
87
+ handler: async (ctx, { token, checkBanned }) => {
88
+ const session = await ctx.runAction(internal.core.sessions.validate, {
89
+ token,
90
+ checkBanned,
91
+ });
92
+ if (!session) {
93
+ return null;
94
+ }
95
+ return ctx.runQuery(internal.core.users.getById, {
96
+ userId: session.userId,
97
+ });
98
+ },
99
+ });
100
+
101
+ export const getUserById = action({
102
+ args: {
103
+ userId: v.string(),
104
+ checkBanned: v.optional(v.boolean()),
105
+ },
106
+ handler: async (ctx, { userId, checkBanned }) => {
107
+ const user = await ctx.runQuery(internal.core.users.getById, {
108
+ userId: userId as Id<"users">,
109
+ });
110
+ if (!user) {
111
+ return null;
112
+ }
113
+ if (!checkBanned || !user.banned) {
114
+ return user;
115
+ }
116
+
117
+ const now = Date.now();
118
+ const banExpires = user.banExpires;
119
+ if (banExpires === undefined || banExpires > now) {
120
+ return null;
121
+ }
122
+
123
+ // Temp ban expired — unban automatically.
124
+ await ctx.runMutation(internal.core.users.update, {
125
+ userId: userId as Id<"users">,
126
+ banned: false,
127
+ });
128
+
129
+ return ctx.runQuery(internal.core.users.getById, {
130
+ userId: userId as Id<"users">,
131
+ });
132
+ },
133
+ });
134
+
135
+ export const invalidateSession = action({
136
+ args: { token: v.string() },
137
+ handler: (ctx, args) =>
138
+ ctx.runMutation(internal.core.sessions.invalidateByToken, args),
139
+ });
140
+
141
+ export const invalidateAllSessions = action({
142
+ args: { userId: v.string() },
143
+ handler: (ctx, { userId }) =>
144
+ ctx.runMutation(internal.core.sessions.invalidateAll, {
145
+ userId: userId as Id<"users">,
146
+ }),
147
+ });
148
+
149
+ // ─── OAuth ─────────────────────────────────────────────────────────────────
150
+
151
+ export const getAuthorizationUrl = action({
152
+ args: {
153
+ provider: oauthProviderConfigValidator,
154
+ redirectUrl: v.optional(v.string()),
155
+ },
156
+ handler: (ctx, args) =>
157
+ ctx.runAction(internal.providers.oauth.getAuthorizationUrl, args),
158
+ });
159
+
160
+ export const handleCallback = action({
161
+ args: {
162
+ provider: oauthProviderConfigValidator,
163
+ code: v.string(),
164
+ state: v.string(),
165
+ redirectUrl: v.optional(v.string()),
166
+ ipAddress: v.optional(v.string()),
167
+ userAgent: v.optional(v.string()),
168
+ },
169
+ handler: (ctx, args) =>
170
+ ctx.runAction(internal.providers.oauth.handleCallback, args),
171
+ });
172
+
173
+ // ─── Admin ─────────────────────────────────────────────────────────────────
174
+
175
+ export const adminListUsers = action({
176
+ args: {
177
+ adminToken: v.string(),
178
+ limit: v.optional(v.number()),
179
+ cursor: v.optional(v.string()),
180
+ },
181
+ handler: async (ctx, { adminToken, limit, cursor }) => {
182
+ const session = await ctx.runAction(internal.core.sessions.validate, {
183
+ token: adminToken,
184
+ checkBanned: true,
185
+ });
186
+ if (!session) {
187
+ throw new Error("Unauthorized");
188
+ }
189
+
190
+ const adminUser = await ctx.runQuery(internal.core.users.getById, {
191
+ userId: session.userId,
192
+ });
193
+ if (!adminUser || adminUser.role !== "admin") {
194
+ throw new Error("Forbidden");
195
+ }
196
+
197
+ return ctx.runQuery(internal.plugins.admin.listUsers, {
198
+ actorUserId: session.userId,
199
+ limit,
200
+ cursor,
201
+ });
202
+ },
203
+ });
204
+
205
+ export const adminBanUser = action({
206
+ args: {
207
+ adminToken: v.string(),
208
+ userId: v.string(),
209
+ reason: v.optional(v.string()),
210
+ expiresAt: v.optional(v.number()),
211
+ },
212
+ handler: async (ctx, { adminToken, userId, reason, expiresAt }) => {
213
+ const session = await ctx.runAction(internal.core.sessions.validate, {
214
+ token: adminToken,
215
+ checkBanned: true,
216
+ });
217
+ if (!session) {
218
+ throw new Error("Unauthorized");
219
+ }
220
+
221
+ const adminUser = await ctx.runQuery(internal.core.users.getById, {
222
+ userId: session.userId,
223
+ });
224
+ if (!adminUser || adminUser.role !== "admin") {
225
+ throw new Error("Forbidden");
226
+ }
227
+
228
+ return ctx.runMutation(internal.plugins.admin.banUser, {
229
+ actorUserId: session.userId,
230
+ userId: userId as Id<"users">,
231
+ reason,
232
+ expiresAt,
233
+ });
234
+ },
235
+ });
236
+
237
+ export const adminUnbanUser = action({
238
+ args: {
239
+ adminToken: v.string(),
240
+ userId: v.string(),
241
+ },
242
+ handler: async (ctx, { adminToken, userId }) => {
243
+ const session = await ctx.runAction(internal.core.sessions.validate, {
244
+ token: adminToken,
245
+ checkBanned: true,
246
+ });
247
+ if (!session) {
248
+ throw new Error("Unauthorized");
249
+ }
250
+
251
+ const adminUser = await ctx.runQuery(internal.core.users.getById, {
252
+ userId: session.userId,
253
+ });
254
+ if (!adminUser || adminUser.role !== "admin") {
255
+ throw new Error("Forbidden");
256
+ }
257
+
258
+ return ctx.runMutation(internal.plugins.admin.unbanUser, {
259
+ actorUserId: session.userId,
260
+ userId: userId as Id<"users">,
261
+ });
262
+ },
263
+ });
264
+
265
+ export const adminSetRole = action({
266
+ args: {
267
+ adminToken: v.string(),
268
+ userId: v.string(),
269
+ role: v.string(),
270
+ },
271
+ handler: async (ctx, { adminToken, userId, role }) => {
272
+ const session = await ctx.runAction(internal.core.sessions.validate, {
273
+ token: adminToken,
274
+ checkBanned: true,
275
+ });
276
+ if (!session) {
277
+ throw new Error("Unauthorized");
278
+ }
279
+
280
+ const adminUser = await ctx.runQuery(internal.core.users.getById, {
281
+ userId: session.userId,
282
+ });
283
+ if (!adminUser || adminUser.role !== "admin") {
284
+ throw new Error("Forbidden");
285
+ }
286
+
287
+ return ctx.runMutation(internal.plugins.admin.setRole, {
288
+ actorUserId: session.userId,
289
+ userId: userId as Id<"users">,
290
+ role,
291
+ });
292
+ },
293
+ });
294
+
295
+ export const adminDeleteUser = action({
296
+ args: {
297
+ adminToken: v.string(),
298
+ userId: v.string(),
299
+ },
300
+ handler: async (ctx, { adminToken, userId }) => {
301
+ const session = await ctx.runAction(internal.core.sessions.validate, {
302
+ token: adminToken,
303
+ checkBanned: true,
304
+ });
305
+ if (!session) {
306
+ throw new Error("Unauthorized");
307
+ }
308
+
309
+ const adminUser = await ctx.runQuery(internal.core.users.getById, {
310
+ userId: session.userId,
311
+ });
312
+ if (!adminUser || adminUser.role !== "admin") {
313
+ throw new Error("Forbidden");
314
+ }
315
+
316
+ return ctx.runMutation(internal.plugins.admin.deleteUser, {
317
+ actorUserId: session.userId,
318
+ userId: userId as Id<"users">,
319
+ });
320
+ },
321
+ });