diff --git a/apps/khix/src/app/_components/team-cascade/profile-picture-url.ts b/apps/khix/src/app/_components/team-cascade/profile-picture-url.ts new file mode 100644 index 000000000..2c47ebacf --- /dev/null +++ b/apps/khix/src/app/_components/team-cascade/profile-picture-url.ts @@ -0,0 +1,41 @@ +const PROFILE_PICTURE_ORIGIN = + "https://minio-y44gsgsskc4ko4kkwsg0csoc.135.237.97.107.sslip.io"; +const PROFILE_PICTURE_BUCKET_PATH = "/guild-profile-pictures/"; +const USER_OWNED_OBJECT_NAME = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[^/\\\0]+$/i; + +function getProfilePictureObjectName(reference: string) { + const trimmedReference = reference.trim(); + if (trimmedReference === "") return null; + + let objectName = trimmedReference; + + try { + const url = new URL(trimmedReference); + if (url.protocol !== "https:") return null; + if (!url.pathname.startsWith(PROFILE_PICTURE_BUCKET_PATH)) return null; + + objectName = decodeURIComponent( + url.pathname.slice(PROFILE_PICTURE_BUCKET_PATH.length), + ); + } catch { + // Current Guild uploads store the object key instead of a full URL. + } + + if (objectName.length > 255 || objectName.includes("..")) return null; + return USER_OWNED_OBJECT_NAME.test(objectName) ? objectName : null; +} + +export function getKhixProfilePictureUrl(reference: string | null) { + if (!reference) return null; + + const objectName = getProfilePictureObjectName(reference); + if (!objectName) return null; + + const encodedObjectName = objectName + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/"); + + return `${PROFILE_PICTURE_ORIGIN}${PROFILE_PICTURE_BUCKET_PATH}${encodedObjectName}`; +} diff --git a/apps/khix/src/app/_components/team-cascade/team-roster.ts b/apps/khix/src/app/_components/team-cascade/team-roster.ts index d052e4174..18f24fd79 100644 --- a/apps/khix/src/app/_components/team-cascade/team-roster.ts +++ b/apps/khix/src/app/_components/team-cascade/team-roster.ts @@ -3,6 +3,7 @@ import type { inferRouterOutputs } from "@trpc/server"; import type { AppRouter } from "@forge/api"; import { getBladeTrpcClient } from "./blade-trpc"; +import { getKhixProfilePictureUrl } from "./profile-picture-url"; type PublicClubTeamRoster = inferRouterOutputs["guild"]["getPublicClubTeamRoster"]; @@ -18,7 +19,6 @@ export interface TeamCascadeGroup { const featuredDesignerIds = [ "design-f06cbff5-b5f8-49d5-8a3c-5b40a59dfcc6", - "design-3a0d6777-2276-4ae8-9281-ace2a26d6c94", "design-f56f4444-7962-4090-b937-f31674a6ac7e", ] as const; @@ -49,15 +49,19 @@ export async function loadTeamCascadeGroups( return teamCascadeRosterGroups.map((group) => { const members = roster.members[group.rosterKey] ?? []; + const visibleMembers = + "memberIds" in group + ? members.filter((member) => + group.memberIds.some((memberId) => memberId === member.id), + ) + : members; return { roleLabel: group.roleLabel, - members: - "memberIds" in group - ? members.filter((member) => - group.memberIds.some((memberId) => memberId === member.id), - ) - : members, + members: visibleMembers.map((member) => ({ + ...member, + imageUrl: getKhixProfilePictureUrl(member.imageUrl), + })), }; }); }