Skip to content

Commit

Permalink
feat: add avatar upload to users
Browse files Browse the repository at this point in the history
  • Loading branch information
Kan-A-Pesh committed Nov 4, 2024
1 parent 8a78b40 commit f538caf
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 25 deletions.
42 changes: 23 additions & 19 deletions database/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@ export default abstract class S3 {

/**
* Uploads a compressed image to Minio as a base64 string.
* @param {Blob} data The image data.
* @param {string} data The base64 string of the image.
* @param {string} options.path The path to upload the image to.
* @param {string} options.creator The creator of the image.
* @returns {Promise<string | null>} The path of the uploaded image.
*/
public static async putImage(data: Blob, options?: { path?: string; creator?: string }): Promise<string | null> {
public static async putImage(data: string, options?: { path?: string; creator?: string }): Promise<string | null> {
try {
const imagePath = options?.path ?? randomUUID();
const image = await compress(data);

const compressedData = await compress(data);
const buffer = Buffer.from(compressedData, "base64");
await S3.client.putObject(globals.env.MINIO_DEFAULT_BUCKETS, imagePath, buffer, compressedData.length, {
await S3.client.putObject(globals.env.MINIO_DEFAULT_BUCKETS, imagePath, image, image.length, {
"Content-Type": "application/octet-stream",
"Last-Modified": new Date().toUTCString(),
"x-amz-acl": "public-read",
Expand All @@ -60,23 +59,28 @@ export default abstract class S3 {
* @returns {Promise<string | null>}
*/
public static async getImage(path: string): Promise<string | null> {
const data: internal.Readable = await S3.client.getObject(globals.env.MINIO_DEFAULT_BUCKETS, path);
if (!data) return null;
try {
const data: internal.Readable = await S3.client.getObject(globals.env.MINIO_DEFAULT_BUCKETS, path);
if (!data) return null;

const buffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
data.on("data", (chunk: Buffer) => {
chunks.push(chunk);
})
.on("end", () => {
resolve(Buffer.concat(chunks));
const buffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
data.on("data", (chunk: Buffer) => {
chunks.push(chunk);
})
.on("error", (err) => {
reject(err);
});
});
.on("end", () => {
resolve(Buffer.concat(chunks));
})
.on("error", (err) => {
reject(err);
});
});

return buffer.toString("base64");
return buffer.toString("base64");
} catch (err) {
Logger.error("s3.ts::getImage | Error getting image", err);
return null;
}
}

/**
Expand Down
5 changes: 4 additions & 1 deletion i18n/en/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"template": "This is a template error message",
"validation": "Validation error",
"database": "Database error",
"image": "Invalid image",
"image": {
"invalid": "Invalid image",
"notFound": "Image not found"
},
"auth": {
"admin": "Invalid X-ADMIN-KEY header",
"toomany": "Too many requests",
Expand Down
2 changes: 1 addition & 1 deletion routes/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default async function Route_Blob_Read(req: Request, res: Response, next:
if (!data) {
return Status.send(req, next, {
status: 404,
error: "errors.notFound"
error: "errors.image.notFound"
});
}

Expand Down
2 changes: 1 addition & 1 deletion routes/users/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default async function Route_Users_Update(req: Request, res: Response, ne
if (!avatarUrl) {
return Status.send(req, next, {
status: 500,
error: "errors.image"
error: "errors.image.invalid"
});
}
}
Expand Down
Binary file added tests/assets/avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 31 additions & 3 deletions tests/e2e/users.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import createApp from "@/app";
import AuthController from "@/controllers/auth";
import { get, post, put } from "../utils";
import { readFileSync } from "fs";
import path from "path";

const app = createApp("e2e-users");

const testGlobals = {
userAvatarUrl: null
};

describe("Test users", () => {
const email = "[email protected]";
const token = AuthController.generateCreationToken(email);
Expand Down Expand Up @@ -112,11 +118,13 @@ describe("Test users", () => {
const authToken = AuthController.generateAuthToken(email);

test("should edit the user", async () => {
const avatar = readFileSync(path.join(__dirname, "..", "assets", "avatar.png"), "base64");

const res = await put(
app,
"/users/:uuid",
{ uuid: userUuid },
{ quote: "test" },
{ quote: "test", avatar: avatar },
{ Authorization: `Bearer ${authToken}` }
);

Expand All @@ -131,13 +139,33 @@ describe("Test users", () => {
uuid: userUuid,
email: email,
username: "test-users",
avatarUrl: null,
avatarUrl: expect.stringMatching(
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/
), // UUID v4 regex
clubId: null,
quote: "test"
}
}
]
});

testGlobals.userAvatarUrl = res.body.response[0].data.avatarUrl;
});

test("should get the user avatar", async () => {
const res = await get(app, "/blob/:uuid", { uuid: testGlobals.userAvatarUrl ?? "invalid" });

expect(res.body).toStrictEqual({
masterStatus: 200,
sentAt: expect.any(Number),
response: [
{
status: 200,
success: true,
data: expect.any(String)
}
]
});
});

test("should get 'invalid token' error", async () => {
Expand Down Expand Up @@ -179,7 +207,7 @@ describe("Test users", () => {
uuid: userUuid,
email: email,
username: "test-users",
avatarUrl: null,
avatarUrl: testGlobals.userAvatarUrl,
clubId: null,
quote: "test"
}
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/blob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import createApp from "@/app";
import { get } from "../utils";

const app = createApp("unit-blob");

describe("Test status page", () => {
test("should get a 'image not found' error", async () => {
const res = await get(app, "/blob/:uuid", { uuid: "definitely-not-a-valid-uuid" });

expect(res.body).toStrictEqual({
masterStatus: 404,
sentAt: expect.any(Number),
response: [
{
status: 404,
success: false,
error: "errors.image.notFound",
translatedError: "Image not found"
}
]
});
});
});

0 comments on commit f538caf

Please sign in to comment.