@intelligo-dev/auth 1.0.0-beta.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/LICENSE +201 -0
- package/dist/client.js +25 -0
- package/dist/client.js.map +1 -0
- package/dist/edge.js +39 -0
- package/dist/edge.js.map +1 -0
- package/dist/helpers.js +248 -0
- package/dist/helpers.js.map +1 -0
- package/dist/impersonation.js +42 -0
- package/dist/impersonation.js.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/onboarding/errors.js +31 -0
- package/dist/onboarding/errors.js.map +1 -0
- package/dist/onboarding/schemas.js +24 -0
- package/dist/onboarding/schemas.js.map +1 -0
- package/dist/onboarding/service.js +147 -0
- package/dist/onboarding/service.js.map +1 -0
- package/dist/org-api.js +86 -0
- package/dist/org-api.js.map +1 -0
- package/dist/profile/errors.js +32 -0
- package/dist/profile/errors.js.map +1 -0
- package/dist/profile/schemas.js +20 -0
- package/dist/profile/schemas.js.map +1 -0
- package/dist/profile/service.js +157 -0
- package/dist/profile/service.js.map +1 -0
- package/dist/roles.js +17 -0
- package/dist/roles.js.map +1 -0
- package/dist/server.js +251 -0
- package/dist/server.js.map +1 -0
- package/dist/team/errors.js +31 -0
- package/dist/team/errors.js.map +1 -0
- package/dist/team/schemas.js +29 -0
- package/dist/team/schemas.js.map +1 -0
- package/dist/team/service.js +438 -0
- package/dist/team/service.js.map +1 -0
- package/dist/workspace/errors.js +32 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/schemas.js +45 -0
- package/dist/workspace/schemas.js.map +1 -0
- package/dist/workspace/service.js +268 -0
- package/dist/workspace/service.js.map +1 -0
- package/dist/workspace-init.js +121 -0
- package/dist/workspace-init.js.map +1 -0
- package/package.json +58 -0
- package/src/client.ts +27 -0
- package/src/edge.ts +43 -0
- package/src/helpers.ts +317 -0
- package/src/impersonation.ts +57 -0
- package/src/index.ts +109 -0
- package/src/onboarding/errors.ts +52 -0
- package/src/onboarding/schemas.ts +27 -0
- package/src/onboarding/service.ts +174 -0
- package/src/org-api.ts +198 -0
- package/src/profile/errors.ts +58 -0
- package/src/profile/schemas.ts +23 -0
- package/src/profile/service.ts +208 -0
- package/src/roles.ts +17 -0
- package/src/server.ts +305 -0
- package/src/team/errors.ts +71 -0
- package/src/team/schemas.ts +35 -0
- package/src/team/service.ts +611 -0
- package/src/workspace/errors.ts +69 -0
- package/src/workspace/schemas.ts +57 -0
- package/src/workspace/service.ts +381 -0
- package/src/workspace-init.ts +140 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better-Auth Server Configuration
|
|
3
|
+
*
|
|
4
|
+
* Configures authentication with:
|
|
5
|
+
* - Email + password authentication
|
|
6
|
+
* - OAuth providers (Google, GitHub) with graceful env var fallback
|
|
7
|
+
* - Drizzle database adapter
|
|
8
|
+
* - Session persistence (7-day expiration, 1-day update age)
|
|
9
|
+
* - Organization plugin for multi-tenant workspace support (Phase 10)
|
|
10
|
+
* - Email sending via Resend for verification, password reset, welcome (Phase 14)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { betterAuth } from "better-auth";
|
|
14
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
15
|
+
import { admin, organization } from "better-auth/plugins";
|
|
16
|
+
import { createAccessControl } from "better-auth/plugins/access";
|
|
17
|
+
import {
|
|
18
|
+
adminAc,
|
|
19
|
+
defaultStatements,
|
|
20
|
+
userAc,
|
|
21
|
+
} from "better-auth/plugins/admin/access";
|
|
22
|
+
import { PLATFORM_ADMIN_ROLE } from "./roles";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Access control for the platform role.
|
|
26
|
+
*
|
|
27
|
+
* Better-Auth refuses an `adminRoles` entry that no role definition
|
|
28
|
+
* backs — "Invalid admin roles" at build time — which is the right
|
|
29
|
+
* behaviour: it stops a typo from silently granting nothing, or a
|
|
30
|
+
* renamed role from silently granting everything. `platform-admin`
|
|
31
|
+
* takes the plugin's own admin statements unchanged; the product
|
|
32
|
+
* defines no extra platform permissions yet.
|
|
33
|
+
*/
|
|
34
|
+
const accessControl = createAccessControl(defaultStatements);
|
|
35
|
+
const platformAdminRole = accessControl.newRole(adminAc.statements);
|
|
36
|
+
const userRole = accessControl.newRole(userAc.statements);
|
|
37
|
+
import { db } from "@intelligo-dev/core/db";
|
|
38
|
+
import {
|
|
39
|
+
users,
|
|
40
|
+
sessions,
|
|
41
|
+
accounts,
|
|
42
|
+
verifications,
|
|
43
|
+
organization as organizationTable,
|
|
44
|
+
member,
|
|
45
|
+
invitation,
|
|
46
|
+
} from "@intelligo-dev/core/db/schema";
|
|
47
|
+
import {
|
|
48
|
+
sendVerifyEmail,
|
|
49
|
+
sendPasswordResetEmail,
|
|
50
|
+
sendWelcomeEmail,
|
|
51
|
+
sendInvitationEmail,
|
|
52
|
+
} from "@intelligo-dev/core/email";
|
|
53
|
+
import { eq } from "drizzle-orm";
|
|
54
|
+
|
|
55
|
+
export const auth = betterAuth({
|
|
56
|
+
database: drizzleAdapter(db, {
|
|
57
|
+
provider: "pg",
|
|
58
|
+
// Map our schema tables to Better-Auth's expected names
|
|
59
|
+
schema: {
|
|
60
|
+
user: users,
|
|
61
|
+
session: sessions,
|
|
62
|
+
account: accounts,
|
|
63
|
+
verification: verifications,
|
|
64
|
+
// Organization plugin tables (Phase 10)
|
|
65
|
+
organization: organizationTable,
|
|
66
|
+
member: member,
|
|
67
|
+
invitation: invitation,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000",
|
|
72
|
+
|
|
73
|
+
emailAndPassword: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
// Only enforce email verification when a real email provider is configured
|
|
76
|
+
// or in production — otherwise dev users (no RESEND_API_KEY) cannot log in.
|
|
77
|
+
requireEmailVerification:
|
|
78
|
+
process.env.NODE_ENV === "production" || !!process.env.RESEND_API_KEY,
|
|
79
|
+
// Password reset email hook (EMAIL-05)
|
|
80
|
+
sendResetPassword: async ({ user, url }) => {
|
|
81
|
+
sendPasswordResetEmail({
|
|
82
|
+
to: user.email,
|
|
83
|
+
userName: user.name || user.email,
|
|
84
|
+
resetUrl: url,
|
|
85
|
+
}).catch((err) =>
|
|
86
|
+
console.error("[Auth] Failed to send password reset email:", err)
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Email verification hook (EMAIL-04)
|
|
92
|
+
emailVerification: {
|
|
93
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
94
|
+
sendVerifyEmail({
|
|
95
|
+
to: user.email,
|
|
96
|
+
userName: user.name || user.email,
|
|
97
|
+
verificationUrl: url,
|
|
98
|
+
}).catch((err) =>
|
|
99
|
+
console.error("[Auth] Failed to send verification email:", err)
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
sendOnSignUp: true,
|
|
103
|
+
autoSignInAfterVerification: true,
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
socialProviders: {
|
|
107
|
+
// Google OAuth - only enabled when env vars are configured
|
|
108
|
+
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
|
109
|
+
? {
|
|
110
|
+
google: {
|
|
111
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
112
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
: {}),
|
|
116
|
+
|
|
117
|
+
// GitHub OAuth - only enabled when env vars are configured
|
|
118
|
+
...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
|
|
119
|
+
? {
|
|
120
|
+
github: {
|
|
121
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
122
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
session: {
|
|
129
|
+
// Sessions expire after 7 days
|
|
130
|
+
expiresIn: 60 * 60 * 24 * 7,
|
|
131
|
+
// Session token updated every 24 hours (extends expiration on active usage)
|
|
132
|
+
updateAge: 60 * 60 * 24,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"],
|
|
136
|
+
|
|
137
|
+
// Database hooks for automatic workspace setup (WORK-01)
|
|
138
|
+
databaseHooks: {
|
|
139
|
+
user: {
|
|
140
|
+
create: {
|
|
141
|
+
after: async (user) => {
|
|
142
|
+
// Auto-create personal workspace for new users
|
|
143
|
+
try {
|
|
144
|
+
const slug = ((user.email || "user").split("@")[0] || "user")
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
147
|
+
.slice(0, 30);
|
|
148
|
+
|
|
149
|
+
await auth.api.createOrganization({
|
|
150
|
+
headers: new Headers(),
|
|
151
|
+
body: {
|
|
152
|
+
name: `${user.name || "User"}'s Workspace`,
|
|
153
|
+
slug: `${slug}-${Date.now().toString(36)}`,
|
|
154
|
+
userId: user.id, // Associate with the user
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// TR-0024: handle race — if hook fires twice for same signup (two
|
|
159
|
+
// concurrent requests), the unique slug constraint catches the dup.
|
|
160
|
+
// Check if an org was created despite the error.
|
|
161
|
+
if (
|
|
162
|
+
error instanceof Error &&
|
|
163
|
+
(error.message?.includes("duplicate") ||
|
|
164
|
+
error.message?.includes("unique") ||
|
|
165
|
+
error.message?.includes("slug"))
|
|
166
|
+
) {
|
|
167
|
+
const orgs = await auth.api.listOrganizations({
|
|
168
|
+
headers: new Headers(),
|
|
169
|
+
});
|
|
170
|
+
if (orgs && orgs.length > 0) {
|
|
171
|
+
// Org already created by the other hook — not an error.
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Log for ops visibility; do not throw (user registration succeeded)
|
|
176
|
+
console.error(
|
|
177
|
+
"[user.create hook] Failed to create workspace:",
|
|
178
|
+
error
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Send welcome email (EMAIL-03, fire-and-forget).
|
|
183
|
+
// NOTE(DB-12): No retry or outbox — downstream failures silently ignored.
|
|
184
|
+
// Acceptable for v0.2; consider transactional outbox for Phase 14.
|
|
185
|
+
const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"}/dashboard`;
|
|
186
|
+
sendWelcomeEmail({
|
|
187
|
+
to: user.email,
|
|
188
|
+
userName: user.name || user.email,
|
|
189
|
+
dashboardUrl,
|
|
190
|
+
}).catch((err) =>
|
|
191
|
+
console.error("[Auth] Failed to send welcome email:", err)
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
session: {
|
|
197
|
+
create: {
|
|
198
|
+
before: async (session): Promise<{ data: typeof session }> => {
|
|
199
|
+
// Check if user is soft-deleted before creating session
|
|
200
|
+
try {
|
|
201
|
+
const userRecord = await db
|
|
202
|
+
.select({ deletedAt: users.deletedAt })
|
|
203
|
+
.from(users)
|
|
204
|
+
.where(eq(users.id, session.userId))
|
|
205
|
+
.limit(1);
|
|
206
|
+
|
|
207
|
+
if (userRecord[0]?.deletedAt) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"This account has been deleted. Contact support@intelligo.dev to recover your account within 30 days."
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(
|
|
214
|
+
"[session.create hook] Deleted user check failed:",
|
|
215
|
+
error
|
|
216
|
+
);
|
|
217
|
+
throw error; // Re-throw to prevent session creation
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Auto-set active organization on session creation
|
|
221
|
+
// NOTE(DB-11): Uses new Headers() — bypasses any future middleware
|
|
222
|
+
// (bot detection, request signing). Acceptable for v0.2; pass request
|
|
223
|
+
// headers through hook context if middleware layering is needed later.
|
|
224
|
+
try {
|
|
225
|
+
const orgs: any = await auth.api.listOrganizations({
|
|
226
|
+
headers: new Headers(),
|
|
227
|
+
query: { userId: session.userId },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (orgs && orgs.length > 0) {
|
|
231
|
+
// Auto-set active org on session creation
|
|
232
|
+
return {
|
|
233
|
+
data: {
|
|
234
|
+
...session,
|
|
235
|
+
activeOrganizationId: orgs[0].id,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(
|
|
241
|
+
"[session.create hook] Failed to set active org:",
|
|
242
|
+
error
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { data: session };
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
plugins: [
|
|
253
|
+
organization({
|
|
254
|
+
// Allow all users to create organizations (will be plan-gated in Phase 15)
|
|
255
|
+
allowUserToCreateOrganization: async () => true,
|
|
256
|
+
// Generous default limit (will be plan-gated later)
|
|
257
|
+
organizationLimit: 5,
|
|
258
|
+
// Creator becomes owner (explicit for clarity)
|
|
259
|
+
creatorRole: "owner",
|
|
260
|
+
// Invitation email sending via Resend (EMAIL-06, replaces Phase 10 placeholder)
|
|
261
|
+
sendInvitationEmail: async (data) => {
|
|
262
|
+
const appUrl =
|
|
263
|
+
process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000";
|
|
264
|
+
sendInvitationEmail({
|
|
265
|
+
to: data.email,
|
|
266
|
+
inviterName: data.inviter?.user?.name || "A team member",
|
|
267
|
+
workspaceName: data.organization?.name || "a workspace",
|
|
268
|
+
role: data.role || "member",
|
|
269
|
+
acceptUrl: `${appUrl}/accept-invitation/${data.id}`,
|
|
270
|
+
declineUrl: `${appUrl}/invitation/decline?id=${data.id}`,
|
|
271
|
+
}).catch((err) =>
|
|
272
|
+
console.error("[Auth] Failed to send invitation email:", err)
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
// Invitations expire after 7 days (matches TEAM-10)
|
|
276
|
+
invitationExpiresIn: 60 * 60 * 24 * 7,
|
|
277
|
+
}),
|
|
278
|
+
/**
|
|
279
|
+
* Platform administration — enabled only for its impersonation
|
|
280
|
+
* endpoints, which the operational console uses for support.
|
|
281
|
+
*
|
|
282
|
+
* `adminRoles` is deliberately a platform role and not a workspace
|
|
283
|
+
* one: workspace `owner` is per-tenant and every self-serve signup
|
|
284
|
+
* owns their own workspace, so gating anything cross-tenant on it
|
|
285
|
+
* grants it to everybody. `users.role` carries the platform role;
|
|
286
|
+
* requirePlatformAdmin promotes from PLATFORM_ADMIN_EMAILS into it.
|
|
287
|
+
*
|
|
288
|
+
* An impersonation session is capped at 30 minutes. Support work
|
|
289
|
+
* is measured in minutes, and a session inherited by whoever next
|
|
290
|
+
* uses that browser is the failure mode worth designing against.
|
|
291
|
+
*
|
|
292
|
+
* `allowImpersonatingAdmins` stays false: one platform admin
|
|
293
|
+
* cannot take over another's account, which is what keeps the
|
|
294
|
+
* audit trail meaningful.
|
|
295
|
+
*/
|
|
296
|
+
admin({
|
|
297
|
+
ac: accessControl,
|
|
298
|
+
roles: { user: userRole, [PLATFORM_ADMIN_ROLE]: platformAdminRole },
|
|
299
|
+
defaultRole: "user",
|
|
300
|
+
adminRoles: [PLATFORM_ADMIN_ROLE],
|
|
301
|
+
impersonationSessionDuration: 30 * 60,
|
|
302
|
+
allowImpersonatingAdmins: false,
|
|
303
|
+
}),
|
|
304
|
+
],
|
|
305
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team service error type.
|
|
3
|
+
*
|
|
4
|
+
* The team service (./service.ts) throws this for every failure it
|
|
5
|
+
* recognizes rather than returning an ad-hoc `{ success, error }`
|
|
6
|
+
* envelope — that shaping is a transport concern (a Server Action, a
|
|
7
|
+
* route handler) and belongs one layer up, alongside
|
|
8
|
+
* revalidatePath/Sentry/toast/i18n, none of which this package may
|
|
9
|
+
* depend on.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* - `member_limit_reached` — the workspace's plan-defined member cap
|
|
14
|
+
* would be exceeded by this invite (see `TeamServicePorts.checkMemberLimit`).
|
|
15
|
+
* - `invitation_not_found` — the invitation id is not (or is no longer)
|
|
16
|
+
* in the caller's own pending list.
|
|
17
|
+
* - `sole_owner` — the caller is the only owner and cannot leave the
|
|
18
|
+
* workspace without transferring ownership first.
|
|
19
|
+
* - `forbidden` — the caller is unauthenticated, has no active
|
|
20
|
+
* workspace, or lacks the required workspace role.
|
|
21
|
+
* - `invalid_input` — schema validation failed.
|
|
22
|
+
* - `accept_verification_failed` — Better-Auth's accept-invitation call
|
|
23
|
+
* returned without the caller actually becoming a member (defence in
|
|
24
|
+
* depth against a stolen invitation id — see `acceptInvitation`).
|
|
25
|
+
* - `provider_error` — the underlying Better-Auth org-plugin call
|
|
26
|
+
* itself failed (network, upstream API error, etc.).
|
|
27
|
+
*/
|
|
28
|
+
export type TeamServiceErrorCode =
|
|
29
|
+
| "member_limit_reached"
|
|
30
|
+
| "invitation_not_found"
|
|
31
|
+
| "sole_owner"
|
|
32
|
+
| "forbidden"
|
|
33
|
+
| "invalid_input"
|
|
34
|
+
| "accept_verification_failed"
|
|
35
|
+
| "provider_error";
|
|
36
|
+
|
|
37
|
+
export interface TeamServiceErrorMeta {
|
|
38
|
+
/** e.g. the plan's member limit, for `member_limit_reached`. */
|
|
39
|
+
limit?: number;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class TeamServiceError extends Error {
|
|
44
|
+
readonly code: TeamServiceErrorCode;
|
|
45
|
+
readonly meta?: TeamServiceErrorMeta;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
code: TeamServiceErrorCode,
|
|
49
|
+
message: string,
|
|
50
|
+
options?: { meta?: TeamServiceErrorMeta; cause?: unknown }
|
|
51
|
+
) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "TeamServiceError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.meta = options?.meta;
|
|
56
|
+
if (options?.cause !== undefined) {
|
|
57
|
+
// ES2020 target predates the standard `cause` constructor option;
|
|
58
|
+
// assign it directly so `instanceof Error` consumers (and Node's
|
|
59
|
+
// own error inspection) still see it.
|
|
60
|
+
(this as { cause?: unknown }).cause = options.cause;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Restore prototype chain (extending built-ins across some
|
|
64
|
+
// transpilation targets loses `instanceof`).
|
|
65
|
+
Object.setPrototypeOf(this, TeamServiceError.prototype);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isTeamServiceError(error: unknown): error is TeamServiceError {
|
|
70
|
+
return error instanceof TeamServiceError;
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team Management Validation Schemas
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for team invite and member management inputs, shared by
|
|
5
|
+
* the team service and its transports.
|
|
6
|
+
*
|
|
7
|
+
* (Ported from the product application’s team validation module — same
|
|
8
|
+
* semantics. Ignite's copy is retired at cutover.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Invite Member Schema
|
|
15
|
+
* Email + role for inviting new team members
|
|
16
|
+
*/
|
|
17
|
+
export const inviteMemberSchema = z.object({
|
|
18
|
+
email: z.string().email("Please enter a valid email address"),
|
|
19
|
+
role: z.enum(["admin", "member"], {
|
|
20
|
+
required_error: "Please select a role",
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type InviteMemberInput = z.infer<typeof inviteMemberSchema>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Update Role Schema
|
|
28
|
+
* For changing an existing member's role
|
|
29
|
+
*/
|
|
30
|
+
export const updateRoleSchema = z.object({
|
|
31
|
+
memberId: z.string().min(1),
|
|
32
|
+
role: z.enum(["admin", "member"]),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type UpdateRoleInput = z.infer<typeof updateRoleSchema>;
|