Initial commit

This commit is contained in:
klaas 2024-06-06 18:45:42 +00:00
commit d282773b3c
28 changed files with 14377 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

7
.env_default Normal file
View File

@ -0,0 +1,7 @@
DATABASE_URL=postgres://user:psw@localhost:5432/postgres
SESSIONSECRET=Your Secret
MAILHOST=
MAILUSER=
MAILFROM=
MAILPASS=
PORT=2000

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
node_modules/
logs/*

0
README.md Normal file
View File

331
app.js Normal file
View File

@ -0,0 +1,331 @@
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const { Pool } = require('pg');
const path = require('path');
const moment = require('moment');
require('dotenv').config();
const log = require('node-file-logger');
const app = express();
const port = process.env.PORT;
const options = {
folderPath: './logs/',
dateBasedFileNaming: true,
fileNamePrefix: 'DailyLogs_',
fileNameExtension: '.log',
dateFormat: 'YYYY_MM_D',
timeFormat: 'h:mm:ss A',
}
log.SetUserOptions(options);
// Middleware
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));
// Session-Konfiguration
app.use(session({
secret: process.env.SESSIONSECRET,
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 * 2 }
}));
// Authentifizierungs-Middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.redirect('/login');
}
next();
};
const requireAdmin = (req, res, next) => {
if (req.session.role !== 'admin') {
return res.status(403).send('Access denied');
}
next();
};
// Email-Konfiguration
const transporter = nodemailer.createTransport({
host: process.env.MAILHOST,
port: 465,
secure: true,
auth: {
user: process.env.MAILUSER,
pass: process.env.MAILPASS
}
});
// Datenbankverbindung
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// Registrierung
app.post('/register', async (req, res) => {
const { username, email, password } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('INSERT INTO users (username, email, password) VALUES ($1, $2, $3)', [username, email, hashedPassword]);
const message = 'Registrierung erfolgreich. Ein Admin wird dich in kürze freischalten';
const mailOptions = {
to: 'admin@boergmann.it',
from: 'admin@boergmann.it',
subject: 'Neue Registrierung',
text: `${username} hat sich registriert`
};
transporter.sendMail(mailOptions, (error) => {
if (error) {
console.error('Error sending email:', error);
const message = 'Error sending Mail:' + error;
res.render('error', { session: req.session, message });
}
});
res.render('error', { session: req.session, message });
} catch (error) {
console.error('Error registering user:', error);
const message = 'Error registering user:' + error;
res.render('error', { session: req.session, message });
}
});
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const userResult = await pool.query("SELECT *, CASE WHEN admin_temp IS NOT NULL AND (now() - admin_temp) > interval '22 hours' THEN 'expired' ELSE 'valid' END AS admin_status FROM users WHERE username = $1", [username]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const match = await bcrypt.compare(password, user.password);
if (match) {
if (user.is_active) {
req.session.userId = user.id;
req.session.userName = user.username;
req.session.activeRiege = 1;
req.session.activeTab = 'geraete';
req.session.message = [title = '', message = '', type = 'none'];
log.Info(username + ' ' + password);
if (user.admin_status === 'expired') {
await pool.query('UPDATE users SET role = $1, admin_temp = NULL WHERE id = $2', ['user', user.id]);
req.session.role = 'user';
} else {
req.session.role = user.role;
}
res.redirect('/');
} else {
res.redirect('/freischaltung')
}
} else {
const message = 'Falscher Benutzername oder falsches Passwort';
res.render('login', { session: req.session, username, message });
}
} else {
const message = 'Falscher Benutzername oder falsches Passwort';
res.render('login', { session: req.session, username, message });
}
} catch (error) {
console.error('Error logging in:', error);
const message = 'Error logging in:' + error;
res.render('error', { session: req.session, message });
}
});
//Wird angezeigt, wenn ein nicht freigeschalteter User sich anmelden will.
app.get('/freischaltung', async (req, res) => {
res.render('freischaltung', { session: req.session });
})
// Logout
app.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('Internal Server Error');
}
res.redirect('/');
});
});
// Benutzer freischalten (nur Admin)
app.post('/userrights', requireAuth, requireAdmin, async (req, res) => {
const { userId, type } = req.body;
try {
if (type === 'activate') {
await pool.query('UPDATE users SET is_active = TRUE WHERE id = $1', [userId]);
const userResult = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
if (userResult.rows.length > 0) {
if (userResult.rows[0].email) {
const mailOptions = {
to: userResult.rows[0].email,
from: 'admin@boergmann.it',
subject: 'Freischaltung',
text: `Hallo ${userResult.rows[0].username}, du wurdest soeben freigeschaltet.`
};
transporter.sendMail(mailOptions, (error) => {
if (error) {
console.error('Error sending email:', error);
const message = 'Error sending Mail:' + error;
res.render('error', { session: req.session, message });
}
})
}
}
} else if (type === 'admin') {
await pool.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
}
else if (type === 'admint') {
await pool.query('UPDATE users SET role = $1, admin_temp = $2 WHERE id = $3', ['admin', moment().toDate(), userId]);
} else if (type === 'delete') {
await pool.query('DELETE FROM users WHERE id = $1', [userId]);
}
res.redirect('/admin');
} catch (error) {
console.error('Error activating user:', error);
const message = 'Error activating user:' + error;
res.render('error', { session: req.session, message });
}
});
// Passwort-Zurücksetzung anfordern
app.post('/send-password', async (req, res) => {
const { email } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const token = crypto.randomBytes(20).toString('hex');
const resetLink = `http://tkd.boergmann.it/reset-password/${token}`;
await pool.query('UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3', [token, selectedDate = moment().add(1, 'd').toDate(), user.id]);
const mailOptions = {
to: user.email,
from: 'admin@boergmann.it',
subject: 'Password Reset',
text: `Click the following link to reset your password: ${resetLink}`
};
transporter.sendMail(mailOptions, (err) => {
if (err) {
console.error('Error sending email:', err);
const message = 'Error sending Mail:' + error;
res.render('error', { session: req.session, message });
} else {
const message = 'Password reset link sent';
res.render('error', { session: req.session, message });
}
});
} else {
const message = 'Email not found';
res.render('error', { session: req.session, message });
}
} catch (error) {
console.error('Error in forgot-password:', error);
const message = 'Error in forgot-password';
res.render('error', { session: req.session, message });
}
});
app.get('/forgot-password', async (req, res) => {
res.render('forgot-password', { session: req.session })
})
// Passwort zurücksetzen
app.get('/reset-password/:token', async (req, res) => {
const { token } = req.params;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
res.render('reset-password', { token }); // Stelle sicher, dass es eine reset-password.ejs gibt
} else {
const message = 'Token ungültig oder abgelaufen';
res.render('error', { session: req.session, message });
}
} catch (error) {
console.error('Error in reset-password:', error);
const message = 'Error in reset-password';
res.render('error', { session: req.session, message });
}
});
app.post('/reset-password/:token', async (req, res) => {
const { token } = req.params;
const { password } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1, reset_password_token = NULL, reset_password_expires = NULL WHERE id = $2', [hashedPassword, user.id]);
res.redirect('/login');
} else {
const message = 'Token ungültig oder abgelaufen';
res.render('error', { session: req.session, message });
}
} catch (error) {
console.error('Error in reset-password:', error);
const message = 'Error in reset-password';
res.render('error', { session: req.session, message });
}
});
// Profilseite
app.get('/profile', requireAuth, (req, res) => {
res.render('profile', { session: req.session }); // Stelle sicher, dass es eine profile.ejs gibt
});
app.post('/profile', requireAuth, async (req, res) => {
const { email, password } = req.body;
try {
if (email) {
await pool.query('UPDATE users SET email = $1 WHERE id = $2', [email, req.session.userId]);
}
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1 WHERE id = $2', [hashedPassword, req.session.userId]);
}
res.redirect('/profile');
} catch (error) {
console.error('Error updating profile:', error);
const message = 'Error updating profile:' + error;
res.render('error', { session: req.session, message });
}
});
// Admin-Seite
app.get('/admin', requireAuth, requireAdmin, async (req, res) => {
const usersResult = await pool.query('SELECT * FROM users');
res.render('admin', { users: usersResult.rows, session: req.session }); // Stelle sicher, dass es eine admin.ejs gibt
});
// Login und Registrierung anzeigen
app.get('/login', (req, res) => {
req.session.message = ['', '', 'none'];
res.render('login', { session: req.session }); // Stelle sicher, dass es eine login.ejs gibt
});
// Registrierung
app.get('/register', (req, res) => {
res.render('register', { session: req.session }); // Stelle sicher, dass es eine register.ejs gibt
});
// Startseite
app.get('/', (req, res) => {
req.session.message = ['', '', 'none']
res.render('index', { session: req.session });
});
const server = app.listen(port, '0.0.0.0', () => {
log.Info(`Server is running on ${process.env.HOST}:${port}/`);
});

1638
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "training",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-session": "^1.18.0",
"fs": "^0.0.1-security",
"moment": "^2.30.1",
"node-file-logger": "^0.9.5",
"nodemailer": "^6.9.13",
"pg": "^8.11.5"
}
}

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/bootstrap/.DS_Store vendored Normal file

Binary file not shown.

12057
public/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
public/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7
public/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

21
utilities/changetable.js Normal file
View File

@ -0,0 +1,21 @@
const { Pool } = require('pg');
require('dotenv').config();
const moment = require('moment');
// Datenbankverbindung
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
async function writeKW() {
const resultTrainings = await pool.query('SELECT * FROM trainings');
const trainings = resultTrainings.rows;
for (const training of trainings) {
const jahr = moment(training.datum).year();
const kw = moment(training.datum).isoWeek();
await pool.query('UPDATE trainings SET kw = $1, jahr = $2 WHERE id = $3', [kw, jahr, training.id])
}
}
writeKW();

91
views/admin.ejs Normal file
View File

@ -0,0 +1,91 @@
<%- include('partials/header') %>
<h1>Admin Panel</h1>
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-1" data-bs-toggle="tab" data-bs-target="#cont-1" type="button" role="tab" aria-controls="tab-1" aria-selected='true'> Mitglied anlegen </button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-2" data-bs-toggle="tab" data-bs-target="#cont-2" type="button" role="tab" aria-controls="tab-2" aria-selected='false'> User Freischalten </button>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="cont-1" role="tabpanel" aria-labelledby="tab-1">
<form method="POST" action="/new-member">
Name: <input type="text" id="vorname" placeholder="Vorname" name="vorname">
<input type="text" id="nachname" placeholder="Nachname" name="nachname"> </br>
Geburtsdatum: <input type="date" id="geburt" name="geburt"></br>
<select name="riege">
<option value=1> Riege 1</option>
<option value=2> Riege 2</option>
<option value=3> Riege 3</option>
<option value=4> Riege 4</option>
<option value=5> Riege 5</option>
</select></br>
Adresse: <input type="text" id="adresse" placeholder="Adresse" name="adresse"></br>
<button type="submit">Speichern</button>
</form>
</div>
<div class="tab-pane fade" id="cont-2" role="tabpanel" aria-labelledby="tab-2">
<ul>
<% users.forEach(user => { %>
<li>
<%= user.username %> - <%= user.email %>
<% if (!user.is_active) { %>
<form action="/userrights" method="post" style="display: inline;">
<input type="hidden" name="type" value="activate">
<input type="hidden" name="userId" value="<%= user.id %>">
<button type="submit" class="btn btn-success">Activate</button>
</form>
<form action="/userrights" method="post" style="display: inline;">
<input type="hidden" name="type" value="delete">
<input type="hidden" name="userId" value="<%= user.id %>">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<% } else if (user.role === 'user') { %>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#adminModal">
Admin
</button>
<div class="modal fade" id="adminModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">Zum Admin machen?</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="/userrights" method="post" style="display: inline;">
<input type="hidden" name="type" value="admin">
<input type="hidden" name="userId" value="<%= user.id %>">
<button type="submit" class="btn btn-success">Dauerhaft</button>
</form>
<form action="/userrights" method="post" style="display: inline;">
<input type="hidden" name="type" value="admint">
<input type="hidden" name="userId" value="<%= user.id %>">
<button type="submit" class="btn btn-success">Temporär (24h)</button>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<% } %>
</li>
<% }); %>
</ul>
</div>
</div>
<%- include('partials/footer') %>

5
views/error.ejs Normal file
View File

@ -0,0 +1,5 @@
<%- include('partials/header') %>
<%= message %>
<%- include('partials/footer') %>

13
views/forgot-password.ejs Normal file
View File

@ -0,0 +1,13 @@
<%- include('partials/header') %>
<h1>Forgot Password</h1>
<form action="/send-password" method="post">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary">Send Reset Link</button>
</form>
<a href="/login">Back to Login</a>
<%- include('partials/footer') %>

6
views/freischaltung.ejs Normal file
View File

@ -0,0 +1,6 @@
<%- include('partials/header') %>
<p>Du musst erst freigeschaltet werden um dich einloggen zu können.</p>
<%- include('partials/footer') %>

9
views/index.ejs Normal file
View File

@ -0,0 +1,9 @@
<%- include('partials/header') %>
<h1>Turnstunden Organisation</h1>
</body>
</html>
<%- include('partials/footer') %>

22
views/login.ejs Normal file
View File

@ -0,0 +1,22 @@
<%- include('partials/header') %>
<h1>Login</h1>
<% if (locals.message) { %>
<p><%= message %></p>
<% } %>
<form action="/login" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<a href="/register">Register</a>
<a href="/forgot-password">Forgot Password?</a>
<%- include('partials/footer') %>

View File

@ -0,0 +1,5 @@
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>

76
views/partials/header.ejs Normal file
View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Turnstunden WebApp</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Klaas</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% if (session && ( session.role =='admin' || session.role =='user' )) { %>
<li class="nav-item">
<a class="nav-link" href="/profile">Profil</a>
</li>
<% } else {%>
<li class="nav-item">
<a class="nav-link" href="/register">Registrieren</a>
</li>
<% } %>
<% if (session && session.role === 'admin') { %>
<li class="nav-item"><a class="nav-link" href="/admin">Admin</a></li>
<% } %>
</ul>
<% if (session && ( session.role =='admin' || session.role =='user' )) { %>
<form class="d-flex" role="search" method="get" action="/logout">
<button class="btn btn-outline-success" type="submit">Logout</button>
</form>
<% } else { %>
<form class="d-flex" role="search" method="POST" action="/login">
<input type="text" class="form-control" placeholder="username" name="username">
<input type="password" class="form-control" placeholder="passwort" name="password">
<button class="btn btn-outline-success" type="submit">Login</button>
</form>
<% } %>
</div>
</div>
</nav>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</symbol>
<symbol id="info-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
<symbol id="exclamation-triangle-fill" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</symbol>
</svg>
<% if (session.message){ %>
<% if (session && session.message[2] === 'success') { %>
<div class="alert alert-success" role="alert">
</svg> <%= session.message[0] %> - <%= session.message[1] %>
</div>
<% session.message[2] = 'none' %>
<% } %>
<% if (session && session.message[2] === 'error') { %>
<div class="alert alert-danger" role="alert">
</svg> <%= session.message[0] %> - <%= session.message[1] %>
</div>
<% session.message[2] = 'none' %>
<% } %>
<% } %>

16
views/profile.ejs Normal file
View File

@ -0,0 +1,16 @@
<%- include('partials/header') %>
<h1>Profile</h1>
<form action="/profile" method="post">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Update Profile</button>
</form>
<%- include('partials/footer') %>

21
views/register.ejs Normal file
View File

@ -0,0 +1,21 @@
<%- include('partials/header') %>
<h1>Register</h1>
<form action="/register" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<a href="/login">Login</a>
<%- include('partials/footer') %>

12
views/reset-password.ejs Normal file
View File

@ -0,0 +1,12 @@
<%- include('partials/header') %>
<h1>Reset Password</h1>
<form action="/reset-password/<%= token %>" method="post">
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Reset Password</button>
</form>
<%- include('partials/footer') %>