Use API error handler depending on error code

This commit is contained in:
MartinBraquet
2026-03-06 15:27:49 +01:00
parent 29445a8aa7
commit 4b58e72607
52 changed files with 309 additions and 249 deletions

View File

@@ -67,7 +67,8 @@ export function ProfileCard({user, profile}: ProfileCardProps) {
We prefer many smaller components that each represent one logical unit, rather than one large component.
Export the main component at the top of the file. Name the component the same as the file (e.g., `profile-card.tsx` `ProfileCard`).
Export the main component at the top of the file. Name the component the same as the file (e.g., `profile-card.tsx`
`ProfileCard`).
### API Calls
@@ -91,18 +92,21 @@ export async function getStaticProps() {
import {useAPIGetter} from 'web/hooks/use-api-getter'
function ProfileList() {
const {data, refresh} = useAPIGetter('get-profiles', {})
const {data, refresh} = useAPIGetter('get-profiles', {})
if (!data) return <Loading />
if (!data) return <Loading / >
return (
<div>
{data.profiles.map((profile) => (
<ProfileCard key={profile.id} user={profile.user} profile={profile} />
))}
<button onClick={refresh}>Refresh</button>
</div>
)
return (
<div>
{
data.profiles.map((profile) => (
<ProfileCard key = {profile.id} user = {profile.user} profile = {profile}
/>
))
}
<button onClick = {refresh} > Refresh < /button>
< /div>
)
}
```
@@ -131,9 +135,12 @@ const {data} = await db.from('profiles').select('*').eq('user_id', userId)
import {useT} from 'web/lib/locale'
function MyComponent() {
const t = useT()
const t = useT()
return <h1>{t('welcome', 'Welcome to Compass')}</h1>
return <h1>{t('welcome', 'Welcome to Compass'
)
}
</h1>
}
```
@@ -144,28 +151,48 @@ Translation files are in `common/messages/` (en.json, fr.json, de.json).
1. Define schema in `common/src/api/schema.ts`:
```typescript
'get-user-and-profile': {
method: 'GET',
authed: false,
rateLimited: true,
props: z.object({
username: z.string().min(1),
}),
returns: {} as {user: User; profile: ProfileRow | null},
summary: 'Get user and profile data by username',
tag: 'Users',
},
'get-user-and-profile'
:
{
method: 'GET',
authed
:
false,
rateLimited
:
true,
props
:
z.object({
username: z.string().min(1),
}),
returns
:
{
}
as
{
user: User;
profile: ProfileRow | null
}
,
summary: 'Get user and profile data by username',
tag
:
'Users',
}
,
```
2. Create handler in `backend/api/src/`:
```typescript
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const getUserAndProfile: APIHandler<'get-user-and-profile'> = async ({username}, _auth) => {
const user = await getUserByUsername(username)
if (!user) {
throw new APIError(404, 'User not found')
throw APIErrors.notFound('User not found')
}
return {user, profile}
@@ -243,10 +270,10 @@ const mockPg = {
### API Errors
```typescript
import {APIError} from './helpers/endpoint'
import {APIErrors} from './helpers/endpoint'
throw new APIError(404, 'User not found')
throw new APIError(400, 'Invalid input', {field: 'email'})
throw APIErrors.notFound('User not found')
throw APIErrors.badRequest('Invalid input', {field: 'email'})
```
### Logging

View File

@@ -163,7 +163,7 @@ import {APIError, APIHandler} from './helpers/endpoint'
export const getUserAndProfile: APIHandler<'get-user-and-profile'> = async ({username}, _auth) => {
const user = await getUserByUsername(username)
if (!user) {
throw new APIError(404, 'User not found')
throw APIErrors.notFound('User not found')
}
return {user, profile}
@@ -243,8 +243,8 @@ const mockPg = {
```typescript
import {APIError} from './helpers/endpoint'
throw new APIError(404, 'User not found')
throw new APIError(400, 'Invalid input', {field: 'email'})
throw APIErrors.notFound('User not found')
throw APIErrors.badRequest('Invalid input', {field: 'email'})
```
### Logging

View File

@@ -129,9 +129,12 @@ const {data} = await db.from('profiles').select('*').eq('user_id', userId)
import {useT} from 'web/lib/locale'
function MyComponent() {
const t = useT()
const t = useT()
return <h1>{t('welcome', 'Welcome to Compass')}</h1>
return <h1>{t('welcome', 'Welcome to Compass'
)
}
</h1>
}
```
@@ -142,17 +145,37 @@ Translation files are in `common/messages/` (en.json, fr.json, de.json).
1. Define schema in `common/src/api/schema.ts`:
```typescript
'get-user-and-profile': {
method: 'GET',
authed: false,
rateLimited: true,
props: z.object({
username: z.string().min(1),
}),
returns: {} as {user: User; profile: ProfileRow | null},
summary: 'Get user and profile data by username',
tag: 'Users',
},
'get-user-and-profile'
:
{
method: 'GET',
authed
:
false,
rateLimited
:
true,
props
:
z.object({
username: z.string().min(1),
}),
returns
:
{
}
as
{
user: User;
profile: ProfileRow | null
}
,
summary: 'Get user and profile data by username',
tag
:
'Users',
}
,
```
2. Create handler in `backend/api/src/`:
@@ -163,7 +186,7 @@ import {APIError, APIHandler} from './helpers/endpoint'
export const getUserAndProfile: APIHandler<'get-user-and-profile'> = async ({username}, _auth) => {
const user = await getUserByUsername(username)
if (!user) {
throw new APIError(404, 'User not found')
throw APIErrors.notFound('User not found')
}
return {user, profile}
@@ -203,8 +226,8 @@ const [interestsRes, causesRes, workRes] = await Promise.all([
```typescript
import {APIError} from './helpers/endpoint'
throw new APIError(404, 'User not found')
throw new APIError(400, 'Invalid input', {field: 'email'})
throw APIErrors.notFound('User not found')
throw APIErrors.badRequest('Invalid input', {field: 'email'})
```
### Logging

View File

@@ -26,7 +26,7 @@
// const tokens = await tokenRes.json();
// if (tokens.error) {
// console.error('Google token error:', tokens);
// throw new APIError(400, 'Google token error: ' + JSON.stringify(tokens))
// throw APIErrors.badRequest('Google token error: ' + JSON.stringify(tokens))
// }
// console.log('Google Tokens:', tokens);
//

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {isAdminId} from 'common/envs/constants'
import {trackPublicEvent} from 'shared/analytics'
import {throwErrorIfNotMod} from 'shared/helpers/auth'
@@ -10,7 +10,7 @@ export const banUser: APIHandler<'ban-user'> = async (body, auth) => {
const {userId, unban} = body
const db = createSupabaseDirectClient()
await throwErrorIfNotMod(auth.uid)
if (isAdminId(userId)) throw new APIError(403, 'Cannot ban admin')
if (isAdminId(userId)) throw APIErrors.forbidden('Cannot ban admin')
await trackPublicEvent(auth.uid, 'ban user', {
userId,
})

View File

@@ -2,10 +2,10 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {updatePrivateUser} from 'shared/supabase/users'
import {FieldVal} from 'shared/supabase/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const blockUser: APIHandler<'user/by-id/:id/block'> = async ({id}, auth) => {
if (auth.uid === id) throw new APIError(400, 'You cannot block yourself')
if (auth.uid === id) throw APIErrors.badRequest('You cannot block yourself')
const pg = createSupabaseDirectClient()
await pg.tx(async (tx) => {

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {update} from 'shared/supabase/utils'
@@ -19,15 +19,15 @@ export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => {
)
if (!event) {
throw new APIError(404, 'Event not found')
throw APIErrors.notFound('Event not found')
}
if (event.creator_id !== auth.uid) {
throw new APIError(403, 'Only the event creator can cancel this event')
throw APIErrors.forbidden('Only the event creator can cancel this event')
}
if (event.status === 'cancelled') {
throw new APIError(400, 'Event is already cancelled')
throw APIErrors.badRequest('Event is already cancelled')
}
// Update event status to cancelled
@@ -39,7 +39,7 @@ export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to cancel event: ' + error.message)
throw APIErrors.internalServerError('Failed to cancel event: ' + error.message)
}
return {success: true}

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
@@ -17,7 +17,7 @@ export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => {
)
if (!rsvp) {
throw new APIError(404, 'RSVP not found')
throw APIErrors.notFound('RSVP not found')
}
// Delete the RSVP
@@ -31,7 +31,7 @@ export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to cancel RSVP: ' + error.message)
throw APIErrors.internalServerError('Failed to cancel RSVP: ' + error.message)
}
return {success: true}

View File

@@ -4,7 +4,7 @@ import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
// Stores a contact message into the `contact` table
// Web sends TipTap JSON in `content`; we store it as string in `description`.
@@ -19,7 +19,7 @@ export const contact: APIHandler<'contact'> = async ({content, userId}, _auth) =
}),
)
if (error) throw new APIError(500, 'Failed to submit contact message')
if (error) throw APIErrors.internalServerError('Failed to submit contact message')
const continuation = async () => {
try {

View File

@@ -1,5 +1,5 @@
import {type JSONContent} from '@tiptap/core'
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {Notification} from 'common/notifications'
import {convertComment} from 'common/supabase/comment'
import {type Row} from 'common/supabase/utils'
@@ -22,7 +22,7 @@ export const createComment: APIHandler<'create-comment'> = async (
const {creator, content} = await validateComment(userId, auth.uid, submittedContent)
const onUser = await getUser(userId)
if (!onUser) throw new APIError(404, 'User not found')
if (!onUser) throw APIErrors.notFound('User not found')
const pg = createSupabaseDirectClient()
const comment = await pg.one<Row<'profile_comments'>>(
@@ -54,18 +54,17 @@ export const createComment: APIHandler<'create-comment'> = async (
const validateComment = async (userId: string, creatorId: string, content: JSONContent) => {
const creator = await getUser(creatorId)
if (!creator) throw new APIError(401, 'Your account was not found')
if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned')
if (!creator) throw APIErrors.unauthorized('Your account was not found')
if (creator.isBannedFromPosting) throw APIErrors.forbidden('You are banned')
const otherUser = await getPrivateUser(userId)
if (!otherUser) throw new APIError(404, 'Other user not found')
if (!otherUser) throw APIErrors.notFound('Other user not found')
if (otherUser.blockedUserIds.includes(creatorId)) {
throw new APIError(404, 'User has blocked you')
throw APIErrors.notFound('User has blocked you')
}
if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(
400,
throw APIErrors.badRequest(
`Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`,
)
}

View File

@@ -3,14 +3,14 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const createCompatibilityQuestion: APIHandler<'create-compatibility-question'> = async (
{question, options},
auth,
) => {
const creator = await getUser(auth.uid)
if (!creator) throw new APIError(401, 'Your account was not found')
if (!creator) throw APIErrors.unauthorized('Your account was not found')
const pg = createSupabaseDirectClient()
@@ -23,7 +23,7 @@ export const createCompatibilityQuestion: APIHandler<'create-compatibility-quest
}),
)
if (error) throw new APIError(401, 'Error creating question')
if (error) throw APIErrors.internalServerError('Error creating question')
return {question: data}
}

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
@@ -8,22 +8,22 @@ export const createEvent: APIHandler<'create-event'> = async (body, auth) => {
// Validate location
if (body.locationType === 'in_person' && !body.locationAddress) {
throw new APIError(400, 'In-person events require a location address')
throw APIErrors.badRequest('In-person events require a location address')
}
if (body.locationType === 'online' && !body.locationUrl) {
throw new APIError(400, 'Online events require a location URL')
throw APIErrors.badRequest('Online events require a location URL')
}
// Validate dates
const startTime = new Date(body.eventStartTime)
if (startTime < new Date()) {
throw new APIError(400, 'Event start time must be in the future')
throw APIErrors.badRequest('Event start time must be in the future')
}
if (body.eventEndTime) {
const endTime = new Date(body.eventEndTime)
if (endTime <= startTime) {
throw new APIError(400, 'Event end time must be after start time')
throw APIErrors.badRequest('Event end time must be after start time')
}
}
@@ -42,7 +42,7 @@ export const createEvent: APIHandler<'create-event'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to create event: ' + error.message)
throw APIErrors.internalServerError('Failed to create event: ' + error.message)
}
return {success: true, event: data}

View File

@@ -1,5 +1,5 @@
import {getConnectionInterests} from 'api/get-connection-interests'
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {addUsersToPrivateMessageChannel} from 'api/helpers/private-messages'
import {filterDefined} from 'common/util/array'
import * as admin from 'firebase-admin'
@@ -15,7 +15,7 @@ export const createPrivateUserMessageChannel: APIHandler<
const user = await admin.auth().getUser(auth.uid)
// console.log(JSON.stringify(user, null, 2))
if (!user?.emailVerified) {
throw new APIError(403, 'You must verify your email to contact people.')
throw APIErrors.forbidden('You must verify your email to contact people.')
}
const userIds = uniq(body.userIds.concat(auth.uid))
@@ -24,13 +24,12 @@ export const createPrivateUserMessageChannel: APIHandler<
const creatorId = auth.uid
const creator = await getUser(creatorId)
if (!creator) throw new APIError(401, 'Your account was not found')
if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned')
if (!creator) throw APIErrors.unauthorized('Your account was not found')
if (creator.isBannedFromPosting) throw APIErrors.forbidden('You are banned')
const toPrivateUsers = filterDefined(await Promise.all(userIds.map((id) => getPrivateUser(id))))
if (toPrivateUsers.length !== userIds.length)
throw new APIError(
404,
throw APIErrors.notFound(
`Private user ${userIds.find(
(uid) => !toPrivateUsers.map((p: any) => p.id).includes(uid),
)} not found`,
@@ -41,7 +40,7 @@ export const createPrivateUserMessageChannel: APIHandler<
user.blockedUserIds.some((blockedId: string) => userIds.includes(blockedId)),
)
) {
throw new APIError(403, `One of the users has blocked another user in the list`)
throw APIErrors.forbidden('One of the users has blocked another user in the list')
}
for (const u of toPrivateUsers) {
@@ -54,7 +53,7 @@ export const createPrivateUserMessageChannel: APIHandler<
const matches = interests.filter((interest: string[]) => targetInterests.includes(interest))
if (matches.length > 0) continue
const failedUser = await getUser(u.id)
throw new APIError(403, `${failedUser?.username} has disabled direct messaging`)
throw APIErrors.forbidden(`${failedUser?.username} has disabled direct messaging`)
}
}

View File

@@ -1,5 +1,5 @@
import {MAX_COMMENT_JSON_LENGTH} from 'api/create-comment'
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {createPrivateUserMessageMain} from 'api/helpers/private-messages'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser} from 'shared/utils'
@@ -10,12 +10,12 @@ export const createPrivateUserMessage: APIHandler<'create-private-user-message'>
) => {
const {content, channelId} = body
if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(400, `Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`)
throw APIErrors.badRequest(`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`)
}
const creator = await getUser(auth.uid)
if (!creator) throw new APIError(401, 'Your account was not found')
if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned')
if (!creator) throw APIErrors.unauthorized('Your account was not found')
if (creator.isBannedFromPosting) throw APIErrors.forbidden('You are banned')
const pg = createSupabaseDirectClient()
return await createPrivateUserMessageMain(creator, channelId, content, pg, 'private')

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {sendDiscordMessage} from 'common/discord/core'
import {debug} from 'common/logger'
import {jsonToMarkdown} from 'common/md'
@@ -19,14 +19,14 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) =>
pg.oneOrNone<{id: string}>('select id from profiles where user_id = $1', [auth.uid]),
)
if (existingProfile) {
throw new APIError(400, 'Profile already exists')
throw APIErrors.badRequest('Profile already exists')
}
await removePinnedUrlFromPhotoUrls(body)
trimStrings(body)
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (!user) throw APIErrors.unauthorized('Your account was not found')
if (user.createdTime > Date.now() - HOUR_MS) {
// If they just signed up, set their avatar to be their pinned photo
updateUser(pg, auth.uid, {avatarUrl: body.pinned_url || undefined})
@@ -38,7 +38,7 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) =>
if (error) {
log.error('Error creating user: ' + error.message)
throw new APIError(500, 'Error creating user')
throw APIErrors.internalServerError('Error creating user')
}
log('Created profile', data)

View File

@@ -16,7 +16,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {getUser, getUserByUsername, log} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
/**
* Create User API Handler
@@ -76,7 +76,7 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) =>
const {user, privateUser} = await pg.tx(async (tx) => {
const preexistingUser = await getUser(auth.uid, tx)
if (preexistingUser)
throw new APIError(403, 'User already exists', {
throw APIErrors.forbidden('User already exists', {
field: 'userId',
context: `User with ID ${auth.uid} already exists`,
})
@@ -84,7 +84,7 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) =>
// Check exact username to avoid problems with duplicate requests
const sameNameUser = await getUserByUsername(username, tx)
if (sameNameUser)
throw new APIError(403, 'Username already taken', {
throw APIErrors.conflict('Username already taken', {
field: 'username',
context: `Username "${username}" is already taken`,
})

View File

@@ -3,14 +3,14 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const createVote: APIHandler<'create-vote'> = async (
{title, description, isAnonymous},
auth,
) => {
const creator = await getUser(auth.uid)
if (!creator) throw new APIError(401, 'Your account was not found')
if (!creator) throw APIErrors.unauthorized('Your account was not found')
const pg = createSupabaseDirectClient()
@@ -24,7 +24,7 @@ export const createVote: APIHandler<'create-vote'> = async (
}),
)
if (error) throw new APIError(401, 'Error creating question')
if (error) throw APIErrors.unauthorized('Error creating question')
return {data}
}

View File

@@ -1,5 +1,4 @@
import {APIHandler} from 'api/helpers/endpoint'
import {APIError} from 'common/api/utils'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {recomputeCompatibilityScoresForUser} from 'shared/compatibility/compute-scores'
import {createSupabaseDirectClient} from 'shared/supabase/init'
@@ -19,7 +18,7 @@ export const deleteCompatibilityAnswer: APIHandler<'delete-compatibility-answer'
)
if (!item) {
throw new APIError(404, 'Item not found')
throw APIErrors.notFound('Item not found')
}
// Delete the answer

View File

@@ -4,16 +4,16 @@ import {deleteUserFiles} from 'shared/firebase-utils'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const deleteMe: APIHandler<'me/delete'> = async ({reasonCategory, reasonDetails}, auth) => {
const user = await getUser(auth.uid)
if (!user) {
throw new APIError(401, 'Your account was not found')
throw APIErrors.unauthorized('Your account was not found')
}
const userId = user.id
if (!userId) {
throw new APIError(400, 'Invalid user ID')
throw APIErrors.badRequest('Invalid user ID')
}
const pg = createSupabaseDirectClient()

View File

@@ -1,7 +1,7 @@
import {broadcastPrivateMessages} from 'api/helpers/private-messages'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
// const DELETED_MESSAGE_CONTENT: JSONContent = {
// type: 'doc',
@@ -31,7 +31,7 @@ export const deleteMessage: APIHandler<'delete-message'> = async ({messageId}, a
)
if (!message) {
throw new APIError(404, 'Message not found')
throw APIErrors.notFound('Message not found')
}
// Soft delete the message

View File

@@ -2,7 +2,7 @@ import {broadcastPrivateMessages} from 'api/helpers/private-messages'
import {encryptMessage} from 'shared/encryption'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const editMessage: APIHandler<'edit-message'> = async ({messageId, content}, auth) => {
const pg = createSupabaseDirectClient()
@@ -19,7 +19,7 @@ export const editMessage: APIHandler<'edit-message'> = async ({messageId, conten
)
if (!message) {
throw new APIError(404, 'Message not found or cannot be edited')
throw APIErrors.notFound('Message not found or cannot be edited')
}
const plaintext = JSON.stringify(content)

View File

@@ -3,7 +3,7 @@ import {PrivateUser} from 'common/user'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const getCurrentPrivateUser: APIHandler<'me/private'> = async (_, auth) => {
const pg = createSupabaseDirectClient()
@@ -13,11 +13,11 @@ export const getCurrentPrivateUser: APIHandler<'me/private'> = async (_, auth) =
)
if (error) {
throw new APIError(500, 'Error fetching private user data: ' + error.message)
throw APIErrors.internalServerError('Error fetching private user data: ' + error.message)
}
if (!data) {
throw new APIError(401, 'Your account was not found')
throw APIErrors.unauthorized('Your account was not found')
}
return data.data as PrivateUser

View File

@@ -1,11 +1,11 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {OPTION_TABLES} from 'common/profiles/constants'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {log} from 'shared/utils'
export const getOptions: APIHandler<'get-options'> = async ({table}, _auth) => {
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
if (!OPTION_TABLES.includes(table)) throw APIErrors.badRequest('Invalid table')
const pg = createSupabaseDirectClient()
@@ -16,7 +16,7 @@ export const getOptions: APIHandler<'get-options'> = async ({table}, _auth) => {
if (result.error) {
log('Error getting profile options', result.error)
throw new APIError(500, 'Error getting profile options')
throw APIErrors.internalServerError('Error getting profile options')
}
const names = result.data.map((row) => row.name)

View File

@@ -4,7 +4,7 @@ import {groupBy, mapValues} from 'lodash'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {convertPrivateChatMessage} from 'shared/supabase/messages'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const getChannelMemberships: APIHandler<'get-channel-memberships'> = async (props, auth) => {
const pg = createSupabaseDirectClient()
@@ -127,12 +127,11 @@ export async function getChannelMessages(props: {
console.error('Error getting messages:', error)
// If it's a connection pool error, provide more specific error message
if (error.message && error.message.includes('MaxClientsInSessionMode')) {
throw new APIError(
503,
throw APIErrors.serviceUnavailable(
'Service temporarily unavailable due to high demand. Please try again in a moment.',
)
}
throw new APIError(500, 'Error getting messages', {
throw APIErrors.internalServerError('Error getting messages', {
field: 'database',
context: error.message || 'Unknown database error',
})

View File

@@ -1,16 +1,16 @@
import {ENV_CONFIG} from 'common/envs/constants'
import {sign} from 'jsonwebtoken'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const getSupabaseToken: APIHandler<'get-supabase-token'> = async (_, auth) => {
const jwtSecret = process.env.SUPABASE_JWT_SECRET
if (jwtSecret == null) {
throw new APIError(500, "No SUPABASE_JWT_SECRET; couldn't sign token.")
throw APIErrors.internalServerError("No SUPABASE_JWT_SECRET; couldn't sign token.")
}
const instanceId = ENV_CONFIG.supabaseInstanceId
if (!instanceId) {
throw new APIError(500, 'No Supabase instance ID in config.')
throw APIErrors.internalServerError('No Supabase instance ID in config.')
}
const payload = {role: 'anon'} // postgres role
return {

View File

@@ -1,5 +1,5 @@
import {toUserAPIResponse} from 'common/api/user-types'
import {APIError} from 'common/api/utils'
import {APIErrors} from 'common/api/utils'
import {convertUser} from 'common/supabase/users'
import {createSupabaseDirectClient} from 'shared/supabase/init'
@@ -11,7 +11,7 @@ export const getUser = async (props: {id: string} | {username: string}) => {
['id' in props ? props.id : props.username],
(r) => (r ? convertUser(r) : null),
)
if (!user) throw new APIError(404, 'User not found')
if (!user) throw APIErrors.notFound('User not found')
return toUserAPIResponse(user)
}
@@ -27,7 +27,7 @@ export const getUser = async (props: {id: string} | {username: string}) => {
// where ${'id' in props ? 'id' : 'username'} = $1`,
// ['id' in props ? props.id : props.username]
// )
// if (!liteUser) throw new APIError(404, 'User not found')
// if (!liteUser) throw APIErrors.notFound('User not found')
//
// return removeNullOrUndefinedProps(liteUser)
// }

View File

@@ -6,14 +6,14 @@ import {
APISchema,
ValidatedAPIParams,
} from 'common/api/schema'
import {APIError} from 'common/api/utils'
import {APIErrors} from 'common/api/utils'
import {PrivateUser} from 'common/user'
import {NextFunction, Request, Response} from 'express'
import * as admin from 'firebase-admin'
import {getPrivateUserByKey, log} from 'shared/utils'
import {z} from 'zod'
export {APIError} from 'common/api/utils'
export {APIErrors} from 'common/api/utils'
// export type Json = Record<string, unknown> | Json[]
// export type JsonHandler<T extends Json> = (
@@ -66,18 +66,18 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth()
const authHeader = req.get('Authorization')
if (!authHeader) {
throw new APIError(401, 'Missing Authorization header.')
throw APIErrors.unauthorized('Missing Authorization header.')
}
const authParts = authHeader.split(' ')
if (authParts.length !== 2) {
throw new APIError(401, 'Invalid Authorization header.')
throw APIErrors.unauthorized('Invalid Authorization header.')
}
const [scheme, payload] = authParts
switch (scheme) {
case 'Bearer':
if (payload === 'undefined') {
throw new APIError(401, 'Firebase JWT payload undefined.')
throw APIErrors.unauthorized('Firebase JWT payload undefined.')
}
try {
return {kind: 'jwt', data: await auth.verifyIdToken(payload)}
@@ -91,12 +91,12 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
Sentry.captureException(err, {
extra: {jwtHeader: _header},
})
throw new APIError(500, 'Error validating token.')
throw APIErrors.internalServerError('Error validating token.')
}
case 'Key':
return {kind: 'key', data: payload}
default:
throw new APIError(401, 'Invalid auth scheme; must be "Key" or "Bearer".')
throw APIErrors.unauthorized('Invalid auth scheme; must be "Key" or "Bearer".')
}
}
@@ -104,7 +104,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
switch (creds.kind) {
case 'jwt': {
if (typeof creds.data.user_id !== 'string') {
throw new APIError(401, 'JWT must contain user ID.')
throw APIErrors.unauthorized('JWT must contain user ID.')
}
return {uid: creds.data.user_id, creds}
}
@@ -112,12 +112,12 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const key = creds.data
const privateUser = await getPrivateUserByKey(key)
if (!privateUser) {
throw new APIError(401, `No private user exists with API key ${key}.`)
throw APIErrors.unauthorized(`No private user exists with API key ${key}.`)
}
return {uid: privateUser.id, creds: {privateUser, ...creds}}
}
default:
throw new APIError(401, 'Invalid credential type.')
throw APIErrors.unauthorized('Invalid credential type.')
}
}
@@ -128,13 +128,13 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const field = i.path.join('.')
return {
field: field === '' ? undefined : field,
error: i.message,
context: i.message,
}
})
if (issues.length > 0) {
log.error(issues.map((i) => `${i.field}: ${i.error}`).join('\n'))
log.error(issues.map((i) => `${i.field}: ${i.context}`).join('\n'))
}
throw new APIError(400, 'Error validating request.', issues)
throw APIErrors.validationFailed(issues)
} else {
return result.data as z.infer<T>
}
@@ -234,7 +234,7 @@ function checkRateLimit(name: string, req: Request, res: Response, auth?: Authed
if (state.count > limit) {
res.setHeader('Retry-After', String(reset))
throw new APIError(429, 'Too Many Requests: rate limit exceeded.')
throw APIErrors.rateLimitExceeded('Too Many Requests: rate limit exceeded.')
}
}

View File

@@ -1,5 +1,5 @@
import {type JSONContent} from '@tiptap/core'
import {APIError} from 'common/api/utils'
import {APIErrors} from 'common/api/utils'
import {ChatVisibility} from 'common/chat-message'
import {debug} from 'common/logger'
import {Json} from 'common/supabase/schema'
@@ -127,7 +127,7 @@ export const createPrivateUserMessageMain = async (
and user_id = $2`,
[channelId, creator.id],
)
if (!authorized) throw new APIError(403, 'You are not authorized to post to this channel')
if (!authorized) throw APIErrors.forbidden('You are not authorized to post to this channel')
await insertPrivateMessage(content, channelId, creator.id, visibility, pg)

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {isAdminId} from 'common/envs/constants'
import {convertComment} from 'common/supabase/comment'
import {Row} from 'common/supabase/utils'
@@ -12,11 +12,11 @@ export const hideComment: APIHandler<'hide-comment'> = async ({commentId, hide},
[commentId],
)
if (!comment) {
throw new APIError(404, 'Comment not found')
throw APIErrors.notFound('Comment not found')
}
if (!isAdminId(auth.uid) && comment.user_id !== auth.uid && comment.on_user_id !== auth.uid) {
throw new APIError(403, 'You are not allowed to hide this comment')
throw APIErrors.forbidden('You are not allowed to hide this comment')
}
await pg.none(`update profile_comments set hidden = $2 where id = $1`, [commentId, hide])

View File

@@ -1,11 +1,11 @@
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
// Hide a profile for the requesting user by inserting a row into hidden_profiles.
// Idempotent: if the pair already exists, succeed silently.
export const hideProfile: APIHandler<'hide-profile'> = async ({hiddenUserId}, auth) => {
if (auth.uid === hiddenUserId) throw new APIError(400, 'You cannot hide yourself')
if (auth.uid === hiddenUserId) throw APIErrors.badRequest('You cannot hide yourself')
const pg = createSupabaseDirectClient()

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {insertPrivateMessage, leaveChatContent} from 'api/helpers/private-messages'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser, log} from 'shared/utils'
@@ -8,14 +8,14 @@ export const leavePrivateUserMessageChannel: APIHandler<
> = async ({channelId}, auth) => {
const pg = createSupabaseDirectClient()
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (!user) throw APIErrors.unauthorized('Your account was not found')
const membershipStatus = await pg.oneOrNone(
`select status from private_user_message_channel_members
where channel_id = $1 and user_id = $2`,
[channelId, auth.uid],
)
if (!membershipStatus) throw new APIError(403, 'You are not authorized to post to this channel')
if (!membershipStatus) throw APIErrors.forbidden('You are not authorized to post to this channel')
log('membershipStatus: ' + membershipStatus)
// add message that the user left the channel

View File

@@ -5,7 +5,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {log} from 'shared/utils'
import {getHasFreeLike} from './has-free-like'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
const {targetUserId, remove} = props
@@ -22,7 +22,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to remove like: ' + error.message)
throw APIErrors.internalServerError('Failed to remove like: ' + error.message)
}
return {status: 'success'}
}
@@ -44,7 +44,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
if (!hasFreeLike) {
// Charge for like.
throw new APIError(403, 'You already liked someone today!')
throw APIErrors.forbidden('You already liked someone today!')
}
// Insert the new like
@@ -56,7 +56,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to add like: ' + error.message)
throw APIErrors.internalServerError('Failed to add like: ' + error.message)
}
const continuation = async () => {

View File

@@ -1,7 +1,7 @@
import {broadcastPrivateMessages} from 'api/helpers/private-messages'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const reactToMessage: APIHandler<'react-to-message'> = async (
{messageId, reaction, toDelete},
@@ -20,7 +20,7 @@ export const reactToMessage: APIHandler<'react-to-message'> = async (
)
if (!message) {
throw new APIError(403, 'Not authorized to react to this message')
throw APIErrors.forbidden('Not authorized to react to this message')
}
if (toDelete) {

View File

@@ -1,4 +1,4 @@
import {APIError, type APIHandler} from 'api/helpers/endpoint'
import {APIErrors, type APIHandler} from 'api/helpers/endpoint'
import {isAdminId} from 'common/envs/constants'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
@@ -11,7 +11,7 @@ export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async (
const {userId} = body
log('remove pinned url', {userId})
if (!isAdminId(auth.uid)) throw new APIError(403, 'Only admins can remove pinned photo')
if (!isAdminId(auth.uid)) throw APIErrors.forbidden('Only admins can remove pinned photo')
const pg = createSupabaseDirectClient()
const {error} = await tryCatch(
@@ -19,7 +19,7 @@ export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async (
)
if (error) {
throw new APIError(500, 'Failed to remove pinned photo')
throw APIErrors.internalServerError('Failed to remove pinned photo')
}
return {

View File

@@ -5,7 +5,7 @@ import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
// abusable: people can report the wrong person, that didn't write the comment
// but in practice we check it manually and nothing bad happens to them automatically
@@ -27,7 +27,7 @@ export const report: APIHandler<'report'> = async (body, auth) => {
)
if (result.error) {
throw new APIError(500, 'Failed to create report: ' + result.error.message)
throw APIErrors.internalServerError('Failed to create report: ' + result.error.message)
}
const continuation = async () => {

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert, update} from 'shared/supabase/utils'
@@ -19,11 +19,11 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => {
)
if (!event) {
throw new APIError(404, 'Event not found')
throw APIErrors.notFound('Event not found')
}
if (event.status !== 'active') {
throw new APIError(400, 'Cannot RSVP to a cancelled or completed event')
throw APIErrors.badRequest('Cannot RSVP to a cancelled or completed event')
}
// Check if already RSVPed
@@ -47,7 +47,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to update RSVP: ' + error.message)
throw APIErrors.internalServerError('Failed to update RSVP: ' + error.message)
}
} else {
// Check max participants limit
@@ -61,7 +61,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => {
)
if (Number(count.count) >= event.max_participants) {
throw new APIError(400, 'Event is at maximum capacity')
throw APIErrors.badRequest('Event is at maximum capacity')
}
}
@@ -75,7 +75,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to RSVP: ' + error.message)
throw APIErrors.internalServerError('Failed to RSVP: ' + error.message)
}
}

View File

@@ -1,6 +1,6 @@
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = async (
body,
@@ -9,7 +9,7 @@ export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = as
const {token} = body
if (!token) {
throw new APIError(400, `Invalid subscription object`)
throw APIErrors.badRequest('Invalid subscription object')
}
const userId = auth?.uid
@@ -28,6 +28,6 @@ export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = as
return {success: true}
} catch (err) {
console.error('Error saving subscription', err)
throw new APIError(500, `Failed to save subscription`)
throw APIErrors.internalServerError('Failed to save subscription')
}
}

View File

@@ -1,12 +1,12 @@
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const saveSubscription: APIHandler<'save-subscription'> = async (body, auth) => {
const {subscription} = body
if (!subscription?.endpoint || !subscription?.keys) {
throw new APIError(400, `Invalid subscription object`)
throw APIErrors.badRequest('Invalid subscription object')
}
const userId = auth?.uid
@@ -37,6 +37,6 @@ export const saveSubscription: APIHandler<'save-subscription'> = async (body, au
return {success: true}
} catch (err) {
console.error('Error saving subscription', err)
throw new APIError(500, `Failed to save subscription`)
throw APIErrors.internalServerError('Failed to save subscription')
}
}

View File

@@ -4,7 +4,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {log} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) => {
const {targetUserId1, targetUserId2, remove} = props
@@ -25,7 +25,8 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
),
)
if (existing.error) throw new APIError(500, 'Error when checking ship: ' + existing.error.message)
if (existing.error)
throw APIErrors.internalServerError('Error when checking ship: ' + existing.error.message)
if (existing.data) {
if (remove) {
@@ -33,7 +34,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
pg.none('delete from profile_ships where ship_id = $1', [existing.data.ship_id]),
)
if (error) {
throw new APIError(500, 'Failed to remove ship: ' + error.message)
throw APIErrors.internalServerError('Failed to remove ship: ' + error.message)
}
} else {
log('Ship already exists, do nothing')
@@ -51,7 +52,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) =>
)
if (error) {
throw new APIError(500, 'Failed to create ship: ' + error.message)
throw APIErrors.internalServerError('Failed to create ship: ' + error.message)
}
const continuation = async () => {

View File

@@ -4,7 +4,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {log} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
const {targetUserId, remove} = props
@@ -21,7 +21,7 @@ export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to remove star: ' + error.message)
throw APIErrors.internalServerError('Failed to remove star: ' + error.message)
}
return {status: 'success'}
}
@@ -45,7 +45,7 @@ export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to add star: ' + error.message)
throw APIErrors.internalServerError('Failed to add star: ' + error.message)
}
return {status: 'success'}

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {update} from 'shared/supabase/utils'
@@ -19,15 +19,15 @@ export const updateEvent: APIHandler<'update-event'> = async (body, auth) => {
)
if (!event) {
throw new APIError(404, 'Event not found')
throw APIErrors.notFound('Event not found')
}
if (event.creator_id !== auth.uid) {
throw new APIError(403, 'Only the event creator can edit this event')
throw APIErrors.forbidden('Only the event creator can edit this event')
}
if (event.status !== 'active') {
throw new APIError(400, 'Cannot edit a cancelled or completed event')
throw APIErrors.badRequest('Cannot edit a cancelled or completed event')
}
// Update event
@@ -46,7 +46,7 @@ export const updateEvent: APIHandler<'update-event'> = async (body, auth) => {
)
if (error) {
throw new APIError(500, 'Failed to update event: ' + error.message)
throw APIErrors.internalServerError('Failed to update event: ' + error.message)
}
return {success: true}

View File

@@ -10,13 +10,13 @@ import {updateUser} from 'shared/supabase/users'
import {getUser, getUserByUsername} from 'shared/utils'
import {broadcastUpdatedUser} from 'shared/websockets/helpers'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
const update = cloneDeep(props)
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (!user) throw APIErrors.unauthorized('Your account was not found')
if (update.name) {
update.name = cleanDisplayName(update.name)
@@ -24,12 +24,12 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
if (update.username) {
const cleanedUsername = cleanUsername(update.username)
if (!cleanedUsername) throw new APIError(400, 'Invalid username')
if (!cleanedUsername) throw APIErrors.badRequest('Invalid username')
const reservedName = RESERVED_PATHS.has(cleanedUsername)
if (reservedName) throw new APIError(403, 'This username is reserved')
if (reservedName) throw APIErrors.forbidden('This username is reserved')
const otherUserExists = await getUserByUsername(cleanedUsername)
if (otherUserExists && otherUserExists.id !== auth.uid)
throw new APIError(403, 'Username already taken')
throw APIErrors.conflict('Username already taken')
update.username = cleanedUsername
}

View File

@@ -1,11 +1,11 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {OPTION_TABLES} from 'common/profiles/constants'
import {tryCatch} from 'common/util/try-catch'
import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init'
import {log} from 'shared/utils'
function validateTable(table: 'interests' | 'causes' | 'work') {
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
if (!OPTION_TABLES.includes(table)) throw APIErrors.badRequest('Invalid table')
}
export async function setProfileOptions(
@@ -72,7 +72,7 @@ export async function setProfileOptions(
export const updateOptions: APIHandler<'update-options'> = async ({table, values}, auth) => {
validateTable(table)
if (!values || !Array.isArray(values)) {
throw new APIError(400, 'No ids provided')
throw APIErrors.badRequest('No ids provided')
}
const pg = createSupabaseDirectClient()
@@ -81,7 +81,7 @@ export const updateOptions: APIHandler<'update-options'> = async ({table, values
'SELECT id FROM profiles WHERE user_id = $1',
[auth.uid],
)
if (!profileIdResult) throw new APIError(404, 'Profile not found')
if (!profileIdResult) throw APIErrors.notFound('Profile not found')
const profileId = profileIdResult.id
const result = await tryCatch(
@@ -93,7 +93,7 @@ export const updateOptions: APIHandler<'update-options'> = async ({table, values
if (result.error) {
log('Error updating profile options', result.error)
throw new APIError(500, 'Error updating profile options')
throw APIErrors.internalServerError('Error updating profile options')
}
return {updatedIds: true}

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {millisToTs} from 'common/supabase/utils'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser, log} from 'shared/utils'
@@ -9,14 +9,14 @@ export const updatePrivateUserMessageChannel: APIHandler<
const {channelId, notifyAfterTime} = body
const pg = createSupabaseDirectClient()
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (!user) throw APIErrors.unauthorized('Your account was not found')
const membershipStatus = await pg.oneOrNone(
`select status from private_user_message_channel_members
where channel_id = $1 and user_id = $2`,
[channelId, auth.uid],
)
if (!membershipStatus) throw new APIError(403, 'You are not authorized to this channel')
if (!membershipStatus) throw APIErrors.forbidden('You are not authorized to this channel')
log('membershipStatus ' + membershipStatus)
await pg.none(

View File

@@ -1,4 +1,4 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {APIErrors, APIHandler} from 'api/helpers/endpoint'
import {trimStrings} from 'common/parsing'
import {type Row} from 'common/supabase/utils'
import {tryCatch} from 'common/util/try-catch'
@@ -35,7 +35,7 @@ export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, au
)
if (!existingProfile) {
throw new APIError(404, 'Profile not found')
throw APIErrors.notFound('Profile not found')
}
log('Updating profile', {userId: auth.uid, parsedBody})
@@ -52,7 +52,7 @@ export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, au
if (error) {
log('Error updating profile', error)
throw new APIError(500, 'Error updating profile')
throw APIErrors.internalServerError('Error updating profile')
}
return data

View File

@@ -2,10 +2,10 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {updatePrivateUser} from 'shared/supabase/users'
import {broadcastUpdatedPrivateUser} from 'shared/websockets/helpers'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const updateUserLocale: APIHandler<'update-user-locale'> = async ({locale}, auth) => {
if (!auth?.uid) throw new APIError(401, 'Not authenticated')
if (!auth?.uid) throw APIErrors.unauthorized('Not authenticated')
const pg = createSupabaseDirectClient()

View File

@@ -1,11 +1,11 @@
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {APIErrors, APIHandler} from './helpers/endpoint'
export const vote: APIHandler<'vote'> = async ({voteId, choice, priority}, auth) => {
const user = await getUser(auth.uid)
if (!user) throw new APIError(401, 'Your account was not found')
if (!user) throw APIErrors.unauthorized('Your account was not found')
const pg = createSupabaseDirectClient()
@@ -17,7 +17,7 @@ export const vote: APIHandler<'vote'> = async ({voteId, choice, priority}, auth)
}
const choiceVal = choiceMap[choice]
if (choiceVal === undefined) {
throw new APIError(400, 'Invalid choice')
throw APIErrors.badRequest('Invalid choice')
}
// Upsert the vote result to ensure one vote per user per vote
@@ -35,6 +35,6 @@ export const vote: APIHandler<'vote'> = async ({voteId, choice, priority}, auth)
const result = await pg.one(query, [user.id, voteId, choiceVal, priority])
return {data: result}
} catch (e) {
throw new APIError(500, 'Error recording vote', e as any)
throw APIErrors.internalServerError('Error recording vote', {originalError: String(e)})
}
}

View File

@@ -1,8 +1,8 @@
import {APIError} from 'common/api/utils'
import {APIErrors} from 'common/api/utils'
import {isAdminId, isModId} from 'common/envs/constants'
export const throwErrorIfNotMod = async (userId: string) => {
if (!isAdminId(userId) && !isModId(userId)) {
throw new APIError(403, `User ${userId} must be an admin or trusted to perform this action.`)
throw APIErrors.forbidden(`User ${userId} must be an admin or trusted to perform this action.`)
}
}

View File

@@ -27,12 +27,11 @@ if (error) {
console.error('Error getting messages:', error)
// If it's a connection pool error, provide more specific error message
if (error.message && error.message.includes('MaxClientsInSessionMode')) {
throw new APIError(
503,
throw APIErrors.serviceUnavailable(
'Service temporarily unavailable due to high demand. Please try again in a moment.',
)
}
throw new APIError(500, 'Error getting messages', {
throw APIErrors.internalServerError('Error getting messages', {
field: 'database',
context: error.message || 'Unknown database error',
})

View File

@@ -2,11 +2,13 @@
## Overview
This guide explains the database connection pooling configuration and best practices for the Compass application. Proper connection pooling is critical for application performance and stability, especially under high load conditions.
This guide explains the database connection pooling configuration and best practices for the Compass application. Proper
connection pooling is critical for application performance and stability, especially under high load conditions.
## Understanding the Problem
The error `MaxClientsInSessionMode: max clients reached - in Session mode max clients are limited to pool_size` indicates that the application has exhausted the database connection pool. This can happen due to:
The error `MaxClientsInSessionMode: max clients reached - in Session mode max clients are limited to pool_size`
indicates that the application has exhausted the database connection pool. This can happen due to:
1. **Connection Leaks**: Database connections not properly released back to the pool
2. **High Concurrent Load**: Too many simultaneous requests exceeding pool capacity
@@ -155,7 +157,7 @@ try {
} catch (error) {
if (error.message && error.message.includes('MaxClientsInSessionMode')) {
// Pool exhaustion - return appropriate error
throw new APIError(503, 'Service temporarily unavailable due to high demand')
throw APIErrors.serviceUnavailable('Service temporarily unavailable due to high demand')
}
throw error
}

View File

@@ -2,7 +2,8 @@
## Overview
This guide provides strategies and best practices for optimizing the performance of the Compass application. It covers frontend, backend, database, and infrastructure optimization techniques to ensure a fast, responsive user experience.
This guide provides strategies and best practices for optimizing the performance of the Compass application. It covers
frontend, backend, database, and infrastructure optimization techniques to ensure a fast, responsive user experience.
## Frontend Performance
@@ -13,9 +14,9 @@ This guide provides strategies and best practices for optimizing the performance
1. **Memoization**: Use `React.memo` for components that render frequently with the same props
```typescript
const ExpensiveComponent = React.memo(({data}: {data: UserProfile}) => {
// Expensive rendering logic
return <div>{/* ... */}</div>
const ExpensiveComponent = React.memo(({data}: { data: UserProfile }) => {
// Expensive rendering logic
return <div>{/* ... */} < /div>
})
```
@@ -37,15 +38,15 @@ const handleClick = useCallback(() => {
// Use react-window or similar libraries for large data sets
import {FixedSizeList as List} from 'react-window'
const VirtualizedList = ({items}: {items: Profile[]}) => (
<List
height={600}
itemCount={items.length}
itemSize={120}
itemData={items}
>
const VirtualizedList = ({items}: { items: Profile[] }) => (
<List
height = {600}
itemCount = {items.length}
itemSize = {120}
itemData = {items}
>
{Row}
</List>
< /List>
)
```
@@ -92,12 +93,13 @@ yarn build && npx webpack-bundle-analyzer .next/static/chunks
import Image from 'next/image'
<Image
src={user.avatarUrl}
alt={`${user.name}'s profile`}
width={100}
height={100}
placeholder="blur"
blurDataURL={blurDataUrl}
src = {user.avatarUrl}
alt = {`${user.name}'s profile`
}
width = {100}
height = {100}
placeholder = "blur"
blurDataURL = {blurDataUrl}
/>
```
@@ -105,10 +107,16 @@ import Image from 'next/image'
```typescript
<Image
src={photo.url}
alt="Profile photo"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ width: '100%', height: 'auto' }}
src = {photo.url}
alt = "Profile photo"
sizes = "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style = {
{
width: '100%', height
:
'auto'
}
}
/>
```
@@ -216,6 +224,7 @@ const profileSummary = await pg.one(
```typescript
// In Express configuration
import compression from 'compression'
app.use(compression())
```
@@ -239,6 +248,7 @@ export const rateLimitedHandler: APIHandler<'expensive-endpoint'> = withRateLimi
```typescript
import redis from 'redis'
const client = redis.createClient()
async function getCachedProfile(userId: string) {
@@ -354,7 +364,7 @@ pool.connectionTimeoutMillis = 10000 // Timeout for acquiring connection
```typescript
// Proper error handling for connection pool issues
if (error.message && error.message.includes('MaxClientsInSessionMode')) {
throw new APIError(503, 'Service temporarily unavailable due to high demand')
throw APIErrors.serviceUnavailable('Service temporarily unavailable due to high demand')
}
```
@@ -507,16 +517,16 @@ export default async function handler(req: Request) {
```yaml
# terraform configuration for load balancer
resource "google_compute_backend_service" "api_backend" {
name = "api-backend"
protocol = "HTTP"
timeout_sec = 30
resource "google_compute_backend_service" "api_backend" {
name = "api-backend"
protocol = "HTTP"
timeout_sec = 30
# Health checks
health_checks = [google_compute_health_check.api.self_link]
health_checks = [google_compute_health_check.api.self_link]
# Load balancing algorithm
balancing_mode = "UTILIZATION"
balancing_mode = "UTILIZATION"
}
```

View File

@@ -1,5 +1,6 @@
import * as Sentry from '@sentry/nextjs'
import {API, APIParams, APIPath} from 'common/api/schema'
import {APIError} from 'common/api/utils'
import {APIErrors} from 'common/api/utils'
import {debug} from 'common/logger'
import {typedAPICall} from 'common/util/api'
import {sleep} from 'common/util/time'
@@ -15,19 +16,20 @@ export async function api<P extends APIPath>(path: P, params: APIParams<P> = {})
if (auth.currentUser === null) {
// User is definitely not logged in
console.error(`api('${path}') called while unauthenticated`)
throw new APIError(401, 'Not authenticated')
throw APIErrors.unauthorized('Not authenticated')
}
}
} catch (e) {
// Remove try / catch once all hooks/components are fixed
console.error('Need to fix this before removing try / catch', e)
Sentry.logger.error('Need to fix this before removing try / catch' + String(e))
let i = 0
while (!auth.currentUser) {
i++
await sleep(i * 500)
if (i > 5) {
console.error('User did not load after 5 iterations')
throw new APIError(401, 'Not authenticated')
throw APIErrors.unauthorized('Not authenticated')
}
}
}