321 lines
9.2 KiB
TypeScript
321 lines
9.2 KiB
TypeScript
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import { jwtVerify } from 'jose';
|
|
import path from 'path';
|
|
import { MemberCredential, comparePassword, hashPassword } from './auth';
|
|
|
|
// Keys for JWT signing and verification
|
|
let privateKey: Uint8Array | null = null;
|
|
let publicKey: Uint8Array | null = null;
|
|
|
|
// Path to store the keypair
|
|
const KEYS_FILE = path.join(process.cwd(), 'data', 'jwt_keys.json');
|
|
|
|
// Path to the blacklist file
|
|
const BLACKLIST_FILE = path.join(process.cwd(), 'data', 'used_tokens.json');
|
|
|
|
// Path to the member credentials file
|
|
const MEMBER_CREDENTIALS_FILE = path.join(process.cwd(), 'data', 'member_credentials.json');
|
|
|
|
// Path to the settings file
|
|
const SETTINGS_FILE = path.join(process.cwd(), 'data', 'settings.json');
|
|
|
|
// Ensure the data directory exists
|
|
function ensureDataDirectory() {
|
|
const dataDir = path.join(process.cwd(), 'data');
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Get all used tokens from the blacklist file
|
|
export function getUsedTokens(): string[] {
|
|
ensureDataDirectory();
|
|
|
|
if (!fs.existsSync(BLACKLIST_FILE)) {
|
|
return [];
|
|
}
|
|
|
|
const data = fs.readFileSync(BLACKLIST_FILE, 'utf-8');
|
|
return JSON.parse(data);
|
|
}
|
|
|
|
// Add a token to the blacklist
|
|
export function addToBlacklist(tokenId: string): void {
|
|
const usedTokens = getUsedTokens();
|
|
|
|
if (!usedTokens.includes(tokenId)) {
|
|
usedTokens.push(tokenId);
|
|
|
|
ensureDataDirectory();
|
|
fs.writeFileSync(BLACKLIST_FILE, JSON.stringify(usedTokens, null, 2));
|
|
}
|
|
}
|
|
|
|
// Get settings
|
|
export function getSettings(): { memberAuthEnabled: boolean } {
|
|
ensureDataDirectory();
|
|
|
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
const defaultSettings = { memberAuthEnabled: false };
|
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(defaultSettings, null, 2));
|
|
return defaultSettings;
|
|
}
|
|
|
|
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
|
return JSON.parse(data);
|
|
}
|
|
|
|
// Update settings
|
|
export function updateSettings(settings: { memberAuthEnabled: boolean }): void {
|
|
ensureDataDirectory();
|
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
}
|
|
|
|
// Get all member credentials
|
|
export function getMemberCredentials(): MemberCredential[] {
|
|
ensureDataDirectory();
|
|
|
|
if (!fs.existsSync(MEMBER_CREDENTIALS_FILE)) {
|
|
fs.writeFileSync(MEMBER_CREDENTIALS_FILE, JSON.stringify([], null, 2));
|
|
return [];
|
|
}
|
|
|
|
const data = fs.readFileSync(MEMBER_CREDENTIALS_FILE, 'utf-8');
|
|
return JSON.parse(data);
|
|
}
|
|
|
|
// Save member credentials
|
|
export function saveMemberCredentials(credentials: MemberCredential[]): void {
|
|
ensureDataDirectory();
|
|
fs.writeFileSync(MEMBER_CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
|
|
}
|
|
|
|
// Add a new member
|
|
export function addMember(memberNumber: string, password: string): boolean {
|
|
const members = getMemberCredentials();
|
|
|
|
// Check if member already exists
|
|
if (members.some(m => m.memberNumber === memberNumber)) {
|
|
return false;
|
|
}
|
|
|
|
// Hash the password
|
|
const hashedPassword = hashPassword(password);
|
|
|
|
// Add the new member
|
|
members.push({
|
|
memberNumber,
|
|
password: hashedPassword,
|
|
hasVoted: false
|
|
});
|
|
|
|
saveMemberCredentials(members);
|
|
return true;
|
|
}
|
|
|
|
// Update a member
|
|
export function updateMember(memberNumber: string, data: { password?: string, hasVoted?: boolean }): boolean {
|
|
const members = getMemberCredentials();
|
|
const memberIndex = members.findIndex(m => m.memberNumber === memberNumber);
|
|
|
|
if (memberIndex === -1) {
|
|
return false;
|
|
}
|
|
|
|
if (data.password) {
|
|
members[memberIndex].password = hashPassword(data.password);
|
|
}
|
|
|
|
if (data.hasVoted !== undefined) {
|
|
members[memberIndex].hasVoted = data.hasVoted;
|
|
}
|
|
|
|
saveMemberCredentials(members);
|
|
return true;
|
|
}
|
|
|
|
// Delete a member
|
|
export function deleteMember(memberNumber: string): boolean {
|
|
const members = getMemberCredentials();
|
|
const initialLength = members.length;
|
|
|
|
const filteredMembers = members.filter(m => m.memberNumber !== memberNumber);
|
|
|
|
if (filteredMembers.length === initialLength) {
|
|
return false;
|
|
}
|
|
|
|
saveMemberCredentials(filteredMembers);
|
|
return true;
|
|
}
|
|
|
|
// Import members from CSV content
|
|
export function importMembersFromCSV(csvContent: string): { added: number, skipped: number } {
|
|
const members = getMemberCredentials();
|
|
const existingMemberNumbers = new Set(members.map(m => m.memberNumber));
|
|
|
|
let added = 0;
|
|
let skipped = 0;
|
|
|
|
// Parse CSV content
|
|
const lines = csvContent.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (!line) continue;
|
|
|
|
// Skip header row if present
|
|
if (i === 0 && (line.toLowerCase().includes('member') || line.toLowerCase().includes('password'))) {
|
|
continue;
|
|
}
|
|
|
|
// Split by comma, semicolon, or tab
|
|
const parts = line.split(/[,;\t]/);
|
|
|
|
if (parts.length >= 2) {
|
|
const memberNumber = parts[0].trim();
|
|
const password = parts[1].trim();
|
|
|
|
// Skip if member number already exists
|
|
if (existingMemberNumbers.has(memberNumber)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Hash the password
|
|
const hashedPassword = hashPassword(password);
|
|
|
|
// Add the new member
|
|
members.push({
|
|
memberNumber,
|
|
password: hashedPassword,
|
|
hasVoted: false
|
|
});
|
|
|
|
existingMemberNumbers.add(memberNumber);
|
|
added++;
|
|
}
|
|
}
|
|
|
|
saveMemberCredentials(members);
|
|
return { added, skipped };
|
|
}
|
|
|
|
// Verify member credentials
|
|
export function verifyMemberCredentials(memberNumber: string, password: string): { valid: boolean, hasVoted: boolean } {
|
|
const members = getMemberCredentials();
|
|
const member = members.find(m => m.memberNumber === memberNumber);
|
|
|
|
if (!member) {
|
|
return { valid: false, hasVoted: false };
|
|
}
|
|
|
|
const passwordValid = comparePassword(password, member.password);
|
|
|
|
if (passwordValid) {
|
|
// Update last login time
|
|
member.lastLogin = new Date().toISOString();
|
|
saveMemberCredentials(members);
|
|
}
|
|
|
|
return { valid: passwordValid, hasVoted: member.hasVoted };
|
|
}
|
|
|
|
// Mark member as voted
|
|
export function markMemberAsVoted(memberNumber: string): boolean {
|
|
return updateMember(memberNumber, { hasVoted: true });
|
|
}
|
|
|
|
// Reset all member voting status
|
|
export function resetMemberVotingStatus(): void {
|
|
const members = getMemberCredentials();
|
|
|
|
for (const member of members) {
|
|
member.hasVoted = false;
|
|
}
|
|
|
|
saveMemberCredentials(members);
|
|
}
|
|
|
|
// Function to generate or load keys
|
|
export async function ensureKeys() {
|
|
if (!privateKey || !publicKey) {
|
|
try {
|
|
// Check if keys exist in the data directory
|
|
if (fs.existsSync(KEYS_FILE)) {
|
|
// Load keys from file
|
|
const keysData = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf-8'));
|
|
const encoder = new TextEncoder();
|
|
privateKey = encoder.encode(keysData.privateKey);
|
|
publicKey = encoder.encode(keysData.publicKey);
|
|
} else {
|
|
// Check if secret key is provided in environment variable
|
|
if (process.env.JWT_SECRET_KEY) {
|
|
// Use the provided secret key
|
|
const encoder = new TextEncoder();
|
|
const secretKey = process.env.JWT_SECRET_KEY;
|
|
privateKey = encoder.encode(secretKey);
|
|
publicKey = encoder.encode(secretKey);
|
|
} else {
|
|
// Generate a random secret key
|
|
const secretKey = crypto.randomBytes(32).toString('hex');
|
|
const encoder = new TextEncoder();
|
|
privateKey = encoder.encode(secretKey);
|
|
publicKey = encoder.encode(secretKey);
|
|
}
|
|
|
|
// Save keys to file
|
|
ensureDataDirectory();
|
|
fs.writeFileSync(KEYS_FILE, JSON.stringify({
|
|
privateKey: new TextDecoder().decode(privateKey),
|
|
publicKey: new TextDecoder().decode(publicKey)
|
|
}, null, 2));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling JWT keys:', error);
|
|
// Fallback to a default key in case of error
|
|
const encoder = new TextEncoder();
|
|
const secretKey = 'schafwaschener-segelverein-secret-key-for-jwt-signing';
|
|
privateKey = encoder.encode(secretKey);
|
|
publicKey = encoder.encode(secretKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify a token and mark it as used
|
|
export async function verifyToken(token: string): Promise<{ valid: boolean, memberNumber?: string }> {
|
|
try {
|
|
// Use a secret key from environment variable or a default one
|
|
const secretKey = process.env.JWT_SECRET_KEY || 'schafwaschener-segelverein-secret-key-for-jwt-signing';
|
|
const encoder = new TextEncoder();
|
|
const key = encoder.encode(secretKey);
|
|
|
|
const { payload } = await jwtVerify(token, key);
|
|
const tokenId = payload.tokenId as string;
|
|
const memberNumber = payload.memberNumber as string | undefined;
|
|
|
|
// Check if token has been used before
|
|
const usedTokens = getUsedTokens();
|
|
if (usedTokens.includes(tokenId)) {
|
|
return { valid: false };
|
|
}
|
|
|
|
// Mark token as used by adding to blacklist
|
|
addToBlacklist(tokenId);
|
|
|
|
// If token contains a member number, mark that member as voted
|
|
if (memberNumber) {
|
|
markMemberAsVoted(memberNumber);
|
|
}
|
|
|
|
return { valid: true, memberNumber };
|
|
} catch (error) {
|
|
console.error('Token verification failed:', error);
|
|
return { valid: false };
|
|
}
|
|
}
|
|
|
|
// These functions are removed because they're causing issues
|
|
// We'll handle cookies directly in the API routes
|