Files

289 lines
10 KiB
Go

package httpapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/administration"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
)
type adminHandler struct {
authorizer *PlatformAuthorizer
service *administration.Service
}
func NewAdminHandler(authorizer *PlatformAuthorizer, service *administration.Service) (http.Handler, error) {
if authorizer == nil || service == nil {
return nil, fmt.Errorf("admin HTTP: authorizer and service are required")
}
return &adminHandler{authorizer: authorizer, service: service}, nil
}
func (handler *adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/admin/accounts/groups":
handler.serveGroups(w, r)
case "/api/admin/accounts/password":
handler.servePasswordReset(w, r)
case "/api/admin/accounts":
handler.serveAccounts(w, r)
case "/api/admin/organizations":
handler.serveOrganizations(w, r)
default:
http.NotFound(w, r)
}
}
func (handler *adminHandler) authorize(w http.ResponseWriter, r *http.Request) (identity.Session, administration.Actor, bool) {
session, err := handler.authorizer.Authorize(r, PlatformAdmin)
if err != nil {
writeAdminError(w, err)
return identity.Session{}, administration.Actor{}, false
}
return session, administration.Actor{ID: session.User.ID, Role: administration.Role(session.User.Role), OrganizationID: session.User.OrganizationID}, true
}
func (handler *adminHandler) serveGroups(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
adminMethodNotAllowed(w, http.MethodPost)
return
}
writeAdminJSON(w, http.StatusGone, map[string]any{"error": "平台账号体系不再使用外部部门接口,请直接管理组织。"})
}
func (handler *adminHandler) serveAccounts(w http.ResponseWriter, r *http.Request) {
session, actor, ok := handler.authorize(w, r)
if !ok {
return
}
switch r.Method {
case http.MethodGet:
organizationID := strings.TrimSpace(r.URL.Query().Get("organizationId"))
if actor.Role != administration.RoleSuperAdmin {
organizationID = actor.OrganizationID
}
members, err := handler.service.ListAccounts(r.Context(), actor, administration.AccountFilters{OrganizationID: organizationID, IncludeDisabled: true})
if err != nil {
writeAdminError(w, err)
return
}
organizations, err := handler.service.ListOrganizations(r.Context(), actor)
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"configured": true, "currentOrganizationId": nullableAdminString(organizationID), "organizations": adminOrganizationProjections(organizations), "members": adminAccountProjections(members), "canManageOrganizations": actor.Role == administration.RoleSuperAdmin, "canAssignOrganizationAdmin": actor.Role == administration.RoleSuperAdmin, "canCreateSuperAdmin": actor.Role == administration.RoleSuperAdmin})
case http.MethodPost:
body, ok := readAdminBody(w, r)
if !ok {
return
}
role := administration.Role(adminOptionalString(body["role"]))
if role == "" {
role = administration.RoleUser
}
account, err := handler.service.CreateAccount(r.Context(), actor, administration.CreateAccountInput{Phone: adminRequiredString(body["phone"]), DisplayName: adminRequiredString(body["displayName"]), Password: adminRequiredString(body["password"]), Role: role, OrganizationID: adminOptionalString(body["organizationId"]), LegacySubject: adminOptionalString(body["legacySubject"])})
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusCreated, map[string]any{"ok": true, "user": adminAccountProjection(administration.ProjectAccount(account))})
case http.MethodPatch:
body, ok := readAdminBody(w, r)
if !ok {
return
}
patch := administration.UpdateAccountInput{ClearLoginLock: body["clearLoginLock"] == true}
if value, exists := body["displayName"]; exists {
v := adminOptionalString(value)
patch.DisplayName = &v
}
if value, exists := body["role"]; exists {
v := administration.Role(adminOptionalString(value))
patch.Role = &v
}
if value, exists := body["organizationId"]; exists {
v := adminOptionalString(value)
patch.OrganizationID = &v
}
if value, exists := body["status"]; exists {
v := administration.Status(adminOptionalString(value))
patch.Status = &v
}
account, err := handler.service.UpdateAccount(r.Context(), actor, adminRequiredString(body["userId"]), patch)
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true, "user": adminAccountProjection(administration.ProjectAccount(account))})
case http.MethodPut:
body, ok := readAdminBody(w, r)
if !ok {
return
}
account, err := handler.service.SetAccountStatus(r.Context(), actor, adminRequiredString(body["userId"]), administration.Status(adminRequiredString(body["status"])))
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true, "user": adminAccountProjection(administration.ProjectAccount(account))})
case http.MethodDelete:
body, ok := readAdminBody(w, r)
if !ok {
return
}
archive, err := handler.service.DeleteAccount(r.Context(), actor, adminRequiredString(body["userId"]))
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true, "archivedOwnerId": archive})
default:
adminMethodNotAllowed(w, "GET, POST, PATCH, PUT, DELETE")
}
_ = session
}
func (handler *adminHandler) servePasswordReset(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
adminMethodNotAllowed(w, http.MethodPost)
return
}
_, actor, ok := handler.authorize(w, r)
if !ok {
return
}
body, ok := readAdminBody(w, r)
if !ok {
return
}
account, err := handler.service.ResetPassword(r.Context(), actor, adminRequiredString(body["userId"]), adminRequiredString(body["newPassword"]))
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true, "userId": account.ID})
}
func (handler *adminHandler) serveOrganizations(w http.ResponseWriter, r *http.Request) {
_, actor, ok := handler.authorize(w, r)
if !ok {
return
}
switch r.Method {
case http.MethodGet:
organizations, err := handler.service.ListOrganizations(r.Context(), actor)
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"organizations": adminOrganizationProjections(organizations)})
case http.MethodPost:
body, ok := readAdminBody(w, r)
if !ok {
return
}
organization, err := handler.service.CreateOrganization(r.Context(), actor, adminRequiredString(body["name"]))
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusCreated, map[string]any{"ok": true, "organization": adminOrganizationProjection(administration.ProjectOrganization(organization))})
case http.MethodPatch:
body, ok := readAdminBody(w, r)
if !ok {
return
}
patch := administration.UpdateOrganizationInput{}
if value, exists := body["name"]; exists {
v := adminOptionalString(value)
patch.Name = &v
}
if value, exists := body["status"]; exists {
v := administration.Status(adminOptionalString(value))
patch.Status = &v
}
organization, err := handler.service.UpdateOrganization(r.Context(), actor, adminRequiredString(body["organizationId"]), patch)
if err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true, "organization": adminOrganizationProjection(administration.ProjectOrganization(organization))})
case http.MethodDelete:
body, ok := readAdminBody(w, r)
if !ok {
return
}
if err := handler.service.DeleteOrganization(r.Context(), actor, adminRequiredString(body["organizationId"])); err != nil {
writeAdminError(w, err)
return
}
writeAdminJSON(w, http.StatusOK, map[string]any{"ok": true})
default:
adminMethodNotAllowed(w, "GET, POST, PATCH, DELETE")
}
}
func readAdminBody(w http.ResponseWriter, r *http.Request) (map[string]any, bool) {
var body map[string]any
if json.NewDecoder(r.Body).Decode(&body) != nil {
writeAdminJSON(w, http.StatusBadRequest, map[string]any{"error": "请求内容格式不正确。"})
return nil, false
}
return body, true
}
func adminOptionalString(value any) string {
text, ok := value.(string)
if !ok {
return ""
}
return strings.TrimSpace(text)
}
func adminRequiredString(value any) string { return adminOptionalString(value) }
func nullableAdminString(value string) any {
if value == "" {
return nil
}
return value
}
func adminMethodNotAllowed(w http.ResponseWriter, allow string) {
w.Header().Set("Allow", allow)
w.WriteHeader(http.StatusMethodNotAllowed)
}
func writeAdminError(w http.ResponseWriter, err error) {
status, message := http.StatusInternalServerError, "服务器内部错误。"
var authErr *PlatformAuthError
if errors.As(err, &authErr) {
status, message = authErr.Status, authErr.Message
} else if administration.StatusCode(err) != http.StatusInternalServerError {
status, message = administration.StatusCode(err), err.Error()
}
writeAdminJSON(w, status, map[string]any{"error": message})
}
func writeAdminJSON(w http.ResponseWriter, status int, body any) { writeJSON(w, status, body) }
func adminAccountProjection(a administration.AccountProjection) map[string]any {
return map[string]any{"id": a.ID, "phone": a.Phone, "displayName": a.DisplayName, "role": a.Role, "organizationId": nullableAdminString(a.OrganizationID), "status": a.Status, "createdAt": a.CreatedAt, "lastLoginAt": a.LastLoginAt, "lockedUntil": a.LockedUntil}
}
func adminAccountProjections(values []administration.AccountProjection) []map[string]any {
out := make([]map[string]any, len(values))
for i, value := range values {
out[i] = adminAccountProjection(value)
}
return out
}
func adminOrganizationProjection(o administration.OrganizationProjection) map[string]any {
return map[string]any{"id": o.ID, "name": o.Name, "status": o.Status}
}
func adminOrganizationProjections(values []administration.OrganizationProjection) []map[string]any {
out := make([]map[string]any, len(values))
for i, value := range values {
out[i] = adminOrganizationProjection(value)
}
return out
}