Compare commits
6 Commits
90444b8f7d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| de28922784 | |||
| 38c18a5ead | |||
| a7863c519f | |||
| 204bf3bf8b | |||
| 46678cb644 | |||
| 74e5f76ec2 |
@@ -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' };
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
'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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 px-4">
|
||||
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-xl">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Anmeldung
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Bitte melden Sie sich an, um fortzufahren
|
||||
</p>
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Ausgaben - Log</h1>
|
||||
</div>
|
||||
|
||||
<form action={loginAction} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-center py-10">
|
||||
<div className="w-full max-w-sm bg-white border border-gray-300 rounded-xl shadow-md p-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-6 text-center">Anmeldung</h2>
|
||||
|
||||
<form action={loginAction} className="space-y-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Benutzername
|
||||
</label>
|
||||
@@ -33,7 +36,7 @@ export default function LoginPage() {
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-gray-900 focus:border-blue-500 focus:outline-none text-sm"
|
||||
placeholder="Benutzername"
|
||||
disabled={isPending}
|
||||
/>
|
||||
@@ -42,25 +45,44 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Passwort
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
className="w-full px-3 py-2 pr-10 border-2 border-gray-400 rounded-lg bg-white text-gray-900 focus:border-blue-500 focus:outline-none text-sm"
|
||||
placeholder="Passwort"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(v => !v)}
|
||||
tabIndex={-1}
|
||||
aria-label={showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'}
|
||||
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" 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" className="h-5 w-5" 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>
|
||||
|
||||
{state?.error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded-lg text-sm">
|
||||
<div className="bg-red-50 border border-red-300 text-red-700 px-3 py-2 rounded-lg text-sm">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
@@ -68,12 +90,27 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
className="w-full py-2 px-4 bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
{isPending ? 'Anmeldung läuft...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4 ">
|
||||
<div>
|
||||
<a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline">
|
||||
mailto:rxf@gmx.de
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
Version {version} - {buildDate}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
83
app/login/page.tsx_xx
Normal file
83
app/login/page.tsx_xx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { login } from './actions';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [state, loginAction, isPending] = useActionState(login, undefined);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-7xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
<h1 className="text-3xl font-bold mb-6">Ausgaben - Log</h1>
|
||||
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="max-w-md w-full space-y-8 bg-white p-8 rounded-2xl shadow-xl">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold mb-2">Anmeldung</h2>
|
||||
<p className="text-gray-600">
|
||||
Bitte melden Sie sich an, um fortzufahren
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action={loginAction} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Benutzername
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 bg-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
placeholder="Benutzername"
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 bg-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
placeholder="Passwort"
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state?.error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{isPending ? 'Anmeldung läuft...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
app/page.tsx
60
app/page.tsx
@@ -4,9 +4,10 @@ import { useState, useEffect } from 'react';
|
||||
import AusgabenForm from '@/components/AusgabenForm';
|
||||
import AusgabenList from '@/components/AusgabenList';
|
||||
import MonatsStatistik from '@/components/MonatsStatistik';
|
||||
import LogoutButton from '@/components/LogoutButton';
|
||||
import TabLayout from '@/components/TabLayout';
|
||||
import { AusgabenEntry } from '@/types/ausgaben';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
const MAX_ENTRIES = 15;
|
||||
|
||||
export default function Home() {
|
||||
const [activeTab, setActiveTab] = useState(0); // 0 = Haushalt, 1 = Privat
|
||||
@@ -15,9 +16,6 @@ export default function Home() {
|
||||
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
|
||||
const [statsRefreshKey, setStatsRefreshKey] = useState(0);
|
||||
|
||||
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' });
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecentEntries();
|
||||
setSelectedEntry(null); // Clear selected entry when switching tabs
|
||||
@@ -26,12 +24,13 @@ export default function Home() {
|
||||
const fetchRecentEntries = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/ausgaben?limit=20&typ=${activeTab}`, {
|
||||
const response = await fetch(`/api/ausgaben?limit=${MAX_ENTRIES}&typ=${activeTab}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setEntries(data.data);
|
||||
@@ -62,37 +61,7 @@ export default function Home() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-7xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Ausgaben - Log</h1>
|
||||
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors shadow-md" />
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab(0)}
|
||||
className={`flex-1 py-3 px-6 rounded-lg font-semibold transition-colors ${
|
||||
activeTab === 0
|
||||
? 'bg-[#85B7D7] text-black'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Haushalt
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab(1)}
|
||||
className={`flex-1 py-3 px-6 rounded-lg font-semibold transition-colors ${
|
||||
activeTab === 1
|
||||
? 'bg-[#85B7D7] text-black'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Privat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TabLayout activeTab={activeTab} onTabChange={setActiveTab}>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
||||
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} />
|
||||
@@ -100,7 +69,7 @@ export default function Home() {
|
||||
<MonatsStatistik typ={activeTab} refreshKey={statsRefreshKey} />
|
||||
|
||||
<div className="mt-6 bg-white border border-black rounded-lg shadow-md p-6">
|
||||
<h3 className="text-xl font-semibold mb-4">Letzte 20 Einträge</h3>
|
||||
<h3 className="text-xl font-semibold mb-4">Letzte {MAX_ENTRIES} Einträge</h3>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">Lade Daten...</div>
|
||||
) : (
|
||||
@@ -108,19 +77,6 @@ export default function Home() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4 ">
|
||||
<div>
|
||||
<a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline">
|
||||
mailto:rxf@gmx.de
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
Version {version} - {buildDate}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
<th className="border-b-2 border-black p-2 w-12">Kat.</th>
|
||||
<th className="border-b-2 border-black p-2 w-8">Betrag</th>
|
||||
<th className="border-b-2 border-black p-2 w-16">Wie</th>
|
||||
<th className="border-b-2 border-black p-2 w-38">Aktion</th>
|
||||
<th className="border-b-2 border-black p-2 w-48">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -79,7 +79,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
onClick={() => onEdit(entry)}
|
||||
className="text-blue-600 hover:text-blue-800 px-3 py-1 rounded text-sm mr-2"
|
||||
>
|
||||
Bearbeiten
|
||||
Editieren
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmId(entry.ID)}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function MonatsStatistik({ typ, refreshKey }: MonatsStatistikProp
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/ausgaben/stats?year=${y}&month=${m}&typ=${typ}`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
if (data.success) setStats(data.data);
|
||||
} catch (error) {
|
||||
|
||||
82
components/TabLayout.tsx
Normal file
82
components/TabLayout.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import LogoutButton from '@/components/LogoutButton';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
interface Tab {
|
||||
label: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface TabLayoutProps {
|
||||
children: ReactNode;
|
||||
activeTab: number;
|
||||
onTabChange: (index: number) => void;
|
||||
}
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{ label: 'Haushalt', index: 0 },
|
||||
{ label: 'Privat', index: 1 },
|
||||
];
|
||||
|
||||
export default function TabLayout({ children, activeTab, onTabChange }: TabLayoutProps) {
|
||||
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 (
|
||||
<div className="min-h-screen py-8 px-4">
|
||||
{/* Outer wrapper with border */}
|
||||
<div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-6">
|
||||
|
||||
{/* Page title */}
|
||||
<h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Ausgaben - Log</h1>
|
||||
|
||||
{/* Inner content */}
|
||||
<div className="max-w-6xl mx-auto">
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex">
|
||||
{TABS.map(tab => {
|
||||
const isActive = activeTab === tab.index;
|
||||
return (
|
||||
<button
|
||||
key={tab.index}
|
||||
onClick={() => onTabChange(tab.index)}
|
||||
className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1 transition-colors"
|
||||
style={
|
||||
isActive
|
||||
? { backgroundColor: '#FFFFDD', color: '#000000', borderColor: '#000000', borderBottom: '2px solid #FFFFDD', marginBottom: '-2px', position: 'relative', zIndex: 10 }
|
||||
: { backgroundColor: '#85B7D7', color: '#374151', borderColor: '#000000' }
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="pb-1">
|
||||
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg shadow-md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content panel */}
|
||||
<main className="border-2 border-black rounded-b-lg rounded-tr-lg p-6 bg-[#FFFFDD]">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
|
||||
<a href="mailto:rxf@gmx.de" className="hover:underline">
|
||||
mailto:rxf@gmx.de
|
||||
</a>
|
||||
<div>Version {version} - {buildDate}</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
deploy.sh
37
deploy.sh
@@ -15,34 +15,39 @@ FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
BUILD_DATE=$(date +%d.%m.%Y)
|
||||
|
||||
echo "=========================================="
|
||||
echo "Ausgaben-Next Deploy Script"
|
||||
echo "ausgaben-next Deploy Script"
|
||||
echo "=========================================="
|
||||
echo "Registry: ${REGISTRY}"
|
||||
echo "Image: ${IMAGE_NAME}"
|
||||
echo "Tag: ${TAG}"
|
||||
echo "Build-Datum: ${BUILD_DATE}"
|
||||
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 1. Docker Image bauen
|
||||
echo ">>> Baue Docker Image..."
|
||||
docker build \
|
||||
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
||||
-t "${IMAGE_NAME}:${TAG}" \
|
||||
-t "${FULL_IMAGE}" \
|
||||
.
|
||||
|
||||
echo ">>> Build erfolgreich!"
|
||||
echo ""
|
||||
|
||||
# 2. Login zur Registry (falls noch nicht eingeloggt)
|
||||
# 1. Login zur Registry (falls noch nicht eingeloggt)
|
||||
echo ">>> Login zu ${REGISTRY}..."
|
||||
docker login "${REGISTRY}"
|
||||
echo ""
|
||||
|
||||
# 3. Image pushen
|
||||
echo ">>> Pushe Image zu ${REGISTRY}..."
|
||||
docker push "${FULL_IMAGE}"
|
||||
# 2. Multiplatform Builder einrichten (docker-container driver erforderlich)
|
||||
echo ">>> Richte Multiplatform Builder ein..."
|
||||
if ! docker buildx inspect multiplatform-builder &>/dev/null; then
|
||||
docker buildx create --name multiplatform-builder --driver docker-container --bootstrap
|
||||
fi
|
||||
docker buildx use multiplatform-builder
|
||||
echo ""
|
||||
|
||||
# 3. Docker Image bauen und pushen (Multiplatform)
|
||||
echo ">>> Baue Multiplatform Docker Image und pushe zu Registry..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
||||
-t "${FULL_IMAGE}" \
|
||||
--push \
|
||||
.
|
||||
|
||||
echo ">>> Build und Push erfolgreich!"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
22
lib/auth.ts
22
lib/auth.ts
@@ -1,20 +1,10 @@
|
||||
/**
|
||||
* Reusable authentication library
|
||||
* Configure users via environment variables in .env:
|
||||
* AUTH_USERS=user1:$2a$10$hash1,user2:$2a$10$hash2
|
||||
*
|
||||
* Use scripts/generate-password.js to generate password hashes
|
||||
*/
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse users from environment variable
|
||||
* Format: username:password,username2:password2
|
||||
*/
|
||||
export function getUsers(): User[] {
|
||||
const usersString = process.env.AUTH_USERS || '';
|
||||
if (!usersString) {
|
||||
@@ -30,21 +20,15 @@ export function getUsers(): User[] {
|
||||
.filter((user) => user.username && user.password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify user credentials
|
||||
*/
|
||||
export function verifyCredentials(username: string, password: string): boolean {
|
||||
export async function verifyCredentials(username: string, password: string): Promise<boolean> {
|
||||
const users = getUsers();
|
||||
const user = users.find(u => u.username === username);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return user.password === password;
|
||||
return bcrypt.compare(password, user.password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if authentication is enabled
|
||||
*/
|
||||
export function isAuthEnabled(): boolean {
|
||||
return !!process.env.AUTH_USERS;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function getDbPool() {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
password: process.env.DB_PASS || process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'RXF',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
|
||||
@@ -2,6 +2,20 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
||||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ausgaben_next",
|
||||
"version": "1.0.1",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ausgaben_next",
|
||||
"version": "1.0.1",
|
||||
"version": "1.2.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"jose": "^6.1.3",
|
||||
@@ -71,6 +71,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1561,6 +1562,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz",
|
||||
"integrity": "sha512-by3/Z0Qp+L9cAySEsSNNwZ6WWw8ywgGLPQGgbQDhNRSitqYgkgp4pErd23ZSCavbtUA2CN4jQtoB3T8nk4j3Rg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -1571,6 +1573,7 @@
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -1630,6 +1633,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -2155,6 +2159,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2516,6 +2521,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3092,6 +3098,7 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3277,6 +3284,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -5569,6 +5577,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5578,6 +5587,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -6287,6 +6297,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6449,6 +6460,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6723,6 +6735,7 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ausgaben_next",
|
||||
"version": "1.1.0",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3005",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { jwtVerify } from 'jose';
|
||||
const SESSION_COOKIE_NAME = 'auth_session';
|
||||
|
||||
/**
|
||||
* Middleware to protect routes with authentication
|
||||
* Proxy to protect routes with authentication
|
||||
* Reusable for other projects - just copy this file
|
||||
*/
|
||||
export async function middleware(request: NextRequest) {
|
||||
export async function proxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Check if authentication is enabled
|
||||
@@ -58,6 +58,8 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
export default proxy;
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
Reference in New Issue
Block a user