package server import ( "encoding/json" "net/http" "time" "sync-live/internal/auth" ) type authRequest struct { Username string `json:"username"` Password string `json:"password"` Role string `json:"role,omitempty"` } type authResponse struct { Token string `json:"token"` RefreshToken string `json:"refresh_token,omitempty"` Username string `json:"username"` Role string `json:"role"` Status string `json:"status,omitempty"` ExpiresAt int64 `json:"expires_at,omitempty"` RefreshExpiresAt int64 `json:"refresh_expires_at,omitempty"` } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } var req authRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) return } if req.Username == "" || req.Password == "" { http.Error(w, "username and password required", http.StatusBadRequest) return } access, refresh, user, err := s.auth.LoginWithRefresh(req.Username, req.Password) if err != nil { // fall back to single token for backwards compat token, u, err2 := s.auth.Login(req.Username, req.Password) if err2 != nil { http.Error(w, "login failed: "+err.Error(), http.StatusUnauthorized) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.TTL()), }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(authResponse{ Token: token, Username: u.Username, Role: u.Role, Status: u.Status, ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), }) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: access, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.TTL()), }) http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: refresh, Path: "/api/auth/refresh", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.RefreshTTL()), }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(authResponse{ Token: access, RefreshToken: refresh, Username: user.Username, Role: user.Role, Status: user.Status, ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), RefreshExpiresAt: time.Now().Add(s.auth.JWT.RefreshTTL()).Unix(), }) } func (s *Server) handleRefresh(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } var req struct { RefreshToken string `json:"refresh_token"` } _ = json.NewDecoder(r.Body).Decode(&req) token := req.RefreshToken if token == "" { if c, err := r.Cookie("refresh_token"); err == nil { token = c.Value } } if token == "" { token = r.Header.Get("X-Refresh-Token") } if token == "" { http.Error(w, "refresh_token required", http.StatusBadRequest) return } newAccess, claims, err := s.auth.RefreshAccess(token) if err != nil { http.Error(w, "refresh failed: "+err.Error(), http.StatusUnauthorized) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: newAccess, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.TTL()), }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "token": newAccess, "username": claims.Username, "role": claims.Role, "expires_at": claims.ExpiresAt.Unix(), }) } func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } if !s.cfg.AllowRegister { http.Error(w, "registration disabled", http.StatusForbidden) return } var req authRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) return } if req.Username == "" || req.Password == "" { http.Error(w, "username and password required", http.StatusBadRequest) return } role := req.Role if role == "" { role = auth.RoleViewer } role = auth.NormalizeRole(role) if role == auth.RoleRoot { http.Error(w, "cannot register as root", http.StatusForbidden) return } if role == auth.RoleAdmin || role == auth.RolePublisher { token := extractAuthToken(r) if token != "" { if claims, _, err := s.auth.VerifyToken(token); err == nil && auth.IsAdminOrAbove(claims.Role) { } else { role = auth.RoleViewer } } else { role = auth.RoleViewer } } token, user, err := s.auth.Register(req.Username, req.Password, role) if err != nil { http.Error(w, "register failed: "+err.Error(), http.StatusBadRequest) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(s.auth.JWT.TTL()), }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(authResponse{ Token: token, Username: user.Username, Role: user.Role, Status: user.Status, ExpiresAt: time.Now().Add(s.auth.JWT.TTL()).Unix(), }) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: "token", Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: "", Path: "/api/auth/refresh", HttpOnly: true, MaxAge: -1, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "logged out"}) } func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } token := extractAuthToken(r) if token == "" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } claims, user, err := s.auth.VerifyToken(token) if err != nil { http.Error(w, "invalid token: "+err.Error(), http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "username": user.Username, "role": user.Role, "status": user.Status, "expires_at": claims.ExpiresAt.Unix(), "issued_at": claims.IssuedAt.Unix(), }) } func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } users := s.auth.Store.List() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"users": users}) } func (s *Server) handleUpdateRole(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } var req struct { Username string `json:"username"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) return } if req.Username == "" || req.Role == "" { http.Error(w, "username and role required", http.StatusBadRequest) return } if err := s.auth.UpdateUserRoleAs(claims.Username, req.Username, req.Role); err != nil { http.Error(w, "update failed: "+err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok", "username": req.Username, "role": req.Role}) } func (s *Server) handleBanUser(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } var req struct { Username string `json:"username"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" { http.Error(w, "username required", http.StatusBadRequest) return } if err := s.auth.BanUser(claims.Username, req.Username); err != nil { http.Error(w, "ban failed: "+err.Error(), http.StatusForbidden) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "banned", "username": req.Username}) } func (s *Server) handleUnbanUser(w http.ResponseWriter, r *http.Request) { if s.auth == nil { http.Error(w, "auth not configured", http.StatusInternalServerError) return } token := extractAuthToken(r) claims, _, err := s.auth.VerifyToken(token) if err != nil || !auth.IsAdminOrAbove(claims.Role) { http.Error(w, "forbidden: admin only", http.StatusForbidden) return } var req struct { Username string `json:"username"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" { http.Error(w, "username required", http.StatusBadRequest) return } if err := s.auth.UnbanUser(claims.Username, req.Username); err != nil { http.Error(w, "unban failed: "+err.Error(), http.StatusForbidden) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "unbanned", "username": req.Username}) } func (s *Server) handleAuthCheck(w http.ResponseWriter, r *http.Request) { if s.auth == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"enabled": false}) return } token := extractAuthToken(r) if token == "" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"enabled": true, "authenticated": false, "role": auth.RoleGuest}) return } claims, user, err := s.auth.VerifyToken(token) if err != nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"enabled": true, "authenticated": false, "error": err.Error()}) return } perms := map[string]bool{} for _, c := range []struct { key, obj, act string }{ {"room:list", "room", "list"}, {"room:publish", "room", "publish"}, {"room:subscribe", "room", "subscribe"}, {"room:watch", "room", "watch"}, {"room:stop", "room", "stop"}, {"room:chat", "room", "chat"}, {"room:manage", "room", "manage"}, {"room:delete", "room", "delete"}, {"user:list", "user", "list"}, {"user:manage", "user", "manage"}, {"system:manage", "system", "manage"}, {"config:read", "config", "read"}, {"config:write", "config", "write"}, {"srs:streams", "srs", "streams"}, } { ok, _ := s.auth.Check(claims.Username, claims.Role, c.obj, c.act) perms[c.key] = ok } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "enabled": true, "authenticated": true, "username": user.Username, "role": user.Role, "status": user.Status, "permissions": perms, "is_admin": auth.IsAdminOrAbove(user.Role), "is_root": user.Role == auth.RoleRoot, }) } func extractAuthToken(r *http.Request) string { if h := r.Header.Get("Authorization"); h != "" { if len(h) > 7 && (h[:7] == "Bearer " || h[:7] == "bearer ") { return h[7:] } } if c, err := r.Cookie("token"); err == nil && c.Value != "" { return c.Value } if q := r.URL.Query().Get("token"); q != "" { return q } if h := r.Header.Get("X-Token"); h != "" { return h } return "" }