414 lines
12 KiB
Go
414 lines
12 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"strings"
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
|
|
v1 "github.com/qianfree/team-api/api/admin/v1"
|
|
"github.com/qianfree/team-api/internal/consts"
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
"github.com/qianfree/team-api/internal/logic/common"
|
|
do "github.com/qianfree/team-api/internal/model/do"
|
|
"github.com/qianfree/team-api/internal/utility/crypto"
|
|
"github.com/qianfree/team-api/internal/utility/export"
|
|
)
|
|
|
|
// CreateMember adds a new member to a specified tenant.
|
|
func (s *sAdmin) CreateMember(ctx context.Context, req *v1.AdminMemberCreateReq) (*v1.AdminMemberCreateRes, error) {
|
|
username := strings.TrimSpace(req.Username)
|
|
email := strings.TrimSpace(strings.ToLower(req.Email))
|
|
displayName := req.DisplayName
|
|
if displayName == "" {
|
|
displayName = username
|
|
}
|
|
|
|
// Validate password
|
|
if err := common.ValidatePassword(req.Password); err != nil {
|
|
return nil, common.NewBusinessError(consts.CodePasswordTooWeak, consts.MsgPasswordTooWeak)
|
|
}
|
|
|
|
passwordHash, err := crypto.HashPassword(req.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userID int64
|
|
|
|
err = dao.TntTenants.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
// Check tenant exists and get max_members
|
|
var tenant *struct {
|
|
Id int64 `json:"id"`
|
|
MaxMembers int `json:"max_members"`
|
|
Status string `json:"status"`
|
|
}
|
|
err := tx.Model("tnt_tenants").Ctx(ctx).
|
|
Where("id", req.TenantID).Scan(&tenant)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tenant == nil {
|
|
return common.NewNotFoundError("租户")
|
|
}
|
|
if tenant.Status != "active" {
|
|
return common.NewBadRequestError("租户状态异常,无法添加成员")
|
|
}
|
|
|
|
// Check member limit
|
|
memberCount, err := tx.Model("tnt_users").Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("status", "active").
|
|
Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if memberCount >= tenant.MaxMembers {
|
|
return common.NewBusinessError(consts.CodeMemberLimitReached, consts.MsgMemberLimitReached)
|
|
}
|
|
|
|
// Check username uniqueness within tenant
|
|
count, err := tx.Model("tnt_users").Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("username", username).Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return common.NewBusinessError(consts.CodeUsernameExists, consts.MsgUsernameExists)
|
|
}
|
|
|
|
// Check email uniqueness within tenant
|
|
count, err = tx.Model("tnt_users").Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("email", email).Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return common.NewBadRequestError("该邮箱已被使用")
|
|
}
|
|
|
|
// Create user
|
|
userResult, err := tx.Model("tnt_users").Ctx(ctx).Data(do.TntUsers{
|
|
TenantId: req.TenantID,
|
|
Username: username,
|
|
Email: email,
|
|
PasswordHash: passwordHash,
|
|
DisplayName: displayName,
|
|
Role: req.Role,
|
|
Status: "active",
|
|
}).Insert()
|
|
if err != nil {
|
|
return gerror.Wrapf(err, "创建用户失败")
|
|
}
|
|
userID, err = userResult.LastInsertId()
|
|
if err != nil {
|
|
return gerror.Wrapf(err, "获取用户ID失败")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v1.AdminMemberCreateRes{Id: userID}, nil
|
|
}
|
|
|
|
// ListAllMembers returns a paginated list of all tenant members across all tenants.
|
|
func (s *sAdmin) ListAllMembers(ctx context.Context, req *v1.AdminMemberListReq) (*v1.AdminMemberListRes, error) {
|
|
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
|
|
|
m := dao.TntUsers.Ctx(ctx).
|
|
LeftJoin("tnt_tenants t ON tnt_users.tenant_id = t.id")
|
|
|
|
if req.Keyword != "" {
|
|
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
|
m = m.Where("tnt_users.username LIKE ? OR tnt_users.email LIKE ? OR tnt_users.display_name LIKE ?", keyword, keyword, keyword)
|
|
}
|
|
if req.Status != "" {
|
|
m = m.Where("tnt_users.status", req.Status)
|
|
}
|
|
if req.Role != "" {
|
|
m = m.Where("tnt_users.role", req.Role)
|
|
}
|
|
if req.TenantID > 0 {
|
|
m = m.Where("tnt_users.tenant_id", req.TenantID)
|
|
}
|
|
|
|
total, err := m.Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Rebuild model for data query (Count() modifies internal state)
|
|
m = dao.TntUsers.Ctx(ctx).
|
|
LeftJoin("tnt_tenants t ON tnt_users.tenant_id = t.id")
|
|
if req.Keyword != "" {
|
|
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
|
m = m.Where("tnt_users.username LIKE ? OR tnt_users.email LIKE ? OR tnt_users.display_name LIKE ?", keyword, keyword, keyword)
|
|
}
|
|
if req.Status != "" {
|
|
m = m.Where("tnt_users.status", req.Status)
|
|
}
|
|
if req.Role != "" {
|
|
m = m.Where("tnt_users.role", req.Role)
|
|
}
|
|
if req.TenantID > 0 {
|
|
m = m.Where("tnt_users.tenant_id", req.TenantID)
|
|
}
|
|
|
|
var members []struct {
|
|
Id int64 `json:"id"`
|
|
TenantId int64 `json:"tenant_id"`
|
|
TenantName string `json:"tenant_name"`
|
|
TenantCode string `json:"tenant_code"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
DisplayName string `json:"display_name"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
LastLoginAt *gtime.Time `json:"last_login_at"`
|
|
LastLoginIp string `json:"last_login_ip"`
|
|
FailedAttempts int `json:"failed_attempts"`
|
|
CreatedAt *gtime.Time `json:"created_at"`
|
|
}
|
|
err = m.Fields("tnt_users.id, tnt_users.tenant_id, t.name as tenant_name, t.code as tenant_code, tnt_users.username, tnt_users.email, tnt_users.display_name, tnt_users.role, tnt_users.status, tnt_users.last_login_at, tnt_users.last_login_ip, tnt_users.failed_attempts, tnt_users.created_at").
|
|
OrderDesc("tnt_users.id").
|
|
Page(page, pageSize).
|
|
Scan(&members)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]v1.AdminMemberItem, len(members))
|
|
for i, m := range members {
|
|
items[i] = v1.AdminMemberItem{
|
|
ID: m.Id,
|
|
TenantID: m.TenantId,
|
|
TenantName: m.TenantName,
|
|
TenantCode: m.TenantCode,
|
|
Username: m.Username,
|
|
Email: m.Email,
|
|
DisplayName: m.DisplayName,
|
|
Role: m.Role,
|
|
Status: m.Status,
|
|
LastLoginAt: m.LastLoginAt.String(),
|
|
LastLoginIP: m.LastLoginIp,
|
|
FailedAttempts: m.FailedAttempts,
|
|
CreatedAt: m.CreatedAt.String(),
|
|
}
|
|
}
|
|
|
|
return &v1.AdminMemberListRes{
|
|
List: items,
|
|
Total: int(total),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}, nil
|
|
}
|
|
|
|
// DisableMember disables a tenant member by admin.
|
|
func (s *sAdmin) DisableMember(ctx context.Context, req *v1.AdminMemberDisableReq) (*v1.AdminMemberDisableRes, error) {
|
|
var user *struct {
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
}
|
|
err := dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Scan(&user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, common.NewNotFoundError("成员")
|
|
}
|
|
if user.Status == "disabled" {
|
|
return nil, common.NewBusinessError(consts.CodeBadRequest, "该成员已是禁用状态")
|
|
}
|
|
|
|
// Revoke all active API keys
|
|
dao.ApiKeys.Ctx(ctx).
|
|
Where("user_id", req.Id).
|
|
Where("status", "active").
|
|
Data(do.ApiKeys{
|
|
Status: "disabled",
|
|
}).Update()
|
|
|
|
_, err = dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Data(do.TntUsers{
|
|
Status: "disabled",
|
|
}).Update()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.AdminMemberDisableRes{}, nil
|
|
}
|
|
|
|
// EnableMember re-enables a tenant member by admin.
|
|
func (s *sAdmin) EnableMember(ctx context.Context, req *v1.AdminMemberEnableReq) (*v1.AdminMemberEnableRes, error) {
|
|
var user *struct {
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
}
|
|
err := dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Scan(&user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, common.NewNotFoundError("成员")
|
|
}
|
|
if user.Status != "disabled" {
|
|
return nil, common.NewBusinessError(consts.CodeBadRequest, "该成员不是禁用状态")
|
|
}
|
|
|
|
// Restore disabled API keys
|
|
dao.ApiKeys.Ctx(ctx).
|
|
Where("user_id", req.Id).
|
|
Where("status", "disabled").
|
|
Data(do.ApiKeys{
|
|
Status: "active",
|
|
}).Update()
|
|
|
|
_, err = dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Data(do.TntUsers{
|
|
Status: "active",
|
|
}).Update()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.AdminMemberEnableRes{}, nil
|
|
}
|
|
|
|
// ResetMemberPassword resets a member's password by admin, returns the new random password.
|
|
func (s *sAdmin) ResetMemberPassword(ctx context.Context, req *v1.AdminMemberResetPasswordReq) (*v1.AdminMemberResetPasswordRes, error) {
|
|
var user *struct {
|
|
Status string `json:"status"`
|
|
}
|
|
err := dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Scan(&user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, common.NewNotFoundError("成员")
|
|
}
|
|
|
|
// Generate random password
|
|
passwordBytes := make([]byte, 16)
|
|
if _, err := rand.Read(passwordBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
newPassword := hex.EncodeToString(passwordBytes)[:12]
|
|
|
|
passwordHash, err := crypto.HashPassword(newPassword)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = dao.TntUsers.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Data(do.TntUsers{
|
|
PasswordHash: passwordHash,
|
|
FailedAttempts: 0,
|
|
LockedUntil: nil,
|
|
}).Update()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v1.AdminMemberResetPasswordRes{NewPassword: newPassword}, nil
|
|
}
|
|
|
|
// buildMemberFilters builds the WHERE conditions for member queries.
|
|
func buildMemberFilters(m *gdb.Model, keyword, status, role string, tenantID int64) *gdb.Model {
|
|
if keyword != "" {
|
|
kw := "%" + strings.TrimSpace(keyword) + "%"
|
|
m = m.Where("tnt_users.username LIKE ? OR tnt_users.email LIKE ? OR tnt_users.display_name LIKE ?", kw, kw, kw)
|
|
}
|
|
if status != "" {
|
|
m = m.Where("tnt_users.status", status)
|
|
}
|
|
if role != "" {
|
|
m = m.Where("tnt_users.role", role)
|
|
}
|
|
if tenantID > 0 {
|
|
m = m.Where("tnt_users.tenant_id", tenantID)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ExportMembers exports member list to CSV or Excel.
|
|
func (s *sAdmin) ExportMembers(ctx context.Context, req *v1.AdminMemberExportReq) (*v1.AdminMemberExportRes, error) {
|
|
memberFields := "tnt_users.id, tnt_users.tenant_id, t.name as tenant_name, t.code as tenant_code, tnt_users.username, tnt_users.email, tnt_users.display_name, tnt_users.role, tnt_users.status, tnt_users.last_login_at, tnt_users.last_login_ip, tnt_users.failed_attempts, tnt_users.created_at"
|
|
|
|
config := export.Config{
|
|
Format: req.Format,
|
|
Filename: "成员_" + gtime.Now().Format("Ymd_His"),
|
|
Columns: []export.Column{
|
|
{Field: "id", Header: "ID"},
|
|
{Field: "tenant_name", Header: "租户名称"},
|
|
{Field: "username", Header: "用户名"},
|
|
{Field: "email", Header: "邮箱"},
|
|
{Field: "display_name", Header: "显示名称"},
|
|
{Field: "role", Header: "角色"},
|
|
{Field: "status", Header: "状态"},
|
|
{Field: "last_login_at", Header: "最后登录时间"},
|
|
{Field: "created_at", Header: "创建时间"},
|
|
},
|
|
}
|
|
|
|
return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) {
|
|
offset := 0
|
|
for {
|
|
m := buildMemberFilters(
|
|
dao.TntUsers.Ctx(ctx).LeftJoin("tnt_tenants t ON tnt_users.tenant_id = t.id"),
|
|
req.Keyword, req.Status, req.Role, req.TenantID,
|
|
)
|
|
var batch []struct {
|
|
Id int64 `json:"id"`
|
|
TenantName string `json:"tenant_name"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
DisplayName string `json:"display_name"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
LastLoginAt *gtime.Time `json:"last_login_at"`
|
|
CreatedAt *gtime.Time `json:"created_at"`
|
|
}
|
|
if err := m.Fields(memberFields).OrderDesc("tnt_users.id").Limit(1000).Offset(offset).Scan(&batch); err != nil {
|
|
return
|
|
}
|
|
for _, m := range batch {
|
|
if !yield(map[string]any{
|
|
"id": m.Id,
|
|
"tenant_name": m.TenantName,
|
|
"username": m.Username,
|
|
"email": m.Email,
|
|
"display_name": m.DisplayName,
|
|
"role": m.Role,
|
|
"status": m.Status,
|
|
"last_login_at": m.LastLoginAt.String(),
|
|
"created_at": m.CreatedAt.String(),
|
|
}) {
|
|
return
|
|
}
|
|
}
|
|
if len(batch) < 1000 {
|
|
break
|
|
}
|
|
offset += 1000
|
|
}
|
|
})
|
|
}
|