413 lines
12 KiB
Go
413 lines
12 KiB
Go
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 ""
|
|
}
|