From 6223e22c0da292f6f2fdc3ddfd8cc4ca618b9f3c Mon Sep 17 00:00:00 2001 From: Robert Hajdu Date: Wed, 13 Aug 2025 20:28:20 +0200 Subject: [PATCH] Refactor login functionality to use email instead of username. Update related tests, API routes, and database schema to support email-based authentication. Ensure consistency across login forms and queries. --- cypress/e2e/login.cy.ts | 16 ++++++++-------- db/mysql/schema.prisma | 2 ++ db/postgresql/schema.prisma | 2 ++ src/app/api/auth/login/route.ts | 10 +++++----- src/app/login/LoginForm.tsx | 8 ++++---- src/lib/authOptions.ts | 8 ++++---- src/queries/prisma/user.ts | 24 ++++++++++++++++++++++++ 7 files changed, 49 insertions(+), 21 deletions(-) diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 507b1b580..506c436b7 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -9,9 +9,9 @@ describe('Login tests', () => { defaultCommandTimeout: 10000, }, () => { - cy.getDataTest('input-username').find('input').as('inputUsername').click(); - cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); - cy.get('@inputUsername').click(); + cy.getDataTest('input-username').find('input').as('inputEmail').click(); + cy.get('@inputEmail').type(Cypress.env('umami_email'), { delay: 0 }); + cy.get('@inputEmail').click(); cy.getDataTest('input-password') .find('input') .type(Cypress.env('umami_password'), { delay: 0 }); @@ -25,12 +25,12 @@ describe('Login tests', () => { cy.getDataTest('button-submit').click(); cy.contains(/Required/i).should('be.visible'); - cy.getDataTest('input-username').find('input').as('inputUsername'); - cy.get('@inputUsername').click(); - cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); - cy.get('@inputUsername').click(); + cy.getDataTest('input-username').find('input').as('inputEmail'); + cy.get('@inputEmail').click(); + cy.get('@inputEmail').type(Cypress.env('umami_email'), { delay: 0 }); + cy.get('@inputEmail').click(); cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 }); cy.getDataTest('button-submit').click(); - cy.contains(/Incorrect username and\/or password./i).should('be.visible'); + cy.contains(/Incorrect username and\/or password\./i).should('be.visible'); }); }); diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 67bd24d22..53479549a 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -11,6 +11,8 @@ datasource db { model User { id String @id @unique @map("user_id") @db.VarChar(36) username String @unique @db.VarChar(255) + email String? @unique @db.VarChar(255) + emailVerified DateTime? @map("email_verified") @db.Timestamp(0) password String @db.VarChar(60) role String @map("role") @db.VarChar(50) logoUrl String? @map("logo_url") @db.VarChar(2183) diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 2535f4963..9d7c1ed58 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -11,6 +11,8 @@ datasource db { model User { id String @id @unique @map("user_id") @db.Uuid username String @unique @db.VarChar(255) + email String? @unique @db.VarChar(255) + emailVerified DateTime? @map("email_verified") @db.Timestamptz(6) password String @db.VarChar(60) role String @map("role") @db.VarChar(50) logoUrl String? @map("logo_url") @db.VarChar(2183) diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index bfac55489..2404d6446 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { checkPassword } from '@/lib/auth'; import { createSecureToken } from '@/lib/jwt'; import redis from '@/lib/redis'; -import { getUserByUsername } from '@/queries'; +import { getUserByEmail } from '@/queries'; import { json, unauthorized } from '@/lib/response'; import { parseRequest } from '@/lib/request'; import { saveAuth } from '@/lib/auth'; @@ -11,7 +11,7 @@ import { ROLES } from '@/lib/constants'; export async function POST(request: Request) { const schema = z.object({ - username: z.string(), + email: z.string().email(), password: z.string(), }); @@ -21,9 +21,9 @@ export async function POST(request: Request) { return error(); } - const { username, password } = body; + const { email, password } = body; - const user = await getUserByUsername(username, { includePassword: true }); + const user = await getUserByEmail(email, { includePassword: true }); if (!user || !checkPassword(password, user.password)) { return unauthorized('message.incorrect-username-password'); @@ -41,6 +41,6 @@ export async function POST(request: Request) { return json({ token, - user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, + user: { id, username: user.username, role, createdAt, isAdmin: role === ROLES.admin }, }); } diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 9bc9dfd22..5784f87bb 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -20,7 +20,7 @@ export function LoginForm() { const handleSubmit = async (data: any) => { const res = await signIn('credentials', { - username: data.username, + email: data.email, password: data.password, redirect: false, }); @@ -39,13 +39,13 @@ export function LoginForm() {
umami
- + - + diff --git a/src/lib/authOptions.ts b/src/lib/authOptions.ts index 0212df467..be8300556 100644 --- a/src/lib/authOptions.ts +++ b/src/lib/authOptions.ts @@ -1,7 +1,7 @@ import type { NextAuthOptions } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import { checkPassword } from '@/lib/auth'; -import { getUserByUsername } from '@/queries'; +import { getUserByEmail } from '@/queries'; const AUTH_SECRET = process.env.NEXTAUTH_SECRET || process.env.APP_SECRET; @@ -12,12 +12,12 @@ const authOptions: NextAuthOptions = { CredentialsProvider({ name: 'Credentials', credentials: { - username: { label: 'Username', type: 'text' }, + email: { label: 'Email', type: 'text' }, password: { label: 'Password', type: 'password' }, }, authorize: async credentials => { - if (!credentials?.username || !credentials?.password) return null; - const user = await getUserByUsername(credentials.username, { + if (!credentials?.email || !credentials?.password) return null; + const user = await getUserByEmail(credentials.email, { includePassword: true, } as any); if (!user) return null; diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 2e6a478fe..c307fdef7 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -32,6 +32,24 @@ async function findUser( }); } +export async function getUserByEmail(email: string, options: GetUserOptions = {}) { + const { includePassword = false, showDeleted = false } = options; + + return prisma.client.user.findFirst({ + where: { + OR: [{ email }, { username: email }], + ...(showDeleted && { deletedAt: null }), + }, + select: { + id: true, + username: true, + password: includePassword, + role: true, + createdAt: true, + }, + }) as unknown as Promise; +} + export async function getUser(userId: string, options: GetUserOptions = {}) { return findUser( { @@ -222,6 +240,12 @@ export async function deleteUser( where: { id: userId, }, + select: { + id: true, + username: true, + role: true, + createdAt: true, + }, }), ]); }