mirror of
https://github.com/umami-software/umami.git
synced 2026-02-18 11:35:37 +01:00
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.
This commit is contained in:
parent
d961c058dd
commit
6223e22c0d
7 changed files with 49 additions and 21 deletions
|
|
@ -9,9 +9,9 @@ describe('Login tests', () => {
|
||||||
defaultCommandTimeout: 10000,
|
defaultCommandTimeout: 10000,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
cy.getDataTest('input-username').find('input').as('inputUsername').click();
|
cy.getDataTest('input-username').find('input').as('inputEmail').click();
|
||||||
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
|
cy.get('@inputEmail').type(Cypress.env('umami_email'), { delay: 0 });
|
||||||
cy.get('@inputUsername').click();
|
cy.get('@inputEmail').click();
|
||||||
cy.getDataTest('input-password')
|
cy.getDataTest('input-password')
|
||||||
.find('input')
|
.find('input')
|
||||||
.type(Cypress.env('umami_password'), { delay: 0 });
|
.type(Cypress.env('umami_password'), { delay: 0 });
|
||||||
|
|
@ -25,12 +25,12 @@ describe('Login tests', () => {
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.contains(/Required/i).should('be.visible');
|
cy.contains(/Required/i).should('be.visible');
|
||||||
|
|
||||||
cy.getDataTest('input-username').find('input').as('inputUsername');
|
cy.getDataTest('input-username').find('input').as('inputEmail');
|
||||||
cy.get('@inputUsername').click();
|
cy.get('@inputEmail').click();
|
||||||
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
|
cy.get('@inputEmail').type(Cypress.env('umami_email'), { delay: 0 });
|
||||||
cy.get('@inputUsername').click();
|
cy.get('@inputEmail').click();
|
||||||
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
|
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
|
||||||
cy.getDataTest('button-submit').click();
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ datasource db {
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.VarChar(36)
|
id String @id @unique @map("user_id") @db.VarChar(36)
|
||||||
username String @unique @db.VarChar(255)
|
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)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ datasource db {
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.Uuid
|
id String @id @unique @map("user_id") @db.Uuid
|
||||||
username String @unique @db.VarChar(255)
|
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)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { checkPassword } from '@/lib/auth';
|
import { checkPassword } from '@/lib/auth';
|
||||||
import { createSecureToken } from '@/lib/jwt';
|
import { createSecureToken } from '@/lib/jwt';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import { getUserByUsername } from '@/queries';
|
import { getUserByEmail } from '@/queries';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { saveAuth } from '@/lib/auth';
|
import { saveAuth } from '@/lib/auth';
|
||||||
|
|
@ -11,7 +11,7 @@ import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string(),
|
email: z.string().email(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -21,9 +21,9 @@ export async function POST(request: Request) {
|
||||||
return error();
|
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)) {
|
if (!user || !checkPassword(password, user.password)) {
|
||||||
return unauthorized('message.incorrect-username-password');
|
return unauthorized('message.incorrect-username-password');
|
||||||
|
|
@ -41,6 +41,6 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
token,
|
token,
|
||||||
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
|
user: { id, username: user.username, role, createdAt, isAdmin: role === ROLES.admin },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export function LoginForm() {
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
const res = await signIn('credentials', {
|
const res = await signIn('credentials', {
|
||||||
username: data.username,
|
email: data.email,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
});
|
});
|
||||||
|
|
@ -39,13 +39,13 @@ export function LoginForm() {
|
||||||
</Icon>
|
</Icon>
|
||||||
<div className={styles.title}>umami</div>
|
<div className={styles.title}>umami</div>
|
||||||
<Form className={styles.form} onSubmit={handleSubmit}>
|
<Form className={styles.form} onSubmit={handleSubmit}>
|
||||||
<FormRow label={formatMessage(labels.username)}>
|
<FormRow label={formatMessage(labels.email)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
data-test="input-username"
|
data-test="input-username"
|
||||||
name="username"
|
name="email"
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
>
|
>
|
||||||
<TextField autoComplete="username" />
|
<TextField autoComplete="email" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.password)}>
|
<FormRow label={formatMessage(labels.password)}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { NextAuthOptions } from 'next-auth';
|
import type { NextAuthOptions } from 'next-auth';
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||||
import { checkPassword } from '@/lib/auth';
|
import { checkPassword } from '@/lib/auth';
|
||||||
import { getUserByUsername } from '@/queries';
|
import { getUserByEmail } from '@/queries';
|
||||||
|
|
||||||
const AUTH_SECRET = process.env.NEXTAUTH_SECRET || process.env.APP_SECRET;
|
const AUTH_SECRET = process.env.NEXTAUTH_SECRET || process.env.APP_SECRET;
|
||||||
|
|
||||||
|
|
@ -12,12 +12,12 @@ const authOptions: NextAuthOptions = {
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: 'Credentials',
|
name: 'Credentials',
|
||||||
credentials: {
|
credentials: {
|
||||||
username: { label: 'Username', type: 'text' },
|
email: { label: 'Email', type: 'text' },
|
||||||
password: { label: 'Password', type: 'password' },
|
password: { label: 'Password', type: 'password' },
|
||||||
},
|
},
|
||||||
authorize: async credentials => {
|
authorize: async credentials => {
|
||||||
if (!credentials?.username || !credentials?.password) return null;
|
if (!credentials?.email || !credentials?.password) return null;
|
||||||
const user = await getUserByUsername(credentials.username, {
|
const user = await getUserByEmail(credentials.email, {
|
||||||
includePassword: true,
|
includePassword: true,
|
||||||
} as any);
|
} as any);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
|
||||||
|
|
@ -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<User>;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUser(userId: string, options: GetUserOptions = {}) {
|
export async function getUser(userId: string, options: GetUserOptions = {}) {
|
||||||
return findUser(
|
return findUser(
|
||||||
{
|
{
|
||||||
|
|
@ -222,6 +240,12 @@ export async function deleteUser(
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue