1
0
forked from baron/baron-sso

fix: 소유자 및 관리자 추가 시 중복 등록 방지 로직 추가 (#440)

This commit is contained in:
2026-03-25 16:05:39 +09:00
parent c244917737
commit d83646a7ef
2 changed files with 88 additions and 8 deletions

View File

@@ -82,14 +82,35 @@ export function TenantAdminsAndOwnersTab() {
const addOwnerMutation = useMutation({
mutationFn: (userId: string) => addTenantOwner(tenantId, userId),
onMutate: async (userId) => {
await queryClient.cancelQueries({ queryKey: ["tenant-owners", tenantId] });
const previousOwners = queryClient.getQueryData<any[]>(["tenant-owners", tenantId]);
// Optimistically add to the list to prevent immediate double clicks
const addedUser = searchResults.find(u => u.id === userId);
if (addedUser) {
queryClient.setQueryData<any[]>(["tenant-owners", tenantId], old => {
if (!old) return [{ id: userId, name: addedUser.name, email: addedUser.email }];
if (old.some(o => o.id === userId)) return old;
return [...old, { id: userId, name: addedUser.name, email: addedUser.email }];
});
}
return { previousOwners };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] });
// Delay invalidation slightly to give the backend outbox time to process
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] });
}, 1000);
toast.success(
t("msg.admin.tenants.owners.add_success", "소유자가 추가되었습니다."),
);
setSearchTerm("");
},
onError: (err: AxiosError<{ error?: string }>) => {
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
if (context?.previousOwners) {
queryClient.setQueryData(["tenant-owners", tenantId], context.previousOwners);
}
toast.error(
err.response?.data?.error ||
t("msg.common.error", "오류가 발생했습니다."),
@@ -99,8 +120,18 @@ export function TenantAdminsAndOwnersTab() {
const removeOwnerMutation = useMutation({
mutationFn: (userId: string) => removeTenantOwner(tenantId, userId),
onMutate: async (userId) => {
await queryClient.cancelQueries({ queryKey: ["tenant-owners", tenantId] });
const previousOwners = queryClient.getQueryData<any[]>(["tenant-owners", tenantId]);
queryClient.setQueryData<any[]>(["tenant-owners", tenantId], old =>
old ? old.filter(o => o.id !== userId) : []
);
return { previousOwners };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] });
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] });
}, 1000);
toast.success(
t(
"msg.admin.tenants.owners.remove_success",
@@ -108,7 +139,10 @@ export function TenantAdminsAndOwnersTab() {
),
);
},
onError: (err: AxiosError<{ error?: string }>) => {
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
if (context?.previousOwners) {
queryClient.setQueryData(["tenant-owners", tenantId], context.previousOwners);
}
toast.error(
err.response?.data?.error ||
t("msg.common.error", "오류가 발생했습니다."),
@@ -118,14 +152,33 @@ export function TenantAdminsAndOwnersTab() {
const addAdminMutation = useMutation({
mutationFn: (userId: string) => addTenantAdmin(tenantId, userId),
onMutate: async (userId) => {
await queryClient.cancelQueries({ queryKey: ["tenant-admins", tenantId] });
const previousAdmins = queryClient.getQueryData<any[]>(["tenant-admins", tenantId]);
const addedUser = searchResults.find(u => u.id === userId);
if (addedUser) {
queryClient.setQueryData<any[]>(["tenant-admins", tenantId], old => {
if (!old) return [{ id: userId, name: addedUser.name, email: addedUser.email }];
if (old.some(a => a.id === userId)) return old;
return [...old, { id: userId, name: addedUser.name, email: addedUser.email }];
});
}
return { previousAdmins };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
}, 1000);
toast.success(
t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다."),
);
setSearchTerm("");
},
onError: (err: AxiosError<{ error?: string }>) => {
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
if (context?.previousAdmins) {
queryClient.setQueryData(["tenant-admins", tenantId], context.previousAdmins);
}
toast.error(
err.response?.data?.error ||
t("msg.common.error", "오류가 발생했습니다."),
@@ -135,13 +188,26 @@ export function TenantAdminsAndOwnersTab() {
const removeAdminMutation = useMutation({
mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId),
onMutate: async (userId) => {
await queryClient.cancelQueries({ queryKey: ["tenant-admins", tenantId] });
const previousAdmins = queryClient.getQueryData<any[]>(["tenant-admins", tenantId]);
queryClient.setQueryData<any[]>(["tenant-admins", tenantId], old =>
old ? old.filter(a => a.id !== userId) : []
);
return { previousAdmins };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
}, 1000);
toast.success(
t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."),
);
},
onError: (err: AxiosError<{ error?: string }>) => {
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
if (context?.previousAdmins) {
queryClient.setQueryData(["tenant-admins", tenantId], context.previousAdmins);
}
toast.error(
err.response?.data?.error ||
t("msg.common.error", "오류가 발생했습니다."),

View File

@@ -529,6 +529,13 @@ func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
}
if h.Keto != nil {
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", "User:"+userID)
if err == nil && len(relations) > 0 {
return errorJSON(c, fiber.StatusConflict, "이미 관리자로 등록된 사용자입니다.")
}
}
if h.KetoOutbox != nil {
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
Namespace: "Tenant",
@@ -660,6 +667,13 @@ func (h *TenantHandler) AddOwner(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
}
if h.Keto != nil {
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", "User:"+userID)
if err == nil && len(relations) > 0 {
return errorJSON(c, fiber.StatusConflict, "이미 소유자로 등록된 사용자입니다.")
}
}
if h.KetoOutbox != nil {
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
Namespace: "Tenant",