Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.

Commit f378b0b

Browse files
Blckbrry-PiNathanFlurry
authored andcommitted
feat(users): users profile pictures
1 parent e7c9c90 commit f378b0b

File tree

15 files changed

+282
-67
lines changed

15 files changed

+282
-67
lines changed

modules/uploads/utils/env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export function getS3EnvConfig(s3Cfg: Config["s3"]): S3Config | null {
2020
S3_AWS_SECRET_ACCESS_KEY,
2121
} = Deno.env.toObject() as Partial<S3EnvConfig>;
2222

23-
const accessKeyId = S3_AWS_ACCESS_KEY_ID ?? s3Cfg.accessKeyId;
24-
const secretAccessKey = S3_AWS_SECRET_ACCESS_KEY ?? s3Cfg.secretAccessKey;
23+
const accessKeyId = s3Cfg.accessKeyId || S3_AWS_ACCESS_KEY_ID;
24+
const secretAccessKey = s3Cfg.secretAccessKey || S3_AWS_SECRET_ACCESS_KEY;
2525

2626
if (
2727
!accessKeyId ||

modules/users/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface Config {
2+
maxProfilePictureBytes: number;
3+
allowedMimes?: string[];
4+
}
5+
6+
export const DEFAULT_MIME_TYPES = [
7+
"image/jpeg",
8+
"image/png",
9+
];

modules/users/db/migrations/20240307013613_init/migration.sql

Lines changed: 0 additions & 35 deletions
This file was deleted.

modules/users/db/migrations/20240312043558_init/migration.sql

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- CreateTable
2+
CREATE TABLE "User" (
3+
"id" UUID NOT NULL,
4+
"username" TEXT NOT NULL,
5+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
"updatedAt" TIMESTAMP(3) NOT NULL,
7+
"avatarUploadId" UUID,
8+
9+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
10+
);
11+
12+
-- CreateIndex
13+
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

modules/users/db/schema.prisma

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ datasource db {
44
}
55

66
model User {
7-
id String @id @default(uuid()) @db.Uuid
8-
username String @unique
9-
createdAt DateTime @default(now())
10-
updatedAt DateTime @updatedAt
7+
id String @id @default(uuid()) @db.Uuid
8+
username String @unique
9+
createdAt DateTime @default(now())
10+
updatedAt DateTime @updatedAt
11+
12+
avatarUploadId String? @db.Uuid
1113
}

modules/users/module.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ tags:
77
authors:
88
- rivet-gg
99
- NathanFlurry
10+
- Blckbrry-Pi
1011
status: stable
1112
dependencies:
1213
rate_limit: {}
1314
tokens: {}
15+
uploads: {}
1416
scripts:
1517
get_user:
1618
name: Get User
@@ -24,8 +26,22 @@ scripts:
2426
create_user_token:
2527
name: Create User Token
2628
description: Create a token for a user to authenticate future requests.
29+
set_profile_picture:
30+
name: Set Profile Picture
31+
description: Set the profile picture for a user.
32+
public: true
33+
prepare_profile_picture_upload:
34+
name: Start Profile Picture Upload
35+
description: Allow the user to begin uploading a profile picture.
36+
public: true
2737
errors:
2838
token_not_user_token:
2939
name: Token Not User Token
3040
unknown_identity_type:
3141
name: Unknown Identity Type
42+
invalid_mime_type:
43+
name: Invalid MIME Type
44+
description: The MIME type for the supposed PFP isn't an image
45+
file_too_large:
46+
name: File Too Large
47+
description: The file is larger than the configured maximum size for a profile picture

modules/users/scripts/create_user.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface Request {
66
}
77

88
export interface Response {
9-
user: User;
9+
user: Omit<User, "profilePictureUrl">;
1010
}
1111

1212
export async function run(
@@ -20,10 +20,16 @@ export async function run(
2020
data: {
2121
username: req.username ?? generateUsername(),
2222
},
23+
select: {
24+
id: true,
25+
username: true,
26+
createdAt: true,
27+
updatedAt: true,
28+
},
2329
});
2430

2531
return {
26-
user,
32+
user: user,
2733
};
2834
}
2935

modules/users/scripts/get_user.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ScriptContext } from "../module.gen.ts";
22
import { User } from "../utils/types.ts";
3+
import { withPfpUrls } from "../utils/pfp.ts";
34

45
export interface Request {
56
userIds: string[];
@@ -20,5 +21,8 @@ export async function run(
2021
orderBy: { username: "desc" },
2122
});
2223

23-
return { users };
24+
25+
const usersWithPfps = await withPfpUrls(ctx, users);
26+
27+
return { users: usersWithPfps };
2428
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ScriptContext, RuntimeError } from "../_gen/scripts/prepare_profile_picture_upload.ts";
2+
import { DEFAULT_MIME_TYPES } from "../config.ts";
3+
4+
export interface Request {
5+
mime: string;
6+
contentLength: string;
7+
userToken: string;
8+
}
9+
10+
export interface Response {
11+
url: string;
12+
uploadId: string;
13+
}
14+
15+
export async function run(
16+
ctx: ScriptContext,
17+
req: Request,
18+
): Promise<Response> {
19+
// Authenticate/rate limit because this is a public route
20+
await ctx.modules.rateLimit.throttlePublic({ period: 60, requests: 5 });
21+
const { userId } = await ctx.modules.users.authenticateUser({ userToken: req.userToken });
22+
23+
// Ensure at least the MIME type says it is an image
24+
const allowedMimes = ctx.userConfig.allowedMimes ?? DEFAULT_MIME_TYPES;
25+
if (!allowedMimes.includes(req.mime)) {
26+
throw new RuntimeError(
27+
"invalid_mime_type",
28+
{ cause: `MIME type ${req.mime} is not an allowed image type` },
29+
);
30+
}
31+
32+
// Ensure the file is within the maximum configured size for a PFP
33+
if (BigInt(req.contentLength) > ctx.userConfig.maxProfilePictureBytes) {
34+
throw new RuntimeError(
35+
"file_too_large",
36+
{ cause: `File is too large (${req.contentLength} bytes)` },
37+
);
38+
}
39+
40+
// Prepare the upload to get the presigned URL
41+
const { upload: presigned } = await ctx.modules.uploads.prepare({
42+
files: [
43+
{
44+
path: `profile-picture`,
45+
contentLength: req.contentLength,
46+
mime: req.mime,
47+
multipart: false,
48+
},
49+
],
50+
});
51+
52+
return {
53+
url: presigned.files[0].presignedChunks[0].url,
54+
uploadId: presigned.id,
55+
}
56+
}
57+

0 commit comments

Comments
 (0)