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:
+28
-2
@@ -27,6 +27,28 @@ in
|
||||
description = "Whether to open the configured port in the firewall.";
|
||||
};
|
||||
|
||||
inviteCodeFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/secrets/puppy-invite-code";
|
||||
description = ''
|
||||
Path to an EnvironmentFile containing the shared registration secret as
|
||||
`PUPPY_INVITE_CODE=...`. Kept out of the Nix store so the code stays
|
||||
secret. When null, registration is disabled (existing accounts can still
|
||||
log in).
|
||||
'';
|
||||
};
|
||||
|
||||
secureCookies = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Mark session cookies Secure. Enable once the service is reached over
|
||||
HTTPS (e.g. behind a TLS-terminating reverse proxy); leave off for plain
|
||||
HTTP on a LAN, or browsers will drop the cookie and logins won't stick.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = serverPkg;
|
||||
@@ -49,12 +71,16 @@ in
|
||||
after = [ "network.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = lib.concatStringsSep " " [
|
||||
ExecStart = lib.concatStringsSep " " ([
|
||||
"${cfg.package}/bin/puppy-tracker-server"
|
||||
"-addr ${cfg.address}:${toString cfg.port}"
|
||||
"-static ${cfg.staticPackage}/share/puppy-tracker"
|
||||
"-data /var/lib/puppy-tracker/puppy.db"
|
||||
];
|
||||
] ++ lib.optional cfg.secureCookies "-secure-cookies");
|
||||
|
||||
# Invite code (registration secret) is read from an env file kept out of
|
||||
# the store, exposed to the server as PUPPY_INVITE_CODE.
|
||||
EnvironmentFile = lib.mkIf (cfg.inviteCodeFile != null) cfg.inviteCodeFile;
|
||||
|
||||
DynamicUser = true;
|
||||
StateDirectory = "puppy-tracker";
|
||||
|
||||
Reference in New Issue
Block a user