1
0
forked from baron/baron-sso

Merge branch 'dev' into feature/issue-919-audit-logs-e2e

This commit is contained in:
2026-05-29 17:01:45 +09:00
44 changed files with 3981 additions and 217 deletions

View File

@@ -662,6 +662,7 @@ function UserCreatePage() {
variant="ghost"
size="sm"
className="absolute right-1 top-1 h-8 text-xs font-bold"
data-testid="add-sub-email-btn"
onClick={() => {
const value = newSubEmail.trim().replace(/,/g, "");
if (
@@ -678,7 +679,7 @@ function UserCreatePage() {
}}
>
{t("ui.common.add", "추가")}
</Button>
</Button>{" "}
</div>
<p className="text-[10px] text-muted-foreground mt-1">
* . .
@@ -877,6 +878,7 @@ function UserCreatePage() {
variant="outline"
size="sm"
onClick={addAppointment}
data-testid="add-appointment-btn"
>
<Plus className="mr-2 h-4 w-4" />
{t("ui.common.add", "추가")}

View File

@@ -94,8 +94,8 @@ import { resolvePersonalTenant } from "./utils/personalTenant";
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
email: string;
metadata: Record<string, Record<string, string | number | boolean>> & {
sub_email?: string[];
metadata: Record<string, unknown> & {
sub_email?: string | string[];
};
};
type UserCategory = "hanmac" | "external" | "personal";
@@ -108,6 +108,44 @@ type AppointmentDraft = UserAppointment & {
const PASSWORD_RESET_MIN_LENGTH = 12;
function isMetadataRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function cleanMetadataValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value
.filter((item): item is string => typeof item === "string")
.map((item) => item.trim())
.filter(Boolean);
}
if (isMetadataRecord(value)) {
return Object.fromEntries(
Object.entries(value).filter(
([_, fieldValue]) =>
fieldValue !== undefined && fieldValue !== null && fieldValue !== "",
),
);
}
return value;
}
function normalizeSubEmails(value: unknown): string[] {
if (Array.isArray(value)) {
return value
.filter((item): item is string => typeof item === "string")
.map((item) => item.trim())
.filter((item) => item.includes("@"));
}
if (typeof value === "string" && value.trim() !== "") {
return value
.split(/[;,\n\r\t]/)
.map((email) => email.trim())
.filter((email) => email.includes("@"));
}
return [];
}
function createDraftId() {
return globalThis.crypto?.randomUUID?.() ?? `appointment-${Date.now()}`;
}
@@ -773,15 +811,17 @@ function UserDetailPage() {
});
const onSubmit = async (data: UserFormValues) => {
// Filter out undefined/null/empty strings from metadata
const cleanMetadata = Object.fromEntries(
Object.entries(data.metadata).map(([tenantId, fields]) => {
const cleanFields = Object.fromEntries(
Object.entries(fields).filter(
([_, v]) => v !== undefined && v !== null && v !== "",
),
);
return [tenantId, cleanFields];
Object.entries(data.metadata ?? {}).flatMap(([key, value]) => {
const cleanedValue = cleanMetadataValue(value);
if (
cleanedValue === undefined ||
cleanedValue === null ||
cleanedValue === ""
) {
return [];
}
return [[key, cleanedValue]];
}),
);
@@ -791,22 +831,11 @@ function UserDetailPage() {
sub_email: rawSubEmail,
...safeMetadata
} = cleanMetadata;
// Parse sub_email
let sub_email: string[] = [];
if (
typeof rawSubEmail === "string" &&
(rawSubEmail as string).trim() !== ""
) {
sub_email = (rawSubEmail as string)
.split(/[;,\n\r\t]/)
.map((e: string) => e.trim())
.filter((e: string) => e.includes("@"));
}
const subEmail = normalizeSubEmails(rawSubEmail);
const metadata: Record<string, unknown> = {
...safeMetadata,
...(sub_email.length > 0 ? { sub_email } : { sub_email: [] }),
...(subEmail.length > 0 ? { sub_email: subEmail } : { sub_email: [] }),
};
const payload: UserUpdateRequest = {
@@ -991,16 +1020,14 @@ function UserDetailPage() {
<Mail size={14} className="text-primary/70" />
{user.email}
</div>
{!!user.metadata?.sub_email &&
Array.isArray(user.metadata.sub_email) &&
(user.metadata.sub_email as unknown[]).length > 0 && (
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
<Mail size={14} className="text-primary/40" />
<span className="text-[10px] font-bold">
+{(user.metadata.sub_email as unknown[]).length}
</span>
</div>
)}
{normalizeSubEmails(user.metadata?.sub_email).length > 0 && (
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
<Mail size={14} className="text-primary/40" />
<span className="text-[10px] font-bold">
+{normalizeSubEmails(user.metadata?.sub_email).length}
</span>
</div>
)}
{user.phone && (
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
<Shield size={14} className="text-primary/70" />
@@ -1166,6 +1193,7 @@ function UserDetailPage() {
variant="ghost"
size="sm"
className="absolute right-1 top-1 h-9 text-xs font-bold"
data-testid="add-sub-email-btn"
onClick={() => {
const value = newSubEmail.trim().replace(/,/g, "");
if (
@@ -1319,6 +1347,7 @@ function UserDetailPage() {
variant="outline"
size="sm"
onClick={addAppointment}
data-testid="add-appointment-btn"
>
<Plus className="mr-2 h-4 w-4" />
{t("ui.common.add", "추가")}

View File

@@ -354,10 +354,12 @@ function applySecondaryEmailMetadata(
value: string,
) {
const emails = splitEmailTokens(value);
item.metadata.sub_email = uniqueEmails([
...metadataEmailList(item.metadata.sub_email),
const uniqueSecondaryEmails = uniqueEmails([
...metadataEmailList(item.metadata.secondary_emails),
...emails,
]);
item.metadata.sub_email = value;
item.metadata.secondary_emails = uniqueSecondaryEmails;
addWorksmobileAliasEmails(item, emails);
}