fix: align admin behavior and Kratos integrations
This commit is contained in:
+288
-48
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
@@ -80,6 +82,119 @@ func toBizAuthority(po authorityPO) biz.Authority {
|
||||
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
|
||||
}
|
||||
|
||||
func baseBizUser(po *userPO) *biz.User {
|
||||
setting := map[string]any(nil)
|
||||
if po.OriginSetting != "" {
|
||||
_ = json.Unmarshal([]byte(po.OriginSetting), &setting)
|
||||
}
|
||||
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
|
||||
}
|
||||
|
||||
func (r *systemRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
|
||||
if len(pos) == 0 {
|
||||
return []*biz.User{}, nil
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
userIDs := make([]uint, 0, len(pos))
|
||||
authorityIDs := make([]uint, 0, len(pos))
|
||||
departmentIDs := make([]uint, 0, len(pos))
|
||||
for i := range pos {
|
||||
userIDs = append(userIDs, pos[i].ID)
|
||||
authorityIDs = append(authorityIDs, pos[i].AuthorityID)
|
||||
if pos[i].DeptID != 0 {
|
||||
departmentIDs = append(departmentIDs, pos[i].DeptID)
|
||||
}
|
||||
}
|
||||
var authorityLinks []userAuthorityPO
|
||||
if err := db.Where("sys_user_id IN ?", userIDs).Find(&authorityLinks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorityByUser := make(map[uint][]uint, len(pos))
|
||||
for _, link := range authorityLinks {
|
||||
authorityByUser[link.SysUserID] = append(authorityByUser[link.SysUserID], link.SysAuthorityAuthorityID)
|
||||
authorityIDs = append(authorityIDs, link.SysAuthorityAuthorityID)
|
||||
}
|
||||
var authorityPOs []authorityPO
|
||||
if err := db.Where("authority_id IN ?", authorityIDs).Find(&authorityPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorityByID := make(map[uint]authorityPO, len(authorityPOs))
|
||||
for _, item := range authorityPOs {
|
||||
authorityByID[item.AuthorityID] = item
|
||||
}
|
||||
|
||||
var departmentLinks []userDepartmentPO
|
||||
if err := db.Where("sys_user_id IN ?", userIDs).Find(&departmentLinks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
departmentByUser := make(map[uint][]uint, len(pos))
|
||||
for _, link := range departmentLinks {
|
||||
departmentByUser[link.UserID] = append(departmentByUser[link.UserID], link.DepartmentID)
|
||||
departmentIDs = append(departmentIDs, link.DepartmentID)
|
||||
}
|
||||
var departmentPOs []departmentPO
|
||||
if len(departmentIDs) > 0 {
|
||||
if err := db.Where("id IN ?", departmentIDs).Find(&departmentPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
departmentByID := make(map[uint]departmentPO, len(departmentPOs))
|
||||
for _, item := range departmentPOs {
|
||||
departmentByID[item.ID] = item
|
||||
}
|
||||
|
||||
var positionLinks []userPositionPO
|
||||
if err := db.Where("sys_user_id IN ?", userIDs).Find(&positionLinks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
positionByUser := make(map[uint][]uint, len(pos))
|
||||
positionIDs := make([]uint, 0, len(positionLinks))
|
||||
for _, link := range positionLinks {
|
||||
positionByUser[link.UserID] = append(positionByUser[link.UserID], link.PositionID)
|
||||
positionIDs = append(positionIDs, link.PositionID)
|
||||
}
|
||||
var positionPOs []positionPO
|
||||
if len(positionIDs) > 0 {
|
||||
if err := db.Where("id IN ?", positionIDs).Find(&positionPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
positionByID := make(map[uint]positionPO, len(positionPOs))
|
||||
for _, item := range positionPOs {
|
||||
positionByID[item.ID] = item
|
||||
}
|
||||
|
||||
users := make([]*biz.User, 0, len(pos))
|
||||
for i := range pos {
|
||||
po := &pos[i]
|
||||
user := baseBizUser(po)
|
||||
user.Authority = toBizAuthority(authorityByID[po.AuthorityID])
|
||||
user.Authorities = make([]biz.Authority, 0, len(authorityByUser[po.ID]))
|
||||
for _, id := range authorityByUser[po.ID] {
|
||||
if item, ok := authorityByID[id]; ok {
|
||||
user.Authorities = append(user.Authorities, toBizAuthority(item))
|
||||
}
|
||||
}
|
||||
user.Departments = make([]biz.Department, 0, len(departmentByUser[po.ID]))
|
||||
for _, id := range departmentByUser[po.ID] {
|
||||
if item, ok := departmentByID[id]; ok {
|
||||
user.Departments = append(user.Departments, *deptFromPO(item))
|
||||
}
|
||||
}
|
||||
if primary, ok := departmentByID[po.DeptID]; ok {
|
||||
user.Department = deptFromPO(primary)
|
||||
}
|
||||
user.Positions = make([]biz.Position, 0, len(positionByUser[po.ID]))
|
||||
for _, id := range positionByUser[po.ID] {
|
||||
if item, ok := positionByID[id]; ok {
|
||||
user.Positions = append(user.Positions, *posFromPO(item))
|
||||
}
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
|
||||
var pos []menuPO
|
||||
err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
|
||||
@@ -90,23 +205,31 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
|
||||
return nil, err
|
||||
}
|
||||
byID := make(map[uint]*biz.Menu, len(pos))
|
||||
menuIDs := make([]uint, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
menu := &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
|
||||
menu.Parameters = []*biz.MenuParameter{}
|
||||
menu.Buttons = []*biz.MenuButton{}
|
||||
byID[po.ID] = menu
|
||||
menuIDs = append(menuIDs, po.ID)
|
||||
}
|
||||
if len(menuIDs) > 0 {
|
||||
var parameters []menuParameterPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Order("id").Find(¶meters).Error; err != nil {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Order("id").Find(¶meters).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, parameter := range parameters {
|
||||
menu := byID[parameter.MenuID]
|
||||
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{ID: parameter.ID, MenuID: parameter.MenuID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||||
}
|
||||
var buttons []menuButtonPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id = ?", authorityID, po.ID).Find(&buttons).Error; err != nil {
|
||||
if err := r.data.gormDB.WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, button := range buttons {
|
||||
menu := byID[button.MenuID]
|
||||
menu.Buttons = append(menu.Buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: button.MenuID, AuthorityID: authorityID})
|
||||
}
|
||||
byID[po.ID] = menu
|
||||
}
|
||||
roots := make([]*biz.Menu, 0)
|
||||
for _, po := range pos {
|
||||
@@ -121,22 +244,7 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]
|
||||
}
|
||||
|
||||
func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx).Model(&userPO{})
|
||||
if scope, ok := biz.DataScopeFromContext(ctx); ok && !scope.All {
|
||||
if scope.OwnerUserID != 0 {
|
||||
db = db.Where("sys_users.id = ?", scope.OwnerUserID)
|
||||
} else if len(scope.DepartmentIDs) > 0 {
|
||||
db = db.Joins("JOIN sys_user_departments ON sys_user_departments.sys_user_id = sys_users.id").Where("sys_user_departments.sys_department_id IN ?", scope.DepartmentIDs).Distinct("sys_users.id")
|
||||
} else {
|
||||
db = db.Where("1 = 0")
|
||||
}
|
||||
}
|
||||
order := "id desc"
|
||||
if filter != nil {
|
||||
if filter.Username != "" {
|
||||
@@ -151,7 +259,7 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
|
||||
if filter.Email != "" {
|
||||
db = db.Where("email LIKE ?", "%"+filter.Email+"%")
|
||||
}
|
||||
allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true, "created_at": true}
|
||||
allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true}
|
||||
if allowed[filter.OrderKey] {
|
||||
order = filter.OrderKey
|
||||
if filter.Desc {
|
||||
@@ -164,21 +272,18 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []userPO
|
||||
if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).Find(&pos).Error; err != nil {
|
||||
if err := applyPagination(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
users := make([]*biz.User, 0, len(pos))
|
||||
for i := range pos {
|
||||
user, err := r.toBizUser(ctx, &pos[i])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, total, nil
|
||||
users, err := r.toBizUsers(ctx, pos)
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
|
||||
return r.CreateUserWithAuthorities(ctx, user, nil)
|
||||
}
|
||||
|
||||
func (r *systemRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) {
|
||||
if user.UUID == "" {
|
||||
user.UUID = uuid.NewString()
|
||||
}
|
||||
@@ -193,23 +298,134 @@ func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User,
|
||||
}
|
||||
now := time.Now()
|
||||
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
|
||||
if len(authorityIDs) == 0 {
|
||||
authorityIDs = []uint{user.AuthorityID}
|
||||
}
|
||||
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := validateAuthorities(tx, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
|
||||
links := make([]userAuthorityPO, 0, len(authorityIDs))
|
||||
for _, authorityID := range authorityIDs {
|
||||
links = append(links, userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: authorityID})
|
||||
}
|
||||
return tx.Create(&links).Error
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindUserByID(ctx, po.ID)
|
||||
}
|
||||
|
||||
func validateAuthorities(tx *gorm.DB, authorityIDs []uint) error {
|
||||
for _, authorityID := range authorityIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("角色不存在")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *systemRepo) UpdateUser(ctx context.Context, user *biz.User) error {
|
||||
return r.updateUser(r.data.gormDB.WithContext(ctx), user)
|
||||
}
|
||||
|
||||
func (r *systemRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
|
||||
updates := make(map[string]any)
|
||||
if user.NickName != "" {
|
||||
updates["nick_name"] = user.NickName
|
||||
}
|
||||
if user.HeaderImg != "" {
|
||||
updates["header_img"] = user.HeaderImg
|
||||
}
|
||||
if user.Phone != "" {
|
||||
updates["phone"] = user.Phone
|
||||
}
|
||||
if user.Email != "" {
|
||||
updates["email"] = user.Email
|
||||
}
|
||||
if user.Enable != 0 {
|
||||
updates["enable"] = user.Enable
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *systemRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error {
|
||||
if user == nil || len(user.Departments) == 0 {
|
||||
return nil
|
||||
}
|
||||
idSet := make(map[uint]bool)
|
||||
for i := range user.Departments {
|
||||
for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
|
||||
if err == nil && id != 0 {
|
||||
idSet[uint(id)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
ids := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
nameByID := make(map[uint]string, len(ids))
|
||||
if len(ids) > 0 {
|
||||
var rows []departmentPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
nameByID[row.ID] = row.Name
|
||||
}
|
||||
}
|
||||
for i := range user.Departments {
|
||||
parts := make([]string, 0)
|
||||
for _, part := range strings.Split(user.Departments[i].Ancestors, ",") {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
|
||||
if err == nil && id != 0 && nameByID[uint(id)] != "" {
|
||||
parts = append(parts, nameByID[uint(id)])
|
||||
}
|
||||
}
|
||||
parts = append(parts, user.Departments[i].Name)
|
||||
user.Departments[i].NamePath = strings.Join(parts, "/")
|
||||
}
|
||||
if user.Department != nil {
|
||||
for i := range user.Departments {
|
||||
if user.Departments[i].ID == user.Department.ID {
|
||||
user.Department.NamePath = user.Departments[i].NamePath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *systemRepo) updateUser(tx *gorm.DB, user *biz.User) error {
|
||||
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
|
||||
if user.AuthorityID != 0 {
|
||||
updates["authority_id"] = user.AuthorityID
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
|
||||
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *systemRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := r.updateUser(tx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(authorityIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return setUserAuthorities(tx, user.ID, authorityIDs)
|
||||
})
|
||||
}
|
||||
func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -248,36 +464,60 @@ func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err
|
||||
|
||||
func (r *systemRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
links := make([]userAuthorityPO, 0, len(authorityIDs))
|
||||
access := &accessRepo{data: r.data}
|
||||
for _, authorityID := range authorityIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil || count == 0 {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("角色不存在")
|
||||
if err := access.checkAuthorityIDAuth(ctx, authorityID); err != nil {
|
||||
return err
|
||||
}
|
||||
links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
|
||||
}
|
||||
if err := tx.Create(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
|
||||
return setUserAuthorities(tx, id, authorityIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error {
|
||||
if len(authorityIDs) == 0 {
|
||||
return errors.New("角色不能为空")
|
||||
}
|
||||
if err := validateAuthorities(tx, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
links := make([]userAuthorityPO, 0, len(authorityIDs))
|
||||
for _, authorityID := range authorityIDs {
|
||||
links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
|
||||
}
|
||||
if err := tx.Create(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
|
||||
}
|
||||
|
||||
func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
var count int64
|
||||
if err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
|
||||
if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("该用户无此角色")
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
|
||||
var authority authorityPO
|
||||
if err := db.Where("authority_id = ?", authorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var menuCount int64
|
||||
if err := db.Model(&menuPO{}).
|
||||
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
|
||||
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, authority.DefaultRouter).
|
||||
Count(&menuCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if menuCount == 0 {
|
||||
return errors.New("找不到默认路由,无法切换本角色")
|
||||
}
|
||||
return db.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
|
||||
}
|
||||
|
||||
func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
|
||||
|
||||
Reference in New Issue
Block a user