V 1.2.2 Login-Seite: bcrypt-Passwörter, Passwort-Toggle, Footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -283,3 +283,34 @@ main {
|
|||||||
}
|
}
|
||||||
.login-submit:hover:not(:disabled) { background-color: #6a9fc5; }
|
.login-submit:hover:not(:disabled) { background-color: #6a9fc5; }
|
||||||
.login-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
.login-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.login-password-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.login-input-password {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
.login-eye-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0; right: 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.login-eye-btn:hover { color: #1f2937; }
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4b5563;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
.login-footer a { color: #2563eb; }
|
||||||
|
.login-footer a:hover { text-decoration: underline; }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { verifyCredentials } from '@/lib/auth';
|
|||||||
import { createSession, deleteSession } from '@/lib/session';
|
import { createSession, deleteSession } from '@/lib/session';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export async function login(prevState: any, formData: FormData) {
|
export async function login(prevState: { error: string } | null | undefined, formData: FormData) {
|
||||||
const username = formData.get('username') as string;
|
const username = formData.get('username') as string;
|
||||||
const password = formData.get('password') as string;
|
const password = formData.get('password') as string;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export async function login(prevState: any, formData: FormData) {
|
|||||||
return { error: 'Bitte Benutzername und Passwort eingeben' };
|
return { error: 'Bitte Benutzername und Passwort eingeben' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = verifyCredentials(username, password);
|
const isValid = await verifyCredentials(username, password);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return { error: 'Ungültige Anmeldedaten' };
|
return { error: 'Ungültige Anmeldedaten' };
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-11
@@ -1,10 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useActionState } from 'react';
|
import { useActionState, useState } from 'react';
|
||||||
import { login } from './actions';
|
import { login } from './actions';
|
||||||
|
import packageJson from '@/package.json';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [state, loginAction, isPending] = useActionState(login, undefined);
|
const [state, loginAction, isPending] = useActionState(login, undefined);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const version = packageJson.version;
|
||||||
|
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-wrapper">
|
<div className="login-wrapper">
|
||||||
@@ -38,16 +43,36 @@ export default function LoginPage() {
|
|||||||
<label htmlFor="password" className="login-label">
|
<label htmlFor="password" className="login-label">
|
||||||
Passwort
|
Passwort
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div className="login-password-wrapper">
|
||||||
id="password"
|
<input
|
||||||
name="password"
|
id="password"
|
||||||
type="password"
|
name="password"
|
||||||
required
|
type={showPassword ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
required
|
||||||
className="login-input"
|
autoComplete="new-password"
|
||||||
placeholder="Passwort"
|
className="login-input login-input-password"
|
||||||
disabled={isPending}
|
placeholder="Passwort"
|
||||||
/>
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(v => !v)}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'}
|
||||||
|
className="login-eye-btn"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 4.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
@@ -66,6 +91,15 @@ export default function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer className="login-footer">
|
||||||
|
<div>
|
||||||
|
<a href="mailto:rxf@gmx.de">mailto:rxf@gmx.de</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Version {version} - {buildDate}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-8
@@ -1,8 +1,4 @@
|
|||||||
/**
|
import bcrypt from 'bcryptjs';
|
||||||
* Authentifizierungsbibliothek
|
|
||||||
* Benutzer via Umgebungsvariable konfigurieren:
|
|
||||||
* AUTH_USERS=user1:passwort1,user2:passwort2
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -24,11 +20,13 @@ export function getUsers(): User[] {
|
|||||||
.filter((user) => user.username && user.password);
|
.filter((user) => user.username && user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyCredentials(username: string, password: string): boolean {
|
export async function verifyCredentials(username: string, password: string): Promise<boolean> {
|
||||||
const users = getUsers();
|
const users = getUsers();
|
||||||
const user = users.find(u => u.username === username);
|
const user = users.find(u => u.username === username);
|
||||||
if (!user) return false;
|
if (!user) {
|
||||||
return user.password === password;
|
return false;
|
||||||
|
}
|
||||||
|
return bcrypt.compare(password, user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthEnabled(): boolean {
|
export function isAuthEnabled(): boolean {
|
||||||
|
|||||||
Generated
+20
-2
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "tabletten_next",
|
"name": "tabletten_next",
|
||||||
"version": "1.0.0",
|
"version": "1.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tabletten_next",
|
"name": "tabletten_next",
|
||||||
"version": "1.0.0",
|
"version": "1.2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"jose": "^6.2.1",
|
"jose": "^6.2.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mysql2": "^3.19.1",
|
"mysql2": "^3.19.1",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/node-schedule": "^2.1.8",
|
"@types/node-schedule": "^2.1.8",
|
||||||
"@types/nodemailer": "^7.0.11",
|
"@types/nodemailer": "^7.0.11",
|
||||||
@@ -1257,6 +1259,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -2226,6 +2235,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabletten_next",
|
"name": "tabletten_next",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"jose": "^6.2.1",
|
"jose": "^6.2.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mysql2": "^3.19.1",
|
"mysql2": "^3.19.1",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/node-schedule": "^2.1.8",
|
"@types/node-schedule": "^2.1.8",
|
||||||
"@types/nodemailer": "^7.0.11",
|
"@types/nodemailer": "^7.0.11",
|
||||||
|
|||||||
Reference in New Issue
Block a user