diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx index f6b9f7d7..9803949b 100644 --- a/frontend/app/(main)/admin/push/components/events-tab.tsx +++ b/frontend/app/(main)/admin/push/components/events-tab.tsx @@ -327,7 +327,7 @@ export function EventsTab() { toggleEventMutation.mutate(event.id)} - className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7" + className="scale-75" /> e.stopPropagation()}> diff --git a/frontend/app/(main)/admin/push/components/settings-tab.tsx b/frontend/app/(main)/admin/push/components/settings-tab.tsx index 2c921cd6..039d2add 100644 --- a/frontend/app/(main)/admin/push/components/settings-tab.tsx +++ b/frontend/app/(main)/admin/push/components/settings-tab.tsx @@ -324,7 +324,7 @@ export function SettingsTab() { } }) }} - className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7" + className="scale-75" /> e.stopPropagation()}> diff --git a/frontend/app/(main)/admin/users/components/edit-user-modal.tsx b/frontend/app/(main)/admin/users/components/edit-user-modal.tsx new file mode 100644 index 00000000..5463fcd8 --- /dev/null +++ b/frontend/app/(main)/admin/users/components/edit-user-modal.tsx @@ -0,0 +1,281 @@ +"use client" + +import {useEffect, useState} from "react" +import {Button} from "@/components/ui/button" +import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog" +import {Input} from "@/components/ui/input" +import {Label} from "@/components/ui/label" +import {Switch} from "@/components/ui/switch" +import {useAdminUsers} from "@/contexts/admin-users-context" +import {useAuth} from "@/components/providers/auth-provider" +import type {AdminUser} from "@/lib/services/admin" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog" +import {Loader2} from "lucide-react" + +interface EditUserForm { + nickname: string + email: string + is_admin: boolean + password?: string +} + +export function EditUserModal({ + isOpen, + onClose, + user, +}: { + isOpen: boolean + onClose: () => void + user: AdminUser | null +}) { + const { updateUser, deleteUser, getUserDetail } = useAdminUsers() + const { user: currentUser } = useAuth() + + const isSelf = currentUser && user && currentUser.id.toString() === user.id.toString() + + const [form, setForm] = useState({ + nickname: "", + email: "", + is_admin: false, + password: "", + }) + const [saving, setSaving] = useState(false) + const [errors, setErrors] = useState>({}) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + + useEffect(() => { + let active = true + if (isOpen && user) { + setForm({ + nickname: user.nickname || "", + email: user.email || "", + is_admin: user.is_admin || false, + password: "", + }) + setErrors({}) + + getUserDetail(user.id) + .then((detail) => { + if (active && detail) { + setForm({ + nickname: detail.nickname || "", + email: detail.email || "", + is_admin: detail.is_admin || false, + password: "", + }) + } + }) + .catch(() => { + // ignore fetching error + }) + } else { + setSaving(false) + } + return () => { + active = false + } + }, [isOpen, user, getUserDetail]) + + const validate = () => { + const newErrors: Record = {} + + if (!form.email.trim()) { + newErrors.email = "邮箱不能为空" + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) { + newErrors.email = "邮箱格式不正确" + } + + if (form.password && form.password.length < 8) { + newErrors.password = "密码长度不能少于 8 位" + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault() + if (!user || !validate()) return + + setSaving(true) + try { + await updateUser(user.id, { + nickname: form.nickname.trim() || undefined, + email: form.email.trim(), + is_admin: form.is_admin, + password: form.password?.trim() || undefined, + }) + onClose() + } catch { + // Errors are handled by context toast notifications + } finally { + setSaving(false) + } + } + + const handleDelete = async () => { + if (!user) return + setSaving(true) + try { + await deleteUser(user) + onClose() + } catch { + // Errors are handled by context toast notifications + } finally { + setSaving(false) + setShowDeleteConfirm(false) + } + } + + if (!user) return null + + return ( + <> + !open && onClose()}> + + + 编辑用户 + + 修改用户档案、授予/撤销管理权限或重置其密码。 + + + +
+
+ + +
+ +
+ + setForm((prev) => ({ ...prev, nickname: e.target.value }))} + placeholder="请输入昵称" + /> +
+ +
+ + setForm((prev) => ({ ...prev, email: e.target.value }))} + placeholder="请输入邮箱地址" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + setForm((prev) => ({ ...prev, password: e.target.value }))} + placeholder="留空表示保持当前密码,输入则重置 (至少 8 位)" + /> + {errors.password && ( +

{errors.password}

+ )} +
+ +
+
+
管理员权限
+
+ {isSelf ? "不能撤销当前登录用户的管理员权限。" : "开启后此账号将拥有后台管理权限。"} +
+
+ { + setForm((prev) => ({ ...prev, is_admin: checked })) + setSaving(true) + try { + await updateUser(user.id, { + nickname: form.nickname.trim() || undefined, + email: form.email.trim(), + is_admin: checked, + }) + } catch { + setForm((prev) => ({ ...prev, is_admin: !checked })) + } finally { + setSaving(false) + } + }} + /> +
+ +
+
+ {!user.is_admin && ( + + )} +
+
+ + +
+
+
+
+
+ + + + + 确认删除用户 + + 确定要删除用户 {user.nickname || user.username} 吗?该操作会移除用户账号,删除后无法撤销。 + + + + 取消 + { + e.preventDefault() + handleDelete() + }} + disabled={saving} + > + {saving && } + 确认删除 + + + + + + ) +} diff --git a/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx b/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx index 662b5b72..0d07e0cf 100644 --- a/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx +++ b/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import {Globe, Loader2, Mail, MapPin, ShieldCheck, Smartphone, Trash2, UserCheck,} from "lucide-react" +import {Globe, Loader2, Mail, MapPin, ShieldCheck, Smartphone, UserCheck,} from "lucide-react" import {Button} from "@/components/ui/button" import {Badge} from "@/components/ui/badge" @@ -16,7 +16,6 @@ interface UserDetailSheetProps { onOpenChange: (open: boolean) => void detailLoading: boolean onStatusToggle: (user: AdminUser) => Promise - onDeleteTarget: (user: AdminUser) => void } export function UserDetailSheet({ @@ -25,7 +24,6 @@ export function UserDetailSheet({ onOpenChange, detailLoading, onStatusToggle, - onDeleteTarget, }: UserDetailSheetProps) { const displayValue = (value?: string) => value && value.trim() ? value : "-" @@ -156,7 +154,7 @@ export function UserDetailSheet({ {!selectedUser.is_admin && ( -
+
-
)}
diff --git a/frontend/app/(main)/admin/users/components/user-filter-bar.tsx b/frontend/app/(main)/admin/users/components/user-filter-bar.tsx index 0835ba12..e3684ec5 100644 --- a/frontend/app/(main)/admin/users/components/user-filter-bar.tsx +++ b/frontend/app/(main)/admin/users/components/user-filter-bar.tsx @@ -18,17 +18,19 @@ export function UserFilterBar() { pageSize, searchUserId, searchUsername, + searchEmail, statusFilter, setPage, setPageSize, setSearchUserId, setSearchUsername, + setSearchEmail, setStatusFilter, fetchUsers } = useAdminUsers() const totalPages = Math.ceil(total / pageSize) - const hasSearchFilter = Boolean(searchUserId || searchUsername) + const hasSearchFilter = Boolean(searchUserId || searchUsername || searchEmail) return (
@@ -72,6 +74,12 @@ export function UserFilterBar() { value={searchUsername} onChange={(e) => setSearchUsername(e.target.value)} /> + setSearchEmail(e.target.value)} + /> {hasSearchFilter && (
@@ -201,23 +181,18 @@ export default function UsersPage() { - {!user.is_admin && ( - - - - - - 删除用户 - - - )} + + + + + + 编辑用户 + + + +
@@ -235,27 +210,16 @@ export default function UsersPage() { onOpenChange={setDetailOpen} detailLoading={detailLoading} onStatusToggle={handleStatusToggle} - onDeleteTarget={setDeleteTarget} /> - {/* 删除确认警告弹窗 */} - !open && !deleteLoading && setDeleteTarget(null)}> - - - 确认删除用户 - - 确定要删除用户 {deleteTarget?.nickname || deleteTarget?.username} 吗?该操作会移除用户账号,删除后无法撤销。 - - - - 取消 - - {deleteLoading && } - 确认删除 - - - - + {/* 编辑用户弹窗 */} + setEditTarget(null)} + /> + + {/* 新建用户模态弹窗 */} setCreateModalOpen(false)} /> diff --git a/frontend/contexts/admin-users-context.tsx b/frontend/contexts/admin-users-context.tsx index d8a50223..4f16c028 100644 --- a/frontend/contexts/admin-users-context.tsx +++ b/frontend/contexts/admin-users-context.tsx @@ -3,7 +3,7 @@ import * as React from "react" import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react" import {toast} from "sonner" -import type {AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services/admin" +import type {AdminUser, CreateUserRequest, ListUsersRequest, UpdateUserRequest} from "@/lib/services/admin" import services from "@/lib/services" /** 用户列表查询参数 */ @@ -12,6 +12,7 @@ export interface UserQueryParams { page_size: number user_id?: string username?: string + email?: string status?: 'all' | 'active' | 'inactive' } @@ -27,6 +28,7 @@ interface AdminUsersContextState { pageSize: number searchUserId: string searchUsername: string + searchEmail: string statusFilter: 'all' | 'active' | 'inactive' // Actions @@ -34,6 +36,7 @@ interface AdminUsersContextState { setPageSize: (size: number) => void setSearchUserId: (userId: string) => void setSearchUsername: (username: string) => void + setSearchEmail: (email: string) => void setStatusFilter: (status: 'all' | 'active' | 'inactive') => void fetchUsers: (force?: boolean) => Promise @@ -41,6 +44,7 @@ interface AdminUsersContextState { getUserDetail: (id: string) => Promise updateUserStatus: (user: AdminUser) => Promise createUser: (req: CreateUserRequest) => Promise + updateUser: (id: string, req: UpdateUserRequest) => Promise deleteUser: (user: AdminUser) => Promise } @@ -60,9 +64,11 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) const [pageSize, setPageSize] = useState(20) const [searchUserId, setSearchUserId] = useState("") const [searchUsername, setSearchUsername] = useState("") + const [searchEmail, setSearchEmail] = useState("") const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all') const [debouncedSearchUserId, setDebouncedSearchUserId] = useState("") const [debouncedSearchUsername, setDebouncedSearchUsername] = useState("") + const [debouncedSearchEmail, setDebouncedSearchEmail] = useState("") // Cache const cacheRef = useRef>({}) @@ -73,12 +79,17 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) const timer = setTimeout(() => { setDebouncedSearchUserId(searchUserId) setDebouncedSearchUsername(searchUsername) - if (searchUserId !== debouncedSearchUserId || searchUsername !== debouncedSearchUsername) { + setDebouncedSearchEmail(searchEmail) + if ( + searchUserId !== debouncedSearchUserId || + searchUsername !== debouncedSearchUsername || + searchEmail !== debouncedSearchEmail + ) { setPage(1) // Reset to page 1 on search change } }, 500) return () => clearTimeout(timer) - }, [searchUserId, searchUsername, debouncedSearchUserId, debouncedSearchUsername]) + }, [searchUserId, searchUsername, searchEmail, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail]) const generateCacheKey = (params: UserQueryParams) => { return JSON.stringify(params) @@ -90,6 +101,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) page_size: pageSize, user_id: debouncedSearchUserId || undefined, username: debouncedSearchUsername || undefined, + email: debouncedSearchEmail || undefined, status: statusFilter } @@ -127,7 +139,8 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) page, page_size: pageSize, user_id: debouncedSearchUserId || undefined, - username: debouncedSearchUsername || undefined + username: debouncedSearchUsername || undefined, + email: debouncedSearchEmail || undefined } const response = await services.adminUser.listUsers(requestParams) @@ -160,7 +173,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) setLoading(false) } } - }, [page, pageSize, debouncedSearchUserId, debouncedSearchUsername, statusFilter]) + }, [page, pageSize, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail, statusFilter]) // Removed auto-fetch useEffect. Consumer (UsersManager) should trigger fetch. @@ -224,6 +237,20 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) } } + const updateUser = async (id: string, req: UpdateUserRequest) => { + try { + await services.adminUser.updateUser(id, req) + setUsers(prev => prev.map(u => + u.id === id ? { ...u, ...req } : u + )) + cacheRef.current = {} + toast.success('更新用户信息成功') + } catch (err) { + toast.error(err instanceof Error ? err.message : '更新用户信息失败') + throw err + } + } + const value = { users, total, @@ -233,17 +260,20 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) pageSize, searchUserId, searchUsername, + searchEmail, statusFilter, setPage, setPageSize, setSearchUserId, setSearchUsername, + setSearchEmail, setStatusFilter, fetchUsers, refresh, getUserDetail, updateUserStatus, createUser, + updateUser, deleteUser } diff --git a/frontend/lib/services/admin/index.ts b/frontend/lib/services/admin/index.ts index ce17d082..bf918c48 100644 --- a/frontend/lib/services/admin/index.ts +++ b/frontend/lib/services/admin/index.ts @@ -27,6 +27,7 @@ export type { ListUsersRequest, ListUsersResponse, UpdateUserStatusRequest, + UpdateUserRequest, SystemStatus, AppUpdateStatus, Schedule, diff --git a/frontend/lib/services/admin/types.ts b/frontend/lib/services/admin/types.ts index 876780ec..c2cdccad 100644 --- a/frontend/lib/services/admin/types.ts +++ b/frontend/lib/services/admin/types.ts @@ -310,6 +310,8 @@ export interface ListUsersRequest { user_id?: string; /** 用户名前缀过滤(可选) */ username?: string; + /** 邮箱前缀过滤(可选) */ + email?: string; } /** @@ -348,6 +350,20 @@ export interface CreateUserRequest { is_admin?: boolean; } +/** + * 更新用户请求参数 + */ +export interface UpdateUserRequest { + /** 昵称 */ + nickname?: string; + /** 邮箱 */ + email: string; + /** 是否管理员 */ + is_admin?: boolean; + /** 密码(可选重置) */ + password?: string; +} + /** * 认证源信息 */ diff --git a/frontend/lib/services/admin/user.service.ts b/frontend/lib/services/admin/user.service.ts index d34f6f5e..b4dc7d80 100644 --- a/frontend/lib/services/admin/user.service.ts +++ b/frontend/lib/services/admin/user.service.ts @@ -5,6 +5,7 @@ import type { ListUsersRequest, ListUsersResponse, UpdateUserStatusRequest, + UpdateUserRequest, } from './types'; export class AdminUserService extends BaseService { @@ -29,4 +30,8 @@ export class AdminUserService extends BaseService { static async deleteUser(id: string): Promise { return this.delete(`/users/${id}`); } + + static async updateUser(id: string, request: UpdateUserRequest): Promise { + return this.put(`/users/${id}`, request); + } } \ No newline at end of file diff --git a/frontend/lib/services/index.ts b/frontend/lib/services/index.ts index 72630107..f7a94ee6 100644 --- a/frontend/lib/services/index.ts +++ b/frontend/lib/services/index.ts @@ -114,6 +114,7 @@ export type { ListUsersResponse, UpdateUserStatusRequest, CreateUserRequest, + UpdateUserRequest, SystemStatus, AppUpdateStatus, Schedule, diff --git a/internal/apps/admin/user/errs.go b/internal/apps/admin/user/errs.go index 1fd8ef5b..d4fdc5fe 100644 --- a/internal/apps/admin/user/errs.go +++ b/internal/apps/admin/user/errs.go @@ -15,7 +15,9 @@ const ( usernameExists = "用户名已存在" usernameRequired = "用户名不能为空" passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - createUserFailed = "创建用户失败" - emailRequired = "邮箱不能为空" - emailExists = "邮箱已被注册" + createUserFailed = "创建用户失败" + emailRequired = "邮箱不能为空" + emailExists = "邮箱已被注册" + cannotRevokeSelfAdmin = "不能撤销当前登录用户的管理员权限" + updateUserInfoFailed = "更新用户信息失败" ) diff --git a/internal/apps/admin/user/logics.go b/internal/apps/admin/user/logics.go index d5704dc5..158a5b17 100644 --- a/internal/apps/admin/user/logics.go +++ b/internal/apps/admin/user/logics.go @@ -20,6 +20,7 @@ func listUsers(ctx context.Context, req listUsersRequest) (int64, []model.User, return repository.ListAdminUsers(ctx, repository.AdminUserListFilter{ UserID: req.UserID, Username: strings.TrimSpace(req.Username), + Email: strings.TrimSpace(req.Email), Page: req.Page, PageSize: req.PageSize, }) @@ -132,3 +133,80 @@ func createUser(ctx context.Context, req createUserRequest) (model.User, error) } return newUser, nil } + +type updateUserParam struct { + ID uint64 + Nickname string + Email string + IsAdmin bool + Password string +} + +func updateUser(ctx context.Context, currentUserID uint64, param updateUserParam) error { + param.Nickname = strings.TrimSpace(param.Nickname) + param.Email = strings.TrimSpace(param.Email) + param.Password = strings.TrimSpace(param.Password) + + if param.Email == "" { + return errors.New(emailRequired) + } + + targetUser, err := repository.GetAdminUserDetail(ctx, param.ID) + if err != nil { + return err + } + + // 不能撤销当前登录用户的管理员权限 + if currentUserID == param.ID && !param.IsAdmin && targetUser.IsAdmin { + return errors.New(cannotRevokeSelfAdmin) + } + + // 如果修改了邮箱,检查邮箱是否被其他用户占用 + if targetUser.Email != param.Email { + count, err := repository.CountUsersByEmail(ctx, param.Email) + if err != nil { + return err + } + if count > 0 { + return errors.New(emailExists) + } + } + + // 密码强度校验(如果输入了新密码) + if param.Password != "" && len(param.Password) < minPasswordLength { + return errors.New(passwordTooShort) + } + + // 是否需要撤销 Token (重置密码或取消管理员) + needRevokeTokens := (param.Password != "") || (targetUser.IsAdmin && !param.IsAdmin) + var tokens []model.AccessToken + if needRevokeTokens { + _ = db.DB(ctx).Where("user_id = ?", param.ID).Find(&tokens).Error + } + + // 更新字段 + targetUser.Nickname = param.Nickname + if targetUser.Nickname == "" { + targetUser.Nickname = targetUser.Username + } + targetUser.Email = param.Email + targetUser.IsAdmin = param.IsAdmin + + if param.Password != "" { + if err := targetUser.SetEncryptedPassword(param.Password); err != nil { + return err + } + } + + // 执行更新 + err = repository.UpdateUser(ctx, &targetUser) + if err == nil { + oauth.InvalidateCachedUser(ctx, param.ID) + if needRevokeTokens { + for _, token := range tokens { + oauth.InvalidateCachedToken(ctx, token.TokenHash) + } + } + } + return err +} diff --git a/internal/apps/admin/user/routers.go b/internal/apps/admin/user/routers.go index b53c67f3..24d41e49 100644 --- a/internal/apps/admin/user/routers.go +++ b/internal/apps/admin/user/routers.go @@ -28,6 +28,7 @@ type listUsersRequest struct { PageSize int `form:"page_size" binding:"min=1,max=100"` UserID *uint64 `form:"user_id" binding:"omitempty,gt=0"` Username string `form:"username"` + Email string `form:"email"` } type user struct { @@ -289,3 +290,59 @@ func CreateUser(c *gin.Context) { c.JSON(http.StatusOK, response.OK(toUser(newUser))) } + +// updateUserRequest 更新用户信息请求 +type updateUserRequest struct { + Nickname string `json:"nickname" binding:"max=64"` + Email string `json:"email" binding:"required,email,max=255"` + IsAdmin bool `json:"is_admin"` + Password string `json:"password" binding:"omitempty,min=8,max=64"` +} + +// UpdateUser 更新用户信息 +// @Summary 更新用户信息 +// @Description 更新指定用户的昵称、邮箱、管理员权限,并可选重置密码,需要管理员权限 +// @Tags admin +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "用户 ID" +// @Param request body user.updateUserRequest true "更新参数" +// @Success 200 {object} response.Any{data=string} "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限或尝试修改自身权限" +// @Failure 404 {object} response.Any "用户不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/admin/users/{id} [put] +func UpdateUser(c *gin.Context) { + var req updateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.AbortBadRequest(c, err.Error()) + return + } + + id, ok := parseUserID(c) + if !ok { + return + } + + currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey) + err := updateUser(c.Request.Context(), currUser.ID, updateUserParam{ + ID: id, + Nickname: req.Nickname, + Email: req.Email, + IsAdmin: req.IsAdmin, + Password: req.Password, + }) + + if err != nil { + if abortUserLogicError(c, err, userNotFound, []string{cannotRevokeSelfAdmin}, []string{emailRequired, emailExists, passwordTooShort}) { + return + } + response.AbortInternal(c, updateUserInfoFailed) + return + } + + c.JSON(http.StatusOK, response.OKNil()) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 27f42a78..b93b1d43 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -55,6 +55,7 @@ func GetFirstAdminUser(ctx context.Context) (model.User, error) { type AdminUserListFilter struct { UserID *uint64 Username string + Email string Page int PageSize int } @@ -68,6 +69,9 @@ func ListAdminUsers(ctx context.Context, filter AdminUserListFilter) (int64, []m if filter.Username != "" { query = query.Where("username LIKE ?", filter.Username+"%") } + if filter.Email != "" { + query = query.Where("email LIKE ?", filter.Email+"%") + } var total int64 if err := query.Count(&total).Error; err != nil { @@ -77,7 +81,7 @@ func ListAdminUsers(ctx context.Context, filter AdminUserListFilter) (int64, []m var users []model.User offset := (filter.Page - 1) * filter.PageSize if err := query. - Select("id, username, nickname, avatar_url, is_active, is_admin, last_login_at, created_at, updated_at"). + Select("id, username, nickname, email, avatar_url, is_active, is_admin, last_login_at, created_at, updated_at"). Order("id ASC"). Offset(offset). Limit(filter.PageSize). @@ -170,3 +174,8 @@ func ListUsersByIDs(ctx context.Context, ids []uint64) ([]model.User, error) { } return users, nil } + +// UpdateUser updates all fields of an existing user. +func UpdateUser(ctx context.Context, user *model.User) error { + return db.DB(ctx).Save(user).Error +} diff --git a/internal/router/v1/admin.go b/internal/router/v1/admin.go index 4aa631ab..819d42d1 100644 --- a/internal/router/v1/admin.go +++ b/internal/router/v1/admin.go @@ -99,6 +99,7 @@ func registerAdminIAMRoutes(adminRouter *gin.RouterGroup) { users.POST("", admin_user.CreateUser) users.GET("/:id", admin_user.GetUser) users.PUT("/:id/status", admin_user.UpdateUserStatus) + users.PUT("/:id", admin_user.UpdateUser) users.DELETE("/:id", admin_user.DeleteUser) }