fixed minor issues,
This commit is contained in:
parent
b53c186342
commit
0886757f89
4
data/editable_text.json
Normal file → Executable file
4
data/editable_text.json
Normal file → Executable file
@ -27,8 +27,8 @@
|
|||||||
"label": "Ich enthalte mich"
|
"label": "Ich enthalte mich"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nichts",
|
"id": "123",
|
||||||
"label": "Wild"
|
"label": "123"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
0
data/jwt_keys.json
Normal file → Executable file
0
data/jwt_keys.json
Normal file → Executable file
1366
data/member_credentials.json
Normal file → Executable file
1366
data/member_credentials.json
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
0
data/responses.json
Normal file → Executable file
0
data/responses.json
Normal file → Executable file
0
data/settings.json
Normal file → Executable file
0
data/settings.json
Normal file → Executable file
0
data/used_tokens.json
Normal file → Executable file
0
data/used_tokens.json
Normal file → Executable file
@ -24,7 +24,7 @@ export default function PublicResultsPage() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.texts && Array.isArray(data.texts)) {
|
if (data.texts && Array.isArray(data.texts)) {
|
||||||
const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
|
const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
|
||||||
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
||||||
setVoteOptions(voteOptionsEntry.content);
|
setVoteOptions(voteOptionsEntry.content);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ export default function AdminPage() {
|
|||||||
setMemberAuthEnabled(data.settings.memberAuthEnabled);
|
setMemberAuthEnabled(data.settings.memberAuthEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Silently fail, user will need to enter password
|
// Silently fail, user will need to enter password
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -190,7 +190,7 @@ export default function AdminPage() {
|
|||||||
setEditableTexts(data.texts);
|
setEditableTexts(data.texts);
|
||||||
|
|
||||||
// Find and set vote options
|
// Find and set vote options
|
||||||
const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
|
const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
|
||||||
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
||||||
setVoteOptions(voteOptionsEntry.content);
|
setVoteOptions(voteOptionsEntry.content);
|
||||||
}
|
}
|
||||||
@ -336,45 +336,6 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update a vote option
|
|
||||||
const handleUpdateVoteOption = async (optionId: string, newLabel: string) => {
|
|
||||||
if (!newLabel.trim()) {
|
|
||||||
setError('Das Label darf nicht leer sein');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update in local state
|
|
||||||
const updatedOptions = voteOptions.map(option =>
|
|
||||||
option.id === optionId ? { ...option, label: newLabel } : option
|
|
||||||
);
|
|
||||||
setVoteOptions(updatedOptions);
|
|
||||||
|
|
||||||
// Save to server
|
|
||||||
const response = await fetch('/api/editable-text', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: 'vote-options',
|
|
||||||
content: updatedOptions
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
if (generatedLink) {
|
if (generatedLink) {
|
||||||
navigator.clipboard.writeText(generatedLink);
|
navigator.clipboard.writeText(generatedLink);
|
||||||
@ -469,7 +430,7 @@ export default function AdminPage() {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Fehler beim Generieren der Tokens');
|
throw new Error(errorData.error || 'Fehler beim Generieren der Tokens');
|
||||||
} catch (jsonError) {
|
} catch {
|
||||||
// If not JSON, use status text
|
// If not JSON, use status text
|
||||||
throw new Error(`Fehler beim Generieren der Tokens: ${response.statusText}`);
|
throw new Error(`Fehler beim Generieren der Tokens: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@ -668,7 +629,7 @@ export default function AdminPage() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="newOptionId" className="block text-sm font-medium text-gray-700 mb-1">
|
<label htmlFor="newOptionId" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Option ID (z.B. "yes", "no")
|
Option ID (z.B. "yes", "no")
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -5,7 +5,7 @@ import path from 'path';
|
|||||||
|
|
||||||
// Path to the editable text file
|
// Path to the editable text file
|
||||||
const TEXT_FILE = path.join(process.cwd(), 'data', 'editable_text.json');
|
const TEXT_FILE = path.join(process.cwd(), 'data', 'editable_text.json');
|
||||||
|
export type EditableText = { id: string; content: string };
|
||||||
// Ensure the data directory exists
|
// Ensure the data directory exists
|
||||||
function ensureDataDirectory() {
|
function ensureDataDirectory() {
|
||||||
const dataDir = path.join(process.cwd(), 'data');
|
const dataDir = path.join(process.cwd(), 'data');
|
||||||
@ -61,7 +61,8 @@ function getEditableTexts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save editable texts
|
// Save editable texts
|
||||||
function saveEditableTexts(texts: any[]) {
|
|
||||||
|
function saveEditableTexts(texts: EditableText[]) {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
fs.writeFileSync(TEXT_FILE, JSON.stringify(texts, null, 2));
|
fs.writeFileSync(TEXT_FILE, JSON.stringify(texts, null, 2));
|
||||||
}
|
}
|
||||||
@ -107,7 +108,9 @@ export async function POST(request: NextRequest) {
|
|||||||
const texts = getEditableTexts();
|
const texts = getEditableTexts();
|
||||||
|
|
||||||
// Find and update the text with the given ID
|
// Find and update the text with the given ID
|
||||||
const textIndex = texts.findIndex((text: any) => text.id === id);
|
const textIndex = texts.findIndex((text: EditableText
|
||||||
|
|
||||||
|
) => text.id === id);
|
||||||
|
|
||||||
if (textIndex === -1) {
|
if (textIndex === -1) {
|
||||||
// Text not found, add new
|
// Text not found, add new
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { checkAdminAuth, generateRandomToken } from '@/lib/auth';
|
import { checkAdminAuth, generateRandomToken } from '@/lib/auth';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
// Get admin password from environment variable or use default for development
|
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'schafwaschener-segelverein-admin';
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
@ -2,7 +2,7 @@ import { checkAdminAuth } from '@/lib/auth';
|
|||||||
import { addMember, deleteMember, getMemberCredentials, updateMember } from '@/lib/server-auth';
|
import { addMember, deleteMember, getMemberCredentials, updateMember } from '@/lib/server-auth';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Check for admin auth
|
// Check for admin auth
|
||||||
const isAuthenticated = await checkAdminAuth();
|
const isAuthenticated = await checkAdminAuth();
|
||||||
@ -18,7 +18,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
|
|
||||||
// Remove password from response
|
// Remove password from response
|
||||||
const sanitizedMembers = members.map(({ password, ...rest }) => rest);
|
const sanitizedMembers = members.map((v) => ({ ...v, password: undefined }));
|
||||||
|
|
||||||
return NextResponse.json({ members: sanitizedMembers });
|
return NextResponse.json({ members: sanitizedMembers });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { getSettings } from '@/lib/server-auth';
|
import { getSettings } from '@/lib/server-auth';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Get settings
|
// Get settings
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
return NextResponse.json({ settings });
|
return NextResponse.json({ settings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting settings:', error);
|
console.error('Error getting settings:', error);
|
||||||
|
@ -4,10 +4,8 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
|
||||||
const { password } = body;
|
|
||||||
// Check for admin auth
|
// Check for admin auth
|
||||||
const isAuthenticated = await checkAdminAuth(password);
|
const isAuthenticated = await checkAdminAuth();
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
|
@ -7,13 +7,13 @@ import { EditableText } from '@/components/EditableText';
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [memberNumber, setMemberNumber] = useState('');
|
const [memberNumber, setMemberNumber] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isEnabled, setIsEnabled] = useState(true);
|
const [isEnabled, setIsEnabled] = useState(true);
|
||||||
|
|
||||||
// Check if member authentication is enabled
|
// Check if member authentication is enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkSettings = async () => {
|
const checkSettings = async () => {
|
||||||
@ -25,29 +25,29 @@ export default function LoginPage() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ memberNumber: '', password: '' }),
|
body: JSON.stringify({ memberNumber: '', password: '' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
setIsEnabled(false);
|
setIsEnabled(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Silently fail, assume enabled
|
// Silently fail, assume enabled
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkSettings();
|
checkSettings();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!memberNumber || !password) {
|
if (!memberNumber || !password) {
|
||||||
setError('Bitte geben Sie Ihre Mitgliedsnummer und Ihr Passwort ein');
|
setError('Bitte geben Sie Ihre Mitgliedsnummer und Ihr Passwort ein');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/member-login', {
|
const response = await fetch('/api/member-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -56,13 +56,13 @@ export default function LoginPage() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ memberNumber, password }),
|
body: JSON.stringify({ memberNumber, password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || 'Anmeldung fehlgeschlagen');
|
throw new Error(data.error || 'Anmeldung fehlgeschlagen');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to voting page with token
|
// Redirect to voting page with token
|
||||||
router.push(data.voteUrl);
|
router.push(data.voteUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -71,19 +71,19 @@ export default function LoginPage() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
|
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
|
||||||
|
|
||||||
<div className="ssvc-main-content">
|
<div className="ssvc-main-content">
|
||||||
<div className="text-center p-6">
|
<div className="text-center p-6">
|
||||||
<div className="mb-4 text-red-600">
|
<div className="mb-4 text-red-600">
|
||||||
Die Mitgliederanmeldung ist derzeit deaktiviert.
|
Die Mitgliederanmeldung ist derzeit deaktiviert.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href="/" className="ssvc-button inline-block">
|
<Link href="/" className="ssvc-button inline-block">
|
||||||
Zurück zur Startseite
|
Zurück zur Startseite
|
||||||
</Link>
|
</Link>
|
||||||
@ -93,17 +93,17 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
|
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
|
||||||
|
|
||||||
<div className="ssvc-main-content">
|
<div className="ssvc-main-content">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<EditableText id="current-vote-text" />
|
<EditableText id="current-vote-text" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="memberNumber" className="block text-sm font-medium text-gray-700 mb-1">
|
<label htmlFor="memberNumber" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
@ -118,7 +118,7 @@ export default function LoginPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Passwort
|
Passwort
|
||||||
@ -132,11 +132,11 @@ export default function LoginPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-red-500 text-sm">{error}</div>
|
<div className="text-red-500 text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -145,7 +145,7 @@ export default function LoginPage() {
|
|||||||
{isLoading ? 'Anmelden...' : 'Anmelden'}
|
{isLoading ? 'Anmelden...' : 'Anmelden'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<Link href="/" className="text-[#0057a6] hover:underline">
|
<Link href="/" className="text-[#0057a6] hover:underline">
|
||||||
Zurück zur Startseite
|
Zurück zur Startseite
|
||||||
|
@ -1,23 +1,35 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, Suspense } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { EditableText } from '@/components/EditableText';
|
import { EditableText } from '@/components/EditableText';
|
||||||
import { VoteOption, VoteOptionConfig } from '@/lib/survey';
|
import { VoteOption, VoteOptionConfig } from '@/lib/survey';
|
||||||
|
|
||||||
|
|
||||||
export default function VotePage() {
|
export default function VotePage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
|
<VotePageContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingComponent() {
|
||||||
|
return <div>Lädt...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VotePageContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
const [voteOptions, setVoteOptions] = useState<VoteOptionConfig[]>([]);
|
const [voteOptions, setVoteOptions] = useState<VoteOptionConfig[]>([]);
|
||||||
const [selectedOption, setSelectedOption] = useState<VoteOption | null>(null);
|
const [selectedOption, setSelectedOption] = useState<VoteOption | null>(null);
|
||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
// Fetch vote options
|
// Fetch vote options
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVoteOptions = async () => {
|
const fetchVoteOptions = async () => {
|
||||||
@ -26,7 +38,7 @@ export default function VotePage() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.texts && Array.isArray(data.texts)) {
|
if (data.texts && Array.isArray(data.texts)) {
|
||||||
const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
|
const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
|
||||||
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
||||||
setVoteOptions(voteOptionsEntry.content);
|
setVoteOptions(voteOptionsEntry.content);
|
||||||
}
|
}
|
||||||
@ -42,28 +54,28 @@ export default function VotePage() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchVoteOptions();
|
fetchVoteOptions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Redirect if no token is provided
|
// Redirect if no token is provided
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
}, [token, router]);
|
}, [token, router]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
setError('Bitte wählen Sie eine Option');
|
setError('Bitte wählen Sie eine Option');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/submit-vote', {
|
const response = await fetch('/api/submit-vote', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -76,13 +88,13 @@ export default function VotePage() {
|
|||||||
comment: comment.trim() || undefined,
|
comment: comment.trim() || undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || 'Fehler beim Übermitteln der Stimme');
|
throw new Error(data.error || 'Fehler beim Übermitteln der Stimme');
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
|
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
|
||||||
@ -90,7 +102,7 @@ export default function VotePage() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSubmitted) {
|
if (isSubmitted) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
@ -106,8 +118,8 @@ export default function VotePage() {
|
|||||||
Ihre Stimme wurde erfolgreich übermittelt.
|
Ihre Stimme wurde erfolgreich übermittelt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="ssvc-button inline-block"
|
className="ssvc-button inline-block"
|
||||||
>
|
>
|
||||||
@ -117,22 +129,22 @@ export default function VotePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">SATZUNGSÄNDERUNG - ABSTIMMUNG</h1>
|
<h1 className="text-2xl font-bold text-[#0057a6] mb-4">SATZUNGSÄNDERUNG - ABSTIMMUNG</h1>
|
||||||
|
|
||||||
<div className="ssvc-main-content">
|
<div className="ssvc-main-content">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-[#0057a6] mb-4">
|
<div className="text-xl font-bold text-[#0057a6] mb-4">
|
||||||
<EditableText id="vote-question" />
|
<EditableText id="vote-question" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{voteOptions.map((option) => (
|
{voteOptions.map((option) => (
|
||||||
<label
|
<label
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="flex items-center p-3 border border-gray-200 cursor-pointer hover:bg-[#e6f0fa] transition-colors"
|
className="flex items-center p-3 border border-gray-200 cursor-pointer hover:bg-[#e6f0fa] transition-colors"
|
||||||
>
|
>
|
||||||
@ -149,7 +161,7 @@ export default function VotePage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="comment" className="block text-sm font-medium text-gray-700 mb-1">
|
<label htmlFor="comment" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Kommentare oder Vorschläge (Optional)
|
Kommentare oder Vorschläge (Optional)
|
||||||
@ -169,11 +181,11 @@ export default function VotePage() {
|
|||||||
Bitte geben Sie keine persönlichen Daten in Ihren Kommentaren an.
|
Bitte geben Sie keine persönlichen Daten in Ihren Kommentaren an.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-red-500 text-sm">{error}</div>
|
<div className="text-red-500 text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, forwardRef } from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
// Import ReactQuill dynamically to avoid SSR issues
|
// Import ReactQuill dynamically to avoid SSR issues
|
||||||
const ReactQuill = dynamic(() => import('react-quill-new'), {
|
const ReactQuill = dynamic(() => import('react-quill-new'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div className="border border-gray-300 h-64 flex items-center justify-center">Loading editor...</div>
|
loading: () => <div className="border border-gray-300 h-64 flex items-center justify-center">Loading editor...</div>
|
||||||
});
|
});
|
||||||
@ -22,7 +22,7 @@ interface QuillEditorProps {
|
|||||||
const QuillEditor = ({ value, onChange, placeholder }: QuillEditorProps) => {
|
const QuillEditor = ({ value, onChange, placeholder }: QuillEditorProps) => {
|
||||||
// Use state to track if component is mounted (client-side)
|
// Use state to track if component is mounted (client-side)
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
// Only render the editor on the client
|
// Only render the editor on the client
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@ -33,7 +33,7 @@ const QuillEditor = ({ value, onChange, placeholder }: QuillEditorProps) => {
|
|||||||
toolbar: [
|
toolbar: [
|
||||||
[{ 'header': [1, 2, 3, false] }],
|
[{ 'header': [1, 2, 3, false] }],
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||||
['link'],
|
['link'],
|
||||||
['clean']
|
['clean']
|
||||||
]
|
]
|
||||||
|
@ -18,8 +18,8 @@ export interface MemberCredential {
|
|||||||
export async function generateRandomToken(memberNumber?: string): Promise<string> {
|
export async function generateRandomToken(memberNumber?: string): Promise<string> {
|
||||||
// This function is used in API routes
|
// This function is used in API routes
|
||||||
const tokenId = uuidv4();
|
const tokenId = uuidv4();
|
||||||
|
type Payload = { tokenId: string; memberNumber?: string };
|
||||||
const payload: any = { tokenId };
|
const payload: Payload = { tokenId };
|
||||||
|
|
||||||
// If memberNumber is provided, include it in the token
|
// If memberNumber is provided, include it in the token
|
||||||
if (memberNumber) {
|
if (memberNumber) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { SignJWT, jwtVerify } from 'jose';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { jwtVerify } from 'jose';
|
||||||
|
import path from 'path';
|
||||||
import { MemberCredential, comparePassword, hashPassword } from './auth';
|
import { MemberCredential, comparePassword, hashPassword } from './auth';
|
||||||
|
|
||||||
// Keys for JWT signing and verification
|
// Keys for JWT signing and verification
|
||||||
@ -32,11 +31,11 @@ function ensureDataDirectory() {
|
|||||||
// Get all used tokens from the blacklist file
|
// Get all used tokens from the blacklist file
|
||||||
export function getUsedTokens(): string[] {
|
export function getUsedTokens(): string[] {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
|
|
||||||
if (!fs.existsSync(BLACKLIST_FILE)) {
|
if (!fs.existsSync(BLACKLIST_FILE)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(BLACKLIST_FILE, 'utf-8');
|
const data = fs.readFileSync(BLACKLIST_FILE, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
}
|
||||||
@ -44,10 +43,10 @@ export function getUsedTokens(): string[] {
|
|||||||
// Add a token to the blacklist
|
// Add a token to the blacklist
|
||||||
export function addToBlacklist(tokenId: string): void {
|
export function addToBlacklist(tokenId: string): void {
|
||||||
const usedTokens = getUsedTokens();
|
const usedTokens = getUsedTokens();
|
||||||
|
|
||||||
if (!usedTokens.includes(tokenId)) {
|
if (!usedTokens.includes(tokenId)) {
|
||||||
usedTokens.push(tokenId);
|
usedTokens.push(tokenId);
|
||||||
|
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
fs.writeFileSync(BLACKLIST_FILE, JSON.stringify(usedTokens, null, 2));
|
fs.writeFileSync(BLACKLIST_FILE, JSON.stringify(usedTokens, null, 2));
|
||||||
}
|
}
|
||||||
@ -56,13 +55,13 @@ export function addToBlacklist(tokenId: string): void {
|
|||||||
// Get settings
|
// Get settings
|
||||||
export function getSettings(): { memberAuthEnabled: boolean } {
|
export function getSettings(): { memberAuthEnabled: boolean } {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
|
|
||||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||||
const defaultSettings = { memberAuthEnabled: false };
|
const defaultSettings = { memberAuthEnabled: false };
|
||||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(defaultSettings, null, 2));
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(defaultSettings, null, 2));
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
}
|
||||||
@ -76,12 +75,12 @@ export function updateSettings(settings: { memberAuthEnabled: boolean }): void {
|
|||||||
// Get all member credentials
|
// Get all member credentials
|
||||||
export function getMemberCredentials(): MemberCredential[] {
|
export function getMemberCredentials(): MemberCredential[] {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
|
|
||||||
if (!fs.existsSync(MEMBER_CREDENTIALS_FILE)) {
|
if (!fs.existsSync(MEMBER_CREDENTIALS_FILE)) {
|
||||||
fs.writeFileSync(MEMBER_CREDENTIALS_FILE, JSON.stringify([], null, 2));
|
fs.writeFileSync(MEMBER_CREDENTIALS_FILE, JSON.stringify([], null, 2));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(MEMBER_CREDENTIALS_FILE, 'utf-8');
|
const data = fs.readFileSync(MEMBER_CREDENTIALS_FILE, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
}
|
||||||
@ -95,22 +94,22 @@ export function saveMemberCredentials(credentials: MemberCredential[]): void {
|
|||||||
// Add a new member
|
// Add a new member
|
||||||
export function addMember(memberNumber: string, password: string): boolean {
|
export function addMember(memberNumber: string, password: string): boolean {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
|
|
||||||
// Check if member already exists
|
// Check if member already exists
|
||||||
if (members.some(m => m.memberNumber === memberNumber)) {
|
if (members.some(m => m.memberNumber === memberNumber)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the password
|
// Hash the password
|
||||||
const hashedPassword = hashPassword(password);
|
const hashedPassword = hashPassword(password);
|
||||||
|
|
||||||
// Add the new member
|
// Add the new member
|
||||||
members.push({
|
members.push({
|
||||||
memberNumber,
|
memberNumber,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
hasVoted: false
|
hasVoted: false
|
||||||
});
|
});
|
||||||
|
|
||||||
saveMemberCredentials(members);
|
saveMemberCredentials(members);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -119,19 +118,19 @@ export function addMember(memberNumber: string, password: string): boolean {
|
|||||||
export function updateMember(memberNumber: string, data: { password?: string, hasVoted?: boolean }): boolean {
|
export function updateMember(memberNumber: string, data: { password?: string, hasVoted?: boolean }): boolean {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
const memberIndex = members.findIndex(m => m.memberNumber === memberNumber);
|
const memberIndex = members.findIndex(m => m.memberNumber === memberNumber);
|
||||||
|
|
||||||
if (memberIndex === -1) {
|
if (memberIndex === -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
members[memberIndex].password = hashPassword(data.password);
|
members[memberIndex].password = hashPassword(data.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.hasVoted !== undefined) {
|
if (data.hasVoted !== undefined) {
|
||||||
members[memberIndex].hasVoted = data.hasVoted;
|
members[memberIndex].hasVoted = data.hasVoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMemberCredentials(members);
|
saveMemberCredentials(members);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -140,13 +139,13 @@ export function updateMember(memberNumber: string, data: { password?: string, ha
|
|||||||
export function deleteMember(memberNumber: string): boolean {
|
export function deleteMember(memberNumber: string): boolean {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
const initialLength = members.length;
|
const initialLength = members.length;
|
||||||
|
|
||||||
const filteredMembers = members.filter(m => m.memberNumber !== memberNumber);
|
const filteredMembers = members.filter(m => m.memberNumber !== memberNumber);
|
||||||
|
|
||||||
if (filteredMembers.length === initialLength) {
|
if (filteredMembers.length === initialLength) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMemberCredentials(filteredMembers);
|
saveMemberCredentials(filteredMembers);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -155,50 +154,50 @@ export function deleteMember(memberNumber: string): boolean {
|
|||||||
export function importMembersFromCSV(csvContent: string): { added: number, skipped: number } {
|
export function importMembersFromCSV(csvContent: string): { added: number, skipped: number } {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
const existingMemberNumbers = new Set(members.map(m => m.memberNumber));
|
const existingMemberNumbers = new Set(members.map(m => m.memberNumber));
|
||||||
|
|
||||||
let added = 0;
|
let added = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
|
||||||
// Parse CSV content
|
// Parse CSV content
|
||||||
const lines = csvContent.split('\n');
|
const lines = csvContent.split('\n');
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i].trim();
|
const line = lines[i].trim();
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
|
|
||||||
// Skip header row if present
|
// Skip header row if present
|
||||||
if (i === 0 && (line.toLowerCase().includes('member') || line.toLowerCase().includes('password'))) {
|
if (i === 0 && (line.toLowerCase().includes('member') || line.toLowerCase().includes('password'))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split by comma, semicolon, or tab
|
// Split by comma, semicolon, or tab
|
||||||
const parts = line.split(/[,;\t]/);
|
const parts = line.split(/[,;\t]/);
|
||||||
|
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
const memberNumber = parts[0].trim();
|
const memberNumber = parts[0].trim();
|
||||||
const password = parts[1].trim();
|
const password = parts[1].trim();
|
||||||
|
|
||||||
// Skip if member number already exists
|
// Skip if member number already exists
|
||||||
if (existingMemberNumbers.has(memberNumber)) {
|
if (existingMemberNumbers.has(memberNumber)) {
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the password
|
// Hash the password
|
||||||
const hashedPassword = hashPassword(password);
|
const hashedPassword = hashPassword(password);
|
||||||
|
|
||||||
// Add the new member
|
// Add the new member
|
||||||
members.push({
|
members.push({
|
||||||
memberNumber,
|
memberNumber,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
hasVoted: false
|
hasVoted: false
|
||||||
});
|
});
|
||||||
|
|
||||||
existingMemberNumbers.add(memberNumber);
|
existingMemberNumbers.add(memberNumber);
|
||||||
added++;
|
added++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMemberCredentials(members);
|
saveMemberCredentials(members);
|
||||||
return { added, skipped };
|
return { added, skipped };
|
||||||
}
|
}
|
||||||
@ -207,19 +206,19 @@ export function importMembersFromCSV(csvContent: string): { added: number, skipp
|
|||||||
export function verifyMemberCredentials(memberNumber: string, password: string): { valid: boolean, hasVoted: boolean } {
|
export function verifyMemberCredentials(memberNumber: string, password: string): { valid: boolean, hasVoted: boolean } {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
const member = members.find(m => m.memberNumber === memberNumber);
|
const member = members.find(m => m.memberNumber === memberNumber);
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return { valid: false, hasVoted: false };
|
return { valid: false, hasVoted: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordValid = comparePassword(password, member.password);
|
const passwordValid = comparePassword(password, member.password);
|
||||||
|
|
||||||
if (passwordValid) {
|
if (passwordValid) {
|
||||||
// Update last login time
|
// Update last login time
|
||||||
member.lastLogin = new Date().toISOString();
|
member.lastLogin = new Date().toISOString();
|
||||||
saveMemberCredentials(members);
|
saveMemberCredentials(members);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: passwordValid, hasVoted: member.hasVoted };
|
return { valid: passwordValid, hasVoted: member.hasVoted };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,11 +230,11 @@ export function markMemberAsVoted(memberNumber: string): boolean {
|
|||||||
// Reset all member voting status
|
// Reset all member voting status
|
||||||
export function resetMemberVotingStatus(): void {
|
export function resetMemberVotingStatus(): void {
|
||||||
const members = getMemberCredentials();
|
const members = getMemberCredentials();
|
||||||
|
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
member.hasVoted = false;
|
member.hasVoted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMemberCredentials(members);
|
saveMemberCredentials(members);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +264,7 @@ export async function ensureKeys() {
|
|||||||
privateKey = encoder.encode(secretKey);
|
privateKey = encoder.encode(secretKey);
|
||||||
publicKey = encoder.encode(secretKey);
|
publicKey = encoder.encode(secretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save keys to file
|
// Save keys to file
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
fs.writeFileSync(KEYS_FILE, JSON.stringify({
|
fs.writeFileSync(KEYS_FILE, JSON.stringify({
|
||||||
@ -291,25 +290,25 @@ export async function verifyToken(token: string): Promise<{ valid: boolean, memb
|
|||||||
const secretKey = process.env.JWT_SECRET_KEY || 'schafwaschener-segelverein-secret-key-for-jwt-signing';
|
const secretKey = process.env.JWT_SECRET_KEY || 'schafwaschener-segelverein-secret-key-for-jwt-signing';
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const key = encoder.encode(secretKey);
|
const key = encoder.encode(secretKey);
|
||||||
|
|
||||||
const { payload } = await jwtVerify(token, key);
|
const { payload } = await jwtVerify(token, key);
|
||||||
const tokenId = payload.tokenId as string;
|
const tokenId = payload.tokenId as string;
|
||||||
const memberNumber = payload.memberNumber as string | undefined;
|
const memberNumber = payload.memberNumber as string | undefined;
|
||||||
|
|
||||||
// Check if token has been used before
|
// Check if token has been used before
|
||||||
const usedTokens = getUsedTokens();
|
const usedTokens = getUsedTokens();
|
||||||
if (usedTokens.includes(tokenId)) {
|
if (usedTokens.includes(tokenId)) {
|
||||||
return { valid: false };
|
return { valid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark token as used by adding to blacklist
|
// Mark token as used by adding to blacklist
|
||||||
addToBlacklist(tokenId);
|
addToBlacklist(tokenId);
|
||||||
|
|
||||||
// If token contains a member number, mark that member as voted
|
// If token contains a member number, mark that member as voted
|
||||||
if (memberNumber) {
|
if (memberNumber) {
|
||||||
markMemberAsVoted(memberNumber);
|
markMemberAsVoted(memberNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, memberNumber };
|
return { valid: true, memberNumber };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Token verification failed:', error);
|
console.error('Token verification failed:', error);
|
||||||
|
@ -33,7 +33,7 @@ function ensureDataDirectory() {
|
|||||||
// Get vote options from editable_text.json
|
// Get vote options from editable_text.json
|
||||||
export function getVoteOptions(): VoteOptionConfig[] {
|
export function getVoteOptions(): VoteOptionConfig[] {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
|
|
||||||
if (!fs.existsSync(TEXT_FILE)) {
|
if (!fs.existsSync(TEXT_FILE)) {
|
||||||
// Default options if file doesn't exist
|
// Default options if file doesn't exist
|
||||||
return [
|
return [
|
||||||
@ -42,17 +42,17 @@ export function getVoteOptions(): VoteOptionConfig[] {
|
|||||||
{ id: 'abstain', label: 'Ich enthalte mich' }
|
{ id: 'abstain', label: 'Ich enthalte mich' }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(TEXT_FILE, 'utf-8');
|
const data = fs.readFileSync(TEXT_FILE, 'utf-8');
|
||||||
const texts = JSON.parse(data);
|
const texts = JSON.parse(data);
|
||||||
|
|
||||||
const voteOptionsEntry = texts.find((text: any) => text.id === 'vote-options');
|
const voteOptionsEntry = texts.find((text: { id: string }) => text.id === 'vote-options');
|
||||||
|
|
||||||
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
|
||||||
return voteOptionsEntry.content;
|
return voteOptionsEntry.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return default options if not found or invalid
|
// Return default options if not found or invalid
|
||||||
return [
|
return [
|
||||||
{ id: 'yes', label: 'Ja, ich stimme zu' },
|
{ id: 'yes', label: 'Ja, ich stimme zu' },
|
||||||
@ -73,11 +73,11 @@ export function getVoteOptions(): VoteOptionConfig[] {
|
|||||||
// Get all survey responses
|
// Get all survey responses
|
||||||
export function getAllResponses(): SurveyResponse[] {
|
export function getAllResponses(): SurveyResponse[] {
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
|
|
||||||
if (!fs.existsSync(DATA_FILE)) {
|
if (!fs.existsSync(DATA_FILE)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = fs.readFileSync(DATA_FILE, 'utf-8');
|
const data = fs.readFileSync(DATA_FILE, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ export function getAllResponses(): SurveyResponse[] {
|
|||||||
// Save a new survey response
|
// Save a new survey response
|
||||||
export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse {
|
export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse {
|
||||||
const responses = getAllResponses();
|
const responses = getAllResponses();
|
||||||
|
|
||||||
// Create a new response with a random ID (not associated with the voter)
|
// Create a new response with a random ID (not associated with the voter)
|
||||||
const newResponse: SurveyResponse = {
|
const newResponse: SurveyResponse = {
|
||||||
id: uuidv4(), // Random ID only for internal tracking
|
id: uuidv4(), // Random ID only for internal tracking
|
||||||
@ -93,12 +93,12 @@ export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse
|
|||||||
comment,
|
comment,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
responses.push(newResponse);
|
responses.push(newResponse);
|
||||||
|
|
||||||
ensureDataDirectory();
|
ensureDataDirectory();
|
||||||
fs.writeFileSync(DATA_FILE, JSON.stringify(responses, null, 2));
|
fs.writeFileSync(DATA_FILE, JSON.stringify(responses, null, 2));
|
||||||
|
|
||||||
return newResponse;
|
return newResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,23 +106,23 @@ export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse
|
|||||||
export function getSurveyStats() {
|
export function getSurveyStats() {
|
||||||
const responses = getAllResponses();
|
const responses = getAllResponses();
|
||||||
const voteOptions = getVoteOptions();
|
const voteOptions = getVoteOptions();
|
||||||
|
|
||||||
// Initialize stats object with total count
|
// Initialize stats object with total count
|
||||||
const stats: Record<string, number> = {
|
const stats: Record<string, number> = {
|
||||||
total: responses.length
|
total: responses.length
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize count for each vote option
|
// Initialize count for each vote option
|
||||||
voteOptions.forEach(option => {
|
voteOptions.forEach(option => {
|
||||||
stats[option.id] = 0;
|
stats[option.id] = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Count votes for each option
|
// Count votes for each option
|
||||||
responses.forEach(response => {
|
responses.forEach(response => {
|
||||||
if (stats[response.vote] !== undefined) {
|
if (stats[response.vote] !== undefined) {
|
||||||
stats[response.vote]++;
|
stats[response.vote]++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
// This middleware doesn't do anything special yet
|
|
||||||
// It's just a placeholder for future authentication middleware
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user