From 2ab58700b2d126289fcb3835bf5b5d75cb3c7eea Mon Sep 17 00:00:00 2001 From: Alejandro Gispert Date: Sun, 22 Feb 2026 07:18:00 +0100 Subject: [PATCH] fix: make signin email case-insensitive Closes #3981 --- src/app/api/users/[userId]/route.ts | 39 ++++++++++++++++++----------- src/app/api/users/route.ts | 24 ++++++++++-------- src/queries/prisma/user.ts | 37 ++++++++++++++++----------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index aade8aa8d..caa737ca6 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,12 +1,20 @@ -import { z } from 'zod'; -import { hashPassword } from '@/lib/password'; -import { parseRequest } from '@/lib/request'; -import { badRequest, json, ok, unauthorized } from '@/lib/response'; -import { userRoleParam } from '@/lib/schema'; -import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions'; -import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma'; +import { z } from "zod"; +import { hashPassword } from "@/lib/password"; +import { parseRequest } from "@/lib/request"; +import { badRequest, json, ok, unauthorized } from "@/lib/response"; +import { userRoleParam } from "@/lib/schema"; +import { canDeleteUser, canUpdateUser, canViewUser } from "@/permissions"; +import { + deleteUser, + getUser, + getUserByUsername, + updateUser, +} from "@/queries/prisma"; -export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { +export async function GET( + request: Request, + { params }: { params: Promise<{ userId: string }> }, +) { const { auth, error } = await parseRequest(request); if (error) { @@ -24,7 +32,10 @@ export async function GET(request: Request, { params }: { params: Promise<{ user return json(user); } -export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { +export async function POST( + request: Request, + { params }: { params: Promise<{ userId: string }> }, +) { const schema = z.object({ username: z.string().max(255).optional(), password: z.string().max(255).optional(), @@ -59,15 +70,15 @@ export async function POST(request: Request, { params }: { params: Promise<{ use } if (username && auth.user.isAdmin) { - data.username = username; + data.username = username.toLowerCase(); } // Check when username changes if (data.username && user.username !== data.username) { - const user = await getUserByUsername(username); + const existingUser = await getUserByUsername(username); - if (user) { - return badRequest({ message: 'User already exists' }); + if (existingUser && existingUser.id !== userId) { + return badRequest({ message: "User already exists" }); } } @@ -93,7 +104,7 @@ export async function DELETE( } if (userId === auth.user.id) { - return badRequest({ message: 'You cannot delete yourself.' }); + return badRequest({ message: "You cannot delete yourself." }); } await deleteUser(userId); diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index dbb114cf7..2790a28d5 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -1,11 +1,11 @@ -import { z } from 'zod'; -import { ROLES } from '@/lib/constants'; -import { uuid } from '@/lib/crypto'; -import { hashPassword } from '@/lib/password'; -import { parseRequest } from '@/lib/request'; -import { badRequest, json, unauthorized } from '@/lib/response'; -import { canCreateUser } from '@/permissions'; -import { createUser, getUserByUsername } from '@/queries/prisma'; +import { z } from "zod"; +import { ROLES } from "@/lib/constants"; +import { uuid } from "@/lib/crypto"; +import { hashPassword } from "@/lib/password"; +import { parseRequest } from "@/lib/request"; +import { badRequest, json, unauthorized } from "@/lib/response"; +import { canCreateUser } from "@/permissions"; +import { createUser, getUserByUsername } from "@/queries/prisma"; export async function POST(request: Request) { const schema = z.object({ @@ -27,15 +27,17 @@ export async function POST(request: Request) { const { id, username, password, role } = body; - const existingUser = await getUserByUsername(username, { showDeleted: true }); + const existingUser = await getUserByUsername(username, { + showDeleted: true, + }); if (existingUser) { - return badRequest({ message: 'User already exists' }); + return badRequest({ message: "User already exists" }); } const user = await createUser({ id: id || uuid(), - username, + username: username.toLowerCase(), password: hashPassword(password), role: role ?? ROLES.user, }); diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 14376fc26..ca669f98c 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -1,8 +1,8 @@ -import { Prisma } from '@/generated/prisma/client'; -import { ROLES } from '@/lib/constants'; -import { getRandomChars } from '@/lib/generate'; -import prisma from '@/lib/prisma'; -import type { QueryFilters, Role } from '@/lib/types'; +import { Prisma } from "@/generated/prisma/client"; +import { ROLES } from "@/lib/constants"; +import { getRandomChars } from "@/lib/generate"; +import prisma from "@/lib/prisma"; +import type { QueryFilters, Role } from "@/lib/types"; import UserFindManyArgs = Prisma.UserFindManyArgs; @@ -11,7 +11,10 @@ export interface GetUserOptions { showDeleted?: boolean; } -async function findUser(criteria: Prisma.UserFindUniqueArgs, options: GetUserOptions = {}) { +async function findUser( + criteria: Prisma.UserFindUniqueArgs, + options: GetUserOptions = {}, +) { const { includePassword = false, showDeleted = false } = options; return prisma.client.user.findUnique({ @@ -41,27 +44,33 @@ export async function getUser(userId: string, options: GetUserOptions = {}) { ); } -export async function getUserByUsername(username: string, options: GetUserOptions = {}) { - return findUser({ where: { username } }, options); +export async function getUserByUsername( + username: string, + options: GetUserOptions = {}, +) { + return findUser({ where: { username: username.toLowerCase() } }, options); } -export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters = {}) { +export async function getUsers( + criteria: UserFindManyArgs, + filters: QueryFilters = {}, +) { const { search } = filters; const where: Prisma.UserWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(search, [{ username: 'contains' }]), + ...prisma.getSearchParameters(search, [{ username: "contains" }]), deletedAt: null, }; return prisma.pagedQuery( - 'user', + "user", { ...criteria, where, }, { - orderBy: 'createdAt', + orderBy: "createdAt", sortDescending: true, ...filters, }, @@ -110,7 +119,7 @@ export async function deleteUser(userId: string) { let websiteIds = []; if (websites.length > 0) { - websiteIds = websites.map(a => a.id); + websiteIds = websites.map((a) => a.id); } const teams = await client.team.findMany({ @@ -124,7 +133,7 @@ export async function deleteUser(userId: string) { }, }); - const teamIds = teams.map(a => a.id); + const teamIds = teams.map((a) => a.id); if (cloudMode) { return transaction([