forked from baron/baron-sso
feat: implement bulk user actions and organization tree search with auto-expansion
This commit is contained in:
@@ -581,19 +581,42 @@ const TenantTreeRow: React.FC<{
|
|||||||
isRoot: boolean;
|
isRoot: boolean;
|
||||||
onRemove: (id: string, name: string) => void;
|
onRemove: (id: string, name: string) => void;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
}> = ({ node, level, isRoot, onRemove, isUpdating }) => {
|
searchTerm?: string;
|
||||||
|
}> = ({ node, level, isRoot, onRemove, isUpdating, searchTerm }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
|
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
|
||||||
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
|
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
|
||||||
const hasChildren = node.children && node.children.length > 0;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
|
|
||||||
|
// Auto expand if search matches children
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (searchTerm) {
|
||||||
|
const hasMatchingChild = (n: TenantNode): boolean => {
|
||||||
|
return n.children.some(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
c.slug.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
hasMatchingChild(c),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (hasMatchingChild(node)) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchTerm, node]);
|
||||||
|
|
||||||
|
const isMatching =
|
||||||
|
searchTerm &&
|
||||||
|
(node.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
node.slug.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
|
||||||
const TypeIcon = getTenantIcon(node.type);
|
const TypeIcon = getTenantIcon(node.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow
|
<TableRow
|
||||||
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""}`}
|
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""} ${isMatching ? "bg-primary/10 ring-1 ring-primary/20" : ""}`}
|
||||||
>
|
>
|
||||||
<TableCell style={{ paddingLeft: `${1 + level * 2}rem` }}>
|
<TableCell style={{ paddingLeft: `${1 + level * 2}rem` }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -750,6 +773,7 @@ const TenantTreeRow: React.FC<{
|
|||||||
isRoot={false}
|
isRoot={false}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
|
searchTerm={searchTerm}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -762,6 +786,7 @@ function TenantUserGroupsTab() {
|
|||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [isHeaderUserAddOpen, setIsHeaderUserAddOpen] = useState(false);
|
const [isHeaderUserAddOpen, setIsHeaderUserAddOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [treeSearchTerm, setTreeSearchTerm] = useState("");
|
||||||
|
|
||||||
if (!tenantId) return null;
|
if (!tenantId) return null;
|
||||||
|
|
||||||
@@ -1008,6 +1033,30 @@ function TenantUserGroupsTab() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<div className="px-6 py-3 bg-muted/5 border-b flex items-center gap-4">
|
||||||
|
<div className="relative flex-1 max-w-sm">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={t(
|
||||||
|
"ui.admin.tenants.sub.tree_search_placeholder",
|
||||||
|
"조직도 내 검색...",
|
||||||
|
)}
|
||||||
|
className="pl-9 h-9"
|
||||||
|
value={treeSearchTerm}
|
||||||
|
onChange={(e) => setTreeSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{treeSearchTerm && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTreeSearchTerm("")}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{t("ui.common.clear_search", "검색 초기화")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-muted/5">
|
<TableHeader className="bg-muted/5">
|
||||||
@@ -1036,6 +1085,7 @@ function TenantUserGroupsTab() {
|
|||||||
isRoot={true}
|
isRoot={true}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
isUpdating={updateParentMutation.isPending}
|
isUpdating={updateParentMutation.isPending}
|
||||||
|
searchTerm={treeSearchTerm}
|
||||||
/>
|
/>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
import {
|
import {
|
||||||
|
bulkDeleteUsers,
|
||||||
|
bulkUpdateUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
fetchMe,
|
fetchMe,
|
||||||
fetchTenant,
|
fetchTenant,
|
||||||
@@ -63,6 +65,7 @@ function UserListPage() {
|
|||||||
const [searchDraft, setSearchDraft] = React.useState("");
|
const [searchDraft, setSearchDraft] = React.useState("");
|
||||||
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||||
const [visibleColumns, setVisibleColumns] = React.useState<Record<string, boolean>>({});
|
const [visibleColumns, setVisibleColumns] = React.useState<Record<string, boolean>>({});
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>([]);
|
||||||
const limit = 50;
|
const limit = 50;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
@@ -160,6 +163,50 @@ function UserListPage() {
|
|||||||
const total = query.data?.total ?? 0;
|
const total = query.data?.total ?? 0;
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selectedUserIds.length === items.length) {
|
||||||
|
setSelectedUserIds([]);
|
||||||
|
} else {
|
||||||
|
setSelectedUserIds(items.map((u) => u.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectUser = (id: string) => {
|
||||||
|
setSelectedUserIds((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeleteMutation = useMutation({
|
||||||
|
mutationFn: bulkDeleteUsers,
|
||||||
|
onSuccess: () => {
|
||||||
|
query.refetch();
|
||||||
|
setSelectedUserIds([]);
|
||||||
|
toast.success(t("msg.admin.users.bulk.delete_success", "선택한 사용자들이 삭제되었습니다."));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bulkUpdateMutation = useMutation({
|
||||||
|
mutationFn: bulkUpdateUsers,
|
||||||
|
onSuccess: () => {
|
||||||
|
query.refetch();
|
||||||
|
setSelectedUserIds([]);
|
||||||
|
toast.success(t("msg.admin.users.bulk.update_success", "선택한 사용자들의 정보가 수정되었습니다."));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBulkStatusChange = (status: string) => {
|
||||||
|
if (selectedUserIds.length === 0) return;
|
||||||
|
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkDelete = () => {
|
||||||
|
if (selectedUserIds.length === 0) return;
|
||||||
|
if (window.confirm(t("msg.admin.users.bulk.delete_confirm", "{{count}}명의 사용자를 정말 삭제하시겠습니까?", { count: selectedUserIds.length }))) {
|
||||||
|
bulkDeleteMutation.mutate(selectedUserIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = (userId: string, userName: string) => {
|
const handleDelete = (userId: string, userName: string) => {
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
@@ -324,6 +371,14 @@ function UserListPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHead className="w-12">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||||
|
checked={items.length > 0 && selectedUserIds.length === items.length}
|
||||||
|
onChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="min-w-[200px]">
|
<TableHead className="min-w-[200px]">
|
||||||
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -377,7 +432,18 @@ function UserListPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{items.map((user) => (
|
{items.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow
|
||||||
|
key={user.id}
|
||||||
|
className={selectedUserIds.includes(user.id) ? "bg-primary/5" : ""}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
|
||||||
|
checked={selectedUserIds.includes(user.id)}
|
||||||
|
onChange={() => toggleSelectUser(user.id)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
|
||||||
@@ -452,6 +518,51 @@ function UserListPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk Action Bar */}
|
||||||
|
{selectedUserIds.length > 0 && (
|
||||||
|
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 px-6 py-3 rounded-2xl bg-foreground text-background shadow-2xl animate-in slide-in-from-bottom-4 duration-300">
|
||||||
|
<span className="text-sm font-medium border-r border-background/20 pr-4 mr-2">
|
||||||
|
{t("ui.admin.users.bulk.selected_count", "{{count}}명 선택됨", { count: selectedUserIds.length })}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-background hover:bg-background/10 h-8"
|
||||||
|
onClick={() => handleBulkStatusChange("active")}
|
||||||
|
>
|
||||||
|
{t("ui.common.status.active", "활성화")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-background hover:bg-background/10 h-8"
|
||||||
|
onClick={() => handleBulkStatusChange("inactive")}
|
||||||
|
>
|
||||||
|
{t("ui.common.status.inactive", "비활성화")}
|
||||||
|
</Button>
|
||||||
|
<div className="w-px h-4 bg-background/20 mx-1" />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive-foreground hover:bg-destructive/20 h-8 gap-1.5"
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
{t("ui.common.delete", "삭제")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-background/50 hover:text-background h-8 w-8 ml-2"
|
||||||
|
onClick={() => setSelectedUserIds([])}
|
||||||
|
>
|
||||||
|
<Plus size={16} className="rotate-45" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="mt-4 flex items-center justify-end gap-2">
|
<div className="mt-4 flex items-center justify-end gap-2">
|
||||||
|
|||||||
@@ -453,6 +453,22 @@ export async function bulkCreateUsers(users: BulkUserItem[]) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bulkUpdateUsers(payload: {
|
||||||
|
userIds: string[];
|
||||||
|
status?: string;
|
||||||
|
role?: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await apiClient.put("/v1/admin/users/bulk", payload);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkDeleteUsers(userIds: string[]) {
|
||||||
|
const { data } = await apiClient.delete("/v1/admin/users/bulk", {
|
||||||
|
data: { userIds },
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateUser(userId: string, payload: UserUpdateRequest) {
|
export async function updateUser(userId: string, payload: UserUpdateRequest) {
|
||||||
const { data } = await apiClient.put<UserSummary>(
|
const { data } = await apiClient.put<UserSummary>(
|
||||||
`/v1/admin/users/${userId}`,
|
`/v1/admin/users/${userId}`,
|
||||||
|
|||||||
@@ -645,6 +645,8 @@ func main() {
|
|||||||
admin.Get("/users", requireAdmin, userHandler.ListUsers) // TODO: TenantAdmin인 경우 해당 테넌트 사용자만 보이도록 Handler 수정 필요
|
admin.Get("/users", requireAdmin, userHandler.ListUsers) // TODO: TenantAdmin인 경우 해당 테넌트 사용자만 보이도록 Handler 수정 필요
|
||||||
admin.Post("/users", requireAdmin, userHandler.CreateUser)
|
admin.Post("/users", requireAdmin, userHandler.CreateUser)
|
||||||
admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers)
|
admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers)
|
||||||
|
admin.Put("/users/bulk", requireAdmin, userHandler.BulkUpdateUsers)
|
||||||
|
admin.Delete("/users/bulk", requireAdmin, userHandler.BulkDeleteUsers)
|
||||||
admin.Get("/users/:id", requireAdmin, userHandler.GetUser)
|
admin.Get("/users/:id", requireAdmin, userHandler.GetUser)
|
||||||
admin.Put("/users/:id", requireAdmin, userHandler.UpdateUser)
|
admin.Put("/users/:id", requireAdmin, userHandler.UpdateUser)
|
||||||
admin.Delete("/users/:id", requireAdmin, userHandler.DeleteUser)
|
admin.Delete("/users/:id", requireAdmin, userHandler.DeleteUser)
|
||||||
|
|||||||
@@ -541,6 +541,155 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
UserIDs []string `json:"userIds"`
|
||||||
|
Status *string `json:"status"`
|
||||||
|
Role *string `json:"role"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.UserIDs) == 0 {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "no user IDs provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
|
if requester == nil {
|
||||||
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build manageable slugs map if tenant_admin
|
||||||
|
manageableSlugs := make(map[string]bool)
|
||||||
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
|
for _, t := range requester.ManageableTenants {
|
||||||
|
manageableSlugs[strings.ToLower(t.Slug)] = true
|
||||||
|
}
|
||||||
|
if requester.CompanyCode != "" {
|
||||||
|
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]map[string]any, 0, len(req.UserIDs))
|
||||||
|
|
||||||
|
for _, id := range req.UserIDs {
|
||||||
|
identity, err := h.KratosAdmin.GetIdentity(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": "user not found"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization check
|
||||||
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
|
userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||||
|
if !manageableSlugs[userComp] {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare updates
|
||||||
|
traits := identity.Traits
|
||||||
|
if req.Role != nil {
|
||||||
|
traits["role"] = *req.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
state := identity.State
|
||||||
|
if req.Status != nil {
|
||||||
|
if *req.Status == "active" {
|
||||||
|
state = "active"
|
||||||
|
} else {
|
||||||
|
state = "inactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.KratosAdmin.UpdateIdentity(c.Context(), id, traits, state)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync to local DB
|
||||||
|
if h.UserRepo != nil {
|
||||||
|
localUser := h.mapToLocalUser(*identity)
|
||||||
|
if req.Role != nil {
|
||||||
|
localUser.Role = *req.Role
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
localUser.Status = *req.Status
|
||||||
|
}
|
||||||
|
_ = h.UserRepo.Update(c.Context(), localUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, map[string]any{"id": id, "success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"results": results})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
UserIDs []string `json:"userIds"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.UserIDs) == 0 {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "no user IDs provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
|
if requester == nil {
|
||||||
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
manageableSlugs := make(map[string]bool)
|
||||||
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
|
for _, t := range requester.ManageableTenants {
|
||||||
|
manageableSlugs[strings.ToLower(t.Slug)] = true
|
||||||
|
}
|
||||||
|
if requester.CompanyCode != "" {
|
||||||
|
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]map[string]any, 0, len(req.UserIDs))
|
||||||
|
|
||||||
|
for _, id := range req.UserIDs {
|
||||||
|
identity, err := h.KratosAdmin.GetIdentity(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": "user not found"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization check
|
||||||
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
|
userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||||
|
if !manageableSlugs[userComp] {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.KratosAdmin.DeleteIdentity(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local DB Sync
|
||||||
|
if h.UserRepo != nil {
|
||||||
|
_ = h.UserRepo.Delete(c.Context(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, map[string]any{"id": id, "success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"results": results})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||||
if h.KratosAdmin == nil {
|
if h.KratosAdmin == nil {
|
||||||
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
|
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
|
||||||
|
|||||||
Reference in New Issue
Block a user