From 28aa8d3a82440dcc9261999015e2c4c05b548eb3 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Thu, 12 Jun 2025 19:00:57 +0200 Subject: [PATCH] feat(api): add user info GET endpoint and blocklist usernames --- src/app/api/user/[user]/route.ts | 72 ++++++++++++++++++++++++++++++++ src/app/api/user/me/route.ts | 21 ++++++++++ src/lib/auth/register.ts | 4 +- src/lib/swagger.ts | 13 +++++- src/lib/validation/user.ts | 4 +- 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/app/api/user/[user]/route.ts diff --git a/src/app/api/user/[user]/route.ts b/src/app/api/user/[user]/route.ts new file mode 100644 index 0000000..d271efb --- /dev/null +++ b/src/app/api/user/[user]/route.ts @@ -0,0 +1,72 @@ +import { auth } from '@/auth'; +import { NextResponse } from 'next/server'; +import { prisma } from '@/prisma'; + +/** + * @swagger + * /api/user/{user}: + * get: + * description: Retrieve the information of a specific user by ID or name. + * parameters: + * - in: path + * name: user + * required: true + * schema: + * type: string + * description: The ID or name of the user to retrieve. + * responses: + * 200: + * description: User information retrieved successfully. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PublicUser' + * 401: + * description: User is not authenticated. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * { + * message: 'Not authenticated' + * } + * 404: + * description: User not found. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * { + * message: 'User not found' + * } +*/ +export const GET = auth(async function GET(req, { params }) { + if (!req.auth) + return NextResponse.json({ message: 'Not authenticated' }, { status: 401 }); + if (!req.auth.user || !req.auth.user.id) + return NextResponse.json({ message: 'User not found' }, { status: 404 }); + + const requestedUser = (await params).user; + const dbUser = await prisma.user.findFirst({ + where: { + OR: [ + { id: requestedUser }, + { name: requestedUser }, + ], + }, + }); + + if (!dbUser) + return NextResponse.json({ message: 'User not found' }, { status: 404 }); + + return NextResponse.json({ + id: dbUser.id, + name: dbUser.name, + first_name: dbUser.first_name, + last_name: dbUser.last_name, + timezone: dbUser.timezone, + image: dbUser.image, + }); +}); diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 7fbcd49..a873a28 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -6,6 +6,7 @@ import { userFirstNameSchema, userNameSchema, userLastNameSchema, + disallowedUsernames, } from '@/lib/validation/user'; /** @@ -155,6 +156,16 @@ export const PUT = auth(async function PUT(req) { { status: 400 }, ); } + // Check if the name already exists for another user + const existingUser = await prisma.user.findUnique({ + where: { name }, + }); + if ((existingUser && existingUser.id !== req.auth.user.id) || disallowedUsernames.includes(name.toLowerCase())) { + return NextResponse.json( + { message: 'Username in use by another account' }, + { status: 400 }, + ); + } updateData.name = name; } if (first_name) { @@ -185,6 +196,16 @@ export const PUT = auth(async function PUT(req) { { status: 400 }, ); } + // Check if the email already exists for another user + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + if (existingUser && existingUser.id !== req.auth.user.id) { + return NextResponse.json( + { message: 'Email in use by another account' }, + { status: 400 }, + ); + } updateData.email = email; } if (image) { diff --git a/src/lib/auth/register.ts b/src/lib/auth/register.ts index 9eba8e9..71990f3 100644 --- a/src/lib/auth/register.ts +++ b/src/lib/auth/register.ts @@ -2,7 +2,7 @@ import type { z } from 'zod'; import bcrypt from 'bcryptjs'; -import { registerSchema } from '@/lib/validation/user'; +import { disallowedUsernames, registerSchema } from '@/lib/validation/user'; import { prisma } from '@/prisma'; export async function registerAction(data: z.infer) { @@ -35,7 +35,7 @@ export async function registerAction(data: z.infer) { }, }); - if (existingUsername) { + if (existingUsername || disallowedUsernames.includes(username.toLowerCase())) { return { error: 'Username already exists', }; diff --git a/src/lib/swagger.ts b/src/lib/swagger.ts index 7295ef3..326fce2 100644 --- a/src/lib/swagger.ts +++ b/src/lib/swagger.ts @@ -32,7 +32,7 @@ export const getApiDocs = async () => { User: { type: 'object', properties: { - id: { type: 'string', format: 'uuid' }, + id: { type: 'string' }, name: { type: 'string' }, first_name: { type: 'string' }, last_name: { type: 'string' }, @@ -41,6 +41,17 @@ export const getApiDocs = async () => { timezone: { type: 'string', description: 'User timezone' }, }, }, + PublicUser: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + first_name: { type: 'string' }, + last_name: { type: 'string' }, + image: { type: 'string', format: 'uri' }, + timezone: { type: 'string', description: 'User timezone' }, + }, + }, }, }, security: [], diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts index 842278e..755f395 100644 --- a/src/lib/validation/user.ts +++ b/src/lib/validation/user.ts @@ -23,7 +23,7 @@ export const userNameSchema = zod /^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores', ); - + export const loginSchema = zod.object({ email: userEmailSchema.or(userNameSchema), password: zod.string().min(1, 'Password is required'), @@ -60,3 +60,5 @@ export const registerSchema = zod path: ['password'], }, ); + +export const disallowedUsernames = ['me', 'admin', 'search'];