feat(api): user management and audit log for admins (M1)
- /admin/users: list with search, role filter and cursor pages; create with a one-time temporary password; rename, change role, block; reset password; delete - every change is checked against the role policy, a forbidden field rejects the whole patch; the last active admin is locked in the transaction and cannot be demoted, blocked or deleted - role change and blocking revoke access tokens at once; blocking and password reset also end all sessions - /admin/audit: newest-first journal with actor names - path and query parameter errors are problem+json now Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed4a492dba
commit
f075863f15
13 files changed
+2578
-70
No files matched your search
@@ -0,0 +1,152 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"pipu/api/internal/api/gen"
|
||||
)
|
||||
|
||||
// ListUsers реализует gen.StrictServerInterface.
|
||||
func (h *Handler) ListUsers(ctx context.Context, req gen.ListUsersRequestObject) (gen.ListUsersResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
q, cursor string
|
||||
role *Role
|
||||
)
|
||||
if req.Params.Q != nil {
|
||||
q = *req.Params.Q
|
||||
}
|
||||
if req.Params.Cursor != nil {
|
||||
cursor = *req.Params.Cursor
|
||||
}
|
||||
if req.Params.Role != nil {
|
||||
r := Role(*req.Params.Role)
|
||||
role = &r
|
||||
}
|
||||
users, next, err := h.svc.ListUsers(ctx, p, q, role, cursor, req.Params.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]gen.AdminUser, 0, len(users))
|
||||
for _, u := range users {
|
||||
items = append(items, AdminUserDTO(u))
|
||||
}
|
||||
return gen.ListUsers200JSONResponse{Items: items, NextCursor: optional(next)}, nil
|
||||
}
|
||||
|
||||
// CreateUser реализует gen.StrictServerInterface.
|
||||
func (h *Handler) CreateUser(ctx context.Context, req gen.CreateUserRequestObject) (gen.CreateUserResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)}
|
||||
if req.Body.DisplayName != nil {
|
||||
in.DisplayName = *req.Body.DisplayName
|
||||
}
|
||||
u, password, err := h.svc.CreateUser(ctx, p, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gen.CreateUser201JSONResponse{User: AdminUserDTO(u), TemporaryPassword: password}, nil
|
||||
}
|
||||
|
||||
// UpdateUser реализует gen.StrictServerInterface.
|
||||
func (h *Handler) UpdateUser(ctx context.Context, req gen.UpdateUserRequestObject) (gen.UpdateUserResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked}
|
||||
if req.Body.Role != nil {
|
||||
r := Role(*req.Body.Role)
|
||||
patch.Role = &r
|
||||
}
|
||||
u, err := h.svc.UpdateUser(ctx, p, req.Id, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gen.UpdateUser200JSONResponse(AdminUserDTO(u)), nil
|
||||
}
|
||||
|
||||
// DeleteUser реализует gen.StrictServerInterface.
|
||||
func (h *Handler) DeleteUser(ctx context.Context, req gen.DeleteUserRequestObject) (gen.DeleteUserResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.svc.DeleteUser(ctx, p, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gen.DeleteUser204Response{}, nil
|
||||
}
|
||||
|
||||
// ResetUserPassword реализует gen.StrictServerInterface.
|
||||
func (h *Handler) ResetUserPassword(ctx context.Context, req gen.ResetUserPasswordRequestObject) (gen.ResetUserPasswordResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password, err := h.svc.ResetPassword(ctx, p, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gen.ResetUserPassword200JSONResponse{TemporaryPassword: password}, nil
|
||||
}
|
||||
|
||||
// ListAudit реализует gen.StrictServerInterface.
|
||||
func (h *Handler) ListAudit(ctx context.Context, req gen.ListAuditRequestObject) (gen.ListAuditResponseObject, error) {
|
||||
p, err := principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cursor := ""
|
||||
if req.Params.Cursor != nil {
|
||||
cursor = *req.Params.Cursor
|
||||
}
|
||||
records, next, err := h.svc.ListAudit(ctx, p, cursor, req.Params.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]gen.AuditEntry, 0, len(records))
|
||||
for _, r := range records {
|
||||
e := gen.AuditEntry{
|
||||
Id: r.ID, Action: r.Action, TargetType: r.TargetType, TargetId: r.TargetID, CreatedAt: r.CreatedAt,
|
||||
}
|
||||
if r.ActorID != nil && r.ActorUsername != nil {
|
||||
e.Actor = &gen.AuditActor{Id: *r.ActorID, Username: *r.ActorUsername}
|
||||
}
|
||||
if r.Payload != nil {
|
||||
payload := r.Payload
|
||||
e.Payload = &payload
|
||||
}
|
||||
items = append(items, e)
|
||||
}
|
||||
return gen.ListAudit200JSONResponse{Items: items, NextCursor: optional(next)}, nil
|
||||
}
|
||||
|
||||
// AdminUserDTO переводит пользователя в схему AdminUser.
|
||||
func AdminUserDTO(u User) gen.AdminUser {
|
||||
return gen.AdminUser{
|
||||
Id: u.ID,
|
||||
Username: u.Username,
|
||||
DisplayName: u.DisplayName,
|
||||
Role: gen.Role(u.Role),
|
||||
Locale: u.Locale,
|
||||
IsBlocked: u.IsBlocked,
|
||||
MustChangePassword: u.MustChangePassword,
|
||||
MustChangeCredentials: u.MustChangeCredentials,
|
||||
CreatedAt: u.CreatedAt,
|
||||
LastLoginAt: u.LastLoginAt,
|
||||
}
|
||||
}
|
||||
|
||||
func optional(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
Reference in new issue
Block a user