feat(api): add user info GET endpoint and blocklist usernames

This commit is contained in:
Dominik 2025-06-12 19:00:57 +02:00
parent 57e420f572
commit 28aa8d3a82
Signed by: dominik
GPG key ID: 06A4003FC5049644
5 changed files with 110 additions and 4 deletions

View file

@ -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,
});
});

View file

@ -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) {

View file

@ -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<typeof registerSchema>) {
@ -35,7 +35,7 @@ export async function registerAction(data: z.infer<typeof registerSchema>) {
},
});
if (existingUsername) {
if (existingUsername || disallowedUsernames.includes(username.toLowerCase())) {
return {
error: 'Username already exists',
};

View file

@ -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: [],

View file

@ -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'];