Files
GL.iNet-Yongping.Xie 55854afba8 feat: add device group sorting and auto-create LDAP/OIDC users
- support device group sorting
- auto-create local users for LDAP and OIDC login

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2026-03-24 02:00:09 -07:00

138 lines
3.7 KiB
Go
Executable File

package middleware
import (
"net/http"
"rttys/internal/domain/identity"
"strings"
"rttys/internal/domain/permission"
"rttys/internal/domain/user"
"rttys/internal/http/dto"
"rttys/internal/store/memory"
"github.com/gin-gonic/gin"
)
const PrincipalKey = "principal"
type Principal struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
Role identity.Role `json:"role"`
AuthProvider string `json:"authProvider"`
PermissionKeys []string `json:"permissions"`
}
func MustPrincipal(c *gin.Context) Principal {
v, ok := c.Get(PrincipalKey)
if !ok {
panic("principal missing")
}
return v.(Principal)
}
func Auth(sessionStore *memory.SessionStore, userSvc *user.Service, permSvc *permission.Service) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := GetTraceID(c)
// 1) Prefer Bearer token
token := parseBearer(c.GetHeader("Authorization"))
// 2) Fallback to cookie sid
if token == "" {
sid, err := c.Cookie("sid")
if err == nil {
token = strings.TrimSpace(sid)
}
}
// 3) Fallback to Token header (compat with API docs)
if token == "" {
token = strings.TrimSpace(c.GetHeader("Token"))
}
if token == "" {
dto.Write(c, dto.Err(traceID, dto.CodeAuthRequired, "Please login", nil))
c.Abort()
return
}
sess, ok := sessionStore.Get(token)
if !ok {
dto.Write(c, dto.Err(traceID, dto.CodeAuthExpired, "Session expired", nil))
c.Abort()
return
}
u, err := userSvc.GetByID(c.Request.Context(), sess.UserID)
if err != nil || u == nil {
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "Permission denied", nil))
c.Abort()
return
}
keys, _ := permSvc.ListByRole(c.Request.Context(), u.Role)
perms := make([]string, 0, len(keys))
for _, k := range keys {
perms = append(perms, string(k))
}
displayName := u.Description
if strings.TrimSpace(displayName) == "" {
displayName = u.Username
}
authProvider := u.AuthProvider
if authProvider == "" {
authProvider = "local"
}
c.Set(PrincipalKey, Principal{
UserID: u.ID,
Username: u.Username,
DisplayName: displayName,
Role: u.Role,
AuthProvider: authProvider,
PermissionKeys: perms,
})
c.Next()
}
}
// Require checks capability keys (frontend/back-end single source of truth).
func Require(required permission.Key) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := GetTraceID(c)
p := MustPrincipal(c)
for _, k := range p.PermissionKeys {
if k == string(required) {
c.Next()
return
}
}
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "Permission denied", map[string]any{
"required": string(required),
}))
c.Abort()
}
}
func parseBearer(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
parts := strings.SplitN(v, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return strings.TrimSpace(parts[1])
}
return ""
}
// Write wrapper for gin to keep consistent HTTP 200.
func Write(c *gin.Context, payload any) {
c.JSON(http.StatusOK, payload)
}