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
1d39eaf
commit 52b0244
Showing
13 changed files
with
295 additions
and
6 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface Config { | ||
maxPfpBytes: number; | ||
} |
22 changes: 22 additions & 0 deletions
22
modules/users/db/migrations/20240401001004_add_pfps/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,22 @@ | ||
-- AlterTable | ||
ALTER TABLE "User" ADD COLUMN "pfpId" UUID; | ||
|
||
-- CreateTable | ||
CREATE TABLE "Pfp" ( | ||
"uploadId" UUID NOT NULL, | ||
"fileId" UUID NOT NULL, | ||
"url" TEXT NOT NULL, | ||
"urlExpiry" TIMESTAMP(3) NOT NULL, | ||
"userId" UUID NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL, | ||
"finishedAt" TIMESTAMP(3), | ||
|
||
CONSTRAINT "Pfp_pkey" PRIMARY KEY ("uploadId") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "Pfp_userId_key" ON "Pfp"("userId"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "Pfp" ADD CONSTRAINT "Pfp_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
8 changes: 8 additions & 0 deletions
8
modules/users/db/migrations/20240401002555_remove_file_id/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,8 @@ | ||
/* | ||
Warnings: | ||
- You are about to drop the column `fileId` on the `Pfp` table. All the data in the column will be lost. | ||
*/ | ||
-- AlterTable | ||
ALTER TABLE "Pfp" DROP COLUMN "fileId"; |
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,71 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/set_pfp.ts"; | ||
import { User } from "../utils/types.ts"; | ||
import { withPfpUrl } 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 oldPfp = await db.pfp.findFirst({ | ||
where: { userId }, | ||
select: { uploadId: true }, | ||
}); | ||
if (oldPfp) { | ||
await ctx.modules.uploads.delete({ uploadId: oldPfp.uploadId }); | ||
await db.pfp.delete({ where: { userId } }); | ||
} | ||
|
||
// Assign the new profile picture to the user | ||
await db.pfp.create({ | ||
data: { | ||
userId, | ||
uploadId: req.uploadId, | ||
url: "", | ||
urlExpiry: new Date(0).toISOString(), | ||
finishedAt: new Date().toISOString(), | ||
}, | ||
}); | ||
|
||
// Get the new user object | ||
const user = await db.user.findFirst({ | ||
where: { id: userId }, | ||
select: { | ||
id: true, | ||
username: true, | ||
createdAt: true, | ||
updatedAt: true, | ||
}, | ||
}); | ||
|
||
if (!user) { | ||
throw new RuntimeError("internal_error", { cause: "User not found" }); | ||
} | ||
|
||
return await withPfpUrl( | ||
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,50 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/start_pfp_upload.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 | ||
if (!req.mime.startsWith("image/")) { | ||
throw new RuntimeError( | ||
"invalid_mime_type", | ||
{ cause: `MIME type ${req.mime} is not an image` }, | ||
); | ||
} | ||
|
||
// Ensure the file is within the maximum configured size for a PFP | ||
if (BigInt(req.contentLength) > ctx.userConfig.maxPfpBytes) { | ||
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: `pfp/${userId}`, contentLength: req.contentLength, mime: req.mime }, | ||
], | ||
}); | ||
|
||
return { | ||
url: presigned.files[0].presignedUrl, | ||
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,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"; | ||
import { decodeBase64 } from "https://deno.land/[email protected]/encoding/base64.ts"; | ||
|
||
const testPfp = "Qk2KBAAAAAAAAIoAAAB8AAAAEAAAAPD///8BACAAAwAAAAAEAAATCwAAEwsAAAAAAAAAAAAAAAD/AAD/AAD/AAAAAAAA/0JHUnMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAARUVFGkRERDxFRUVKRUVFUURERFJERERSRUVFUUVFRUpEREQ8RUVFGgAAAAAAAAAAAAAAAgAAAABAQEAIQ0NDm0NDQ/JFRUX/RkZG/0dHR/9HR0f/R0dH/0dHR/9HR0f/RUVF/0NDQ/JDQ0ObQEBACAAAAAAAAAAAQEBAnEhISP9BQUH/Q0ND/0tLS/1ISEj9RERE/UpKSv1KSkr9RERE/UBAQP9CQkL/SEhI/0FBQZwAAAAAOzs7Gj4+PvI+Pj7/Pz8/+jg4OP4AAAD+AAAA/i4uLv4AAAD+AAAA/jk5Of4/Pz/+PT09+j4+Pv8+Pj7yOzs7Gjw8PDw8PDz/PT09/zMzM/5PT0//p6en/5qamv9ubm7/ra2t/5+fn/9GRkb/MzMz/z09Pf47Ozv/PDw8/zw8PDw3NzdKOjo6/0JCQv0AAAD+j4+P///////k5OT/oqKi////////////wsLC/wAAAP8/Pz/+ODg4/To6Ov83NzdKNjY2UTc3N/89PT39AAAA/o6Ojv/29vb/09PT/5ubm//29vb/+vr6/9LS0v8AAAD/Ojo6/jY2Nv03Nzf/NjY2UTIyMlI0NDT/Ojo6/QAAAP6Hh4f/7e3t/8/Pz/99fX3/wsLC/7q6uv9lZWX/Hx8f/zY2Nv4xMTH9NDQ0/zIyMlIvLy9SMTEx/zc3N/0AAAD+gYGB/+Pj4//Jycn/WVlZ/5iYmP+YmJj/YWFh/xkZGf8zMzP+Li4u/TExMf8vLy9SLCwsUS4uLv80NDT9AAAA/nx8fP/Y2Nj/wMDA/z8/P//ExMT/4uLi/8PDw/8zMzP/IyMj/jAwMP0uLi7/LCwsUSkpKUoqKir/MTEx/QAAAP5ycnL/0dHR/7+/v/8AAAD/ZmZm/8fHx//S0tL/l5eX/wAAAP4wMDD9Kioq/ykpKUomJiY8JiYm/ygoKP8gICD+NjY2/3t7e/91dXX/LCws/xEREf9paWn/goKC/3R0dP8kJCT+JiYm/ycnJ/8mJiY8JycnGiMjI/IjIyP/JSUl+h0dHf4AAAD+AAAA/iIiIv4nJyf+AAAA/gAAAP4AAAD+JCQk+iMjI/8jIyPyJiYmGwAAAAAhISGcIyMj/yAgIP8iIiL/Kioq/SgoKP0gICD9ICAg/SgoKP0qKir9Jycn/yAgIP8jIyP/ISEhnAAAAAAAAAAAICAgCB4eHpseHh7yHR0d/x0dHf8eHh7/Hh4e/x4eHv8eHh7/HR0d/x0dHf8cHBzyHh4emyAgIAgAAAAAAAAAAgAAAAAAAAAAHR0dGhoaGjwcHBxKHBwcURwcHFIcHBxSHBwcURwcHEoaGho8HR0dGgAAAAAAAAAAAAAAAg==" | ||
const testPfpArr = decodeBase64(testPfp); | ||
|
||
test("e2e", async (ctx: TestContext) => { | ||
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.startPfpUpload({ | ||
mime: "image/bmp", | ||
contentLength: atob(testPfp).length.toString(), | ||
userToken: token.token, | ||
}); | ||
|
||
// Upload the profile picture | ||
await fetch(url, { | ||
method: "PUT", | ||
body: testPfpArr, | ||
}); | ||
|
||
// Set the profile picture | ||
await ctx.modules.users.setPfp({ | ||
uploadId, | ||
userToken: token.token, | ||
}); | ||
|
||
// Get PFP from URL | ||
const { users: [{ pfpUrl }] } = await ctx.modules.users.getUser({ userIds: [user.id] }); | ||
assertExists(pfpUrl); | ||
|
||
// Get PFP from URL | ||
const getPfpFromUrl = await fetch(pfpUrl); | ||
const pfp = new Uint8Array(await getPfpFromUrl.arrayBuffer()); | ||
assertEquals(pfp, testPfpArr); | ||
}); |
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,49 @@ | ||
import { ModuleContext } from "../_gen/mod.ts"; | ||
import { User } from "./types.ts"; | ||
|
||
const PRE_EXPIRY_BUFFER = 1000 * 60 * 60; // 1 hour | ||
const EXPIRY_TIME = 1000 * 60 * 60 * 24 * 7; // 1 week | ||
|
||
export const refreshPfpUrl = async (ctx: ModuleContext, userId: string) => { | ||
const pfp = await ctx.db.pfp.findFirst({ | ||
where: { | ||
userId, | ||
}, | ||
}); | ||
|
||
if (!pfp) { | ||
return null; | ||
} | ||
|
||
if (Date.now() + PRE_EXPIRY_BUFFER > pfp.urlExpiry.getTime()) { | ||
const reqTime = Date.now(); | ||
|
||
const path = `pfp/${userId}`; | ||
const { files: [{ url }] } = await ctx.modules.uploads.getFileLinks({ | ||
files: [{ uploadId: pfp.uploadId, path }], | ||
validSecs: EXPIRY_TIME / 1000, | ||
}); | ||
|
||
await ctx.db.pfp.update({ | ||
where: { | ||
userId, | ||
}, | ||
data: { | ||
url, | ||
urlExpiry: new Date(reqTime + EXPIRY_TIME), | ||
}, | ||
}); | ||
|
||
return url; | ||
} else { | ||
return pfp.url; | ||
} | ||
}; | ||
|
||
export const withPfpUrl = async <T extends ModuleContext>(ctx: T, user: Omit<User, "pfpUrl">) => { | ||
const url = await refreshPfpUrl(ctx, user.id); | ||
return { | ||
...user, | ||
pfpUrl: url, | ||
}; | ||
} |
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 |
---|---|---|
|
@@ -3,4 +3,5 @@ export interface User { | |
username: string; | ||
createdAt: Date; | ||
updatedAt: Date; | ||
pfpUrl: string | null; | ||
} |
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