fix(auth):修复管理员权限问题
This commit is contained in:
1 parent
9601678d46
commit
fa70d8790c
11 files changed
+180
-80
No files matched your search
@@ -47,6 +47,8 @@ type SensitiveLogListReq struct {
|
||||
PageSize int `json:"page_size" d:"20"`
|
||||
UserID int `json:"user_id" dc:"用户ID"`
|
||||
ResourceType string `json:"resource_type" dc:"资源类型"`
|
||||
StartDate string `json:"start_date" dc:"开始日期 YYYY-MM-DD"`
|
||||
EndDate string `json:"end_date" dc:"结束日期 YYYY-MM-DD"`
|
||||
}
|
||||
|
||||
type SensitiveLogListRes struct {
|
||||
|
||||
@@ -20,36 +20,12 @@ import (
|
||||
func (s *sAdmin) ListUsers(ctx context.Context, req *v1.AdminUserListReq) (*v1.AdminUserListRes, error) {
|
||||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||||
|
||||
m := dao.SysAdminUsers.Ctx(ctx)
|
||||
|
||||
if req.Keyword != "" {
|
||||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||||
m = m.Where("username LIKE ? OR email LIKE ?", keyword, keyword)
|
||||
}
|
||||
if req.Role != "" {
|
||||
m = m.Where("role", req.Role)
|
||||
}
|
||||
if req.Status != "" {
|
||||
m = m.Where("status", req.Status)
|
||||
}
|
||||
|
||||
total, err := m.Count()
|
||||
total, err := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Rebuild model for data query (Count() modifies internal state)
|
||||
m = dao.SysAdminUsers.Ctx(ctx)
|
||||
if req.Keyword != "" {
|
||||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||||
m = m.Where("username LIKE ? OR email LIKE ?", keyword, keyword)
|
||||
}
|
||||
if req.Role != "" {
|
||||
m = m.Where("role", req.Role)
|
||||
}
|
||||
if req.Status != "" {
|
||||
m = m.Where("status", req.Status)
|
||||
}
|
||||
m := buildUserFilters(dao.SysAdminUsers.Ctx(ctx), req.Keyword, req.Role, req.Status)
|
||||
|
||||
var users []struct {
|
||||
Id int64 `json:"id"`
|
||||
@@ -156,6 +132,22 @@ func (s *sAdmin) CreateUser(ctx context.Context, req *v1.AdminUserCreateReq) (*v
|
||||
|
||||
// UpdateUser updates an admin user.
|
||||
func (s *sAdmin) UpdateUser(ctx context.Context, req *v1.AdminUserUpdateReq) (*v1.AdminUserUpdateRes, error) {
|
||||
// Prevent modification of super_admin accounts
|
||||
var targetUser *struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
err := dao.SysAdminUsers.Ctx(ctx).
|
||||
Where("id", req.Id).Scan(&targetUser)
|
||||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if targetUser == nil {
|
||||
return nil, common.NewNotFoundError("管理员")
|
||||
}
|
||||
if targetUser.Role == "super_admin" {
|
||||
return nil, common.NewBadRequestError("不能修改超级管理员信息")
|
||||
}
|
||||
|
||||
data := do.SysAdminUsers{}
|
||||
if req.DisplayName != nil {
|
||||
data.DisplayName = *req.DisplayName
|
||||
@@ -180,7 +172,7 @@ func (s *sAdmin) UpdateUser(ctx context.Context, req *v1.AdminUserUpdateReq) (*v
|
||||
data.Role = *req.Role
|
||||
}
|
||||
|
||||
_, err := dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Update(data)
|
||||
_, err = dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Update(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,15 +204,16 @@ func (s *sAdmin) DeleteUser(ctx context.Context, req *v1.AdminUserDeleteReq) (*v
|
||||
return nil, common.NewBadRequestError("不能删除超级管理员")
|
||||
}
|
||||
|
||||
// Revoke all sessions
|
||||
common.RevokeAllSessions(ctx, "admin", req.Id)
|
||||
|
||||
// Delete user
|
||||
// Delete user first, then revoke sessions to avoid leaving the user
|
||||
// in a state where sessions are gone but the user still exists.
|
||||
_, err = dao.SysAdminUsers.Ctx(ctx).Where("id", req.Id).Delete()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Revoke all sessions
|
||||
common.RevokeAllSessions(ctx, "admin", req.Id)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ var validAuditLevels = map[string]bool{
|
||||
AuditLevelNone: true,
|
||||
}
|
||||
|
||||
// GetAuditConfig retrieves the global audit level from sys_options.
|
||||
// GetGlobalAuditConfig retrieves the global audit level from sys_options.
|
||||
// Package-level wrapper for use by tenant package.
|
||||
func GetAuditConfig(ctx context.Context) (string, error) {
|
||||
func GetGlobalAuditConfig(ctx context.Context) (string, error) {
|
||||
return common.GetAuditLevel(ctx), nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ func (s *sAdmin) UpdateAuditConfig(ctx context.Context, req *v1.AuditConfigUpdat
|
||||
}
|
||||
|
||||
// queryPage 使用 GoFrame ORM 分页查询,返回 []map[string]any。
|
||||
// 注意:该函数使用 g.DB().Model(table) 直接操作动态表名而非 DAO,
|
||||
// 适用于通用分页工具场景(多表共用)。固定表查询应优先使用对应的 DAO。
|
||||
// 分开调用 Count 和 All 避免 ScanAndCount 对 map[string]any 的 bug。
|
||||
func queryPage(ctx context.Context, table, fields, where, orderBy string, page, pageSize int, args ...any) ([]map[string]any, int, error) {
|
||||
m := g.DB().Ctx(ctx).Model(table).Safe()
|
||||
@@ -198,6 +200,14 @@ func (s *sAdmin) ListSensitiveAccessLogs(ctx context.Context, req *v1.SensitiveL
|
||||
conditions = append(conditions, "resource_type = ?")
|
||||
args = append(args, req.ResourceType)
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
args = append(args, req.StartDate+" 00:00:00")
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
conditions = append(conditions, "created_at <= ?")
|
||||
args = append(args, req.EndDate+" 23:59:59")
|
||||
}
|
||||
|
||||
where := strings.Join(conditions, " AND ")
|
||||
items, total, err := queryPage(ctx,
|
||||
@@ -311,6 +321,9 @@ func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditL
|
||||
total = countResult[0]["total"].Int()
|
||||
}
|
||||
|
||||
// NOTE: fromClause and whereClause are constructed from hardcoded strings
|
||||
// and parameterized conditions (using ? placeholders). No user input is
|
||||
// directly interpolated into the SQL template.
|
||||
dataSQL := fmt.Sprintf(
|
||||
`SELECT a.id, a.tenant_id, COALESCE(tn.name, '') AS tenant_name, a.user_id, COALESCE(t.username, '') AS username, a.project_id, COALESCE(p.name, '') AS project_name, a.api_key_id, COALESCE(ak.name, '') AS api_key_name, a.request_id, a.method, a.path, a.query_params, a.status_code, a.client_ip, a.user_agent, a.latency_ms, a.first_token_ms, a.created_at, a.updated_at, a.audit_level
|
||||
FROM %s%s ORDER BY a.created_at DESC LIMIT ? OFFSET ?`,
|
||||
@@ -417,7 +430,22 @@ func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogEx
|
||||
offset := 0
|
||||
for {
|
||||
where, args := buildOpLogWhere()
|
||||
items, _, err := queryPage(ctx, "aud_operation_logs", selectFields, where, "created_at DESC", offset/1000+1, 1000, args...)
|
||||
q := g.DB().Ctx(ctx).Model("aud_operation_logs").Fields(selectFields).OrderDesc("created_at").Offset(offset).Limit(1000)
|
||||
if where != "" {
|
||||
q = q.Where(where, args...)
|
||||
}
|
||||
result, err := q.All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
items := make([]map[string]any, 0, len(result))
|
||||
for _, row := range result {
|
||||
m := make(map[string]any, len(row))
|
||||
for k, v := range row {
|
||||
m[k] = v.Val()
|
||||
}
|
||||
items = append(items, m)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,13 +104,15 @@ func (s *sAdmin) Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLog
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update last login
|
||||
dao.SysAdminUsers.Ctx(ctx).
|
||||
// Update last login (non-critical, log errors only)
|
||||
if _, err := dao.SysAdminUsers.Ctx(ctx).
|
||||
Where("id", user.Id).
|
||||
Data(do.SysAdminUsers{
|
||||
LastLoginAt: gtime.Now(),
|
||||
LastLoginIp: ipAddress,
|
||||
}).Update()
|
||||
}).Update(); err != nil {
|
||||
g.Log().Warningf(ctx, "update last_login for user %d: %v", user.Id, err)
|
||||
}
|
||||
|
||||
// Record login history
|
||||
_ = common.RecordLoginHistory(ctx, "admin", user.Id, 0, "password", ipAddress, ua, deviceFP, true, "")
|
||||
@@ -195,12 +197,12 @@ func (s *sAdmin) Refresh(ctx context.Context, req *v1.AdminRefreshReq) (*v1.Admi
|
||||
|
||||
// Fetch current role from user table
|
||||
var adminUser *entity.SysAdminUsers
|
||||
err = dao.SysAdminUsers.Ctx(ctx).Where("id", session.UserId).Fields("role").Scan(&adminUser)
|
||||
err = dao.SysAdminUsers.Ctx(ctx).Where("id", session.UserId).Fields("role, status").Scan(&adminUser)
|
||||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if adminUser == nil {
|
||||
return nil, common.NewUnauthorizedError("用户不存在")
|
||||
if adminUser == nil || adminUser.Status != "active" {
|
||||
return nil, common.NewUnauthorizedError("账号已被禁用或不存在")
|
||||
}
|
||||
|
||||
// Generate new token pair with same session ID and jti
|
||||
@@ -339,7 +341,7 @@ func (s *sAdmin) ChangePassword(ctx context.Context, req *v1.AdminChangePassword
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, common.NewUnauthorizedError("用户不存在")
|
||||
return nil, common.NewUnauthorizedError("账号已被禁用或不存在")
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
|
||||
@@ -2,14 +2,14 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/logic/common"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// CronJobList returns all registered cron jobs with their last execution status.
|
||||
@@ -121,7 +121,7 @@ func (s *sAdmin) CronJobExecutions(ctx context.Context, req *v1.CronJobExecution
|
||||
func (s *sAdmin) CronJobTrigger(ctx context.Context, req *v1.CronJobTriggerReq) (*v1.CronJobTriggerRes, error) {
|
||||
err := common.GetCronScheduler().TriggerJob(ctx, req.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("触发任务失败: %w", err)
|
||||
return nil, gerror.Wrapf(err, "触发定时任务失败")
|
||||
}
|
||||
return &v1.CronJobTriggerRes{Message: "任务已触发"}, nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@@ -46,7 +47,7 @@ func (s *sAdmin) DataGovernanceExport(ctx context.Context, req *v1.DataGovernanc
|
||||
count, err := dao.TskTasks.Ctx(ctx).
|
||||
Where("handler", "data_export").
|
||||
Where("status", "pending").
|
||||
Where("payload->>'tenant_id'", fmt.Sprintf("%d", req.TenantID)).
|
||||
Where("(payload->>'tenant_id')::bigint = ?", req.TenantID).
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -80,7 +81,7 @@ func (s *sAdmin) DataGovernanceDeletion(ctx context.Context, req *v1.DataGoverna
|
||||
count, err := dao.TskTasks.Ctx(ctx).
|
||||
Where("handler", "data_deletion_request").
|
||||
Where("status IN (?)", []string{"pending", "running"}).
|
||||
Where("payload->>'tenant_id'", fmt.Sprintf("%d", req.TenantID)).
|
||||
Where("(payload->>'tenant_id')::bigint = ?", req.TenantID).
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -193,13 +194,30 @@ func cleanupTableByDate(ctx context.Context, table, dateColumn string, days int)
|
||||
|
||||
cutoff := time.Now().AddDate(0, 0, -days)
|
||||
batchSize := 5000
|
||||
|
||||
// Build safe DELETE SQL via switch/case to avoid fmt.Sprintf on table/column names.
|
||||
// The whitelist above guarantees table and dateColumn are from a trusted set.
|
||||
var deleteSQL string
|
||||
switch table {
|
||||
case "aud_request_logs":
|
||||
deleteSQL = "DELETE FROM aud_request_logs WHERE id IN (SELECT id FROM aud_request_logs WHERE created_at < ? LIMIT ?)"
|
||||
case "aud_operation_logs":
|
||||
deleteSQL = "DELETE FROM aud_operation_logs WHERE id IN (SELECT id FROM aud_operation_logs WHERE created_at < ? LIMIT ?)"
|
||||
case "aud_sensitive_access_logs":
|
||||
deleteSQL = "DELETE FROM aud_sensitive_access_logs WHERE id IN (SELECT id FROM aud_sensitive_access_logs WHERE created_at < ? LIMIT ?)"
|
||||
default:
|
||||
return fmt.Errorf("invalid table for cleanup: %s", table)
|
||||
}
|
||||
|
||||
for {
|
||||
result, err := g.DB().Ctx(ctx).Exec(ctx,
|
||||
fmt.Sprintf("DELETE FROM %s WHERE id IN (SELECT id FROM %s WHERE %s < $1 LIMIT %d)", table, table, dateColumn, batchSize), cutoff)
|
||||
result, err := g.DB().Ctx(ctx).Exec(ctx, deleteSQL, cutoff, batchSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
break
|
||||
}
|
||||
@@ -224,16 +242,26 @@ func cleanupDeactivatedTenants(ctx context.Context) error {
|
||||
}
|
||||
for _, t := range tenants {
|
||||
g.Log().Infof(ctx, "cleaning data for deactivated tenant %d (status=%s)", t.ID, t.StatusCode)
|
||||
if _, err := dao.TntUsers.Ctx(ctx).Where("tenant_id", t.ID).
|
||||
Data(do.TntUsers{DisplayName: "[deleted]", Email: fmt.Sprintf("deleted_%d@deleted.local", t.ID)}).Update(); err != nil {
|
||||
return gerror.Wrapf(err, "anonymize users for tenant %d", t.ID)
|
||||
}
|
||||
if _, err := dao.ApiKeys.Ctx(ctx).Where("tenant_id", t.ID).Delete(); err != nil {
|
||||
return gerror.Wrapf(err, "delete api keys for tenant %d", t.ID)
|
||||
}
|
||||
if _, err := dao.TntTenants.Ctx(ctx).Where("id", t.ID).
|
||||
Data(do.TntTenants{DataRemovalAt: gtime.Now()}).Update(); err != nil {
|
||||
return gerror.Wrapf(err, "update data removal for tenant %d", t.ID)
|
||||
|
||||
// Use transaction to ensure atomicity: if any step fails, the tenant's data
|
||||
// remains consistent (no partial anonymization/deletion).
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
tenantID := t.ID
|
||||
if _, err := tx.Model("tnt_users").Ctx(ctx).Where("tenant_id", tenantID).
|
||||
Data(do.TntUsers{DisplayName: "[deleted]", Email: fmt.Sprintf("deleted_%d@deleted.local", tenantID)}).Update(); err != nil {
|
||||
return gerror.Wrapf(err, "anonymize users for tenant %d", tenantID)
|
||||
}
|
||||
if _, err := tx.Model("api_keys").Ctx(ctx).Where("tenant_id", tenantID).Delete(); err != nil {
|
||||
return gerror.Wrapf(err, "delete api keys for tenant %d", tenantID)
|
||||
}
|
||||
if _, err := tx.Model("tnt_tenants").Ctx(ctx).Where("id", tenantID).
|
||||
Data(do.TntTenants{DataRemovalAt: gtime.Now()}).Update(); err != nil {
|
||||
return gerror.Wrapf(err, "update data removal for tenant %d", tenantID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -53,9 +53,10 @@ func (s *sAdmin) ErrorLogList(ctx context.Context, req *v1.ErrorLogListReq) (*v1
|
||||
}
|
||||
|
||||
where := strings.Join(conditions, " AND ")
|
||||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||||
items, total, err := queryPage(ctx,
|
||||
"sys_error_logs", "*", where, "id DESC",
|
||||
req.Page, req.PageSize, args...)
|
||||
page, pageSize, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,8 +67,8 @@ func (s *sAdmin) ErrorLogList(ctx context.Context, req *v1.ErrorLogListReq) (*v1
|
||||
return &v1.ErrorLogListRes{
|
||||
List: items,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
do "github.com/qianfree/team-api/internal/model/do"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
"github.com/qianfree/team-api/internal/logic/common"
|
||||
@@ -181,6 +182,14 @@ func (s *sAdmin) UpdateUserPermissions(ctx context.Context, req *v1.AdminPermiss
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate permission points against predefined set
|
||||
validPerms := buildValidPermissionSet()
|
||||
for _, p := range req.Permissions {
|
||||
if !validPerms[p] {
|
||||
return gerror.Newf("无效的权限点: %s", p)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new permissions
|
||||
if len(req.Permissions) > 0 {
|
||||
data := make([]do.SysAdminRolePerms, len(req.Permissions))
|
||||
@@ -311,3 +320,14 @@ func GetDataScopes(ctx context.Context, userID int64, role string) ([]v1.DataSco
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// buildValidPermissionSet returns a set of all valid permission points.
|
||||
func buildValidPermissionSet() map[string]bool {
|
||||
set := make(map[string]bool)
|
||||
for _, g := range predefinedPermissionGroups {
|
||||
for _, p := range g.Permissions {
|
||||
set[p] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -75,11 +76,13 @@ func (s *sAdmin) Verify2FA(ctx context.Context, req *v1.Admin2FAVerifyReq) (*v1.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update last login
|
||||
dao.SysAdminUsers.Ctx(ctx).Where("id", user.Id).Data(do.SysAdminUsers{
|
||||
// Update last login (non-critical, log errors only)
|
||||
if _, err := dao.SysAdminUsers.Ctx(ctx).Where("id", user.Id).Data(do.SysAdminUsers{
|
||||
LastLoginAt: gtime.Now(),
|
||||
LastLoginIp: ipAddress,
|
||||
}).Update()
|
||||
}).Update(); err != nil {
|
||||
g.Log().Warningf(ctx, "update last_login for user %d: %v", user.Id, err)
|
||||
}
|
||||
|
||||
// Record login history
|
||||
ua := g.RequestFromCtx(ctx).Header.Get("User-Agent")
|
||||
@@ -107,7 +110,10 @@ func (s *sAdmin) Setup2FA(ctx context.Context, _ *v1.Admin2FASetupReq) (*v1.Admi
|
||||
}
|
||||
|
||||
// Store secret in Redis temporarily (5 min TTL) for the enable step
|
||||
encKey := getEncKey(ctx)
|
||||
encKey, err := getEncKey(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted, err := crypto.EncryptString(encKey, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -305,10 +311,12 @@ func buildAdminUserMap(ctx context.Context, records []entity.AudLoginHistory) ma
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []entity.SysAdminUsers
|
||||
_ = dao.SysAdminUsers.Ctx(ctx).
|
||||
if err := dao.SysAdminUsers.Ctx(ctx).
|
||||
Fields("id, username, display_name").
|
||||
WhereIn("id", ids).
|
||||
Scan(&users)
|
||||
Scan(&users); err != nil {
|
||||
g.Log().Warningf(ctx, "load admin users for session list: %v", err)
|
||||
}
|
||||
for _, u := range users {
|
||||
m[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName}
|
||||
}
|
||||
@@ -429,10 +437,12 @@ func buildTenantUserMap(ctx context.Context, records []entity.AudLoginHistory) m
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []entity.TntUsers
|
||||
_ = dao.TntUsers.Ctx(ctx).
|
||||
if err := dao.TntUsers.Ctx(ctx).
|
||||
Fields("id, username, display_name").
|
||||
WhereIn("id", ids).
|
||||
Scan(&users)
|
||||
Scan(&users); err != nil {
|
||||
g.Log().Warningf(ctx, "load tenant users for session list: %v", err)
|
||||
}
|
||||
for _, u := range users {
|
||||
m[u.Id] = tenantUserBrief{Username: u.Username, DisplayName: u.DisplayName}
|
||||
}
|
||||
@@ -450,7 +460,10 @@ func getPendingTOTPSecret(ctx context.Context, userID int64) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
// The secret is stored encrypted, decrypt it
|
||||
encKey := getEncKey(ctx)
|
||||
encKey, err := getEncKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return crypto.DecryptString(encKey, val.String())
|
||||
}
|
||||
|
||||
@@ -459,10 +472,17 @@ func clearPendingTOTPSecret(ctx context.Context, userID int64) {
|
||||
_, _ = g.Redis().Do(ctx, "DEL", key)
|
||||
}
|
||||
|
||||
func getEncKey(ctx context.Context) []byte {
|
||||
func getEncKey(ctx context.Context) ([]byte, error) {
|
||||
hexKey := g.Cfg().MustGet(ctx, "crypto.encryptionKey").String()
|
||||
if hexKey == "" {
|
||||
hexKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
return nil, gerror.New("crypto.encryptionKey is not configured")
|
||||
}
|
||||
return crypto.MustGetEncryptionKey(hexKey)
|
||||
key, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrapf(err, "invalid crypto.encryptionKey")
|
||||
}
|
||||
if len(key) != 32 {
|
||||
return nil, gerror.Newf("crypto.encryptionKey must be 32 bytes, got %d", len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/logic/common"
|
||||
@@ -26,7 +25,7 @@ func (s *sAdmin) GetSettingsCategories(ctx context.Context, _ *v1.AdminSettingsC
|
||||
// GetSettings retrieves settings with schema for a given category.
|
||||
func (s *sAdmin) GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) (*v1.AdminSettingsGetRes, error) {
|
||||
if !isValidCategory(req.Category) {
|
||||
return nil, fmt.Errorf("未知的设置分类: %s", req.Category)
|
||||
return nil, common.NewBadRequestError("无效的设置分类")
|
||||
}
|
||||
|
||||
items := common.Config().GetCategoryWithValues(ctx, req.Category)
|
||||
@@ -49,7 +48,7 @@ func (s *sAdmin) GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) (
|
||||
// UpdateSettings batch-updates settings for a given category.
|
||||
func (s *sAdmin) UpdateSettings(ctx context.Context, req *v1.AdminSettingsUpdateReq) (*v1.AdminSettingsUpdateRes, error) {
|
||||
if !isValidCategory(req.Category) {
|
||||
return nil, fmt.Errorf("未知的设置分类: %s", req.Category)
|
||||
return nil, common.NewBadRequestError("无效的设置分类")
|
||||
}
|
||||
|
||||
// Normalize interface{} values to strings for storage
|
||||
|
||||
@@ -102,6 +102,13 @@ func AutoInitAdmin(ctx context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isDuplicateKeyError checks if the error is a PostgreSQL unique constraint violation.
|
||||
// PostgreSQL error code 23505 = unique_violation.
|
||||
// NOTE: Uses string matching on the error message. Consider switching to
|
||||
// pq.Error type assertion if github.com/lib/pq becomes a direct dependency.
|
||||
func isDuplicateKeyError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "23505")
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "23505") || strings.Contains(err.Error(), "duplicate key")
|
||||
}
|
||||
Reference in new issue
Block a user