Commit Graph
12 Commits
Author SHA1 Message Date
ksmithandClaude Sonnet 5 b26643527d Add per-device active sessions with revocation (#69)
Replaces the stateless signed-cookie session (bare user_id) with a
real server-side sessions table -- the cookie now just carries an
opaque session id, resolved against the DB on every request. Each
session records IP address (respects X-Forwarded-For), a parsed
device label, and last-seen time (throttled updates, not written on
every request).

New GET/DELETE /api/auth/sessions endpoints and an "Active sessions"
section in Profile settings let a user see every device they're
logged in from and revoke one they don't recognize -- including their
own current session, which just signs them out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 20:22:50 -06:00
ksmithandClaude Sonnet 5 bd3e621e9f Support multiple named, saved custom themes per user
Replaces the single custom_theme_colors blob (one palette per user) with
a proper CustomTheme table -- users can now save, name, and switch
between as many custom palettes as they like, not just one.

Data model: users.active_custom_theme_id references whichever saved
CustomTheme (if any) is currently active; theme='custom' + that id
together determine what's rendered. The migration data-migrates any
already-saved single palette into a named CustomTheme row on upgrade,
and best-effort backfills the active one back into the old column shape
on downgrade.

New endpoints under /api/custom-themes: list, create, rename/recolor,
delete (falls back the user to a preset if the deleted theme was
active, so the two theme columns can never disagree), and activate.
UserRead.active_custom_theme is only populated when theme == 'custom'
even though the DB deliberately keeps the id set while a preset is
active, so switching to a preset and back doesn't lose the saved
palette.

ProfileModal now lists saved themes as swatches (click to activate,
pencil to edit -- active or not, trash to delete with a confirm), plus
a "+ New" button that creates, activates, and opens the editor for a
fresh theme immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:53:05 -06:00
ksmithandClaude Sonnet 5 53d16973fb Add custom theme colors, full per-token control (#30)
Adds a 5th "Custom" option to the theme swatch grid alongside the
existing 4 presets, opening a picker for all ~12 CSS custom properties
(backgrounds, borders, text, three accent tiers, highlight, danger) plus
a light/dark toggle for native control rendering.

Persisted as a new users.custom_theme_colors JSONB column, validated
server-side against exactly what a native <input type="color"> can ever
produce. Colors survive switching to a preset and back, since there's no
reason picking a preset for a moment should force redoing every color
pick. Applied at runtime as inline custom properties on :root (presets
stay static CSS) via a shared applyTheme() helper, which is also
responsible for clearing those inline overrides when switching away --
otherwise they'd silently keep winning over whatever preset's own
stylesheet values should apply next.

Live preview on every color change; persists only on explicit "Save
colors" (not per keystroke, since a color input fires continuously while
dragging), and closing the modal without saving reverts the preview back
to whatever's actually persisted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 06:15:49 -06:00
ksmithandClaude Sonnet 5 7ef6cfca65 Add presence indicators and a manual "appear offline" override (#36)
Every avatar in the app (chat messages, room member list, your own
avatar in the top bar/profile, the admin user list, the room-invite
search) now shows a green/red presence dot. Also adds a global
"Appear offline" toggle in the account menu, letting a user lurk in a
room undetected -- it overrides the real connection state everywhere,
not per-room.

Backend: new GlobalPresence (backend/app/ws/global_presence.py), a
cross-instance Redis-backed connection tracker parallel to the
existing per-room Presence, incremented/decremented on WS connect/
disconnect. A new users.appear_offline column (migration
f0f6e494454a) always wins over actual connection state when computing
displayed status. RoomMemberRead gained a computed `status` field;
add_member/change_member_role/list_room_members all compute it via a
shared _member_status() helper. Connect/disconnect and profile
updates (display_name, avatar, appear_offline) all broadcast
member_updated to every room the user belongs to, reusing the
broadcast infrastructure from the earlier avatar-staleness fix, so
chat surfaces update live with no new WS envelope type needed. A new
GET /api/users/online gives the admin list and user-search a snapshot
(deliberately not live -- see backend/app/routers/users.py) for
surfaces where "accurate as of page load" is good enough.

Frontend: UserAvatar renders an optional status dot; every call site
threads status/appear_offline through from whichever data source it
already has (room members, the current user, or the new online-ids
snapshot for admin/search).

4 new backend tests (backend/tests/test_presence.py); existing
broadcast-adjacent WS tests updated to tolerate the new member_updated
noise on connect. Verified end-to-end in the browser with two real
users: presence dot flips live on connect/disconnect via the existing
room-broadcast channel, and the lurk toggle correctly forces offline
while still connected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 15:39:56 -06:00
ksmithandClaude Sonnet 5 1b4d681ad0 Broadcast profile updates so member lists stay live (no reload needed)
Another user's new display name or avatar didn't show up until you
reloaded -- update_profile/upload_avatar/remove_avatar never told
anyone. Same root cause and fix shape as #26 (room_added): the
frontend's already-fetched member list had no way to hear about a
change, since nothing ever pushed one.

Reuses the existing per-room broadcast channel (not the per-user one
#26 added, since this only matters for rooms the affected user shares
with someone currently looking at them) -- publishes member_updated to
every room the user belongs to; ChatShellPage refetches members when
it arrives for the currently open room.

Verified end-to-end in the browser: one user's room-info member list
updated live when another user changed their display name from a
separate session, no reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 11:08:36 -06:00
ksmithandClaude Sonnet 5 afdd573182 Add UI themes (Dark, Light, Midnight, Sunset)
Four preset color themes, switchable from Profile settings with instant
preview and server-side persistence (User.theme, applied via a data-theme
attribute the CSS custom-property overrides in themes.css key off). Dark
stays the existing DarkSingularity palette and default. Light is a genuine
new light-mode design; Midnight is a higher-contrast OLED-friendly dark
variant; Sunset swaps in a warm amber/coral accent family.

Custom theme building (pick-your-own-colors) is out of scope for this
pass -- presets only.

Also: fixed the PATCH /api/auth/me handler to only apply fields actually
present in the request body. It previously always overwrote display_name
unconditionally, which happened to be harmless when it was the only
field on ProfileUpdate but would have silently cleared it on any
theme-only update. And switched two hardcoded hex colors
(.btn-primary:hover, .role-badge-admin) to token-derived color-mix()
values so they adapt across themes instead of staying fixed to the
original cyan/violet palette.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 22:02:38 -06:00
ksmithandClaude Sonnet 5 62e4760c8a Add admin-configurable upload size limits
The 8MB image/file/avatar cap is now a site setting (UploadSettings,
single-row table like SmtpSettings) editable from the Admin Settings tab,
instead of a hardcoded constant. All three upload endpoints read the live
value and interpolate it into their 413 messages. A new GET
/api/uploads/limit endpoint (open to any authenticated user, unlike the
admin-only settings endpoints) lets the composer reject an oversized file
client-side before it ever hits the network, though the server still
enforces the same cap independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:44:05 -06:00
ksmithandClaude Sonnet 5 c78d7454b6 Add generic file attachments to chat messages
Messages can now carry an arbitrary file (MessageFile), parallel to the
existing MessageImage feature rather than a refactor of it. Files serve
with Content-Disposition: attachment to force a download and prevent an
uploaded HTML/SVG from executing same-origin. No content-type allowlist,
same 8MB cap as images for now (a separate size-limit redesign is tracked
as its own issue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 08:02:55 -06:00
ksmith fc96e85014 Add self-service password change, forgot-password flow, and fix admin UI bugs
Users can change their own password from the profile modal, and a
"forgot password" link sends a 15-minute expiring reset link (same
hashed-token pattern as site invites). The forgot-password response is
always generic so it never reveals which emails are registered.

Also fixes two admin-page display bugs found while testing: table row
divider lines that didn't line up across a row (the actions column had
`display: flex` on the <td> itself, breaking it out of normal table-cell
layout -- moved to a child <div>), and the pending-invites list floating
with no visual grouping (now boxed with a label and per-status badges).
2026-08-14 21:06:17 -06:00
ksmith 8ca3e2e23d Add user profile management: display name + avatar upload (Gitea issue #12)
Users can set a display name (shown instead of username in the message
list, room member list, TopBar, and admin Users tab) and upload a real
avatar, replacing the generated color-initial avatars everywhere a user
appears. Avatars are square-cropped and downscaled to 512px, reusing
app/storage.py's upload primitives from image uploads with a new square
option.

Two deliberate divergences from message-image handling, documented in
backend/README.md: the previous avatar file is deleted on replace/remove
(safe since it's strictly one file per user), and avatar serving is not
room-gated and uses a short cache (identity-addressed and mutable, unlike
a message image's permanent content-addressed URL).

Frontend: new ProfileModal reachable from the TopBar account menu;
AuthContext gains updateUser() so a profile change reflects instantly
everywhere without a refetch.
2026-08-14 16:59:56 -06:00
ksmithandClaude Sonnet 5 4aa8ef89c5 Phase 6: Admin portal
Adds is_site_admin-gated site administration: user management (list,
deactivate/reactivate, reset password, promote/demote), room management
(list all rooms including private ones, archive/unarchive, force-transfer
ownership), and an audit log of every admin action.

Backend: User.is_active (deactivation) and Room.is_archived (archive) are
new columns; AdminAuditLog is a new table matching ARCHITECTURE.md's
admin_audit_log design, written to in the same transaction as every
mutating admin action. require_site_admin (dependencies.py) gates all
/api/admin/* routes. get_current_user now rechecks is_active on every
request, so deactivating a user kills their already-open session
immediately, not just future logins. An admin can't deactivate or demote
their own account (the one self-lockout guard included). Archived rooms
drop out of the open-room browse list but stay readable for existing
members.

Frontend: new /admin route (AdminRoute guard, redirects non-admins to
/rooms) with a tabbed Users / Rooms / Audit log / Settings page, plus an
"Admin" link in the account menu for site admins.

Bot/integration management and system settings -- both listed in the
original issue -- are intentionally not here: bot management has nothing
to manage until Phase 7 builds the actual bot data model, and there's no
settings storage or concrete setting to configure yet. Settings has an
empty placeholder tab; bot management is deferred entirely to Phase 7.
Confirmed this scope cut with the repo owner before implementing.

New tests/test_admin.py (14 tests, full suite now 58/58) covers every
admin endpoint's permission gate, the self-action guards, deactivation's
immediate effect on an already-open session, and that every mutating
action produces exactly one audit log row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 07:37:29 -06:00
ksmithandClaude Sonnet 5 99aa029c0d Phase 1: auth, room CRUD, WebSocket chat, PWA frontend
Invite-only FastAPI + SQLAlchemy(async) + Postgres backend (session-cookie
auth via CLI-provisioned accounts, open-room CRUD, single-instance /ws/chat)
and a React + Vite PWA frontend (login, room list, chat view). Backend tests
pass against a local Postgres DB. See README.md and backend/README.md for
setup, and ARCHITECTURE.md for the full phased design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:01:17 -06:00