diff --git a/app/globals.css b/app/globals.css
index f109252..133b981 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -283,3 +283,34 @@ main {
}
.login-submit:hover:not(:disabled) { background-color: #6a9fc5; }
.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; }
diff --git a/app/login/actions.ts b/app/login/actions.ts
index 5dabed6..4ff702b 100644
--- a/app/login/actions.ts
+++ b/app/login/actions.ts
@@ -4,7 +4,7 @@ import { verifyCredentials } from '@/lib/auth';
import { createSession, deleteSession } from '@/lib/session';
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 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' };
}
- const isValid = verifyCredentials(username, password);
+ const isValid = await verifyCredentials(username, password);
if (!isValid) {
return { error: 'Ungültige Anmeldedaten' };
}
diff --git a/app/login/page.tsx b/app/login/page.tsx
index e4f3589..bfebb5a 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,10 +1,15 @@
'use client';
-import { useActionState } from 'react';
+import { useActionState, useState } from 'react';
import { login } from './actions';
+import packageJson from '@/package.json';
export default function LoginPage() {
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 (
@@ -38,16 +43,36 @@ export default function LoginPage() {
-
+
+
+
+
{state?.error && (
@@ -66,6 +91,15 @@ export default function LoginPage() {
+
+
);
diff --git a/lib/auth.ts b/lib/auth.ts
index d117b19..d7198ad 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,8 +1,4 @@
-/**
- * Authentifizierungsbibliothek
- * Benutzer via Umgebungsvariable konfigurieren:
- * AUTH_USERS=user1:passwort1,user2:passwort2
- */
+import bcrypt from 'bcryptjs';
export interface User {
username: string;
@@ -24,11 +20,13 @@ export function getUsers(): User[] {
.filter((user) => user.username && user.password);
}
-export function verifyCredentials(username: string, password: string): boolean {
+export async function verifyCredentials(username: string, password: string): Promise {
const users = getUsers();
const user = users.find(u => u.username === username);
- if (!user) return false;
- return user.password === password;
+ if (!user) {
+ return false;
+ }
+ return bcrypt.compare(password, user.password);
}
export function isAuthEnabled(): boolean {
diff --git a/package-lock.json b/package-lock.json
index 9763992..8797da8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,14 @@
{
"name": "tabletten_next",
- "version": "1.0.0",
+ "version": "1.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tabletten_next",
- "version": "1.0.0",
+ "version": "1.2.1",
"dependencies": {
+ "bcryptjs": "^3.0.3",
"jose": "^6.2.1",
"moment": "^2.30.1",
"mysql2": "^3.19.1",
@@ -18,6 +19,7 @@
"react-dom": "19.2.3"
},
"devDependencies": {
+ "@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/node-schedule": "^2.1.8",
"@types/nodemailer": "^7.0.11",
@@ -1257,6 +1259,13 @@
"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": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2226,6 +2235,15 @@
"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": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
diff --git a/package.json b/package.json
index 5fae618..2787af2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tabletten_next",
- "version": "1.2.1",
+ "version": "1.2.2",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
+ "bcryptjs": "^3.0.3",
"jose": "^6.2.1",
"moment": "^2.30.1",
"mysql2": "^3.19.1",
@@ -19,6 +20,7 @@
"react-dom": "19.2.3"
},
"devDependencies": {
+ "@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/node-schedule": "^2.1.8",
"@types/nodemailer": "^7.0.11",