-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathserver.ts
More file actions
254 lines (219 loc) · 7.28 KB
/
server.ts
File metadata and controls
254 lines (219 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import 'server-only'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { COOKIE_KEYS, KV_KEYS } from '@/configs/keys'
import { PROTECTED_URLS } from '@/configs/urls'
import { kv } from '@/lib/clients/kv'
import { supabaseAdmin } from '@/lib/clients/supabase/admin'
import { createClient } from '@/lib/clients/supabase/server'
import { getSessionInsecure } from '@/server/auth/get-session'
import { E2BError, UnauthenticatedError } from '@/types/errors'
import { unstable_noStore } from 'next/cache'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { cache } from 'react'
import { serializeError } from 'serialize-error'
import { z } from 'zod'
import { infra } from '../clients/api'
import { l } from '../clients/logger/logger'
import { returnServerError } from './action'
/*
* This function checks if the user is authenticated and returns the user and the supabase client.
* If the user is not authenticated, it throws an error.
*
* @params request - an optional NextRequest object to create a supabase client for route handlers
*/
export async function checkAuthenticated() {
const supabase = await createClient()
// retrieve session from storage medium (cookies)
// if no stored session found, not authenticated
// it's fine to use the "insecure" cookie session here, since we only use it for quick denial and do a proper auth check (auth.getUser) afterwards.
const session = await getSessionInsecure(supabase)
if (!session) {
throw UnauthenticatedError()
}
// now retrieve user from supabase to use further
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
throw UnauthenticatedError()
}
return { user, session, supabase }
}
/*
* This function generates an e2b user access token for a given user.
*/
export async function generateE2BUserAccessToken(supabaseAccessToken: string) {
const TOKEN_NAME = 'e2b_dashboard_generated_access_token'
const res = await infra.POST('/access-tokens', {
body: {
name: TOKEN_NAME,
},
headers: {
...SUPABASE_AUTH_HEADERS(supabaseAccessToken),
},
})
if (res.error) {
l.error({
key: 'GENERATE_E2B_USER_ACCESS_TOKEN:INFRA_ERROR',
message: res.error.message,
error: res.error,
context: {
status: res.response.status,
method: 'POST',
path: '/access-tokens',
name: TOKEN_NAME,
},
})
return returnServerError(`Failed to generate e2b user access token`)
}
return res.data
}
// TODO: we should probably add some team permission system here
/*
* This function checks if a user is authorized to access a team.
* If the user is not authorized, it returns false.
*/
export async function checkUserTeamAuthorization(
userId: string,
teamId: string
) {
if (
!z.string().uuid().safeParse(userId).success ||
!z.string().uuid().safeParse(teamId).success
) {
return false
}
const { data: userTeamsRelationData, error: userTeamsRelationError } =
await supabaseAdmin
.from('users_teams')
.select('*')
.eq('user_id', userId)
.eq('team_id', teamId)
if (userTeamsRelationError) {
throw new Error(
`Failed to fetch users_teams relation (user: ${userId}, team: ${teamId})`
)
}
return !!userTeamsRelationData.length
}
/**
* Forces a component to be dynamically rendered at runtime.
* This opts out of Partial Prerendering (PPR) for the component and its children.
*
* Use this when you need to ensure a component is rendered at request time,
* for example when dealing with user authentication or dynamic data that
* must be fresh on every request.
*
* IMPORTANT: When used in PPR scopes, this must be called before any try-catch blocks
* to properly opt out of static optimization. Placing it inside try-catch blocks
* may result in unexpected behavior.
*
* @example
* // Correct usage - before try-catch
* bailOutFromPPR();
* try {
* // dynamic code
* } catch (e) {}
*
* @see https://nextjs.org/docs/app/api-reference/functions/cookies
*/
export function bailOutFromPPR() {
unstable_noStore()
}
/**
* Resolves a team identifier (UUID or slug) to a team ID.
* If the input is a valid UUID, returns it directly.
* If it's a slug, attempts to resolve it to an ID using Redis cache first, then database.
*
* @param identifier - Team UUID or slug
* @returns Promise<string> - Resolved team ID
* @throws E2BError if team not found or identifier invalid
*/
export async function resolveTeamId(identifier: string): Promise<string> {
// If identifier is UUID, return directly
if (z.string().uuid().safeParse(identifier).success) {
return identifier
}
// Try to get from cache first
const cacheKey = KV_KEYS.TEAM_SLUG_TO_ID(identifier)
const cachedId = await kv.get<string>(cacheKey)
if (cachedId) return cachedId
// Not in cache or invalid cache, query database
const { data: team, error } = await supabaseAdmin
.from('teams')
.select('id')
.eq('slug', identifier)
.single()
if (error || !team) {
l.error({
key: 'resolve_team_id:failed_to_resolve_team_id_from_slug',
message: error.message,
error: serializeError(error),
context: {
identifier,
},
})
throw new E2BError('INVALID_PARAMETERS', 'Invalid team identifier')
}
// Cache the result
await Promise.all([
kv.set(cacheKey, team.id, { ex: 60 * 60 }), // 1 hour
kv.set(KV_KEYS.TEAM_ID_TO_SLUG(team.id), identifier, { ex: 60 * 60 }),
])
return team.id
}
/**
* Resolves a team identifier (UUID or slug) to a team ID.
* If the input is a valid UUID, returns it directly.
* If it's a slug, attempts to resolve it to an ID using Redis cache first, then database.
*
* This function should be used in page components rather than client components for better performance,
* as it avoids unnecessary database queries by checking cookies first.
*
* @param identifier - Team UUID or slug
* @returns Promise<string> - Resolved team ID
*/
export async function resolveTeamIdInServerComponent(identifier?: string) {
const cookiesStore = await cookies()
let teamId = cookiesStore.get(COOKIE_KEYS.SELECTED_TEAM_ID)?.value
if (!teamId) {
if (!identifier) {
throw redirect(PROTECTED_URLS.DASHBOARD)
}
// Middleware should prevent this case, but just in case
teamId = await resolveTeamId(identifier)
cookiesStore.set(COOKIE_KEYS.SELECTED_TEAM_ID, teamId)
l.info({
key: 'resolve_team_id_in_server_component:resolving_team_id_from_data_sources',
team_id: teamId,
context: {
identifier,
},
})
}
return teamId
}
/**
* Resolves a team slug from cookies.
* If no slug is found, it returns null.
*
*
*/
export async function resolveTeamSlugInServerComponent() {
const cookiesStore = await cookies()
return cookiesStore.get(COOKIE_KEYS.SELECTED_TEAM_SLUG)?.value
}
/**
* Returns a consistent "now" timestamp for the entire request.
* Memoized using React cache() to ensure all server components
* in the same request tree get the exact same timestamp.
*
* The timestamp is rounded to the nearest 5 seconds for better cache alignment
* and to reduce cache fragmentation.
*/
export const getNowMemo = cache(() => {
const now = Date.now()
// round to nearest 5 seconds for better cache alignment
return Math.floor(now / 5000) * 5000
})