ssvc-poll/src/lib/server-auth.ts

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