Add accounts and multi-tenancy
Every event, profile and photo is now scoped to a signed-in account, so separate people can track separate puppies on one server. Server: - users + sessions tables; bcrypt passwords; random session tokens stored hashed and set as an HttpOnly cookie. Middleware gates /api/* behind a valid session. - register/login/logout/me endpoints. Registration requires a shared invite code (-invite-code / PUPPY_INVITE_CODE); empty disables it. - events, config and photos are keyed by user_id; the sync upsert guards against cross-user overwrites and reads are scoped, so accounts are isolated. Photos live under photos/<user_id>/ and are only served to their owner. - in-place schema migration adds user_id and reshapes config; legacy single-tenant data (including imported events.json) is parked ownerless and adopted by the first account to register. Client: - login/register gate in front of the app; the tracker only boots once the session check resolves. localStorage is namespaced per user. - 401s bounce back to login; an offline reload falls back to the last cached session so offline-first still works. Logout clears the session and reloads. Deployment: - module.nix gains inviteCodeFile (secret via EnvironmentFile) and secureCookies options. Verified end to end (curl + a headless-browser run of the auth flow): isolation between accounts, invite enforcement, first-user adoption, photo ownership, and session persistence across reload.
This commit is contained in:
@@ -11,6 +11,33 @@
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Login / register gate. Shown until the session check succeeds; the app
|
||||
(#app) stays hidden behind it so no puppy data paints while logged out. -->
|
||||
<div id="auth-screen" class="auth-screen" hidden>
|
||||
<div class="auth-card">
|
||||
<h1>🐶 Puppy Tracker</h1>
|
||||
<p class="auth-sub" id="auth-sub">Sign in to continue</p>
|
||||
<form id="auth-form">
|
||||
<label>Email
|
||||
<input type="email" id="auth-email" autocomplete="username" required />
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" id="auth-password" autocomplete="current-password" required minlength="8" />
|
||||
</label>
|
||||
<label id="auth-invite-field" hidden>Invite code
|
||||
<input type="text" id="auth-invite" autocomplete="off" placeholder="Ask the owner for this" />
|
||||
</label>
|
||||
<p id="auth-error" class="auth-error" hidden></p>
|
||||
<button type="submit" id="auth-submit">Sign in</button>
|
||||
</form>
|
||||
<p class="auth-toggle">
|
||||
<span id="auth-toggle-text">No account yet?</span>
|
||||
<button type="button" id="auth-toggle-btn" class="linklike">Create one</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" hidden>
|
||||
<header>
|
||||
<div class="title">
|
||||
<h1 id="app-title">🐶 Puppy Tracker</h1>
|
||||
@@ -18,6 +45,7 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" id="settings-btn" class="ghost icon-btn" aria-label="Settings" title="Settings">⚙️</button>
|
||||
<button type="button" id="logout-btn" class="ghost icon-btn" aria-label="Log out" title="Log out">🚪</button>
|
||||
<div id="online-status" class="status-pill"></div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -149,6 +177,7 @@
|
||||
<p id="empty-state" class="empty">No events logged for this day.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div><!-- /#app -->
|
||||
|
||||
<dialog id="settings-dialog">
|
||||
<form method="dialog" id="settings-form">
|
||||
|
||||
Reference in New Issue
Block a user