mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-18 11:11:10 +01:00
🐛 Fix issues with login page
This commit is contained in:
@@ -39,7 +39,7 @@
|
|||||||
"@mantine/modals": "^6.0.0",
|
"@mantine/modals": "^6.0.0",
|
||||||
"@mantine/next": "^6.0.0",
|
"@mantine/next": "^6.0.0",
|
||||||
"@mantine/notifications": "^6.0.0",
|
"@mantine/notifications": "^6.0.0",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@nivo/core": "^0.83.0",
|
"@nivo/core": "^0.83.0",
|
||||||
"@nivo/line": "^0.83.0",
|
"@nivo/line": "^0.83.0",
|
||||||
"@prisma/client": "^5.0.0",
|
"@prisma/client": "^5.0.0",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"i18next": "^22.5.1",
|
"i18next": "^22.5.1",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"next": "13.4.10",
|
"next": "13.4.10",
|
||||||
"next-auth": "^4.22.3",
|
"next-auth": "^4.21.0",
|
||||||
"next-i18next": "^13.0.0",
|
"next-i18next": "^13.0.0",
|
||||||
"nzbget-api": "^0.0.3",
|
"nzbget-api": "^0.0.3",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
|
|||||||
BIN
prisma/db.sqlite
BIN
prisma/db.sqlite
Binary file not shown.
@@ -1,27 +1,18 @@
|
|||||||
{
|
{
|
||||||
"title": "Welcome back!",
|
"title": "Welcome back!",
|
||||||
"text": "Please enter your password",
|
"text": "Please enter your credentials",
|
||||||
"form": {
|
"form": {
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"username": {
|
||||||
|
"label": "Username"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password"
|
||||||
"placeholder": "Your password"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"submit": "Sign in"
|
"submit": "Sign in"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"alert": "Your credentials are incorrect or this account doesn't exist. Please try again."
|
||||||
"checking": {
|
|
||||||
"title": "Checking your password",
|
|
||||||
"message": "Your password is being checked..."
|
|
||||||
},
|
|
||||||
"correct": {
|
|
||||||
"title": "Sign in successful, redirecting..."
|
|
||||||
},
|
|
||||||
"wrong": {
|
|
||||||
"title": "The password you entered is incorrect, please try again."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,116 +1,88 @@
|
|||||||
import { Button, Container, Paper, PasswordInput, Text, Title } from '@mantine/core';
|
import {
|
||||||
import { useForm } from '@mantine/form';
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Flex,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useForm, zodResolver } from '@mantine/form';
|
||||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import { IconCheck, IconX } from '@tabler/icons-react';
|
import { IconAlertCircle, IconAlertTriangle, IconCheck, IconX } from '@tabler/icons-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { setCookie } from 'cookies-next';
|
import { setCookie } from 'cookies-next';
|
||||||
|
import { signIn } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import { z } from 'zod';
|
||||||
|
import { signInSchema } from '~/validations/user';
|
||||||
|
|
||||||
import { loginNamespaces } from '../tools/server/translation-namespaces';
|
import { loginNamespaces } from '../tools/server/translation-namespaces';
|
||||||
|
|
||||||
// TODO: Add links to the wiki articles about the login process.
|
export default function LoginPage() {
|
||||||
export default function AuthenticationTitle() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation('authentication/login');
|
const { t } = useTranslation('authentication/login');
|
||||||
|
const queryParams = useRouter().query as { error?: 'CredentialsSignin' | (string & {}) };
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm<z.infer<typeof signInSchema>>({
|
||||||
initialValues: {
|
validateInputOnChange: true,
|
||||||
password: '',
|
validateInputOnBlur: true,
|
||||||
},
|
validate: zodResolver(signInSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (values: z.infer<typeof signInSchema>) => {
|
||||||
|
signIn('credentials', {
|
||||||
|
redirect: true,
|
||||||
|
name: values.name,
|
||||||
|
password: values.password,
|
||||||
|
callbackUrl: '/',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||||
size="lg"
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||||
style={{
|
<Title align="center" weight={900}>
|
||||||
height: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper
|
|
||||||
withBorder
|
|
||||||
shadow="md"
|
|
||||||
p={30}
|
|
||||||
mt={30}
|
|
||||||
radius="md"
|
|
||||||
style={{ width: '100%', maxWidth: 420 }}
|
|
||||||
>
|
|
||||||
<Title
|
|
||||||
align="center"
|
|
||||||
sx={(theme) => ({ fontFamily: `Greycliff CF, ${theme.fontFamily}`, fontWeight: 900 })}
|
|
||||||
>
|
|
||||||
{t('title')}
|
{t('title')}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||||
{t('text')}
|
{t('text')}
|
||||||
</Text>
|
</Text>
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit((values) => {
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
setCookie('password', values.password, {
|
<Stack>
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
<TextInput
|
||||||
sameSite: 'lax',
|
variant="filled"
|
||||||
});
|
label={t('form.fields.username.label')}
|
||||||
showNotification({
|
withAsterisk
|
||||||
id: 'load-data',
|
{...form.getInputProps('name')}
|
||||||
loading: true,
|
/>
|
||||||
title: t('notifications.checking.title'),
|
|
||||||
message: t('notifications.checking.message'),
|
<PasswordInput
|
||||||
autoClose: false,
|
id="password"
|
||||||
withCloseButton: false,
|
variant="filled"
|
||||||
});
|
label={t('form.fields.password.label')}
|
||||||
axios
|
withAsterisk
|
||||||
.post('/api/configs/tryPassword', {
|
{...form.getInputProps('password')}
|
||||||
tried: values.password,
|
/>
|
||||||
})
|
|
||||||
.then((res) => {
|
<Button fullWidth type="submit">
|
||||||
setTimeout(() => {
|
{t('form.buttons.submit')}
|
||||||
if (res.data.success === true) {
|
</Button>
|
||||||
router.push('/');
|
|
||||||
updateNotification({
|
{queryParams.error === 'CredentialsSignin' && (
|
||||||
id: 'load-data',
|
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||||
color: 'teal',
|
{t('alert')}
|
||||||
title: t('notifications.correct.title'),
|
</Alert>
|
||||||
message: undefined,
|
)}
|
||||||
icon: <IconCheck />,
|
</Stack>
|
||||||
autoClose: 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (res.data.success === false) {
|
|
||||||
updateNotification({
|
|
||||||
id: 'load-data',
|
|
||||||
color: 'red',
|
|
||||||
title: t('notifications.wrong.title'),
|
|
||||||
message: undefined,
|
|
||||||
icon: <IconX />,
|
|
||||||
autoClose: 2000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PasswordInput
|
|
||||||
id="password"
|
|
||||||
label={t('form.fields.password.label')}
|
|
||||||
placeholder={t('form.fields.password.placeholder') ?? undefined}
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
mt="md"
|
|
||||||
{...form.getInputProps('password')}
|
|
||||||
/>
|
|
||||||
<Button fullWidth type="submit" mt="xl">
|
|
||||||
{t('form.buttons.submit')}
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Card>
|
||||||
</Container>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { type DefaultSession, type NextAuthOptions, getServerSession } from 'nex
|
|||||||
import { decode, encode } from 'next-auth/jwt';
|
import { decode, encode } from 'next-auth/jwt';
|
||||||
import Credentials from 'next-auth/providers/credentials';
|
import Credentials from 'next-auth/providers/credentials';
|
||||||
import { prisma } from '~/server/db';
|
import { prisma } from '~/server/db';
|
||||||
|
import EmptyNextAuthProvider from '~/utils/empty-provider';
|
||||||
import { fromDate, generateSessionToken } from '~/utils/session';
|
import { fromDate, generateSessionToken } from '~/utils/session';
|
||||||
import { signInSchema } from '~/validations/user';
|
import { signInSchema } from '~/validations/user';
|
||||||
|
|
||||||
@@ -63,9 +64,6 @@ export const constructAuthOptions = (
|
|||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
include: {
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user.isAdmin = userFromDatabase.isAdmin;
|
session.user.isAdmin = userFromDatabase.isAdmin;
|
||||||
@@ -102,6 +100,10 @@ export const constructAuthOptions = (
|
|||||||
strategy: 'database',
|
strategy: 'database',
|
||||||
maxAge: sessionMaxAgeInSeconds,
|
maxAge: sessionMaxAgeInSeconds,
|
||||||
},
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/login',
|
||||||
|
error: '/login',
|
||||||
|
},
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
providers: [
|
providers: [
|
||||||
Credentials({
|
Credentials({
|
||||||
@@ -117,9 +119,6 @@ export const constructAuthOptions = (
|
|||||||
const data = await signInSchema.parseAsync(credentials);
|
const data = await signInSchema.parseAsync(credentials);
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
include: {
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
},
|
},
|
||||||
@@ -146,6 +145,7 @@ export const constructAuthOptions = (
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
EmptyNextAuthProvider(),
|
||||||
],
|
],
|
||||||
jwt: {
|
jwt: {
|
||||||
async encode(params) {
|
async encode(params) {
|
||||||
|
|||||||
14
src/utils/empty-provider.ts
Normal file
14
src/utils/empty-provider.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { OAuthConfig } from 'next-auth/providers';
|
||||||
|
|
||||||
|
export default function EmptyNextAuthProvider(): OAuthConfig<any> {
|
||||||
|
return {
|
||||||
|
id: 'empty',
|
||||||
|
name: 'Empty',
|
||||||
|
type: 'oauth',
|
||||||
|
profile: () => {
|
||||||
|
throw new Error(
|
||||||
|
'EmptyNextAuthProvider can not be used and is only a placeholder because credentials authentication can not be used as session authentication without additional providers.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user