Add security hardening, captcha login, and password reset via email
- .htaccess: HTTPS enforcement, security headers, block sensitive files - data/.htaccess: deny all direct access to data directory - Secure session settings (httponly, secure, strict mode, samesite) - Rate limiting on login (10 attempts per 15 min per IP) - Math captcha on login form (server-side validated) - Password reset via email with token (1 hour expiry) - Forgot password UI with reset link flow - Email field added to user management - Updated .gitignore for reset_tokens.json and login_attempts.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
101
script.js
101
script.js
@@ -35,7 +35,96 @@ async function apiCall(action, method = 'GET', body = null) {
|
||||
|
||||
// ==================== AUTH ====================
|
||||
|
||||
const forgotBox = document.getElementById('forgot-box');
|
||||
const resetBox = document.getElementById('reset-box');
|
||||
const loginBox = document.querySelector('.login-box');
|
||||
|
||||
function showLoginView() {
|
||||
loginBox.style.display = '';
|
||||
forgotBox.style.display = 'none';
|
||||
resetBox.style.display = 'none';
|
||||
}
|
||||
|
||||
function showForgotView() {
|
||||
loginBox.style.display = 'none';
|
||||
forgotBox.style.display = '';
|
||||
resetBox.style.display = 'none';
|
||||
}
|
||||
|
||||
function showResetView() {
|
||||
loginBox.style.display = 'none';
|
||||
forgotBox.style.display = 'none';
|
||||
resetBox.style.display = '';
|
||||
}
|
||||
|
||||
document.getElementById('forgot-link').addEventListener('click', (e) => { e.preventDefault(); showForgotView(); });
|
||||
document.getElementById('forgot-back').addEventListener('click', (e) => { e.preventDefault(); showLoginView(); });
|
||||
|
||||
async function loadCaptcha() {
|
||||
try {
|
||||
const data = await apiCall('captcha');
|
||||
document.getElementById('captcha-question').textContent = data.question;
|
||||
} catch (e) {
|
||||
document.getElementById('captcha-question').textContent = 'Virhe';
|
||||
}
|
||||
}
|
||||
|
||||
// Salasanan palautuspyyntö
|
||||
document.getElementById('forgot-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('forgot-username').value;
|
||||
const forgotMsg = document.getElementById('forgot-msg');
|
||||
const forgotError = document.getElementById('forgot-error');
|
||||
forgotMsg.style.display = 'none';
|
||||
forgotError.style.display = 'none';
|
||||
try {
|
||||
await apiCall('password_reset_request', 'POST', { username });
|
||||
forgotMsg.textContent = 'Jos käyttäjätunnukselle on sähköposti, palautuslinkki on lähetetty.';
|
||||
forgotMsg.style.display = 'block';
|
||||
} catch (err) {
|
||||
forgotError.textContent = err.message;
|
||||
forgotError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Salasanan vaihto (reset token)
|
||||
document.getElementById('reset-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const pw1 = document.getElementById('reset-password').value;
|
||||
const pw2 = document.getElementById('reset-password2').value;
|
||||
const resetMsg = document.getElementById('reset-msg');
|
||||
const resetError = document.getElementById('reset-error');
|
||||
resetMsg.style.display = 'none';
|
||||
resetError.style.display = 'none';
|
||||
if (pw1 !== pw2) { resetError.textContent = 'Salasanat eivät täsmää'; resetError.style.display = 'block'; return; }
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('reset');
|
||||
try {
|
||||
await apiCall('password_reset', 'POST', { token, password: pw1 });
|
||||
resetMsg.textContent = 'Salasana vaihdettu! Voit nyt kirjautua.';
|
||||
resetMsg.style.display = 'block';
|
||||
document.getElementById('reset-form').style.display = 'none';
|
||||
setTimeout(() => { window.location.href = window.location.pathname; }, 3000);
|
||||
} catch (err) {
|
||||
resetError.textContent = err.message;
|
||||
resetError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
async function checkAuth() {
|
||||
// Tarkista onko URL:ssa reset-token
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('reset')) {
|
||||
try {
|
||||
const data = await apiCall('validate_reset_token&token=' + encodeURIComponent(params.get('reset')));
|
||||
if (data.valid) { showResetView(); return; }
|
||||
} catch (e) {}
|
||||
showResetView();
|
||||
document.getElementById('reset-error').textContent = 'Palautuslinkki on vanhentunut tai virheellinen';
|
||||
document.getElementById('reset-error').style.display = 'block';
|
||||
document.getElementById('reset-form').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await apiCall('check_auth');
|
||||
if (data.authenticated) {
|
||||
@@ -49,14 +138,17 @@ loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
const captcha = document.getElementById('login-captcha').value;
|
||||
try {
|
||||
const data = await apiCall('login', 'POST', { username, password });
|
||||
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
|
||||
loginError.style.display = 'none';
|
||||
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
||||
showDashboard();
|
||||
} catch (err) {
|
||||
loginError.textContent = err.message;
|
||||
loginError.style.display = 'block';
|
||||
document.getElementById('login-captcha').value = '';
|
||||
loadCaptcha();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,6 +158,9 @@ document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
loginScreen.style.display = 'flex';
|
||||
document.getElementById('login-username').value = '';
|
||||
document.getElementById('login-password').value = '';
|
||||
document.getElementById('login-captcha').value = '';
|
||||
showLoginView();
|
||||
loadCaptcha();
|
||||
});
|
||||
|
||||
async function showDashboard() {
|
||||
@@ -595,6 +690,7 @@ async function loadUsers() {
|
||||
utbody.innerHTML = users.map(u => `<tr>
|
||||
<td><strong>${esc(u.username)}</strong></td>
|
||||
<td>${esc(u.nimi)}</td>
|
||||
<td>${esc(u.email || '')}</td>
|
||||
<td><span class="role-badge role-${u.role}">${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span></td>
|
||||
<td>${esc(u.luotu)}</td>
|
||||
<td class="actions-cell">
|
||||
@@ -616,6 +712,7 @@ function openUserForm(user = null) {
|
||||
document.getElementById('user-form-username').value = user ? user.username : '';
|
||||
document.getElementById('user-form-username').disabled = !!user;
|
||||
document.getElementById('user-form-nimi').value = user ? user.nimi : '';
|
||||
document.getElementById('user-form-email').value = user ? (user.email || '') : '';
|
||||
document.getElementById('user-form-password').value = '';
|
||||
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
|
||||
document.getElementById('user-form-role').value = user ? user.role : 'user';
|
||||
@@ -644,6 +741,7 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
|
||||
const data = {
|
||||
username: document.getElementById('user-form-username').value,
|
||||
nimi: document.getElementById('user-form-nimi').value,
|
||||
email: document.getElementById('user-form-email').value,
|
||||
role: document.getElementById('user-form-role').value,
|
||||
};
|
||||
const pw = document.getElementById('user-form-password').value;
|
||||
@@ -671,4 +769,5 @@ document.addEventListener('keydown', (e) => {
|
||||
});
|
||||
|
||||
// Init
|
||||
loadCaptcha();
|
||||
checkAuth();
|
||||
|
||||
Reference in New Issue
Block a user