feat: broadcast username changes to all connected clients via WebSocket

Add user_update event so other clients see profile changes in real-time
without needing to reconnect. Also updates saved credentials in Windows
Credential Manager when the current user changes their username.

Fixes: livekit-session test mock missing unpublishTrack property.
This commit is contained in:
J3vb
2026-04-02 16:33:11 +02:00
parent 371862861f
commit 7cdc2ef1ca
13 changed files with 93 additions and 7 deletions
+1 -1
View File
@@ -748,7 +748,7 @@ func buildCombinedRouter(t *testing.T) (http.Handler, *auth.RateLimiter, string)
r := chi.NewRouter()
api.MountAuthRoutes(r, database, limiter, nil, testTOTPKey)
api.MountProfileRoutes(r, database, limiter, nil)
api.MountProfileRoutes(r, database, limiter, nil, nil)
api.MountInviteRoutes(r, database)
token := loginAndGetToken(t, r, database, "combined1", 2)
+14 -3
View File
@@ -45,13 +45,19 @@ type sessionsListResponse struct {
// ─── Route mounting ──────────────────────────────────────────────────────────
// ProfileBroadcaster is the interface the profile handler uses to notify
// connected WebSocket clients about profile changes.
type ProfileBroadcaster interface {
BroadcastUserUpdate(userID int64, username string, avatar *string)
}
// MountProfileRoutes registers user profile management endpoints.
// All routes require authentication. trustedProxies is used for rate limiting.
func MountProfileRoutes(r chi.Router, database *db.DB, limiter *auth.RateLimiter, trustedProxies []string) {
func MountProfileRoutes(r chi.Router, database *db.DB, limiter *auth.RateLimiter, trustedProxies []string, broadcaster ProfileBroadcaster) {
r.Route("/api/v1/users/me", func(r chi.Router) {
r.Use(AuthMiddleware(database))
r.Patch("/", handleUpdateProfile(database))
r.Patch("/", handleUpdateProfile(database, broadcaster))
r.With(RateLimitMiddleware(limiter, profilePasswordRateLimitPerMinute, time.Minute, trustedProxies)).
Put("/password", handleChangePassword(database, limiter))
@@ -64,7 +70,7 @@ func MountProfileRoutes(r chi.Router, database *db.DB, limiter *auth.RateLimiter
// ─── Handlers ────────────────────────────────────────────────────────────────
// handleUpdateProfile processes PATCH /api/v1/users/me.
func handleUpdateProfile(database *db.DB) http.HandlerFunc {
func handleUpdateProfile(database *db.DB, broadcaster ProfileBroadcaster) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(UserKey).(*db.User)
if !ok || user == nil {
@@ -138,6 +144,11 @@ func handleUpdateProfile(database *db.DB) http.HandlerFunc {
slog.Info("profile updated", "user_id", user.ID, "new_username", req.Username)
_ = database.LogAudit(user.ID, "profile_update", "user", user.ID, "profile updated")
// Broadcast profile change to all connected WebSocket clients.
if broadcaster != nil {
broadcaster.BroadcastUserUpdate(updated.ID, updated.Username, updated.Avatar)
}
writeJSON(w, http.StatusOK, toUserResponse(updated))
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
func buildProfileRouter(database *db.DB) http.Handler {
r := chi.NewRouter()
limiter := auth.NewRateLimiter()
api.MountProfileRoutes(r, database, limiter, nil)
api.MountProfileRoutes(r, database, limiter, nil, nil)
return r
}
+6 -2
View File
@@ -83,8 +83,8 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
// Auth routes: register, login, logout, me.
MountAuthRoutes(r, database, limiter, cfg.Server.TrustedProxies, totpKey)
// Profile routes: update profile, change password, session management.
MountProfileRoutes(r, database, limiter, cfg.Server.TrustedProxies)
// Profile routes are mounted after hub creation (below) so the hub can
// broadcast user_update events for real-time profile changes.
// Invite management routes (require MANAGE_INVITES permission).
MountInviteRoutes(r, database)
@@ -167,6 +167,10 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri
Handle("/livekit/*", http.StripPrefix("/livekit", NewLiveKitProxy(cfg.Voice.LiveKitURL, cfg.Server.AllowedOrigins)))
}
// Profile routes: update profile, change password, session management.
// Mounted after hub creation so the hub can broadcast user_update events.
MountProfileRoutes(r, database, limiter, cfg.Server.TrustedProxies, hub)
// DM (direct message) REST routes — mounted after hub creation so the
// hub can send real-time dm_channel_close events to WebSocket clients.
MountDMRoutes(r, database, hub)