@@ -24,6 +24,7 @@ const usersV1InternalRefs = internal as unknown as {
2424 removeOrgPublisherMemberInternal : unknown ;
2525 removeOfficialPublisherInternal : unknown ;
2626 recoverPersonalPublisherInternal : unknown ;
27+ updateOrgPublisherProfileInternal : unknown ;
2728 } ;
2829 users : {
2930 getBanAppealContextByGitHubProviderAccountIdInternal : unknown ;
@@ -98,12 +99,21 @@ export async function usersPostRouterV1Handler(ctx: ActionCtx, request: Request)
9899 action !== "publisher-delete" &&
99100 action !== "publisher-official" &&
100101 action !== "publisher-member" &&
102+ action !== "publisher-profile" &&
101103 action !== "publisher-reclaim" &&
102104 action !== "publisher-recovery"
103105 ) {
104106 return text ( "Not found" , 404 , rate . headers ) ;
105107 }
106108
109+ if ( action === "publisher-profile" ) {
110+ const authResult = await requireApiTokenUserOrResponse ( ctx , request , rate . headers ) ;
111+ if ( ! authResult . ok ) return authResult . response ;
112+ const admin = requireAdminOrResponse ( authResult . user , rate . headers ) ;
113+ if ( ! admin . ok ) return admin . response ;
114+ return handleAdminUpdatePublisherProfile ( ctx , request , authResult . userId , rate . headers ) ;
115+ }
116+
107117 const payloadResult = await parseJsonPayload ( request , rate . headers ) ;
108118 if ( ! payloadResult . ok ) return payloadResult . response ;
109119 const payload = payloadResult . payload ;
@@ -930,6 +940,85 @@ async function handleAdminEnsurePublisher(
930940 }
931941}
932942
943+ const PUBLISHER_PROFILE_IMAGE_MAX_BYTES = 2 * 1024 * 1024 ;
944+ const PUBLISHER_PROFILE_IMAGE_CONTENT_TYPES = new Set ( [ "image/jpeg" , "image/png" , "image/webp" ] ) ;
945+
946+ async function handleAdminUpdatePublisherProfile (
947+ ctx : ActionCtx ,
948+ request : Request ,
949+ actorUserId : Id < "users" > ,
950+ headers : HeadersInit ,
951+ ) {
952+ let form : FormData ;
953+ try {
954+ form = await request . formData ( ) ;
955+ } catch {
956+ return text ( "Invalid multipart form" , 400 , headers ) ;
957+ }
958+ const payloadRaw = form . get ( "payload" ) ;
959+ if ( typeof payloadRaw !== "string" ) return text ( "Missing payload" , 400 , headers ) ;
960+ let payload : Record < string , unknown > ;
961+ try {
962+ const parsed = JSON . parse ( payloadRaw ) as unknown ;
963+ if ( ! parsed || typeof parsed !== "object" || Array . isArray ( parsed ) ) {
964+ return text ( "JSON payload must be an object" , 400 , headers ) ;
965+ }
966+ payload = parsed as Record < string , unknown > ;
967+ } catch {
968+ return text ( "Invalid JSON payload" , 400 , headers ) ;
969+ }
970+
971+ const handle = typeof payload . handle === "string" ? payload . handle . trim ( ) . toLowerCase ( ) : "" ;
972+ const reason = typeof payload . reason === "string" ? payload . reason . trim ( ) : "" ;
973+ const hasBio = Object . prototype . hasOwnProperty . call ( payload , "bio" ) ;
974+ const bio = typeof payload . bio === "string" ? payload . bio . trim ( ) : undefined ;
975+ if ( ! handle ) return text ( "Missing handle" , 400 , headers ) ;
976+ if ( ! reason ) return text ( "Missing reason" , 400 , headers ) ;
977+ if ( reason . length > 500 ) return text ( "Reason too long (max 500 chars)" , 400 , headers ) ;
978+ if ( hasBio && typeof payload . bio !== "string" ) return text ( "bio must be a string" , 400 , headers ) ;
979+
980+ const logoParts = form . getAll ( "logo" ) ;
981+ if ( logoParts . length > 1 ) return text ( "Upload one logo" , 400 , headers ) ;
982+ const logo = logoParts [ 0 ] ;
983+ if ( typeof logo === "string" ) return text ( "logo must be a file" , 400 , headers ) ;
984+ if ( ! hasBio && ! logo ) return text ( "bio or logo required" , 400 , headers ) ;
985+ if (
986+ logo &&
987+ ( logo . size <= 0 ||
988+ logo . size > PUBLISHER_PROFILE_IMAGE_MAX_BYTES ||
989+ ! PUBLISHER_PROFILE_IMAGE_CONTENT_TYPES . has ( logo . type ) )
990+ ) {
991+ return text ( "Logo must be a PNG, JPEG, or WebP image smaller than 2 MB" , 400 , headers ) ;
992+ }
993+
994+ let imageStorageId : Id < "_storage" > | undefined ;
995+ try {
996+ if ( logo ) imageStorageId = await ctx . storage . store ( logo ) ;
997+ const result = await runUsersV1MutationRef < {
998+ ok : true ;
999+ publisherId : Id < "publishers" > ;
1000+ handle : string ;
1001+ bio : string | null ;
1002+ image : string | null ;
1003+ bioUpdated : boolean ;
1004+ logoUpdated : boolean ;
1005+ } > ( ctx , usersV1InternalRefs . publishers . updateOrgPublisherProfileInternal , {
1006+ actorUserId,
1007+ handle,
1008+ ...( hasBio ? { bio : bio ?? "" } : { } ) ,
1009+ ...( imageStorageId ? { imageStorageId } : { } ) ,
1010+ reason,
1011+ } ) ;
1012+ return json ( result , 200 , headers ) ;
1013+ } catch ( error ) {
1014+ if ( imageStorageId ) await ctx . storage . delete ( imageStorageId ) ;
1015+ const message = error instanceof Error ? error . message : "Publisher profile update failed" ;
1016+ if ( / n o t f o u n d / i. test ( message ) ) return text ( message , 404 , headers ) ;
1017+ if ( / u n a u t h o r i z e d | f o r b i d d e n / i. test ( message ) ) return text ( "Forbidden" , 403 , headers ) ;
1018+ return text ( message , 400 , headers ) ;
1019+ }
1020+ }
1021+
9331022async function handleBanAppealUnban (
9341023 ctx : ActionCtx ,
9351024 request : Request ,
0 commit comments