This commit is contained in:
MLH
2025-04-07 01:13:36 +02:00
parent 3bf2aae1a3
commit cdd3e180a0
24 changed files with 5156 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
// src/controllers/authController.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const db = require('../db/db'); // Import database query function
const JWT_SECRET = process.env.JWT_SECRET;
const SALT_ROUNDS = 10; // Cost factor for bcrypt hashing
// Register a new user
exports.register = async (req, res, next) => {
const { username, password, role } = req.body; // Role might be optional or restricted
// Basic validation
if (!username || !password) {
return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' });
}
// TODO: Add more robust validation (e.g., password complexity, username format)
// Determine the role - default to 'spectator' if not provided or invalid
// In a real scenario, only admins should be able to assign 'admin' or 'referee' roles.
const validRoles = ['admin', 'referee', 'spectator'];
const assignedRole = validRoles.includes(role) ? role : 'spectator';
// Check if user already exists
try {
const existingUser = await db.query('SELECT * FROM users WHERE username = $1', [username]);
if (existingUser.rows.length > 0) {
return res.status(409).json({ message: 'Benutzername bereits vergeben.' }); // Conflict
}
// Hash the password
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// Insert the new user into the database
const newUser = await db.query(
'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3) RETURNING user_id, username, role, created_at',
[username, passwordHash, assignedRole]
);
res.status(201).json({
message: 'Benutzer erfolgreich registriert.',
user: newUser.rows[0],
});
} catch (error) {
// Pass error to the central error handler
next(error);
}
};
// Login a user
exports.login = async (req, res, next) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' });
}
try {
// Find the user by username
const result = await db.query('SELECT user_id, username, password_hash, role FROM users WHERE username = $1', [username]);
const user = result.rows[0];
if (!user) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten.' }); // Unauthorized
}
// Compare the provided password with the stored hash
const isMatch = await bcrypt.compare(password, user.password_hash);
if (!isMatch) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten.' }); // Unauthorized
}
// Passwords match, create JWT payload
const payload = {
userId: user.user_id,
username: user.username,
role: user.role,
};
// Sign the JWT
const token = jwt.sign(
payload,
JWT_SECRET,
{ expiresIn: '1h' } // Token expires in 1 hour (adjust as needed)
// Consider adding refresh tokens for better security/UX
);
// Send the token back to the client
res.json({
message: 'Login erfolgreich.',
token: token,
user: { // Send back some user info (excluding password hash)
userId: user.user_id,
username: user.username,
role: user.role
}
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,179 @@
// src/controllers/tournamentController.js
const db = require('../db/db');
// Get all tournaments
exports.getAllTournaments = async (req, res, next) => {
try {
// Add filtering or pagination later if needed
const result = await db.query('SELECT * FROM tournaments ORDER BY date DESC, created_at DESC');
res.json(result.rows);
} catch (error) {
next(error); // Pass error to the error handler
}
};
// Get a single tournament by ID
exports.getTournamentById = async (req, res, next) => {
const { id } = req.params;
try {
const result = await db.query('SELECT * FROM tournaments WHERE tournament_id = $1', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
}
res.json(result.rows[0]);
} catch (error) {
next(error);
}
};
// Create a new tournament (Admin only)
exports.createTournament = async (req, res, next) => {
// Destructure expected fields from request body
const {
name,
date,
location,
description,
logo_url, // Handle logo upload separately if needed
tournament_type,
game_type,
max_players,
status
} = req.body;
// Basic validation
if (!name || !tournament_type || !game_type) {
return res.status(400).json({ message: 'Name, Turnier-Typ und Spiel-Typ sind erforderlich.' });
}
// Validate enum types
const validTournamentTypes = ['knockout', 'group'];
const validGameTypes = ['11_points', '21_points'];
if (!validTournamentTypes.includes(tournament_type)) {
return res.status(400).json({ message: `Ungültiger Turnier-Typ: ${tournament_type}` });
}
if (!validGameTypes.includes(game_type)) {
return res.status(400).json({ message: `Ungültiger Spiel-Typ: ${game_type}` });
}
// Get user ID from the authenticated request (set by authMiddleware)
const created_by = req.user.userId;
try {
const result = await db.query(
`INSERT INTO tournaments (name, date, location, description, logo_url, tournament_type, game_type, max_players, status, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`, // Return the newly created tournament
[name, date || null, location, description, logo_url, tournament_type, game_type, max_players || null, status || 'planned', created_by]
);
res.status(201).json(result.rows[0]);
} catch (error) {
next(error);
}
};
// Update an existing tournament (Admin only)
exports.updateTournament = async (req, res, next) => {
const { id } = req.params;
// Destructure fields that can be updated
const {
name,
date,
location,
description,
logo_url,
tournament_type,
game_type,
max_players,
status
} = req.body;
// Build the update query dynamically based on provided fields
const fieldsToUpdate = [];
const values = [];
let queryIndex = 1;
// Helper function to add field and value if value is provided
const addUpdate = (field, value) => {
if (value !== undefined) {
// Validate enums if they are being updated
if (field === 'tournament_type' && !['knockout', 'group'].includes(value)) {
throw new Error(`Ungültiger Turnier-Typ: ${value}`);
}
if (field === 'game_type' && !['11_points', '21_points'].includes(value)) {
throw new Error(`Ungültiger Spiel-Typ: ${value}`);
}
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
// Handle empty strings potentially being passed for nullable fields
values.push(value === '' ? null : value);
}
};
try {
addUpdate('name', name);
addUpdate('date', date);
addUpdate('location', location);
addUpdate('description', description);
addUpdate('logo_url', logo_url);
addUpdate('tournament_type', tournament_type);
addUpdate('game_type', game_type);
addUpdate('max_players', max_players);
addUpdate('status', status);
if (fieldsToUpdate.length === 0) {
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
}
// Add the tournament ID for the WHERE clause
values.push(id);
const updateQuery = `UPDATE tournaments SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE tournament_id = $${queryIndex} RETURNING *`;
const result = await db.query(updateQuery, values);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
}
res.json(result.rows[0]);
} catch (error) {
// Handle potential validation errors from the helper
if (error.message.startsWith('Ungültiger')) {
return res.status(400).json({ message: error.message });
}
next(error); // Pass other errors to the handler
}
};
// Delete a tournament (Admin only)
exports.deleteTournament = async (req, res, next) => {
const { id } = req.params;
try {
// ON DELETE CASCADE in the schema should handle related data (matches, tournament_players)
const result = await db.query('DELETE FROM tournaments WHERE tournament_id = $1 RETURNING tournament_id', [id]);
if (result.rowCount === 0) {
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
}
res.status(204).send(); // No Content, successful deletion
} catch (error) {
next(error);
}
};
// --- Placeholder functions for other features ---
exports.importTournaments = async (req, res, next) => {
// TODO: Implement CSV import logic using multer and csv-parser
res.status(501).json({ message: 'Funktion noch nicht implementiert (Import).' });
};
exports.exportTournaments = async (req, res, next) => {
// TODO: Implement CSV export logic using fast-csv
res.status(501).json({ message: 'Funktion noch nicht implementiert (Export).' });
};
exports.addLogo = async (req, res, next) => {
// TODO: Implement file upload logic using multer
// Update the tournament's logo_url field
res.status(501).json({ message: 'Funktion noch nicht implementiert (Logo Upload).' });
};

View File

@@ -0,0 +1,174 @@
// src/controllers/userController.js
const db = require('../db/db');
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10; // Must match authController
// Function to create the initial admin user if none exist
exports.createInitialAdminUser = async () => {
try {
const result = await db.query('SELECT COUNT(*) FROM users');
const userCount = parseInt(result.rows[0].count, 10);
if (userCount === 0) {
console.log('No users found. Creating initial admin user...');
const adminUsername = 'admin';
const adminPassword = 'password'; // !!! CHANGE THIS IN PRODUCTION !!!
const adminRole = 'admin';
const passwordHash = await bcrypt.hash(adminPassword, SALT_ROUNDS);
await db.query(
'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3)',
[adminUsername, passwordHash, adminRole]
);
console.log(`Initial admin user created: username='${adminUsername}', password='${adminPassword}'.`);
console.warn("!!! IMPORTANT: Change the default admin password immediately! !!!");
} else {
console.log('Users already exist in the database. Skipping initial admin creation.');
}
} catch (error) {
console.error('Error creating initial admin user:', error);
// Decide if the application should exit or continue if this fails
}
};
// --- User Management API Functions (Example) ---
// Get all users (Admin only)
exports.getAllUsers = async (req, res, next) => {
try {
// Select relevant fields, exclude password_hash
const result = await db.query('SELECT user_id, username, role, created_at FROM users ORDER BY username');
res.json(result.rows);
} catch (error) {
next(error);
}
};
// Get a single user by ID (Admin only)
exports.getUserById = async (req, res, next) => {
const { id } = req.params;
try {
const result = await db.query('SELECT user_id, username, role, created_at FROM users WHERE user_id = $1', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Benutzer nicht gefunden.' });
}
res.json(result.rows[0]);
} catch (error) {
next(error);
}
};
// Create a new user (Admin only) - Similar to register but called by an admin
exports.createUser = async (req, res, next) => {
const { username, password, role } = req.body;
if (!username || !password || !role) {
return res.status(400).json({ message: 'Benutzername, Passwort und Rolle sind erforderlich.' });
}
const validRoles = ['admin', 'referee', 'spectator'];
if (!validRoles.includes(role)) {
return res.status(400).json({ message: 'Ungültige Rolle angegeben.' });
}
try {
// Check if username exists
const existingUser = await db.query('SELECT 1 FROM users WHERE username = $1', [username]);
if (existingUser.rows.length > 0) {
return res.status(409).json({ message: 'Benutzername bereits vergeben.' });
}
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const newUser = await db.query(
'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3) RETURNING user_id, username, role, created_at',
[username, passwordHash, role]
);
res.status(201).json(newUser.rows[0]);
} catch (error) {
next(error);
}
};
// Update a user (Admin only) - e.g., change role or reset password
exports.updateUser = async (req, res, next) => {
const { id } = req.params;
const { username, role, password } = req.body; // Allow updating username, role, password
// Basic validation
if (!username && !role && !password) {
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
}
const fieldsToUpdate = [];
const values = [];
let queryIndex = 1;
if (username) {
fieldsToUpdate.push(`username = $${queryIndex++}`);
values.push(username);
}
if (role) {
const validRoles = ['admin', 'referee', 'spectator'];
if (!validRoles.includes(role)) {
return res.status(400).json({ message: 'Ungültige Rolle angegeben.' });
}
fieldsToUpdate.push(`role = $${queryIndex++}`);
values.push(role);
}
if (password) {
// Hash new password if provided
try {
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
fieldsToUpdate.push(`password_hash = $${queryIndex++}`);
values.push(passwordHash);
} catch (hashError) {
return next(hashError); // Pass hashing error
}
}
// Add the user ID for the WHERE clause
values.push(id);
if (fieldsToUpdate.length === 0) {
return res.status(400).json({ message: 'Keine gültigen Felder zum Aktualisieren angegeben.' });
}
const updateQuery = `UPDATE users SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE user_id = $${queryIndex} RETURNING user_id, username, role, updated_at`;
try {
const result = await db.query(updateQuery, values);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Benutzer nicht gefunden.' });
}
res.json(result.rows[0]);
} catch (error) {
// Handle potential unique constraint violation for username
if (error.code === '23505' && error.constraint === 'users_username_key') {
return res.status(409).json({ message: 'Benutzername bereits vergeben.' });
}
next(error);
}
};
// Delete a user (Admin only)
exports.deleteUser = async (req, res, next) => {
const { id } = req.params;
// Optional: Prevent admin from deleting themselves?
// if (req.user.userId === id) {
// return res.status(403).json({ message: "Admins können sich nicht selbst löschen." });
// }
try {
const result = await db.query('DELETE FROM users WHERE user_id = $1 RETURNING user_id', [id]);
if (result.rowCount === 0) {
return res.status(404).json({ message: 'Benutzer nicht gefunden.' });
}
res.status(204).send(); // No Content
} catch (error) {
next(error);
}
};

46
src/db/db.js Normal file
View File

@@ -0,0 +1,46 @@
// src/db/db.js
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
// Create a new PostgreSQL connection pool
// It reads connection details from environment variables:
// PGUSER, PGHOST, PGDATABASE, PGPASSWORD, PGPORT
// Or from DATABASE_URL environment variable.
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Optional: Add SSL configuration if required by your PostgreSQL server
// ssl: {
// rejectUnauthorized: false // Necessary for self-signed certificates or some cloud providers
// }
});
// Function to execute the initialization script
const initializeDatabase = async () => {
const client = await pool.connect();
try {
console.log('Checking database schema...');
// Read the init.sql file
const initSqlPath = path.join(__dirname, 'init.sql');
const initSql = fs.readFileSync(initSqlPath, 'utf8');
// Execute the SQL script
// This script uses "CREATE TABLE IF NOT EXISTS" and "CREATE TYPE IF NOT EXISTS"
// so it's safe to run multiple times.
await client.query(initSql);
console.log('Database schema check/initialization complete.');
} catch (err) {
console.error('Error during database initialization:', err.stack);
throw err; // Re-throw error to be caught by server start logic
} finally {
client.release(); // Release the client back to the pool
}
};
// Export the pool and the initialization function
module.exports = {
query: (text, params) => pool.query(text, params), // Method to run queries
pool, // Export the pool itself if needed elsewhere
initializeDatabase,
};

215
src/db/init.sql Normal file
View File

@@ -0,0 +1,215 @@
-- Database Initialization Script for Tischtennis-Turnierverwaltung
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- User Roles Enum (optional but good practice)
DO $$ BEGIN
CREATE TYPE user_role AS ENUM ('admin', 'referee', 'spectator');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Users Table
CREATE TABLE IF NOT EXISTS users (
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role user_role NOT NULL DEFAULT 'spectator', -- Default role
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Function to update 'updated_at' timestamp
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for users table
DO $$ BEGIN
CREATE TRIGGER set_timestamp_users
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
EXCEPTION
WHEN duplicate_object THEN null; -- Ignore if trigger already exists
END $$;
-- Tournament Type Enum (KO or Group)
DO $$ BEGIN
CREATE TYPE tournament_type AS ENUM ('knockout', 'group');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Game Type Enum (Points per set)
DO $$ BEGIN
CREATE TYPE game_type AS ENUM ('11_points', '21_points');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tournaments Table
CREATE TABLE IF NOT EXISTS tournaments (
tournament_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
date DATE,
location VARCHAR(255),
description TEXT,
logo_url VARCHAR(255), -- URL or path to the logo file
tournament_type tournament_type NOT NULL DEFAULT 'knockout',
game_type game_type NOT NULL DEFAULT '11_points',
max_players INTEGER,
status VARCHAR(50) DEFAULT 'planned', -- e.g., planned, running, finished, cancelled
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL -- Link to the admin who created it
);
-- Trigger for tournaments table
DO $$ BEGIN
CREATE TRIGGER set_timestamp_tournaments
BEFORE UPDATE ON tournaments
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Players Table
CREATE TABLE IF NOT EXISTS players (
player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
club VARCHAR(150),
qttr_points INTEGER,
age_class VARCHAR(50), -- e.g., U18, Herren A, Damen
-- Add more fields as needed (e.g., date_of_birth, gender)
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Trigger for players table
DO $$ BEGIN
CREATE TRIGGER set_timestamp_players
BEFORE UPDATE ON players
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tournament Players Table (Many-to-Many relationship)
-- Links players to specific tournaments they are participating in
CREATE TABLE IF NOT EXISTS tournament_players (
tournament_player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
player_id UUID NOT NULL REFERENCES players(player_id) ON DELETE CASCADE,
seed INTEGER, -- Optional seeding position
group_name VARCHAR(50), -- For group stage
status VARCHAR(50) DEFAULT 'registered', -- e.g., registered, active, eliminated
UNIQUE (tournament_id, player_id), -- Ensure a player is only added once per tournament
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Matches Table
CREATE TABLE IF NOT EXISTS matches (
match_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
round INTEGER, -- Round number (e.g., 1, 2, Quarterfinal=3, Semifinal=4, Final=5)
match_number_in_round INTEGER, -- Order within the round
player1_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Reference to player 1
player2_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Reference to player 2
winner_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Determined after match completion
scheduled_time TIMESTAMPTZ,
status VARCHAR(50) DEFAULT 'scheduled', -- e.g., scheduled, ongoing, finished, postponed
table_number VARCHAR(20), -- Which table the match is played on
referee_id UUID REFERENCES users(user_id) ON DELETE SET NULL, -- Assigned referee
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
-- Add constraints or logic to ensure player1_id != player2_id if needed
);
-- Trigger for matches table
DO $$ BEGIN
CREATE TRIGGER set_timestamp_matches
BEFORE UPDATE ON matches
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Sets Table (Stores individual set scores for each match)
CREATE TABLE IF NOT EXISTS sets (
set_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
match_id UUID NOT NULL REFERENCES matches(match_id) ON DELETE CASCADE,
set_number INTEGER NOT NULL CHECK (set_number > 0), -- e.g., 1, 2, 3, 4, 5
player1_score INTEGER NOT NULL CHECK (player1_score >= 0),
player2_score INTEGER NOT NULL CHECK (player2_score >= 0),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (match_id, set_number) -- Ensure set numbers are unique per match
);
-- Notifications Table (For push notifications or general announcements)
CREATE TABLE IF NOT EXISTS notifications (
notification_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tournament_id UUID REFERENCES tournaments(tournament_id) ON DELETE CASCADE, -- Optional: Link to a specific tournament
message TEXT NOT NULL,
type VARCHAR(50) DEFAULT 'info', -- e.g., info, warning, match_start, result_update
is_sticky BOOLEAN DEFAULT FALSE, -- If true, needs manual dismissal
target_audience VARCHAR(50) DEFAULT 'all', -- e.g., all, spectators, referees, specific_player_id
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ -- Optional: Auto-hide after this time
);
-- Add Indexes for performance on frequently queried columns
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_tournaments_date ON tournaments(date);
CREATE INDEX IF NOT EXISTS idx_players_last_name ON players(last_name);
CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament ON tournament_players(tournament_id);
CREATE INDEX IF NOT EXISTS idx_tournament_players_player ON tournament_players(player_id);
CREATE INDEX IF NOT EXISTS idx_matches_tournament ON matches(tournament_id);
CREATE INDEX IF NOT EXISTS idx_matches_status ON matches(status);
CREATE INDEX IF NOT EXISTS idx_sets_match ON sets(match_id);
-- Optional: Table for storing settings like backup interval
CREATE TABLE IF NOT EXISTS application_settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value TEXT,
description TEXT,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Trigger for application_settings table
DO $$ BEGIN
CREATE TRIGGER set_timestamp_application_settings
BEFORE UPDATE ON application_settings
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Example: Insert default backup setting if not present
-- INSERT INTO application_settings (setting_key, setting_value, description)
-- VALUES ('backup_interval_minutes', '1440', 'Interval for automated database backups in minutes.')
-- ON CONFLICT (setting_key) DO NOTHING;
-- Initial Data (Example: Add first admin user - handled by application logic now)
-- Consider adding default age classes or other lookup data here if needed.

View File

@@ -0,0 +1,58 @@
// src/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
// Middleware to verify JWT token
const authenticateToken = (req, res, next) => {
// Get token from the Authorization header ('Bearer TOKEN')
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) {
// No token provided
return res.status(401).json({ message: 'Zugriff verweigert. Kein Token bereitgestellt.' }); // Unauthorized
}
// Verify the token
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
console.error("JWT Verification Error:", err.message);
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: 'Token abgelaufen.' });
}
return res.status(403).json({ message: 'Zugriff verweigert. Ungültiges Token.' }); // Forbidden
}
// Token is valid, attach user payload (with id and role) to the request object
req.user = user; // Contains { userId, username, role } from the JWT payload
next(); // Proceed to the next middleware or route handler
});
};
// Middleware to check for specific roles (e.g., 'admin')
// Usage: router.post('/', authenticateToken, authorizeRole('admin'), (req, res) => { ... });
const authorizeRole = (requiredRole) => {
return (req, res, next) => {
if (!req.user || req.user.role !== requiredRole) {
return res.status(403).json({ message: `Zugriff verweigert. Rolle '${requiredRole}' erforderlich.` }); // Forbidden
}
next(); // User has the required role
};
};
// Middleware to check for multiple roles (e.g., 'admin' or 'referee')
const authorizeRoles = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
return res.status(403).json({ message: `Zugriff verweigert. Erforderliche Rollen: ${allowedRoles.join(' oder ')}.` }); // Forbidden
}
next(); // User has one of the allowed roles
};
};
module.exports = {
authenticateToken,
authorizeRole,
authorizeRoles
};

View File

@@ -0,0 +1,33 @@
// src/middleware/errorMiddleware.js
// Basic error handling middleware
// Catches errors passed via next(error) from route handlers or other middleware
const errorHandler = (err, req, res, next) => {
console.error("Unhandled Error:", err.stack || err); // Log the full error stack
// Default error status and message
let statusCode = err.statusCode || 500; // Use specific status code if available, otherwise default to 500
let message = err.message || 'Interner Serverfehler.';
// Handle specific error types if needed
if (err.name === 'ValidationError') { // Example for a potential validation error library
statusCode = 400;
message = `Validierungsfehler: ${err.message}`;
} else if (err.code === '23505') { // Handle PostgreSQL unique violation errors
statusCode = 409; // Conflict
message = `Konflikt: Ein Eintrag mit diesen Daten existiert bereits. (${err.detail})`;
} else if (err.code === '23503') { // Handle PostgreSQL foreign key violation
statusCode = 400; // Bad Request or Conflict (409) might be appropriate
message = `Referenzfehler: Der verknüpfte Eintrag existiert nicht. (${err.detail})`;
}
// Add more specific error handling as needed
// Send the error response
res.status(statusCode).json({
message: message,
// Optionally include stack trace in development environment
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
};
module.exports = errorHandler;

25
src/routes/authRoutes.js Normal file
View File

@@ -0,0 +1,25 @@
// src/routes/authRoutes.js
const express = require('express');
const authController = require('../controllers/authController');
// Import role authorization if needed for registration logic later
// const { authorizeRole } = require('../middleware/authMiddleware');
const router = express.Router();
// POST /api/auth/register - Register a new user
// Initially, anyone might register, or you might restrict this later.
// The createInitialAdminUser function handles the very first admin.
// You might want to add authorizeRole('admin') here later so only admins can create users.
router.post('/register', authController.register);
// POST /api/auth/login - Login a user
router.post('/login', authController.login);
// Example of a protected route to test authentication
// GET /api/auth/profile - Get current user's profile (requires login)
// router.get('/profile', authenticateToken, (req, res) => {
// // req.user is populated by authenticateToken middleware
// res.json({ user: req.user });
// });
module.exports = router;

41
src/routes/matchRoutes.js Normal file
View File

@@ -0,0 +1,41 @@
// src/routes/matchRoutes.js
const express = require('express');
const { authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
const router = express.Router();
// --- Placeholder Controller Functions (replace with actual controller) ---
const placeholderController = (feature) => (req, res) => {
res.status(501).json({ message: `Funktion '${feature}' noch nicht implementiert.` });
};
// --- Routes ---
// GET /api/matches - Get all matches (e.g., for a specific tournament via query param)
// Accessible by all logged-in users
router.get('/', placeholderController('getAllMatches')); // Add query param filtering later
// GET /api/matches/:id - Get details of a specific match
// Accessible by all logged-in users
router.get('/:id', placeholderController('getMatchById'));
// POST /api/matches - Manually add a match (Admin only)
router.post('/', authorizeRole('admin'), placeholderController('createMatch'));
// PUT /api/matches/:id - Update match details (e.g., schedule, table) (Admin only)
router.put('/:id', authorizeRole('admin'), placeholderController('updateMatch'));
// DELETE /api/matches/:id - Delete a match (Admin only)
router.delete('/:id', authorizeRole('admin'), placeholderController('deleteMatch'));
// POST /api/matches/:id/score - Enter/Update scores for a match (Admin or Referee)
router.post('/:id/score', authorizeRoles('admin', 'referee'), placeholderController('updateMatchScore'));
// GET /api/matches/export/pdf - Export matches (current, future, past) as PDF (Admin only)
router.get('/export/pdf', authorizeRole('admin'), placeholderController('exportMatchesPdf'));
// POST /api/matches/generate-bracket - Generate tournament bracket (Admin only)
// Requires tournament ID and potentially other parameters in the body
router.post('/generate-bracket', authorizeRole('admin'), placeholderController('generateBracket'));
module.exports = router;

View File

@@ -0,0 +1,36 @@
// src/routes/playerRoutes.js
const express = require('express');
const { authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
const router = express.Router();
// --- Placeholder Controller Functions (replace with actual controller) ---
const placeholderController = (feature) => (req, res) => {
res.status(501).json({ message: `Funktion '${feature}' noch nicht implementiert.` });
};
// --- Routes ---
// GET /api/players - Get all players (accessible by admin, maybe referee?)
router.get('/', authorizeRoles('admin', 'referee'), placeholderController('getAllPlayers'));
// GET /api/players/:id - Get a specific player (accessible by admin, maybe referee?)
router.get('/:id', authorizeRoles('admin', 'referee'), placeholderController('getPlayerById'));
// POST /api/players - Add a new player (Admin only)
router.post('/', authorizeRole('admin'), placeholderController('createPlayer'));
// PUT /api/players/:id - Update a player (Admin only)
router.put('/:id', authorizeRole('admin'), placeholderController('updatePlayer'));
// DELETE /api/players/:id - Remove a player (Admin only)
router.delete('/:id', authorizeRole('admin'), placeholderController('deletePlayer'));
// POST /api/players/import - Import players from CSV (Admin only)
router.post('/import', authorizeRole('admin'), placeholderController('importPlayers'));
// GET /api/players/export - Export players to CSV (Admin only)
router.get('/export', authorizeRole('admin'), placeholderController('exportPlayers'));
module.exports = router;

View File

@@ -0,0 +1,38 @@
// src/routes/tournamentRoutes.js
const express = require('express');
const tournamentController = require('../controllers/tournamentController');
const { authorizeRole } = require('../middleware/authMiddleware'); // Import role authorization
const router = express.Router();
// --- Public routes (accessible by anyone logged in) ---
// GET /api/tournaments - Get all tournaments
router.get('/', tournamentController.getAllTournaments);
// GET /api/tournaments/:id - Get a specific tournament
router.get('/:id', tournamentController.getTournamentById);
// --- Admin only routes ---
// POST /api/tournaments - Create a new tournament
router.post('/', authorizeRole('admin'), tournamentController.createTournament);
// PUT /api/tournaments/:id - Update a tournament
router.put('/:id', authorizeRole('admin'), tournamentController.updateTournament);
// DELETE /api/tournaments/:id - Delete a tournament
router.delete('/:id', authorizeRole('admin'), tournamentController.deleteTournament);
// --- Placeholder routes for future implementation (Admin only) ---
// POST /api/tournaments/import - Import tournaments from CSV
router.post('/import', authorizeRole('admin'), tournamentController.importTournaments);
// GET /api/tournaments/export - Export tournaments to CSV
router.get('/export', authorizeRole('admin'), tournamentController.exportTournaments);
// POST /api/tournaments/:id/logo - Add/Update logo for a tournament
router.post('/:id/logo', authorizeRole('admin'), tournamentController.addLogo);
module.exports = router;

26
src/routes/userRoutes.js Normal file
View File

@@ -0,0 +1,26 @@
// src/routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const { authorizeRole } = require('../middleware/authMiddleware'); // Only admins manage users
const router = express.Router();
// All user management routes require admin privileges
router.use(authorizeRole('admin'));
// GET /api/users - Get all users
router.get('/', userController.getAllUsers);
// POST /api/users - Create a new user
router.post('/', userController.createUser);
// GET /api/users/:id - Get a single user by ID
router.get('/:id', userController.getUserById);
// PUT /api/users/:id - Update a user
router.put('/:id', userController.updateUser);
// DELETE /api/users/:id - Delete a user
router.delete('/:id', userController.deleteUser);
module.exports = router;

View File

@@ -0,0 +1,43 @@
// src/utils/generateCertificate.js
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const keyPath = path.join(__dirname, '..', '..', 'key.pem'); // Place in root directory
const certPath = path.join(__dirname, '..', '..', 'cert.pem'); // Place in root directory
// Check if files already exist
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
console.log('Certificate files (key.pem, cert.pem) already exist. Skipping generation.');
console.log('Delete the files if you want to regenerate them.');
process.exit(0); // Exit successfully
}
// OpenSSL command to generate a self-signed certificate
// -x509: Output a self-signed certificate instead of a certificate request.
// -nodes: Don't encrypt the private key.
// -days 365: Validity period.
// -newkey rsa:2048: Generate a new RSA key of 2048 bits.
// -keyout: Path to write the private key.
// -out: Path to write the certificate.
// -subj: Subject information (avoids interactive prompts). Customize as needed.
const opensslCommand = `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -subj "/C=DE/ST=Niedersachsen/L=Hannover/O=TT Tournament Dev/OU=Development/CN=localhost"`;
console.log('Generating self-signed certificate...');
console.log(`Executing: ${opensslCommand}`);
try {
// Execute the command synchronously
child_process.execSync(opensslCommand, { stdio: 'inherit' }); // Show output/errors
console.log('Successfully generated key.pem and cert.pem');
console.log(`Key file: ${keyPath}`);
console.log(`Certificate file: ${certPath}`);
console.log('\nNote: Your browser will likely show a security warning for this self-signed certificate.');
} catch (error) {
console.error('\n-----------------------------------------');
console.error('Error generating certificate:', error.message);
console.error('-----------------------------------------');
console.error('Please ensure OpenSSL is installed and accessible in your system PATH.');
console.error('Command failed:', opensslCommand);
process.exit(1); // Exit with error code
}