Skip to content

Commit 72f22d3

Browse files
committed
refactor(users): improve cloudinary profile upload
1 parent 97f93e0 commit 72f22d3

5 files changed

Lines changed: 56 additions & 20 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"db",
66
"validation",
77
"comments",
8-
"dashboard"
8+
"dashboard",
9+
"users"
910
],
1011
"cSpell.words": ["unliked"],
1112
"postman.settings.dotenv-detection-notification-visibility": false

src/modules/user/user.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export async function adminUpdateUser(req: Request, res: Response) {
3333
}
3434

3535
export async function updateAvatar(req: Request, res: Response) {
36-
const { avatarUrl } = updateAvatarSchema.parse(req.body);
36+
const { publicId } = updateAvatarSchema.parse(req.body);
3737

38-
const user = await UserService.updateAvatar(req.user!.id, avatarUrl);
38+
const user = await UserService.updateAvatar(req.user!.id, publicId);
3939

4040
return sendResponse(res, {
4141
message: "Profile picture updated",
@@ -44,11 +44,11 @@ export async function updateAvatar(req: Request, res: Response) {
4444
}
4545

4646
export async function getAvatarUpload(req: Request, res: Response) {
47-
const signatureData = UserService.getAvatarUploadSignature(req.user!.id);
47+
const signature = UserService.getAvatarUploadSignature(req.user!.id);
4848

4949
return sendResponse(res, {
5050
message: "Upload signature generated",
51-
data: signatureData,
51+
data: signature,
5252
});
5353
}
5454

src/modules/user/user.schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const adminListUsersQuerySchema = z.object({
1919
});
2020

2121
export const updateAvatarSchema = z.object({
22-
avatarUrl: z.url(),
22+
publicId: z.string(),
2323
});
2424

2525
export const userIdParamSchema = z.object({

src/modules/user/user.service.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ForbiddenError, NotFoundError } from "@/errors/http-errors.js";
44
import { canUpdateUser } from "./user.policy.js";
55
import { AuthUser } from "@/@types/auth.js";
66
import { cloudinary } from "@/config/cloudinary.js";
7-
import crypto from "node:crypto";
7+
import { buildAvatarUrl } from "./user.utils.js";
88

99
export async function updateMe(
1010
user: AuthUser,
@@ -32,32 +32,51 @@ export async function adminUpdateUser(
3232
return user;
3333
}
3434

35-
export async function updateAvatar(userId: string, avatarUrl: string) {
35+
export async function updateAvatar(userId: string, publicId: string) {
36+
const expectedPublicId = `blog/avatars/avatar_${userId}`;
37+
if (publicId !== expectedPublicId) {
38+
throw new ForbiddenError("Invalid avatar reference");
39+
}
40+
41+
let resource;
42+
try {
43+
resource = await cloudinary.api.resource(publicId, {
44+
resource_type: "image",
45+
});
46+
} catch {
47+
throw new NotFoundError("Uploaded avatar not found");
48+
}
49+
50+
const avatarUrl = buildAvatarUrl(resource, 256);
3651
const [user] = await UserRepo.updateUser(userId, { avatarUrl });
3752
return user;
3853
}
3954

4055
export function getAvatarUploadSignature(userId: string) {
56+
const { cloud_name, api_key, api_secret } = cloudinary.config();
57+
4158
const timestamp = Math.floor(Date.now() / 1000);
59+
const publicId = `avatar_${userId}`;
4260

43-
const publicId = `avatars/${userId}-${crypto.randomUUID()}`;
61+
const paramsToSign = {
62+
timestamp,
63+
folder: "blog/avatars",
64+
public_id: publicId,
65+
};
4466

4567
const signature = cloudinary.utils.api_sign_request(
46-
{
47-
timestamp,
48-
public_id: publicId,
49-
folder: "avatars",
50-
},
51-
cloudinary.config().api_secret!,
68+
paramsToSign,
69+
api_secret!,
5270
);
5371

5472
return {
55-
cloudName: cloudinary.config().cloud_name,
56-
apiKey: cloudinary.config().api_key,
57-
timestamp,
58-
signature,
73+
uploadUrl: `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`,
74+
fields: {
75+
api_key,
76+
signature,
77+
...paramsToSign,
78+
},
5979
publicId,
60-
uploadPreset: undefined,
6180
};
6281
}
6382

src/modules/user/user.utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { cloudinary } from "@/config/cloudinary.js";
2+
3+
export function buildAvatarUrl(
4+
resource: { public_id: string; version: string },
5+
size = 256,
6+
) {
7+
return cloudinary.url(resource.public_id, {
8+
version: resource.version,
9+
width: size,
10+
height: size,
11+
crop: "fill",
12+
gravity: "face",
13+
quality: "auto",
14+
fetch_format: "auto",
15+
});
16+
}

0 commit comments

Comments
 (0)