feat(web): add profile management
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { requireCampaignAccess, requireStageTwoUser, stageTwoApiError, stageTwoDatabase, stageTwoUuid } from '~/server/utils/stage-two-supabase'
|
||||
import { visibleProfile, type ProfileRecord } from '~/server/utils/profile'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -30,11 +31,12 @@ export default defineEventHandler(async (event) => {
|
||||
const round = openRounds[0] ?? fallbackRounds[0] ?? null
|
||||
const userIds = [...new Set(members.map(member => String(member.user_id)))]
|
||||
const profiles = userIds.length
|
||||
? await stageTwoDatabase<Array<{ id: string; display_name: string }>>(
|
||||
`profiles?select=id,display_name&id=in.(${userIds.join(',')})`,
|
||||
? await stageTwoDatabase<ProfileRecord[]>(
|
||||
`profiles?select=id,display_name,description,avatar_path,created_at,updated_at&id=in.(${userIds.join(',')})`,
|
||||
)
|
||||
: []
|
||||
const profileById = new Map(profiles.map(profile => [profile.id, profile]))
|
||||
const supabaseUrl = useRuntimeConfig().supabaseUrl
|
||||
const profileById = new Map(profiles.map(profile => [profile.id, visibleProfile(profile, supabaseUrl)]))
|
||||
const visibleMembers = members.map(member => ({ ...member, profile: profileById.get(String(member.user_id)) ?? null }))
|
||||
const visibleRounds = rounds.slice().reverse()
|
||||
const visibleRoundIds = visibleRounds.map(item => String(item.id))
|
||||
|
||||
37
apps/web/server/api/v1/profile.patch.ts
Normal file
37
apps/web/server/api/v1/profile.patch.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod'
|
||||
import { requireStageTwoSafeText, requireStageTwoUser, stageTwoApiError, stageTwoDatabase } from '~/server/utils/stage-two-supabase'
|
||||
import { visibleProfile, type ProfileRecord } from '~/server/utils/profile'
|
||||
|
||||
const ProfileUpdateSchema = z.object({
|
||||
displayName: z.string().trim().min(1).max(80).optional(),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
avatarPath: z.string().nullable().optional(),
|
||||
}).strict().refine(value => Object.keys(value).length > 0, 'At least one profile field is required.')
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const user = await requireStageTwoUser(event)
|
||||
const update = ProfileUpdateSchema.parse(await readBody(event))
|
||||
const expectedAvatarPath = `${user.id}/avatar`
|
||||
if (update.avatarPath !== undefined && update.avatarPath !== null && update.avatarPath !== expectedAvatarPath) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid profile picture path.' })
|
||||
}
|
||||
if (update.displayName !== undefined) requireStageTwoSafeText(update.displayName)
|
||||
if (update.description !== undefined) requireStageTwoSafeText(update.description)
|
||||
|
||||
const changes: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
||||
if (update.displayName !== undefined) changes.display_name = update.displayName
|
||||
if (update.description !== undefined) changes.description = update.description
|
||||
if (update.avatarPath !== undefined) changes.avatar_path = update.avatarPath
|
||||
|
||||
const profiles = await stageTwoDatabase<ProfileRecord[]>(`profiles?id=eq.${user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(changes),
|
||||
prefer: 'return=representation',
|
||||
})
|
||||
if (!profiles[0]) throw createError({ statusCode: 404, statusMessage: 'Profile not found.' })
|
||||
return { profile: visibleProfile(profiles[0], useRuntimeConfig().supabaseUrl) }
|
||||
} catch (error) {
|
||||
stageTwoApiError(error)
|
||||
}
|
||||
})
|
||||
65
apps/web/server/api/v1/profiles/[id].get.ts
Normal file
65
apps/web/server/api/v1/profiles/[id].get.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { requireStageTwoUser, stageTwoApiError, stageTwoDatabase, stageTwoUuid } from '~/server/utils/stage-two-supabase'
|
||||
import { visibleProfile, type ProfileRecord } from '~/server/utils/profile'
|
||||
|
||||
interface MembershipRow {
|
||||
campaign_id: string
|
||||
role: string
|
||||
joined_at: string
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const user = await requireStageTwoUser(event)
|
||||
const profileId = stageTwoUuid(getRouterParam(event, 'id'), 'profile id')
|
||||
const viewerMemberships = await stageTwoDatabase<Array<{ campaign_id: string }>>(
|
||||
`campaign_members?select=campaign_id&user_id=eq.${user.id}&active=eq.true`,
|
||||
)
|
||||
const viewerCampaignIds = [...new Set(viewerMemberships.map(member => member.campaign_id))]
|
||||
|
||||
let sharedMemberships: MembershipRow[]
|
||||
if (profileId === user.id) {
|
||||
sharedMemberships = await stageTwoDatabase<MembershipRow[]>(
|
||||
`campaign_members?select=campaign_id,role,joined_at&user_id=eq.${profileId}&active=eq.true&order=joined_at.desc`,
|
||||
)
|
||||
} else if (viewerCampaignIds.length) {
|
||||
sharedMemberships = await stageTwoDatabase<MembershipRow[]>(
|
||||
`campaign_members?select=campaign_id,role,joined_at&user_id=eq.${profileId}&active=eq.true&campaign_id=in.(${viewerCampaignIds.join(',')})&order=joined_at.desc`,
|
||||
)
|
||||
} else {
|
||||
sharedMemberships = []
|
||||
}
|
||||
if (profileId !== user.id && !sharedMemberships.length) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Profile not found in your campaigns.' })
|
||||
}
|
||||
|
||||
const profiles = await stageTwoDatabase<ProfileRecord[]>(
|
||||
`profiles?select=id,display_name,description,avatar_path,created_at,updated_at&id=eq.${profileId}&limit=1`,
|
||||
)
|
||||
if (!profiles[0]) throw createError({ statusCode: 404, statusMessage: 'Profile not found.' })
|
||||
|
||||
const campaignIds = sharedMemberships.map(member => member.campaign_id)
|
||||
const campaigns = campaignIds.length
|
||||
? await stageTwoDatabase<Array<{ id: string; title: string; status: string }>>(
|
||||
`campaigns?select=id,title,status&id=in.(${campaignIds.join(',')})`,
|
||||
)
|
||||
: []
|
||||
const campaignById = new Map(campaigns.map(campaign => [campaign.id, campaign]))
|
||||
const sharedCampaigns = sharedMemberships.flatMap((membership) => {
|
||||
const campaign = campaignById.get(membership.campaign_id)
|
||||
return campaign ? [{
|
||||
id: campaign.id,
|
||||
title: campaign.title,
|
||||
status: campaign.status,
|
||||
role: membership.role,
|
||||
joined_at: membership.joined_at,
|
||||
}] : []
|
||||
})
|
||||
|
||||
return {
|
||||
profile: visibleProfile(profiles[0], useRuntimeConfig().supabaseUrl),
|
||||
sharedCampaigns,
|
||||
}
|
||||
} catch (error) {
|
||||
stageTwoApiError(error)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user