From 0bdf559bdcb379dc4e813db171b5d1049caa1c6f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 13 Sep 2023 09:02:16 -1000 Subject: [PATCH] Add new user patch route --- database/entities/Token.ts | 40 ++++++ .../v1/accounts/update_credentials/index.ts | 125 ++++++++++++++++++ utils/auth.ts | 13 ++ utils/config.ts | 2 + 4 files changed, 180 insertions(+) create mode 100644 database/entities/Token.ts create mode 100644 server/api/v1/accounts/update_credentials/index.ts create mode 100644 utils/auth.ts diff --git a/database/entities/Token.ts b/database/entities/Token.ts new file mode 100644 index 00000000..04e68b86 --- /dev/null +++ b/database/entities/Token.ts @@ -0,0 +1,40 @@ +import { + Entity, + BaseEntity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, +} from "typeorm"; +import { User } from "./User"; +import { Application } from "./Application"; + +export enum TokenType { + BEARER = "bearer", +} + +@Entity({ + name: "tokens", +}) +export class Token extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column("varchar") + token_type!: TokenType; + + @Column("varchar") + scope!: string; + + @Column("varchar") + access_token!: string; + + @CreateDateColumn() + created_at!: Date; + + @ManyToOne(() => User, user => user.id) + user!: User; + + @ManyToOne(() => Application, application => application.id) + application!: Application; +} diff --git a/server/api/v1/accounts/update_credentials/index.ts b/server/api/v1/accounts/update_credentials/index.ts new file mode 100644 index 00000000..a303df9d --- /dev/null +++ b/server/api/v1/accounts/update_credentials/index.ts @@ -0,0 +1,125 @@ +import { getUserByToken } from "@auth"; +import { getConfig } from "@config"; +import { errorResponse, jsonResponse } from "@response"; + +/** + * Patches a user + */ +export default async (req: Request): Promise => { + // Check if request is a PATCH request + if (req.method !== "PATCH") + return errorResponse("This method requires a PATCH request", 405); + + // Check auth token + const token = req.headers.get("Authorization")?.split(" ")[1] || null; + + if (!token) + return errorResponse("This method requires an authenticated user", 422); + + const user = await getUserByToken(token); + + if (!user) return errorResponse("Unauthorized", 401); + + const config = getConfig(); + const body = await req.formData(); + + const display_name = body.get("display_name")?.toString() || null; + const note = body.get("note")?.toString() || null; + // Avatar is a file element + const avatar = (body.get("avatar") as File | null) || null; + const header = (body.get("header") as File | null) || null; + const locked = body.get("locked")?.toString() || null; + const bot = body.get("bot")?.toString() || null; + const discoverable = body.get("discoverable")?.toString() || null; + // TODO: Implement other options like field or source + // const source_privacy = body.get("source[privacy]")?.toString() || null; + // const source_sensitive = body.get("source[sensitive]")?.toString() || null; + // const source_language = body.get("source[language]")?.toString() || null; + + if (display_name) { + // Check if within allowed display name lengths + if ( + display_name.length < 3 || + display_name.length > config.validation.max_displayname_size + ) { + return errorResponse( + `Display name must be between 3 and ${config.validation.max_displayname_size} characters`, + 422 + ); + } + + user.display_name = display_name; + } + + if (note) { + // Check if within allowed note length + if (note.length > config.validation.max_note_size) { + return errorResponse( + `Note must be less than ${config.validation.max_note_size} characters`, + 422 + ); + } + + user.bio = note; + } + + if (avatar) { + // Check if within allowed avatar length (avatar is an image) + if (avatar.size > config.validation.max_avatar_size) { + return errorResponse( + `Avatar must be less than ${config.validation.max_avatar_size} bytes`, + 422 + ); + } + + // TODO: Store the file somewhere and then change the user's actual avatar + } + + if (header) { + // Check if within allowed header length (header is an image) + if (header.size > config.validation.max_header_size) { + return errorResponse( + `Header must be less than ${config.validation.max_avatar_size} bytes`, + 422 + ); + } + // TODO: Store the file somewhere and then change the user's actual header + } + + if (locked) { + // Check if locked is a boolean + if (locked !== "true" && locked !== "false") { + return errorResponse("Locked must be a boolean", 422); + } + + // TODO: Add a user value for Locked + // user.locked = locked === "true"; + } + + if (bot) { + // Check if bot is a boolean + if (bot !== "true" && bot !== "false") { + return errorResponse("Bot must be a boolean", 422); + } + + // TODO: Add a user value for bot + // user.bot = bot === "true"; + } + + if (discoverable) { + // Check if discoverable is a boolean + if (discoverable !== "true" && discoverable !== "false") { + return errorResponse("Discoverable must be a boolean", 422); + } + + // TODO: Add a user value for discoverable + // user.discoverable = discoverable === "true"; + } + + return jsonResponse( + { + error: `Not really implemented yet`, + }, + 501 + ); +}; diff --git a/utils/auth.ts b/utils/auth.ts new file mode 100644 index 00000000..bd4526c8 --- /dev/null +++ b/utils/auth.ts @@ -0,0 +1,13 @@ +import { Token } from "~database/entities/Token"; + +export const getUserByToken = async (access_token: string | null) => { + if (!access_token) return null; + + const token = await Token.findOneBy({ + access_token, + }); + + if (!token) return null; + + return token.user; +}; diff --git a/utils/config.ts b/utils/config.ts index d9b441bc..903385fd 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -17,6 +17,8 @@ export interface ConfigType { max_bio_size: number; max_username_size: number; max_note_size: number; + max_avatar_size: number; + max_header_size: number; max_media_size: number; max_media_attachments: number; max_media_description_size: number;