updated image profile url

This commit is contained in:
Zai Shi 2025-07-30 10:21:24 -07:00
parent 0a3e088578
commit 1b9d1b01fd
6 changed files with 54 additions and 40 deletions

View File

@ -3,3 +3,6 @@ ALTER TABLE "ProjectUser" ADD COLUMN "profileImageKey" TEXT;
-- AlterTable
ALTER TABLE "Team" ADD COLUMN "profileImageKey" TEXT;
-- AlterTable
ALTER TABLE "TeamMember" ADD COLUMN "profileImageKey" TEXT;

View File

@ -105,8 +105,13 @@ model TeamMember {
teamId String @db.Uuid
// This will override the displayName of the user in this team.
displayName String?
displayName String?
// This will override the profileImageUrl of the user in this team.
// if profileImageKey is set, we fetch the image from S3
// otherwise if the profileImageUrl is set, we send the user to the URL.
// if neither is set, we return null.
profileImageKey String?
profileImageUrl String?
createdAt DateTime @default(now())

View File

@ -1,6 +1,7 @@
import { ensureTeamExists, ensureTeamMembershipExists, ensureUserExists, ensureUserTeamPermissionExists } from "@/lib/request-checks";
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { uploadAndGetImageUpdateInfo } from "@/s3";
import { Prisma } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared";
import { teamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles";
@ -149,7 +150,7 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
},
data: {
displayName: data.display_name,
profileImageUrl: data.profile_image_url,
...await uploadAndGetImageUpdateInfo(data.profile_image_url, "team-member-profile-images")
},
include: fullInclude,
});

View File

@ -2,6 +2,7 @@ import { ensureTeamExists, ensureTeamMembershipExists, ensureUserExists, ensureU
import { sendTeamCreatedWebhook, sendTeamDeletedWebhook, sendTeamUpdatedWebhook } from "@/lib/webhooks";
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { uploadAndGetImageUpdateInfo } from "@/s3";
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
import { Prisma } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared";
@ -17,7 +18,7 @@ export function teamPrismaToCrud(prisma: Prisma.TeamGetPayload<{}>) {
return {
id: prisma.teamId,
display_name: prisma.displayName,
profile_image_url: prisma.profileImageUrl,
profile_image_url: prisma.profileImageKey ? getS3PublicUrl(prisma.profileImageKey) : prisma.profileImageUrl,
created_at_millis: prisma.createdAt.getTime(),
client_metadata: prisma.clientMetadata,
client_read_only_metadata: prisma.clientReadOnlyMetadata,
@ -77,10 +78,10 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
mirroredProjectId: auth.project.id,
mirroredBranchId: auth.branchId,
tenancyId: auth.tenancy.id,
profileImageUrl: data.profile_image_url,
clientMetadata: data.client_metadata === null ? Prisma.JsonNull : data.client_metadata,
clientReadOnlyMetadata: data.client_read_only_metadata === null ? Prisma.JsonNull : data.client_read_only_metadata,
serverMetadata: data.server_metadata === null ? Prisma.JsonNull : data.server_metadata,
...await uploadAndGetImageUpdateInfo(data.profile_image_url, "team-profile-images")
},
});
@ -161,10 +162,10 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
},
data: {
displayName: data.display_name,
profileImageUrl: data.profile_image_url,
clientMetadata: data.client_metadata === null ? Prisma.JsonNull : data.client_metadata,
clientReadOnlyMetadata: data.client_read_only_metadata === null ? Prisma.JsonNull : data.client_read_only_metadata,
serverMetadata: data.server_metadata === null ? Prisma.JsonNull : data.server_metadata,
...await uploadAndGetImageUpdateInfo(data.profile_image_url, "team-profile-images")
},
});
});

View File

@ -7,7 +7,7 @@ import { PrismaTransaction } from "@/lib/types";
import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks";
import { RawQuery, getPrismaClientForSourceOfTruth, getPrismaClientForTenancy, getPrismaSchemaForSourceOfTruth, getPrismaSchemaForTenancy, globalPrismaClient, rawQuery, retryTransaction, sqlQuoteIdent } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { checkImageString, getS3PublicUrl, uploadBase64Image } from "@/s3";
import { getS3PublicUrl, uploadAndGetImageUpdateInfo } from "@/s3";
import { log } from "@/utils/telemetry";
import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel";
import { BooleanTrue, Prisma } from "@prisma/client";
@ -324,7 +324,7 @@ export function getUserQuery(projectId: string, branchId: string, userId: string
selected_team: row.SelectedTeamMember ? {
id: row.SelectedTeamMember.Team.teamId,
display_name: row.SelectedTeamMember.Team.displayName,
profile_image_url: row.SelectedTeamMember.Team.profileImageUrl,
profile_image_url: row.SelectedTeamMember.Team.profileImageKey ? getS3PublicUrl(row.SelectedTeamMember.Team.profileImageKey) : row.SelectedTeamMember.Team.profileImageUrl,
created_at_millis: new Date(row.SelectedTeamMember.Team.createdAt + "Z").getTime(),
client_metadata: row.SelectedTeamMember.Team.clientMetadata,
client_read_only_metadata: row.SelectedTeamMember.Team.clientReadOnlyMetadata,
@ -347,37 +347,6 @@ export function getUserIfOnGlobalPrismaClientQuery(projectId: string, branchId:
};
}
async function getProfileImageUrl(input: string | null | undefined) {
let profileImageKey: string | null | undefined = undefined;
let profileImageUrl: string | null | undefined = undefined;
if (input) {
const checkResult = checkImageString(input);
if (checkResult.isBase64Image) {
const { key } = await uploadBase64Image({ input, folderName: "profile-images" });
profileImageKey = key;
} else if (checkResult.isUrl) {
profileImageUrl = input;
} else {
throw new StatusError(StatusError.BadRequest, "Invalid profile image URL");
}
return {
profileImageKey,
profileImageUrl,
};
} else if (input === null) {
return {
profileImageKey: null,
profileImageUrl: null,
};
} else {
return {
profileImageKey: undefined,
profileImageUrl: undefined,
};
}
}
export async function getUser(options: { userId: string } & ({ projectId: string, branchId: string } | { tenancyId: string })) {
let projectId, branchId;
if (!("tenancyId" in options)) {
@ -520,7 +489,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
serverMetadata: data.server_metadata === null ? Prisma.JsonNull : data.server_metadata,
totpSecret: data.totp_secret_base64 == null ? data.totp_secret_base64 : Buffer.from(decodeBase64(data.totp_secret_base64)),
isAnonymous: data.is_anonymous ?? false,
...await getProfileImageUrl(data.profile_image_url)
...await uploadAndGetImageUpdateInfo(data.profile_image_url, "user-profile-images")
},
include: userFullInclude,
});
@ -976,7 +945,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
requiresTotpMfa: data.totp_secret_base64 === undefined ? undefined : (data.totp_secret_base64 !== null),
totpSecret: data.totp_secret_base64 == null ? data.totp_secret_base64 : Buffer.from(decodeBase64(data.totp_secret_base64)),
isAnonymous: data.is_anonymous ?? undefined,
...await getProfileImageUrl(data.profile_image_url)
...await uploadAndGetImageUpdateInfo(data.profile_image_url, "user-profile-images")
},
include: userFullInclude,
});

View File

@ -1,5 +1,6 @@
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { parseBase64Image } from "./lib/images";
export const s3 = new S3Client({
@ -52,3 +53,37 @@ export function checkImageString(input: string) {
isUrl: /^https?:\/\//.test(input),
};
}
export async function uploadAndGetImageUpdateInfo(
input: string | null | undefined,
folderName: 'user-profile-images' | 'team-profile-images' | 'team-member-profile-images'
) {
let profileImageKey: string | null | undefined = undefined;
let profileImageUrl: string | null | undefined = undefined;
if (input) {
const checkResult = checkImageString(input);
if (checkResult.isBase64Image) {
const { key } = await uploadBase64Image({ input, folderName });
profileImageKey = key;
} else if (checkResult.isUrl) {
profileImageUrl = input;
} else {
throw new StatusError(StatusError.BadRequest, "Invalid profile image URL");
}
return {
profileImageKey,
profileImageUrl,
};
} else if (input === null) {
return {
profileImageKey: null,
profileImageUrl: null,
};
} else {
return {
profileImageKey: undefined,
profileImageUrl: undefined,
};
}
}