@startup-api/cloudflare 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/LICENSE +13 -0
- package/README.md +114 -0
- package/package.json +53 -0
- package/public/index.html +405 -0
- package/public/users/accounts.html +504 -0
- package/public/users/admin/index.html +765 -0
- package/public/users/power-strip.js +658 -0
- package/public/users/profile.html +443 -0
- package/public/users/style.css +493 -0
- package/src/CookieManager.ts +56 -0
- package/src/PowerStrip.ts +23 -0
- package/src/StartupAPIEnv.ts +12 -0
- package/src/auth/GoogleProvider.ts +67 -0
- package/src/auth/OAuthProvider.ts +52 -0
- package/src/auth/TwitchProvider.ts +64 -0
- package/src/auth/index.ts +231 -0
- package/src/billing/PaymentEngine.ts +20 -0
- package/src/billing/Plan.ts +80 -0
- package/src/billing/plansConfig.ts +48 -0
- package/src/handlers/account.ts +246 -0
- package/src/handlers/admin.ts +144 -0
- package/src/handlers/auth.ts +54 -0
- package/src/handlers/ssr.ts +274 -0
- package/src/handlers/user.ts +168 -0
- package/src/handlers/utils.ts +120 -0
- package/src/index.ts +190 -0
- package/src/schemas/account.ts +37 -0
- package/src/schemas/admin.ts +10 -0
- package/src/schemas/billing.ts +11 -0
- package/src/schemas/credential.ts +38 -0
- package/src/schemas/membership.ts +9 -0
- package/src/schemas/session.ts +10 -0
- package/src/schemas/user.ts +22 -0
- package/src/storage/AccountDO.ts +370 -0
- package/src/storage/CredentialDO.ts +82 -0
- package/src/storage/SystemDO.ts +264 -0
- package/src/storage/UserDO.ts +385 -0
- package/worker-configuration.d.ts +11696 -0
- package/wrangler.template.jsonc +55 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { StartupAPIEnv } from '../StartupAPIEnv';
|
|
2
|
+
import { CookieManager } from '../CookieManager';
|
|
3
|
+
|
|
4
|
+
export function getActiveProviders(env: StartupAPIEnv): string[] {
|
|
5
|
+
const providers: string[] = [];
|
|
6
|
+
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
7
|
+
providers.push('google');
|
|
8
|
+
}
|
|
9
|
+
if (env.TWITCH_CLIENT_ID && env.TWITCH_CLIENT_SECRET) {
|
|
10
|
+
providers.push('twitch');
|
|
11
|
+
}
|
|
12
|
+
return providers;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isAdmin(user: any, env: StartupAPIEnv): boolean {
|
|
16
|
+
if (!env.ADMIN_IDS) return false;
|
|
17
|
+
const adminIds = env.ADMIN_IDS.split(',')
|
|
18
|
+
.map((e) => e.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
const userId = user.id;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
adminIds.includes(userId) ||
|
|
25
|
+
(env.ENVIRONMENT === 'test' &&
|
|
26
|
+
adminIds.some((id) => {
|
|
27
|
+
try {
|
|
28
|
+
return userId === env.USER.idFromName(id).toString();
|
|
29
|
+
} catch (_e) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCookies(cookieHeader: string): Record<string, string> {
|
|
37
|
+
return cookieHeader.split(';').reduce(
|
|
38
|
+
(acc, cookie) => {
|
|
39
|
+
const [key, value] = cookie.split('=').map((c) => c.trim());
|
|
40
|
+
if (key && value) acc[key] = value;
|
|
41
|
+
return acc;
|
|
42
|
+
},
|
|
43
|
+
{} as Record<string, string>,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getUserFromSession(request: Request, env: StartupAPIEnv, cookieManager: CookieManager): Promise<any> {
|
|
48
|
+
const cookieHeader = request.headers.get('Cookie');
|
|
49
|
+
if (!cookieHeader) return null;
|
|
50
|
+
|
|
51
|
+
const cookies = parseCookies(cookieHeader);
|
|
52
|
+
const sessionCookieEncrypted = cookies['session_id'];
|
|
53
|
+
if (!sessionCookieEncrypted) return null;
|
|
54
|
+
|
|
55
|
+
const sessionCookie = await cookieManager.decrypt(sessionCookieEncrypted);
|
|
56
|
+
if (!sessionCookie || !sessionCookie.includes(':')) return null;
|
|
57
|
+
|
|
58
|
+
const [sessionId, doId] = sessionCookie.split(':');
|
|
59
|
+
try {
|
|
60
|
+
const id = env.USER.idFromString(doId);
|
|
61
|
+
const userStub = env.USER.get(id);
|
|
62
|
+
const result = await userStub.validateSession(sessionId);
|
|
63
|
+
if (result.valid) return { id: doId, sessionId, profile: result.profile, credential: result.credential };
|
|
64
|
+
} catch (_e) {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function checkAndClearStaleSession(
|
|
71
|
+
request: Request,
|
|
72
|
+
env: StartupAPIEnv,
|
|
73
|
+
cookieManager: CookieManager,
|
|
74
|
+
originalResponse: Response,
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
const cookieHeader = request.headers.get('Cookie');
|
|
77
|
+
if (!cookieHeader) return originalResponse;
|
|
78
|
+
|
|
79
|
+
const cookies = parseCookies(cookieHeader);
|
|
80
|
+
const sessionCookieEncrypted = cookies['session_id'];
|
|
81
|
+
if (!sessionCookieEncrypted) return originalResponse;
|
|
82
|
+
|
|
83
|
+
const sessionCookie = await cookieManager.decrypt(sessionCookieEncrypted);
|
|
84
|
+
if (!sessionCookie || !sessionCookie.includes(':')) return originalResponse;
|
|
85
|
+
|
|
86
|
+
const [sessionId, doId] = sessionCookie.split(':');
|
|
87
|
+
try {
|
|
88
|
+
const id = env.USER.idFromString(doId);
|
|
89
|
+
const userStub = env.USER.get(id);
|
|
90
|
+
|
|
91
|
+
// If we are here, it means getUserFromSession already returned null.
|
|
92
|
+
// We want to know IF it's because the user was deleted.
|
|
93
|
+
const profile = await userStub.getProfile();
|
|
94
|
+
if (Object.keys(profile).length === 0) {
|
|
95
|
+
// User was deleted! Clear session in DO and remove cookie.
|
|
96
|
+
await userStub.deleteSession(sessionId);
|
|
97
|
+
|
|
98
|
+
const headers = new Headers(originalResponse.headers);
|
|
99
|
+
headers.set('Set-Cookie', 'session_id=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0');
|
|
100
|
+
|
|
101
|
+
// If it was a redirect, we just update the headers
|
|
102
|
+
if (originalResponse.status === 301 || originalResponse.status === 302) {
|
|
103
|
+
return new Response(null, {
|
|
104
|
+
status: originalResponse.status,
|
|
105
|
+
headers,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Otherwise return new response with same body/status but updated headers
|
|
110
|
+
return new Response(originalResponse.body, {
|
|
111
|
+
status: originalResponse.status,
|
|
112
|
+
headers,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return originalResponse;
|
|
120
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { handleAuth } from './auth/index';
|
|
2
|
+
import { injectPowerStrip } from './PowerStrip';
|
|
3
|
+
import { UserDO } from './storage/UserDO';
|
|
4
|
+
import { AccountDO } from './storage/AccountDO';
|
|
5
|
+
import { SystemDO } from './storage/SystemDO';
|
|
6
|
+
import { CredentialDO } from './storage/CredentialDO';
|
|
7
|
+
import { CookieManager } from './CookieManager';
|
|
8
|
+
import { initPlans } from './billing/plansConfig';
|
|
9
|
+
import { getActiveProviders, parseCookies, getUserFromSession } from './handlers/utils';
|
|
10
|
+
import { handleAdmin } from './handlers/admin';
|
|
11
|
+
import {
|
|
12
|
+
handleMe,
|
|
13
|
+
handleUpdateProfile,
|
|
14
|
+
handleListCredentials,
|
|
15
|
+
handleDeleteCredential,
|
|
16
|
+
handleMeImage,
|
|
17
|
+
handleUserImage,
|
|
18
|
+
} from './handlers/user';
|
|
19
|
+
import { handleMyAccounts, handleSwitchAccount, handleAccountDetails, handleAccountImage, handleAccountMembers } from './handlers/account';
|
|
20
|
+
import { handleLogout } from './handlers/auth';
|
|
21
|
+
import { handleSSR } from './handlers/ssr';
|
|
22
|
+
import { Plan } from './billing/Plan';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_USERS_PATH = '/users/';
|
|
25
|
+
|
|
26
|
+
export { UserDO, AccountDO, SystemDO, CredentialDO };
|
|
27
|
+
|
|
28
|
+
import type { StartupAPIEnv } from './StartupAPIEnv';
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
/**
|
|
32
|
+
* Main Worker fetch handler.
|
|
33
|
+
*/
|
|
34
|
+
async fetch(request: Request, env: StartupAPIEnv): Promise<Response> {
|
|
35
|
+
if (!Plan.isInitialized()) {
|
|
36
|
+
initPlans();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Prevent infinite loops when serving assets
|
|
40
|
+
if (request.headers.has('x-skip-worker')) {
|
|
41
|
+
return env.ASSETS.fetch(request);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!env.ORIGIN_URL || !env.SESSION_SECRET) {
|
|
45
|
+
return env.ASSETS.fetch(request);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const url = new URL(request.url);
|
|
49
|
+
const usersPath = env.USERS_PATH || DEFAULT_USERS_PATH;
|
|
50
|
+
|
|
51
|
+
const cookieManager = new CookieManager(env.SESSION_SECRET);
|
|
52
|
+
|
|
53
|
+
// SSR Routes
|
|
54
|
+
const usersPathNormalized = usersPath.endsWith('/') ? usersPath : usersPath + '/';
|
|
55
|
+
if (url.pathname.startsWith(usersPathNormalized)) {
|
|
56
|
+
const subPath = url.pathname.slice(usersPathNormalized.length);
|
|
57
|
+
const isProfile = subPath === 'profile.html' || subPath === 'profile';
|
|
58
|
+
const isAccounts = subPath === 'accounts.html' || subPath === 'accounts';
|
|
59
|
+
|
|
60
|
+
if (isProfile || isAccounts) {
|
|
61
|
+
return handleSSR(request, env, url, usersPath, cookieManager);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle OAuth Routes
|
|
66
|
+
if (url.pathname.startsWith(usersPath + 'auth/')) {
|
|
67
|
+
return handleAuth(request, env, url, usersPath, cookieManager);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (url.pathname === usersPath + 'me/avatar') {
|
|
71
|
+
return handleMeImage(request, env, 'avatar', cookieManager);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle API Routes
|
|
75
|
+
if (url.pathname.startsWith(usersPath + 'api/')) {
|
|
76
|
+
const apiPath = url.pathname.replace(usersPath + 'api/', '/');
|
|
77
|
+
|
|
78
|
+
if (apiPath === '/me') {
|
|
79
|
+
return handleMe(request, env, cookieManager);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (apiPath === '/me/profile' && request.method === 'POST') {
|
|
83
|
+
return handleUpdateProfile(request, env, cookieManager);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (apiPath === '/me/credentials') {
|
|
87
|
+
if (request.method === 'GET') {
|
|
88
|
+
return handleListCredentials(request, env, cookieManager);
|
|
89
|
+
} else if (request.method === 'DELETE') {
|
|
90
|
+
return handleDeleteCredential(request, env, cookieManager);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (apiPath === '/stop-impersonation' && request.method === 'POST') {
|
|
95
|
+
const cookieHeader = request.headers.get('Cookie');
|
|
96
|
+
const cookies = parseCookies(cookieHeader || '');
|
|
97
|
+
const backupSessionEncrypted = cookies['backup_session_id'];
|
|
98
|
+
|
|
99
|
+
if (!backupSessionEncrypted) {
|
|
100
|
+
return new Response('No impersonation session found', { status: 400 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const backupSession = await cookieManager.decrypt(backupSessionEncrypted);
|
|
104
|
+
if (!backupSession) {
|
|
105
|
+
return new Response('Invalid backup session', { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headers = new Headers();
|
|
109
|
+
const newSessionIdEncrypted = await cookieManager.encrypt(backupSession);
|
|
110
|
+
headers.set('Set-Cookie', `session_id=${newSessionIdEncrypted}; Path=/; HttpOnly; Secure; SameSite=Lax`);
|
|
111
|
+
headers.append('Set-Cookie', `backup_session_id=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`);
|
|
112
|
+
|
|
113
|
+
return Response.json({ success: true }, { headers });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (apiPath === '/me/accounts') {
|
|
117
|
+
return handleMyAccounts(request, env, cookieManager);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (apiPath === '/me/accounts/switch' && request.method === 'POST') {
|
|
121
|
+
return handleSwitchAccount(request, env, cookieManager);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (apiPath.startsWith('/me/accounts/')) {
|
|
125
|
+
const parts = apiPath.split('/');
|
|
126
|
+
if (parts.length === 4) {
|
|
127
|
+
return handleAccountDetails(request, env, parts[3], cookieManager);
|
|
128
|
+
}
|
|
129
|
+
if (parts.length === 5 && parts[4] === 'avatar') {
|
|
130
|
+
return handleAccountImage(request, env, parts[3], 'avatar', cookieManager);
|
|
131
|
+
}
|
|
132
|
+
if (parts.length >= 5 && parts[4] === 'members') {
|
|
133
|
+
return handleAccountMembers(request, env, parts[3], parts.slice(5), cookieManager);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (apiPath.startsWith('/users/')) {
|
|
138
|
+
const parts = apiPath.split('/');
|
|
139
|
+
if (parts.length === 4 && parts[3] === 'avatar') {
|
|
140
|
+
return handleUserImage(request, env, parts[2], 'avatar', cookieManager);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (url.pathname === usersPath + 'logout') {
|
|
146
|
+
return handleLogout(request, env, url, usersPath, cookieManager);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Admin Routes
|
|
150
|
+
if (url.pathname.startsWith(usersPath + 'admin/')) {
|
|
151
|
+
return handleAdmin(request, env, usersPath, cookieManager);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Intercept requests to usersPath and serve them from the public/users directory.
|
|
155
|
+
if (url.pathname.startsWith(usersPath)) {
|
|
156
|
+
url.pathname = url.pathname.replace(usersPath, '/users/');
|
|
157
|
+
const newRequest = new Request(url.toString(), request);
|
|
158
|
+
newRequest.headers.set('x-skip-worker', 'true');
|
|
159
|
+
return env.ASSETS.fetch(newRequest);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (env.ORIGIN_URL) {
|
|
163
|
+
const originUrl = new URL(env.ORIGIN_URL);
|
|
164
|
+
url.protocol = originUrl.protocol;
|
|
165
|
+
url.host = originUrl.host;
|
|
166
|
+
url.port = originUrl.port;
|
|
167
|
+
|
|
168
|
+
const newRequest = new Request(url.toString(), request);
|
|
169
|
+
newRequest.headers.set('Host', url.host);
|
|
170
|
+
|
|
171
|
+
const user = await getUserFromSession(request, env, cookieManager);
|
|
172
|
+
if (user) {
|
|
173
|
+
newRequest.headers.set('X-StartupAPI-User-Id', user.id);
|
|
174
|
+
const userStub = env.USER.get(env.USER.idFromString(user.id));
|
|
175
|
+
const currentAccount = await userStub.getCurrentAccount();
|
|
176
|
+
if (currentAccount) {
|
|
177
|
+
newRequest.headers.set('X-StartupAPI-Account-Id', currentAccount.account_id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const response = await fetch(newRequest);
|
|
182
|
+
const providers = getActiveProviders(env);
|
|
183
|
+
|
|
184
|
+
return injectPowerStrip(response, usersPath, providers);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// do not modify the request as it will loop through the same worker again
|
|
188
|
+
return env.ASSETS.fetch(request);
|
|
189
|
+
},
|
|
190
|
+
} satisfies ExportedHandler<StartupAPIEnv>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const AccountInfoSchema = z.object({
|
|
4
|
+
id: z.string().optional(),
|
|
5
|
+
name: z.string().nullable().optional(),
|
|
6
|
+
plan: z.string().optional(),
|
|
7
|
+
personal: z.boolean().optional(),
|
|
8
|
+
billing: z.record(z.any()).optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type AccountInfo = z.infer<typeof AccountInfoSchema>;
|
|
12
|
+
|
|
13
|
+
export const MemberSchema = z.object({
|
|
14
|
+
user_id: z.string(),
|
|
15
|
+
role: z.number(),
|
|
16
|
+
joined_at: z.number().optional(),
|
|
17
|
+
name: z.string().optional(),
|
|
18
|
+
picture: z.string().nullable().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type Member = z.infer<typeof MemberSchema>;
|
|
22
|
+
|
|
23
|
+
export const SystemAccountSchema = z.object({
|
|
24
|
+
id: z.string().optional(),
|
|
25
|
+
name: z.string().max(100),
|
|
26
|
+
status: z.string().optional(),
|
|
27
|
+
plan: z.string().optional(),
|
|
28
|
+
member_count: z.number().optional(),
|
|
29
|
+
created_at: z.number().optional(),
|
|
30
|
+
ownerId: z.string().optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type SystemAccount = z.infer<typeof SystemAccountSchema>;
|
|
34
|
+
|
|
35
|
+
export const SwitchAccountSchema = z.object({
|
|
36
|
+
account_id: z.string(),
|
|
37
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const ImpersonateSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
user_id: z.string().optional(),
|
|
6
|
+
userId: z.string().optional(),
|
|
7
|
+
})
|
|
8
|
+
.refine((data) => data.user_id || data.userId, {
|
|
9
|
+
message: 'Either user_id or userId must be provided',
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const BillingStateSchema = z.object({
|
|
4
|
+
plan_slug: z.string(),
|
|
5
|
+
status: z.enum(['active', 'canceled', 'past_due', 'unpaid', 'trialing']),
|
|
6
|
+
schedule_idx: z.number().optional(),
|
|
7
|
+
next_billing_date: z.number().optional(),
|
|
8
|
+
next_plan_slug: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type BillingState = z.infer<typeof BillingStateSchema>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const CredentialSchema = z.object({
|
|
4
|
+
provider: z.string(),
|
|
5
|
+
subject_id: z.coerce.string(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const PublicCredentialSchema = z.object({
|
|
9
|
+
provider: z.string(),
|
|
10
|
+
subject_id: z.coerce.string(),
|
|
11
|
+
email: z.string().optional(),
|
|
12
|
+
created_at: z.number().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type PublicCredential = z.infer<typeof PublicCredentialSchema>;
|
|
16
|
+
|
|
17
|
+
export const OAuthCredentialSchema = z.object({
|
|
18
|
+
subject_id: z.coerce.string(),
|
|
19
|
+
user_id: z.coerce.string(),
|
|
20
|
+
access_token: z.string().nullable().optional(),
|
|
21
|
+
refresh_token: z.string().nullable().optional(),
|
|
22
|
+
expires_at: z.coerce.number().nullable().optional(),
|
|
23
|
+
scope: z
|
|
24
|
+
.union([z.string(), z.array(z.string())])
|
|
25
|
+
.transform((val) => (Array.isArray(val) ? val.join(' ') : val))
|
|
26
|
+
.nullable()
|
|
27
|
+
.optional(),
|
|
28
|
+
profile_data: z.record(z.any()).nullable().optional(),
|
|
29
|
+
created_at: z.number().optional(),
|
|
30
|
+
updated_at: z.number().optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type OAuthCredential = z.input<typeof OAuthCredentialSchema>;
|
|
34
|
+
export type OAuthCredentialOutput = z.output<typeof OAuthCredentialSchema>;
|
|
35
|
+
|
|
36
|
+
export const DeleteCredentialSchema = z.object({
|
|
37
|
+
provider: z.string(),
|
|
38
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const UserProfileSchema = z.object({
|
|
4
|
+
id: z.string().optional(),
|
|
5
|
+
name: z.coerce.string().optional(),
|
|
6
|
+
email: z.string().nullable().optional(),
|
|
7
|
+
picture: z.string().nullable().optional(),
|
|
8
|
+
provider: z.string().nullable().optional(),
|
|
9
|
+
verified_email: z.coerce.boolean().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type UserProfile = z.infer<typeof UserProfileSchema>;
|
|
13
|
+
|
|
14
|
+
export const SystemUserSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
name: z.coerce.string(),
|
|
17
|
+
email: z.string().nullable().optional(),
|
|
18
|
+
provider: z.string().nullable().optional(),
|
|
19
|
+
created_at: z.number().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type SystemUser = z.infer<typeof SystemUserSchema>;
|