mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-24 09:33:42 -04:00
Use API error handler depending on error code
This commit is contained in:
@@ -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
|
||||
|
||||
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
//
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`,
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user