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, userFirstNameSchema,
userNameSchema, userNameSchema,
userLastNameSchema, userLastNameSchema,
disallowedUsernames,
} from '@/lib/validation/user'; } from '@/lib/validation/user';
/** /**
@ -155,6 +156,16 @@ export const PUT = auth(async function PUT(req) {
{ status: 400 }, { 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; updateData.name = name;
} }
if (first_name) { if (first_name) {
@ -185,6 +196,16 @@ export const PUT = auth(async function PUT(req) {
{ status: 400 }, { 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; updateData.email = email;
} }
if (image) { if (image) {

View file

@ -2,7 +2,7 @@
import type { z } from 'zod'; import type { z } from 'zod';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { registerSchema } from '@/lib/validation/user'; import { disallowedUsernames, registerSchema } from '@/lib/validation/user';
import { prisma } from '@/prisma'; import { prisma } from '@/prisma';
export async function registerAction(data: z.infer<typeof registerSchema>) { 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 { return {
error: 'Username already exists', error: 'Username already exists',
}; };

View file

@ -32,7 +32,7 @@ export const getApiDocs = async () => {
User: { User: {
type: 'object', type: 'object',
properties: { properties: {
id: { type: 'string', format: 'uuid' }, id: { type: 'string' },
name: { type: 'string' }, name: { type: 'string' },
first_name: { type: 'string' }, first_name: { type: 'string' },
last_name: { type: 'string' }, last_name: { type: 'string' },
@ -41,6 +41,17 @@ export const getApiDocs = async () => {
timezone: { type: 'string', description: 'User timezone' }, 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: [], security: [],

View file

@ -60,3 +60,5 @@ export const registerSchema = zod
path: ['password'], path: ['password'],
}, },
); );
export const disallowedUsernames = ['me', 'admin', 'search'];