chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
This commit is contained in:
@@ -17,7 +17,7 @@ const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
||||
* table (so an S3↔filesystem swap carries it correctly), and writes
|
||||
* the file id into `user_profiles.avatar_file_id`.
|
||||
*
|
||||
* Files are scoped to the user's CURRENT port — the rep can't end up
|
||||
* Files are scoped to the user's CURRENT port - the rep can't end up
|
||||
* with an avatar that's only visible from one port. (Avatars render
|
||||
* via the GET handler below, which presigns by id regardless of port.)
|
||||
*/
|
||||
@@ -98,7 +98,7 @@ export const POST = withAuth(async (req, ctx) => {
|
||||
.where(eq(userProfiles.userId, ctx.userId));
|
||||
|
||||
if (priorAvatarId && priorAvatarId !== record.id) {
|
||||
// Best-effort delete — a stale-blob failure shouldn't fail the
|
||||
// Best-effort delete - a stale-blob failure shouldn't fail the
|
||||
// new-avatar response. deleteFile handles ref-check + blob
|
||||
// delete + audit so a referenced file (somehow) is safe.
|
||||
try {
|
||||
@@ -111,7 +111,7 @@ export const POST = withAuth(async (req, ctx) => {
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err, priorAvatarId, userId: ctx.userId },
|
||||
'avatar replace: failed to clean up prior avatar file — orphan blob possible',
|
||||
'avatar replace: failed to clean up prior avatar file - orphan blob possible',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
/**
|
||||
* Public confirmation endpoint — clicked from the email sent to the
|
||||
* Public confirmation endpoint - clicked from the email sent to the
|
||||
* NEW address. Applies the email change atomically and redirects the
|
||||
* user back to /settings with a success flag.
|
||||
*
|
||||
|
||||
@@ -45,7 +45,7 @@ export const PATCH = withAuth(async (req, ctx) => {
|
||||
}
|
||||
|
||||
if (!REQUIRES_VERIFICATION) {
|
||||
// Instant change — dev only.
|
||||
// Instant change - dev only.
|
||||
const [updated] = await db
|
||||
.update(user)
|
||||
.set({ email, emailVerified: false, updatedAt: new Date() })
|
||||
@@ -67,7 +67,7 @@ export const PATCH = withAuth(async (req, ctx) => {
|
||||
return NextResponse.json({ data: { email: updated.email, instant: true } });
|
||||
}
|
||||
|
||||
// Verification flow — generate a single-use token, hash it, persist.
|
||||
// Verification flow - generate a single-use token, hash it, persist.
|
||||
const rawToken = crypto.randomBytes(32).toString('base64url');
|
||||
const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||
const expiresAt = new Date(Date.now() + VERIFY_TOKEN_TTL_MINUTES * 60 * 1000);
|
||||
@@ -111,7 +111,7 @@ export const PATCH = withAuth(async (req, ctx) => {
|
||||
const confirmBody = `
|
||||
<p style="margin-bottom:16px;">Hi,</p>
|
||||
<p style="margin-bottom:16px;">You (or someone using your account) requested to change the sign-in email on your ${appName} account from <strong>${safeOldEmail}</strong> to <strong>${safeNewEmail}</strong>.</p>
|
||||
<p style="margin-bottom:16px;"><a href="${safeUrl(confirmUrl)}" style="color:#2563eb;font-weight:600;">Click here to confirm this change</a> — the link expires in ${VERIFY_TOKEN_TTL_MINUTES} minutes.</p>
|
||||
<p style="margin-bottom:16px;"><a href="${safeUrl(confirmUrl)}" style="color:#2563eb;font-weight:600;">Click here to confirm this change</a> - the link expires in ${VERIFY_TOKEN_TTL_MINUTES} minutes.</p>
|
||||
<p style="color:#64748b;">If you didn't request this, ignore this email.</p>
|
||||
`;
|
||||
const cancelBody = `
|
||||
|
||||
@@ -10,7 +10,7 @@ import { errorResponse } from '@/lib/errors';
|
||||
* one-time reset token and dispatches the email via the
|
||||
* `sendResetPassword` callback configured in src/lib/auth/index.ts.
|
||||
*
|
||||
* The email always goes to the user's CURRENT account email — no way
|
||||
* The email always goes to the user's CURRENT account email - no way
|
||||
* to redirect to a different inbox here, so the endpoint is safe even
|
||||
* if a session is hijacked (the attacker can't move the reset email
|
||||
* to themselves).
|
||||
|
||||
@@ -13,14 +13,14 @@ import { errorResponse } from '@/lib/errors';
|
||||
*
|
||||
* M-NEW-1: this endpoint INTENTIONALLY skips `withAuth`'s port-context
|
||||
* requirement. Callers hit /me/ports specifically to LEARN which ports
|
||||
* they have access to — they can't have selected one yet, so the
|
||||
* they have access to - they can't have selected one yet, so the
|
||||
* X-Port-Id header is by definition absent on the first call. Pre-fix
|
||||
* this meant non-super-admins got a 400 "Port context required" and
|
||||
* the client had to special-case the response shape.
|
||||
*
|
||||
* Auth is still enforced (session check); permissions logic skipped
|
||||
* because the endpoint exposes only IDs+slugs+names of ports the user
|
||||
* is already a member of — same surface area as a `me` profile read.
|
||||
* is already a member of - same surface area as a `me` profile read.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
|
||||
@@ -18,12 +18,12 @@ const updateProfileSchema = z.object({
|
||||
* Optional sign-in alias. `null` clears the existing value; a string
|
||||
* must match the 2–30 lowercase shape pinned by USERNAME_REGEX (also
|
||||
* enforced by `chk_user_profiles_username_shape` in migration 0054).
|
||||
* Uniqueness is checked below before the UPDATE — collisions surface
|
||||
* Uniqueness is checked below before the UPDATE - collisions surface
|
||||
* as a 409 with a friendly message.
|
||||
*/
|
||||
username: z.union([z.string().transform((s) => s.trim().toLowerCase()), z.null()]).optional(),
|
||||
phone: z.string().nullable().optional(),
|
||||
// Refuse `javascript:` / `data:` schemes — z.string().url() lets them
|
||||
// Refuse `javascript:` / `data:` schemes - z.string().url() lets them
|
||||
// through and `<a href={avatarUrl}>` would otherwise be a stored-XSS
|
||||
// vector if any future renderer treated the value as a link.
|
||||
avatarUrl: z
|
||||
@@ -32,7 +32,7 @@ const updateProfileSchema = z.object({
|
||||
.refine((u) => /^https?:\/\//i.test(u), 'must be an http(s) URL')
|
||||
.nullable()
|
||||
.optional(),
|
||||
// Strict allow-list — no `.passthrough()` here. The previous schema let
|
||||
// Strict allow-list - no `.passthrough()` here. The previous schema let
|
||||
// arbitrary client-supplied keys survive validation and persist into
|
||||
// `userProfiles.preferences` JSONB unbounded; auditor-E3 §28 caught this.
|
||||
// Add new keys here as the UI surfaces them rather than letting the
|
||||
@@ -51,7 +51,7 @@ const updateProfileSchema = z.object({
|
||||
// be set via hand-rolled SQL because the allow-list at line
|
||||
// 154 silently stripped unknown keys.
|
||||
defaultPortId: z.string().uuid().optional(),
|
||||
// Per-table column visibility. Keyed by entity type — entries
|
||||
// Per-table column visibility. Keyed by entity type - entries
|
||||
// with an empty `hiddenColumns` mean "all visible". The validator
|
||||
// caps total entries / IDs so a malicious client can't bloat the
|
||||
// 8 KB preferences blob; see merge step below for the byte cap.
|
||||
@@ -65,7 +65,7 @@ const updateProfileSchema = z.object({
|
||||
.strict(),
|
||||
)
|
||||
.optional(),
|
||||
// Phase 4 — per-user default reminder firing time-of-day. HH:MM in
|
||||
// Phase 4 - per-user default reminder firing time-of-day. HH:MM in
|
||||
// 24h local clock (validated below). Per-reminder dueAt overrides
|
||||
// this; this is only the dialog's default when the rep doesn't
|
||||
// pick an explicit time. Server clamps to '00:00'–'23:59'.
|
||||
@@ -158,7 +158,7 @@ export const PATCH = withAuth(async (req, ctx: AuthContext) => {
|
||||
}
|
||||
}
|
||||
if (body.preferences !== undefined) {
|
||||
// Allow-list — only retain keys defined in the strict schema. Pre-
|
||||
// Allow-list - only retain keys defined in the strict schema. Pre-
|
||||
// strict rows may carry extra keys from when the schema was
|
||||
// .passthrough(); the merge prunes them so legacy bloat doesn't
|
||||
// accumulate forever, and a future schema regression that tries
|
||||
@@ -169,7 +169,7 @@ export const PATCH = withAuth(async (req, ctx: AuthContext) => {
|
||||
'timezone',
|
||||
'tablePreferences',
|
||||
'defaultPortId',
|
||||
// Phase 4 — reminder default firing time.
|
||||
// Phase 4 - reminder default firing time.
|
||||
'digestTimeOfDay',
|
||||
]);
|
||||
const existing = (profile.preferences as Record<string, unknown>) ?? {};
|
||||
@@ -178,7 +178,7 @@ export const PATCH = withAuth(async (req, ctx: AuthContext) => {
|
||||
ALLOWED_PREF_KEYS.has(k),
|
||||
),
|
||||
);
|
||||
// Hard cap on the merged JSONB — defense in depth against any
|
||||
// Hard cap on the merged JSONB - defense in depth against any
|
||||
// future schema growth that might re-introduce free-form keys.
|
||||
const serialized = JSON.stringify(merged);
|
||||
if (Buffer.byteLength(serialized, 'utf8') > 8 * 1024) {
|
||||
@@ -190,7 +190,7 @@ export const PATCH = withAuth(async (req, ctx: AuthContext) => {
|
||||
// concurrency-auditor M-2: pre-check at line 132-139 is TOCTOU
|
||||
// against `idx_user_profiles_username_unique`. Two concurrent claims
|
||||
// on the same username will see "available" in their own pre-check
|
||||
// and the loser's UPDATE fails with 23505 — surface that as
|
||||
// and the loser's UPDATE fails with 23505 - surface that as
|
||||
// ConflictError rather than letting it bubble as a generic 500.
|
||||
let updated;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user