mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-09-16 23:55:08 +00:00
b943a18959
Verify device group sorting. Test automatic user creation for LDAP and OIDC login. Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
249 lines
7.7 KiB
Go
Executable File
249 lines
7.7 KiB
Go
Executable File
package handler
|
|
|
|
import (
|
|
"rttys/internal/domain/identity"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"rttys/internal/domain/user"
|
|
"rttys/internal/http/dto"
|
|
"rttys/internal/http/middleware"
|
|
"rttys/internal/store/memory"
|
|
"rttys/internal/store/sqlite"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type UserHandler struct {
|
|
userSvc *user.Service
|
|
groupRepo *sqlite.GroupRepo
|
|
relationsRepo *sqlite.RelationsRepo
|
|
sessionStore *memory.SessionStore
|
|
}
|
|
|
|
func NewUserHandler(userSvc *user.Service, groupRepo *sqlite.GroupRepo, relationsRepo *sqlite.RelationsRepo, sessionStore *memory.SessionStore) *UserHandler {
|
|
return &UserHandler{
|
|
userSvc: userSvc,
|
|
groupRepo: groupRepo,
|
|
relationsRepo: relationsRepo,
|
|
sessionStore: sessionStore,
|
|
}
|
|
}
|
|
|
|
func (h *UserHandler) ListUsers(c *gin.Context) {
|
|
traceID := middleware.GetTraceID(c)
|
|
|
|
items, err := h.userSvc.List(c.Request.Context())
|
|
if err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", nil))
|
|
return
|
|
}
|
|
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
rank := func(u user.User) int {
|
|
if u.IsSystem {
|
|
return 0
|
|
}
|
|
if u.Role == identity.RoleAdmin {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
ri := rank(items[i])
|
|
rj := rank(items[j])
|
|
if ri != rj {
|
|
return ri < rj
|
|
}
|
|
return false
|
|
})
|
|
|
|
userIDs := make([]int64, 0, len(items))
|
|
for _, u := range items {
|
|
userIDs = append(userIDs, u.ID)
|
|
}
|
|
var groupsByUserID map[int64][]sqlite.UserGroupBrief
|
|
if h.groupRepo != nil {
|
|
groupsByUserID, err = h.groupRepo.ListUserGroupsByUserIDs(c.Request.Context(), userIDs)
|
|
if err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", nil))
|
|
return
|
|
}
|
|
}
|
|
|
|
out := make([]dto.User, 0, len(items))
|
|
for _, u := range items {
|
|
groups := make([]dto.UserGroupRef, 0)
|
|
if list, ok := groupsByUserID[u.ID]; ok {
|
|
for _, g := range list {
|
|
groups = append(groups, dto.UserGroupRef{
|
|
UserGroupID: g.ID,
|
|
UserGroupName: g.Name,
|
|
})
|
|
}
|
|
}
|
|
out = append(out, dto.User{
|
|
ID: u.ID,
|
|
Role: string(u.Role),
|
|
Username: u.Username,
|
|
Description: u.Description,
|
|
IsSystem: u.IsSystem,
|
|
AuthProvider: u.AuthProvider,
|
|
UserGroupList: groups,
|
|
})
|
|
}
|
|
dto.Write(c, dto.Ok(traceID, dto.ListUsersResp{Items: out}))
|
|
}
|
|
|
|
func (h *UserHandler) CreateUser(c *gin.Context) {
|
|
traceID := middleware.GetTraceID(c)
|
|
|
|
var req dto.CreateUserReq
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInvalidArgument, "Invalid argument", map[string]any{
|
|
"field": "username/password",
|
|
}))
|
|
return
|
|
}
|
|
if req.Repassword != "" && req.Repassword != req.Password {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeValidationFailed, "Passwords do not match", map[string]any{
|
|
"field": "repassword",
|
|
}))
|
|
return
|
|
}
|
|
|
|
if req.Role == "" {
|
|
req.Role = "user"
|
|
}
|
|
status := "active"
|
|
|
|
id, err := h.userSvc.CreateUser(c.Request.Context(), req.Username, req.Description, req.Password, req.Role, status)
|
|
if err != nil {
|
|
// best-effort conflict detection
|
|
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeConflict, "Username already exists", nil))
|
|
return
|
|
}
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", nil))
|
|
return
|
|
}
|
|
|
|
if h.relationsRepo != nil {
|
|
if err := h.relationsRepo.SetUserGroups(c.Request.Context(), id, req.UserGroupIDs); err != nil {
|
|
_ = h.userSvc.DeleteUser(c.Request.Context(), id)
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", map[string]any{"detail": err.Error()}))
|
|
return
|
|
}
|
|
}
|
|
|
|
dto.Write(c, dto.Ok(traceID, dto.CreateUserResp{}))
|
|
}
|
|
|
|
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
|
traceID := middleware.GetTraceID(c)
|
|
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInvalidArgument, "Invalid argument", map[string]any{
|
|
"field": "id",
|
|
}))
|
|
return
|
|
}
|
|
|
|
var req dto.UpdateUserReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInvalidArgument, "Invalid argument", nil))
|
|
return
|
|
}
|
|
if req.Password != nil && req.Repassword != nil && *req.Password != *req.Repassword {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeValidationFailed, "Passwords do not match", map[string]any{
|
|
"field": "repassword",
|
|
}))
|
|
return
|
|
}
|
|
|
|
p := middleware.MustPrincipal(c)
|
|
|
|
target, err := h.userSvc.FindByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeNotFound, "Not found", nil))
|
|
return
|
|
}
|
|
if target.IsSystem {
|
|
req.Username = nil
|
|
req.Role = nil
|
|
req.Password = nil
|
|
req.Repassword = nil
|
|
}
|
|
// Users cannot change their own role
|
|
if id == p.UserID {
|
|
req.Role = nil
|
|
}
|
|
// External users (OIDC/LDAP): username and password are managed by the IdP
|
|
if target.AuthProvider != "" && target.AuthProvider != "local" {
|
|
req.Username = nil
|
|
req.Password = nil
|
|
req.Repassword = nil
|
|
}
|
|
|
|
if err := h.userSvc.UpdateUser(c.Request.Context(), id, req.Username, req.Description, req.Password, req.Role, nil); err != nil {
|
|
if strings.Contains(strings.ToLower(err.Error()), "not found") {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeNotFound, "Not found", nil))
|
|
return
|
|
}
|
|
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeConflict, "Username already exists", nil))
|
|
return
|
|
}
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", nil))
|
|
return
|
|
}
|
|
|
|
if h.relationsRepo != nil && req.UserGroupIDs != nil {
|
|
if err := h.relationsRepo.SetUserGroups(c.Request.Context(), id, *req.UserGroupIDs); err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", map[string]any{"detail": err.Error()}))
|
|
return
|
|
}
|
|
}
|
|
|
|
dto.Write(c, dto.Ok(traceID, struct{}{}))
|
|
}
|
|
|
|
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
|
traceID := middleware.GetTraceID(c)
|
|
p := middleware.MustPrincipal(c)
|
|
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInvalidArgument, "Invalid argument", map[string]any{
|
|
"field": "id",
|
|
}))
|
|
return
|
|
}
|
|
|
|
if id == p.UserID {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "Cannot delete your own account", nil))
|
|
return
|
|
}
|
|
|
|
u, err := h.userSvc.FindByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeNotFound, "Not found", nil))
|
|
return
|
|
}
|
|
if u.IsSystem {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "System user cannot be deleted", nil))
|
|
return
|
|
}
|
|
|
|
if h.sessionStore != nil {
|
|
h.sessionStore.DeleteByUserID(id)
|
|
}
|
|
|
|
if err := h.userSvc.DeleteUser(c.Request.Context(), id); err != nil {
|
|
dto.Write(c, dto.Err(traceID, dto.CodeInternalError, "Internal error", nil))
|
|
return
|
|
}
|
|
dto.Write(c, dto.Ok(traceID, dto.DeleteUserResp{}))
|
|
}
|