This repository has been archived by the owner on Sep 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
10c688f
commit 54b0b83
Showing
15 changed files
with
282 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export interface Config { | ||
maxProfilePictureBytes: number; | ||
allowedMimes?: string[]; | ||
} | ||
|
||
export const DEFAULT_MIME_TYPES = [ | ||
"image/jpeg", | ||
"image/png", | ||
]; |
35 changes: 0 additions & 35 deletions
35
modules/users/db/migrations/20240307013613_init/migration.sql
This file was deleted.
Oops, something went wrong.
23 changes: 0 additions & 23 deletions
23
modules/users/db/migrations/20240312043558_init/migration.sql
This file was deleted.
Oops, something went wrong.
13 changes: 13 additions & 0 deletions
13
modules/users/db/migrations/20240419134812_initial_setup/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
-- CreateTable | ||
CREATE TABLE "User" ( | ||
"id" UUID NOT NULL, | ||
"username" TEXT NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL, | ||
"avatarUploadId" UUID, | ||
|
||
CONSTRAINT "User_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/start_profile_picture_upload.ts"; | ||
import { DEFAULT_MIME_TYPES } from "../config.ts"; | ||
|
||
export interface Request { | ||
mime: string; | ||
contentLength: string; | ||
userToken: string; | ||
} | ||
|
||
export interface Response { | ||
url: string; | ||
uploadId: string; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
// Authenticate/rate limit because this is a public route | ||
await ctx.modules.rateLimit.throttlePublic({ period: 60, requests: 5 }); | ||
const { userId } = await ctx.modules.users.authenticateUser({ userToken: req.userToken }); | ||
|
||
// Ensure at least the MIME type says it is an image | ||
const allowedMimes = ctx.userConfig.allowedMimes ?? DEFAULT_MIME_TYPES; | ||
if (!allowedMimes.includes(req.mime)) { | ||
throw new RuntimeError( | ||
"invalid_mime_type", | ||
{ cause: `MIME type ${req.mime} is not an allowed image type` }, | ||
); | ||
} | ||
|
||
// Ensure the file is within the maximum configured size for a PFP | ||
if (BigInt(req.contentLength) > ctx.userConfig.maxProfilePictureBytes) { | ||
throw new RuntimeError( | ||
"file_too_large", | ||
{ cause: `File is too large (${req.contentLength} bytes)` }, | ||
); | ||
} | ||
|
||
// Prepare the upload to get the presigned URL | ||
const { upload: presigned } = await ctx.modules.uploads.prepare({ | ||
files: [ | ||
{ | ||
path: `profile-picture`, | ||
contentLength: req.contentLength, | ||
mime: req.mime, | ||
multipart: false, | ||
}, | ||
], | ||
}); | ||
|
||
return { | ||
url: presigned.files[0].presignedUrls[0].url, | ||
uploadId: presigned.id, | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/set_profile_picture.ts"; | ||
import { User } from "../utils/types.ts"; | ||
import { withPfpUrls } from "../utils/pfp.ts"; | ||
|
||
export interface Request { | ||
uploadId: string; | ||
userToken: string; | ||
} | ||
|
||
export interface Response { | ||
user: User; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
// Authenticate/rate limit because this is a public route | ||
await ctx.modules.rateLimit.throttlePublic({ period: 60, requests: 5 }); | ||
const { userId } = await ctx.modules.users.authenticateUser({ userToken: req.userToken }); | ||
|
||
// Complete the upload in the `uploads` module | ||
await ctx.modules.uploads.complete({ uploadId: req.uploadId }); | ||
|
||
// Delete the old uploaded profile picture and replace it with the new one | ||
const [user] = await ctx.db.$transaction(async (db) => { | ||
// If there is an existing profile picture, delete it | ||
const oldUser = await db.user.findFirst({ | ||
where: { id: userId }, | ||
}); | ||
|
||
// (This means that `users.authenticateUser` is broken!) | ||
if (!oldUser) { | ||
throw new RuntimeError( | ||
"internal_error", | ||
{ | ||
meta: "Existing user not found", | ||
}, | ||
); | ||
} | ||
|
||
if (oldUser.avatarUploadId) { | ||
await ctx.modules.uploads.delete({ uploadId: oldUser.avatarUploadId }); | ||
} | ||
|
||
// Update the user upload ID | ||
const user = await db.user.update({ | ||
where: { | ||
id: userId, | ||
}, | ||
data: { | ||
avatarUploadId: req.uploadId, | ||
}, | ||
select: { | ||
id: true, | ||
username: true, | ||
avatarUploadId: true, | ||
createdAt: true, | ||
updatedAt: true, | ||
}, | ||
}); | ||
|
||
if (!user) { | ||
throw new RuntimeError("internal_error", { cause: "User not found" }); | ||
} | ||
|
||
return await withPfpUrls( | ||
ctx, | ||
[user], | ||
); | ||
}); | ||
|
||
return { user }; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { test, TestContext } from "../_gen/test.ts"; | ||
import { faker } from "https://deno.land/x/[email protected]/mod.ts"; | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/assert_equals.ts"; | ||
import { assertExists } from "https://deno.land/[email protected]/assert/assert_exists.ts"; | ||
|
||
test("e2e", async (ctx: TestContext) => { | ||
const imageReq = await fetch("https://picsum.photos/200/300"); | ||
const imageData = new Uint8Array(await imageReq.arrayBuffer()); | ||
|
||
|
||
const { user } = await ctx.modules.users.createUser({ | ||
username: faker.internet.userName(), | ||
}); | ||
|
||
const { token } = await ctx.modules.users.createUserToken({ | ||
userId: user.id, | ||
}); | ||
|
||
const { url, uploadId } = await ctx.modules.users.prepareProfilePictureUpload({ | ||
mime: imageReq.headers.get("Content-Type") ?? "image/jpeg", | ||
contentLength: imageData.length.toString(), | ||
userToken: token.token, | ||
}); | ||
|
||
// Upload the profile picture | ||
await fetch(url, { | ||
method: "PUT", | ||
body: imageData, | ||
}); | ||
|
||
// Set the profile picture | ||
await ctx.modules.users.setProfilePicture({ | ||
uploadId, | ||
userToken: token.token, | ||
}); | ||
|
||
// Get PFP from URL | ||
const { users: [{ profilePictureUrl }] } = await ctx.modules.users.getUser({ userIds: [user.id] }); | ||
assertExists(profilePictureUrl); | ||
|
||
// Get PFP from URL | ||
const getPfpFromUrl = await fetch(profilePictureUrl); | ||
const pfp = new Uint8Array(await getPfpFromUrl.arrayBuffer()); | ||
assertEquals(pfp, imageData); | ||
}); |
Oops, something went wrong.