Files
intra.cuitunet.fi/index.html
Jukka Lampikoski 8a07689a1f 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>
2026-03-10 01:00:19 +02:00

376 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CuituNet Intra - Asiakashallinta</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Login -->
<div id="login-screen" class="login-screen">
<div class="login-box">
<h1>CuituNet Intra</h1>
<p>Kirjaudu sisään</p>
<form id="login-form">
<input type="text" id="login-username" placeholder="Käyttäjätunnus" required autofocus>
<input type="password" id="login-password" placeholder="Salasana" required>
<div class="captcha-row">
<span id="captcha-question" class="captcha-question">Ladataan...</span>
<input type="number" id="login-captcha" placeholder="Vastaus" required>
</div>
<button type="submit">Kirjaudu</button>
</form>
<div id="login-error" class="error" style="display:none"></div>
<a href="#" id="forgot-link" class="forgot-link">Unohdin salasanan</a>
</div>
<!-- Salasanan palautuspyyntö -->
<div class="login-box" id="forgot-box" style="display:none">
<h1>CuituNet Intra</h1>
<p>Salasanan palautus</p>
<form id="forgot-form">
<input type="text" id="forgot-username" placeholder="Käyttäjätunnus" required autofocus>
<button type="submit">Lähetä palautuslinkki</button>
</form>
<div id="forgot-msg" class="success-msg" style="display:none"></div>
<div id="forgot-error" class="error" style="display:none"></div>
<a href="#" id="forgot-back" class="forgot-link">Takaisin kirjautumiseen</a>
</div>
<!-- Uusi salasana (reset token) -->
<div class="login-box" id="reset-box" style="display:none">
<h1>CuituNet Intra</h1>
<p>Aseta uusi salasana</p>
<form id="reset-form">
<input type="password" id="reset-password" placeholder="Uusi salasana" required>
<input type="password" id="reset-password2" placeholder="Salasana uudelleen" required>
<button type="submit">Vaihda salasana</button>
</form>
<div id="reset-msg" class="success-msg" style="display:none"></div>
<div id="reset-error" class="error" style="display:none"></div>
</div>
</div>
<!-- Dashboard -->
<div id="dashboard" style="display:none">
<header>
<div class="header-left">
<div class="header-brand">
<span class="brand-icon">&#9889;</span>
<div>
<h1>CuituNet Intra</h1>
<span class="subtitle">Kuituasiakkaiden hallinta</span>
</div>
</div>
</div>
<div class="header-right">
<span id="user-info" class="user-info"></span>
<button id="btn-add" class="btn-primary">+ Lisää asiakas</button>
<button id="btn-logout" class="btn-secondary">Kirjaudu ulos</button>
</div>
</header>
<!-- Tabs -->
<div class="tab-bar">
<button class="tab active" data-tab="customers">Asiakkaat</button>
<button class="tab" data-tab="archive">Arkisto</button>
<button class="tab" data-tab="changelog">Muutosloki</button>
<button class="tab" data-tab="users" id="tab-users" style="display:none">Käyttäjät</button>
</div>
<!-- Tab: Asiakkaat -->
<div class="tab-content active" id="tab-content-customers">
<div class="main-container">
<div class="content-layout">
<div class="content-main">
<div class="toolbar">
<div class="search-bar">
<span class="search-icon">&#128269;</span>
<input type="text" id="search-input" placeholder="Hae yrityksen nimellä, osoitteella tai yhteyshenkilöllä...">
</div>
</div>
<div class="table-card">
<table id="customer-table">
<thead>
<tr>
<th data-sort="yritys">Yritys &#8597;</th>
<th data-sort="asennusosoite">Osoite &#8597;</th>
<th data-sort="kaupunki">Kaupunki &#8597;</th>
<th data-sort="liittymanopeus">Nopeus &#8597;</th>
<th data-sort="hinta">Hinta/kk &#8597;</th>
<th data-sort="sopimuskausi">Sopimus &#8597;</th>
<th>Toiminnot</th>
</tr>
</thead>
<tbody id="customer-tbody"></tbody>
</table>
<div id="no-customers" class="empty-state" style="display:none">
<div class="empty-icon">&#128203;</div>
<p>Ei asiakkaita vielä.</p>
<p class="empty-hint">Klikkaa "+ Lisää asiakas" lisätäksesi ensimmäisen asiakkaan.</p>
</div>
</div>
<div class="summary-bar">
<span id="customer-count">0 asiakasta</span>
<span id="total-billing">Laskutus yhteensä: 0,00 €/kk</span>
</div>
</div>
<aside class="sidebar-stats">
<div class="stat-card">
<div class="stat-label">Asiakkaita</div>
<div class="stat-value" id="stat-count">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Liittymiä</div>
<div class="stat-value" id="stat-connections">0</div>
</div>
<div class="stat-card highlight">
<div class="stat-label">Laskutus / kk</div>
<div class="stat-value stat-highlight" id="stat-billing">0,00 €</div>
</div>
<div class="stat-card">
<div class="stat-label">Laskutus / vuosi</div>
<div class="stat-value" id="stat-yearly">0,00 €</div>
</div>
<div class="stat-card trivia">
<div class="stat-label">Keskihinta / kk</div>
<div class="stat-value" id="stat-avg-price">-</div>
</div>
<div class="stat-card trivia">
<div class="stat-label">Suosituin postinumero</div>
<div class="stat-value" id="stat-top-zip">-</div>
<div class="stat-sub" id="stat-top-zip-detail"></div>
</div>
<div class="stat-card trivia">
<div class="stat-label">Nopeudet</div>
<div id="stat-speed-table" class="speed-table"></div>
</div>
</aside>
</div>
</div>
</div>
<!-- Tab: Arkisto -->
<div class="tab-content" id="tab-content-archive">
<div class="main-container">
<div class="table-card">
<table id="archive-table">
<thead>
<tr>
<th>Yritys</th>
<th>Liittymiä</th>
<th>Arkistoitu</th>
<th>Arkistoija</th>
<th>Toiminnot</th>
</tr>
</thead>
<tbody id="archive-tbody"></tbody>
</table>
<div id="no-archive" class="empty-state" style="display:none">
<div class="empty-icon">&#128451;</div>
<p>Arkisto on tyhjä.</p>
</div>
</div>
</div>
</div>
<!-- Tab: Muutosloki -->
<div class="tab-content" id="tab-content-changelog">
<div class="main-container">
<div class="table-card">
<table id="changelog-table">
<thead>
<tr>
<th>Aika</th>
<th>Käyttäjä</th>
<th>Toiminto</th>
<th>Asiakas</th>
<th>Lisätiedot</th>
</tr>
</thead>
<tbody id="changelog-tbody"></tbody>
</table>
<div id="no-changelog" class="empty-state" style="display:none">
<div class="empty-icon">&#128220;</div>
<p>Ei lokimerkintöjä.</p>
</div>
</div>
</div>
</div>
<!-- Tab: Käyttäjät (vain admin) -->
<div class="tab-content" id="tab-content-users">
<div class="main-container">
<div style="margin-bottom:1rem;">
<button class="btn-primary" id="btn-add-user">+ Lisää käyttäjä</button>
</div>
<div class="table-card">
<table id="users-table">
<thead>
<tr>
<th>Käyttäjätunnus</th>
<th>Nimi</th>
<th>Sähköposti</th>
<th>Rooli</th>
<th>Luotu</th>
<th>Toiminnot</th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
</div>
</div>
<footer>
<p>CuituNet Intra &mdash; Asiakashallintajärjestelmä</p>
</footer>
</div>
<!-- Asiakas-modal -->
<div id="customer-modal" class="modal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2 id="modal-title">Lisää asiakas</h2>
<button class="modal-close" id="modal-close">&times;</button>
</div>
<form id="customer-form">
<input type="hidden" id="form-id">
<h3>Perustiedot</h3>
<div class="form-grid">
<div class="form-group">
<label for="form-yritys">Yritys *</label>
<input type="text" id="form-yritys" required>
</div>
<div class="form-group">
<label for="form-ytunnus">Y-tunnus</label>
<input type="text" id="form-ytunnus" placeholder="1234567-8">
</div>
</div>
<h3>Liittymät <button type="button" class="btn-add-row" id="btn-add-liittyma">+ Lisää liittymä</button></h3>
<div id="liittymat-container"></div>
<h3>Yhteystiedot</h3>
<div class="form-grid">
<div class="form-group">
<label for="form-yhteyshenkilo">Yhteyshenkilö</label>
<input type="text" id="form-yhteyshenkilo">
</div>
<div class="form-group">
<label for="form-puhelin">Puhelin</label>
<input type="text" id="form-puhelin">
</div>
<div class="form-group">
<label for="form-sahkoposti">Sähköposti</label>
<input type="email" id="form-sahkoposti">
</div>
</div>
<h3>Laskutustiedot</h3>
<div class="form-group" style="margin-bottom:0.75rem;">
<label class="checkbox-label">
<input type="checkbox" id="form-billing-same">
Käytä samoja kuin ensimmäisen liittymän asennusosoite
</label>
</div>
<div id="billing-fields">
<div class="form-grid">
<div class="form-group full-width">
<label for="form-laskutusosoite">Laskutusosoite</label>
<input type="text" id="form-laskutusosoite">
</div>
<div class="form-group">
<label for="form-laskutuspostinumero">Postinumero</label>
<input type="text" id="form-laskutuspostinumero" placeholder="20100">
</div>
<div class="form-group">
<label for="form-laskutuskaupunki">Kaupunki</label>
<input type="text" id="form-laskutuskaupunki">
</div>
</div>
</div>
<div class="form-grid" style="margin-top:0.75rem;">
<div class="form-group">
<label for="form-laskutussahkoposti">Laskutussähköposti</label>
<input type="email" id="form-laskutussahkoposti">
</div>
<div class="form-group">
<label for="form-elaskuosoite">E-laskuosoite</label>
<input type="text" id="form-elaskuosoite" placeholder="esim. 003712345678">
</div>
<div class="form-group">
<label for="form-elaskuvalittaja">E-laskuvälittäjä</label>
<input type="text" id="form-elaskuvalittaja" placeholder="esim. DABAFIHH">
</div>
</div>
<h3>Lisätiedot</h3>
<div class="form-group full-width">
<textarea id="form-lisatiedot" rows="3" placeholder="Vapaamuotoiset muistiinpanot..."></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary" id="form-submit">Tallenna</button>
<button type="button" class="btn-secondary" id="form-cancel">Peruuta</button>
</div>
</form>
</div>
</div>
<!-- Tiedot-modal -->
<div id="detail-modal" class="modal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2 id="detail-title">Asiakkaan tiedot</h2>
<button class="modal-close" id="detail-close">&times;</button>
</div>
<div id="detail-body"></div>
<div class="form-actions">
<button class="btn-primary" id="detail-edit">Muokkaa</button>
<button class="btn-danger" id="detail-delete">Arkistoi</button>
<button class="btn-secondary" id="detail-cancel">Sulje</button>
</div>
</div>
</div>
<!-- Käyttäjä-modal -->
<div id="user-modal" class="modal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2 id="user-modal-title">Lisää käyttäjä</h2>
<button class="modal-close" id="user-modal-close">&times;</button>
</div>
<form id="user-form">
<input type="hidden" id="user-form-id">
<div class="form-grid">
<div class="form-group">
<label for="user-form-username">Käyttäjätunnus *</label>
<input type="text" id="user-form-username" required>
</div>
<div class="form-group">
<label for="user-form-nimi">Nimi</label>
<input type="text" id="user-form-nimi">
</div>
<div class="form-group">
<label for="user-form-email">Sähköposti</label>
<input type="email" id="user-form-email" placeholder="nimi@esimerkki.fi">
</div>
<div class="form-group">
<label for="user-form-password">Salasana <span id="user-pw-hint"></span></label>
<input type="password" id="user-form-password">
</div>
<div class="form-group">
<label for="user-form-role">Rooli</label>
<select id="user-form-role">
<option value="user">Käyttäjä</option>
<option value="admin">Ylläpitäjä</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Tallenna</button>
<button type="button" class="btn-secondary" id="user-form-cancel">Peruuta</button>
</div>
</form>
</div>
</div>
<script src="script.js"></script>
</body>
</html>