forked from baron/baron-sso
Merge branch 'dev' into feature/1058-adminfront-tab-rebac-permissions
This commit is contained in:
@@ -141,6 +141,15 @@ function isMetadataRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeCustomClaimPermission(
|
||||
value: unknown,
|
||||
fallback: CustomClaimPermission,
|
||||
): CustomClaimPermission {
|
||||
return value === "admin_only" || value === "user_and_admin"
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function cleanMetadataValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
@@ -209,9 +218,18 @@ function createGlobalCustomClaimRows(
|
||||
const rawClaims = isMetadataRecord(metadata.global_custom_claims)
|
||||
? metadata.global_custom_claims
|
||||
: {};
|
||||
const rawPermissions = isMetadataRecord(
|
||||
metadata.global_custom_claim_permissions,
|
||||
)
|
||||
? metadata.global_custom_claim_permissions
|
||||
: {};
|
||||
|
||||
return definitions.map((definition, index) => {
|
||||
const value = rawClaims[definition.key];
|
||||
const rawPermission = rawPermissions[definition.key];
|
||||
const permission: Record<string, unknown> = isMetadataRecord(rawPermission)
|
||||
? rawPermission
|
||||
: {};
|
||||
return {
|
||||
id: `${definition.key}-${index}`,
|
||||
key: definition.key,
|
||||
@@ -224,8 +242,14 @@ function createGlobalCustomClaimRows(
|
||||
? ""
|
||||
: JSON.stringify(value),
|
||||
valueType: definition.valueType,
|
||||
readPermission: definition.readPermission,
|
||||
writePermission: definition.writePermission,
|
||||
readPermission: normalizeCustomClaimPermission(
|
||||
permission.readPermission,
|
||||
definition.readPermission,
|
||||
),
|
||||
writePermission: normalizeCustomClaimPermission(
|
||||
permission.writePermission,
|
||||
definition.writePermission,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -291,6 +315,48 @@ async function resolveTenantSelection(
|
||||
};
|
||||
}
|
||||
|
||||
function getTenantVisibility(tenant?: TenantSummary) {
|
||||
const value = tenant?.config?.visibility;
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "public";
|
||||
}
|
||||
|
||||
function isPrivateTenant(tenant?: TenantSummary) {
|
||||
return getTenantVisibility(tenant) === "private";
|
||||
}
|
||||
|
||||
function appointmentTenantsFromMetadata(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
tenants: TenantSummary[],
|
||||
) {
|
||||
const rawAppointments = metadata?.additionalAppointments;
|
||||
if (!Array.isArray(rawAppointments)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawAppointments
|
||||
.map((raw) => {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return null;
|
||||
}
|
||||
const appointment = raw as Record<string, unknown>;
|
||||
const tenantId =
|
||||
typeof appointment.tenantId === "string" ? appointment.tenantId : "";
|
||||
const tenantSlug =
|
||||
typeof appointment.tenantSlug === "string"
|
||||
? appointment.tenantSlug
|
||||
: typeof appointment.slug === "string"
|
||||
? appointment.slug
|
||||
: "";
|
||||
return tenants.find(
|
||||
(tenant) =>
|
||||
(tenantId && tenant.id === tenantId) ||
|
||||
(tenantSlug && tenant.slug === tenantSlug),
|
||||
);
|
||||
})
|
||||
.filter((tenant): tenant is TenantSummary => Boolean(tenant))
|
||||
.filter((tenant) => !isPrivateTenant(tenant));
|
||||
}
|
||||
|
||||
function createEmptyAppointment(): AppointmentDraft {
|
||||
return {
|
||||
draftId: createDraftId(),
|
||||
@@ -385,8 +451,6 @@ function TenantMetadataFields({
|
||||
register: UseFormRegister<UserFormValues>;
|
||||
errors: FieldErrors<UserFormValues>;
|
||||
}) {
|
||||
if (schema.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card overflow-hidden shadow-sm">
|
||||
<div className="bg-muted/30 px-5 py-3 border-b border-border flex items-center justify-between">
|
||||
@@ -401,74 +465,85 @@ function TenantMetadataFields({
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-6 grid gap-6 md:grid-cols-2">
|
||||
{schema.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label
|
||||
htmlFor={`metadata.${tenant.id}.${field.key}`}
|
||||
className="text-xs font-semibold text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive">*</span>}
|
||||
{field.adminOnly && (
|
||||
<span className="ml-2 text-[9px] bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded uppercase font-bold">
|
||||
Admin Only
|
||||
</span>
|
||||
)}
|
||||
{field.isLoginId && (
|
||||
<span className="ml-2 text-[9px] bg-green-500/10 text-green-600 px-1.5 py-0.5 rounded uppercase font-bold">
|
||||
{t("ui.admin.users.detail.form.is_login_id", "로그인 ID")}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id={`metadata.${tenant.id}.${field.key}`}
|
||||
type={
|
||||
field.type === "number"
|
||||
? "number"
|
||||
: field.type === "date"
|
||||
? "date"
|
||||
: field.type === "boolean"
|
||||
? "checkbox"
|
||||
: "text"
|
||||
}
|
||||
className={field.type === "boolean" ? "w-5 h-5" : "h-10 text-sm"}
|
||||
{...register(`metadata.${tenant.id}.${field.key}` as const, {
|
||||
required: field.required
|
||||
? t(
|
||||
"msg.admin.users.detail.form.field_required",
|
||||
"필수입니다.",
|
||||
)
|
||||
: false,
|
||||
pattern: field.validation
|
||||
? {
|
||||
value: new RegExp(field.validation),
|
||||
message: t(
|
||||
"msg.admin.users.detail.form.invalid_format",
|
||||
"형식이 올바르지 않습니다.",
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
})}
|
||||
/>
|
||||
{(
|
||||
errors.metadata as unknown as Record<
|
||||
string,
|
||||
Record<string, { message?: string }>
|
||||
>
|
||||
)?.[tenant.id]?.[field.key] && (
|
||||
<p className="text-[10px] text-destructive font-medium">
|
||||
{
|
||||
(
|
||||
errors.metadata as unknown as Record<
|
||||
string,
|
||||
Record<string, { message?: string }>
|
||||
>
|
||||
)?.[tenant.id]?.[field.key]?.message
|
||||
}
|
||||
</p>
|
||||
{schema.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground md:col-span-2">
|
||||
{t(
|
||||
"msg.admin.users.detail.tenant_schema_empty",
|
||||
"이 테넌트에 설정된 프로필 필드가 없습니다.",
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</p>
|
||||
) : (
|
||||
schema.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label
|
||||
htmlFor={`metadata.${tenant.id}.${field.key}`}
|
||||
className="text-xs font-semibold text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive">*</span>}
|
||||
{field.adminOnly && (
|
||||
<span className="ml-2 text-[9px] bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded uppercase font-bold">
|
||||
Admin Only
|
||||
</span>
|
||||
)}
|
||||
{field.isLoginId && (
|
||||
<span className="ml-2 text-[9px] bg-green-500/10 text-green-600 px-1.5 py-0.5 rounded uppercase font-bold">
|
||||
{t("ui.admin.users.detail.form.is_login_id", "로그인 ID")}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id={`metadata.${tenant.id}.${field.key}`}
|
||||
type={
|
||||
field.type === "number"
|
||||
? "number"
|
||||
: field.type === "date"
|
||||
? "date"
|
||||
: field.type === "boolean"
|
||||
? "checkbox"
|
||||
: "text"
|
||||
}
|
||||
className={
|
||||
field.type === "boolean" ? "w-5 h-5" : "h-10 text-sm"
|
||||
}
|
||||
{...register(`metadata.${tenant.id}.${field.key}` as const, {
|
||||
required: field.required
|
||||
? t(
|
||||
"msg.admin.users.detail.form.field_required",
|
||||
"필수입니다.",
|
||||
)
|
||||
: false,
|
||||
pattern: field.validation
|
||||
? {
|
||||
value: new RegExp(field.validation),
|
||||
message: t(
|
||||
"msg.admin.users.detail.form.invalid_format",
|
||||
"형식이 올바르지 않습니다.",
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
})}
|
||||
/>
|
||||
{(
|
||||
errors.metadata as unknown as Record<
|
||||
string,
|
||||
Record<string, { message?: string }>
|
||||
>
|
||||
)?.[tenant.id]?.[field.key] && (
|
||||
<p className="text-[10px] text-destructive font-medium">
|
||||
{
|
||||
(
|
||||
errors.metadata as unknown as Record<
|
||||
string,
|
||||
Record<string, { message?: string }>
|
||||
>
|
||||
)?.[tenant.id]?.[field.key]?.message
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1103,12 +1178,30 @@ function UserDetailPage() {
|
||||
const userAffiliatedTenants = React.useMemo(() => {
|
||||
const joined = user?.joinedTenants || [];
|
||||
const primary = user?.tenant;
|
||||
const all = [...joined];
|
||||
if (primary && !joined.some((t) => t.id === primary.id)) {
|
||||
const appointmentTenants = appointmentTenantsFromMetadata(
|
||||
user?.metadata as Record<string, unknown> | undefined,
|
||||
tenants,
|
||||
);
|
||||
const all = joined.filter((tenant) => {
|
||||
const fullTenant = tenants.find((item) => item.id === tenant.id);
|
||||
return !isPrivateTenant(fullTenant ?? tenant);
|
||||
});
|
||||
if (
|
||||
primary &&
|
||||
!isPrivateTenant(
|
||||
tenants.find((tenant) => tenant.id === primary.id) ?? primary,
|
||||
) &&
|
||||
!all.some((t) => t.id === primary.id)
|
||||
) {
|
||||
all.unshift(primary);
|
||||
}
|
||||
for (const tenant of appointmentTenants) {
|
||||
if (!all.some((item) => item.id === tenant.id)) {
|
||||
all.push(tenant);
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}, [user?.joinedTenants, user?.tenant]);
|
||||
}, [tenants, user?.joinedTenants, user?.metadata, user?.tenant]);
|
||||
const selectableRepresentativeTenants = React.useMemo(
|
||||
() =>
|
||||
filterNonHanmacFamilyTenants(userAffiliatedTenants, hanmacFamilyTenantId),
|
||||
@@ -1962,7 +2055,7 @@ function UserDetailPage() {
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.admin.users.detail.custom_claims.description",
|
||||
"전역으로 정의된 custom claim의 이 사용자 값을 관리합니다. Claim 정의 추가와 타입 변경은 전역 설정 화면에서만 가능합니다.",
|
||||
"전역으로 정의된 custom claim의 이 사용자 값을 관리합니다. 읽기/쓰기 표시는 사용자가 본인 claim 값을 조회하거나 직접 수정할 수 있는지에 대한 권한이며, claim 정의 추가와 타입 변경은 전역 설정 화면에서만 가능합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user