mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(auth): signup captures browser-detected timezone
v1.4.25 W7 phase 6 — proposal §4 Option C as default-value layer.
The registration form now reads
Intl.DateTimeFormat().resolvedOptions().timeZone and sends it with
the signup payload. The server:
1. Validates the value against Intl.supportedValuesOf via
isValidTimezone().
2. On any invalid value (or absence), resolves the admin-
configured server default (AppSettings.defaultUserTimezone) and
uses that.
3. On no admin default, the resolver falls back to the
'Europe/Berlin' string, which is also the User.timezone column
default — so the worst case still produces the legacy
pre-v1.4.25 behaviour.
The Zod registerSchema gains an optional 'timezone' string field
capped at 64 chars (matches the longest IANA canonical zone
identifier plus headroom). Audit-log entry now carries the resolved
zone so an operator can spot a Tokyo user mis-attributed to Berlin
if browser detection fails.
No new tests — the existing register-route integration coverage is
thin, and the v1.4.25 W7 phase 7 integration test in
Pacific/Auckland covers the full signup → measurement → export
round-trip end-to-end.
This commit is contained in:
1 parent
80544b73cb
commit
46f2cfb9e7
3 files changed
+39
-3
No files matched your search
@@ -15,6 +15,10 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import {
|
||||
isValidTimezone,
|
||||
resolveServerDefaultTimezone,
|
||||
} from "@/lib/tz/resolver";
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const ip = getClientIp(request) ?? "unknown";
|
||||
@@ -59,7 +63,21 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError(parsed.error.issues[0].message, 422);
|
||||
}
|
||||
|
||||
const { email, username, password } = parsed.data;
|
||||
const { email, username, password, timezone: timezoneInput } = parsed.data;
|
||||
|
||||
// v1.4.25 W7 — accept a browser-detected timezone from the
|
||||
// registration form. Validate against the runtime IANA list; on
|
||||
// any invalid value (or absence), fall back to the admin-
|
||||
// configured server default. The `User.timezone` column has a
|
||||
// hard-coded "Europe/Berlin" default at the schema layer, so the
|
||||
// worst-case chain still produces a usable string.
|
||||
let timezone: string;
|
||||
const trimmedTimezone = timezoneInput?.trim() ?? "";
|
||||
if (trimmedTimezone && isValidTimezone(trimmedTimezone)) {
|
||||
timezone = trimmedTimezone;
|
||||
} else {
|
||||
timezone = await resolveServerDefaultTimezone();
|
||||
}
|
||||
|
||||
// Check if email or username already taken (unified message to prevent enumeration)
|
||||
const [existingEmail, existingUsername] = await Promise.all([
|
||||
@@ -91,6 +109,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
username,
|
||||
passwordHash,
|
||||
role,
|
||||
timezone,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -104,7 +123,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
await auditLog("auth.register", {
|
||||
userId: user.id,
|
||||
ipAddress: ip,
|
||||
details: { method: "password" },
|
||||
details: { method: "password", timezone },
|
||||
});
|
||||
|
||||
annotate({ action: { name: "auth.register" } });
|
||||
|
||||
Reference in new issue
Block a user