forked from baron/baron-sso
feat: implement enhanced user schema management with validation and admin_only fields
This commit is contained in:
@@ -17,7 +17,7 @@ import { Label } from "../../../components/ui/label";
|
|||||||
import { fetchTenant, updateTenant } from "../../../lib/adminApi";
|
import { fetchTenant, updateTenant } from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
|
|
||||||
type SchemaFieldType = "text" | "number" | "boolean";
|
type SchemaFieldType = "text" | "number" | "boolean" | "date";
|
||||||
|
|
||||||
type SchemaField = {
|
type SchemaField = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,6 +25,8 @@ type SchemaField = {
|
|||||||
label: string;
|
label: string;
|
||||||
type: SchemaFieldType;
|
type: SchemaFieldType;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
adminOnly: boolean;
|
||||||
|
validation?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createFieldId() {
|
function createFieldId() {
|
||||||
@@ -62,10 +64,14 @@ export function TenantSchemaPage() {
|
|||||||
key: typeof field?.key === "string" ? field.key : "",
|
key: typeof field?.key === "string" ? field.key : "",
|
||||||
label: typeof field?.label === "string" ? field.label : "",
|
label: typeof field?.label === "string" ? field.label : "",
|
||||||
type:
|
type:
|
||||||
field?.type === "number" || field?.type === "boolean"
|
field?.type === "number" ||
|
||||||
|
field?.type === "boolean" ||
|
||||||
|
field?.type === "date"
|
||||||
? field.type
|
? field.type
|
||||||
: "text",
|
: "text",
|
||||||
required: Boolean(field?.required),
|
required: Boolean(field?.required),
|
||||||
|
adminOnly: Boolean(field?.adminOnly),
|
||||||
|
validation: typeof field?.validation === "string" ? field.validation : "",
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,6 +111,8 @@ export function TenantSchemaPage() {
|
|||||||
label: "",
|
label: "",
|
||||||
type: "text",
|
type: "text",
|
||||||
required: false,
|
required: false,
|
||||||
|
adminOnly: false,
|
||||||
|
validation: "",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -141,7 +149,7 @@ export function TenantSchemaPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-6">
|
||||||
{fields.length === 0 && (
|
{fields.length === 0 && (
|
||||||
<div className="py-12 text-center text-muted-foreground border border-dashed rounded-lg bg-muted/10">
|
<div className="py-12 text-center text-muted-foreground border border-dashed rounded-lg bg-muted/10">
|
||||||
{t(
|
{t(
|
||||||
@@ -153,84 +161,142 @@ export function TenantSchemaPage() {
|
|||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className="flex items-end gap-4 p-5 border border-border rounded-xl bg-muted/20 hover:bg-muted/30 transition-colors"
|
className="p-5 border border-border rounded-xl bg-muted/20 hover:bg-muted/30 transition-colors space-y-4"
|
||||||
>
|
>
|
||||||
<div className="flex-1 space-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
<div className="space-y-2">
|
||||||
{t("ui.admin.tenants.schema.field.key", "필드 키 (ID)")}
|
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||||
</Label>
|
{t("ui.admin.tenants.schema.field.key", "필드 키 (ID)")}
|
||||||
<Input
|
</Label>
|
||||||
value={field.key}
|
<Input
|
||||||
onChange={(e) => updateField(index, { key: e.target.value })}
|
value={field.key}
|
||||||
placeholder={t(
|
onChange={(e) => updateField(index, { key: e.target.value })}
|
||||||
"ui.admin.tenants.schema.field.key_placeholder",
|
placeholder={t(
|
||||||
"예: employee_id",
|
"ui.admin.tenants.schema.field.key_placeholder",
|
||||||
)}
|
"예: employee_id",
|
||||||
className="h-10"
|
)}
|
||||||
/>
|
className="h-10"
|
||||||
</div>
|
/>
|
||||||
<div className="flex-1 space-y-2">
|
</div>
|
||||||
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
<div className="space-y-2">
|
||||||
{t("ui.admin.tenants.schema.field.label", "표시 라벨")}
|
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||||
</Label>
|
{t("ui.admin.tenants.schema.field.label", "표시 라벨")}
|
||||||
<Input
|
</Label>
|
||||||
value={field.label}
|
<Input
|
||||||
onChange={(e) =>
|
value={field.label}
|
||||||
updateField(index, { label: e.target.value })
|
onChange={(e) =>
|
||||||
}
|
updateField(index, { label: e.target.value })
|
||||||
placeholder={t(
|
|
||||||
"ui.admin.tenants.schema.field.label_placeholder",
|
|
||||||
"예: 사번",
|
|
||||||
)}
|
|
||||||
className="h-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-40 space-y-2">
|
|
||||||
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
||||||
{t("ui.admin.tenants.schema.field.type", "유형")}
|
|
||||||
</Label>
|
|
||||||
<select
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:ring-1 focus:ring-primary"
|
|
||||||
value={field.type}
|
|
||||||
onChange={(e) => {
|
|
||||||
const nextType = e.target.value;
|
|
||||||
if (
|
|
||||||
nextType === "text" ||
|
|
||||||
nextType === "number" ||
|
|
||||||
nextType === "boolean"
|
|
||||||
) {
|
|
||||||
updateField(index, { type: nextType });
|
|
||||||
}
|
}
|
||||||
}}
|
placeholder={t(
|
||||||
>
|
"ui.admin.tenants.schema.field.label_placeholder",
|
||||||
<option value="text">
|
"예: 사번",
|
||||||
{t(
|
|
||||||
"ui.admin.tenants.schema.field.type_text",
|
|
||||||
"텍스트 (Text)",
|
|
||||||
)}
|
)}
|
||||||
</option>
|
className="h-10"
|
||||||
<option value="number">
|
/>
|
||||||
{t(
|
</div>
|
||||||
"ui.admin.tenants.schema.field.type_number",
|
<div className="space-y-2">
|
||||||
"숫자 (Number)",
|
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||||
)}
|
{t("ui.admin.tenants.schema.field.type", "유형")}
|
||||||
</option>
|
</Label>
|
||||||
<option value="boolean">
|
<select
|
||||||
{t(
|
className="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:ring-1 focus:ring-primary"
|
||||||
"ui.admin.tenants.schema.field.type_boolean",
|
value={field.type}
|
||||||
"불리언 (Boolean)",
|
onChange={(e) => {
|
||||||
)}
|
const nextType = e.target.value;
|
||||||
</option>
|
if (
|
||||||
</select>
|
nextType === "text" ||
|
||||||
|
nextType === "number" ||
|
||||||
|
nextType === "boolean" ||
|
||||||
|
nextType === "date"
|
||||||
|
) {
|
||||||
|
updateField(index, { type: nextType });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="text">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_text",
|
||||||
|
"텍스트 (Text)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="number">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_number",
|
||||||
|
"숫자 (Number)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="boolean">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_boolean",
|
||||||
|
"불리언 (Boolean)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="date">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_date",
|
||||||
|
"날짜 (Date)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.required}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(index, { required: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t("ui.admin.tenants.schema.field.required", "필수 입력")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.adminOnly}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(index, { adminOnly: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.admin_only",
|
||||||
|
"관리자 전용",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={field.validation}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(index, { validation: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder={t(
|
||||||
|
"ui.admin.tenants.schema.field.validation_placeholder",
|
||||||
|
"정규식 (예: ^[0-9]+$)",
|
||||||
|
)}
|
||||||
|
className="h-9 text-xs font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-destructive hover:bg-destructive/10 h-10 w-10"
|
||||||
|
onClick={() => removeField(index)}
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="text-destructive hover:bg-destructive/10 h-10 w-10"
|
|
||||||
onClick={() => removeField(index)}
|
|
||||||
>
|
|
||||||
<Trash2 size={18} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -249,3 +315,4 @@ export function TenantSchemaPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import { t } from "../../lib/i18n";
|
|||||||
type UserSchemaField = {
|
type UserSchemaField = {
|
||||||
key: string;
|
key: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
type?: "text" | "number" | "boolean";
|
type?: "text" | "number" | "boolean" | "date";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
adminOnly?: boolean;
|
||||||
|
validation?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserFormValues = UserCreateRequest & { metadata: Record<string, unknown> };
|
type UserFormValues = UserCreateRequest & { metadata: Record<string, unknown> };
|
||||||
@@ -85,8 +87,24 @@ function UserCreatePage() {
|
|||||||
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const registerMetadata = (key: string) =>
|
const registerMetadata = (field: UserSchemaField) =>
|
||||||
register(`metadata.${key}` as `metadata.${string}`);
|
register(`metadata.${field.key}` as `metadata.${string}`, {
|
||||||
|
required: field.required
|
||||||
|
? t("msg.admin.users.create.form.field_required", "{{label}}은(는) 필수입니다.", {
|
||||||
|
label: field.label || field.key,
|
||||||
|
})
|
||||||
|
: false,
|
||||||
|
pattern: field.validation
|
||||||
|
? {
|
||||||
|
value: new RegExp(field.validation),
|
||||||
|
message: t(
|
||||||
|
"msg.admin.users.create.form.field_invalid",
|
||||||
|
"{{label}} 형식이 올바르지 않습니다.",
|
||||||
|
{ label: field.label || field.key },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: createUser,
|
mutationFn: createUser,
|
||||||
@@ -107,17 +125,16 @@ function UserCreatePage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: UserCreateRequest) => {
|
const onSubmit = (data: UserFormValues) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setGeneratedPassword(null);
|
setGeneratedPassword(null);
|
||||||
setCreatedEmail(null);
|
setCreatedEmail(null);
|
||||||
|
|
||||||
if (autoPassword) {
|
const payload = { ...data };
|
||||||
mutation.mutate({ ...data, password: "" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.password) {
|
if (autoPassword) {
|
||||||
|
payload.password = "";
|
||||||
|
} else if (!data.password) {
|
||||||
setError(
|
setError(
|
||||||
t(
|
t(
|
||||||
"msg.admin.users.create.password_required",
|
"msg.admin.users.create.password_required",
|
||||||
@@ -127,7 +144,7 @@ function UserCreatePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutation.mutate(data);
|
mutation.mutate(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCopyPassword = async () => {
|
const onCopyPassword = async () => {
|
||||||
@@ -414,13 +431,37 @@ function UserCreatePage() {
|
|||||||
<div key={field.key} className="space-y-2">
|
<div key={field.key} className="space-y-2">
|
||||||
<Label htmlFor={`metadata.${field.key}`}>
|
<Label htmlFor={`metadata.${field.key}`}>
|
||||||
{field.label}
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
{field.adminOnly && (
|
||||||
|
<span className="ml-2 text-[10px] bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded uppercase font-bold tracking-tighter">
|
||||||
|
Admin Only
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id={`metadata.${field.key}`}
|
id={`metadata.${field.key}`}
|
||||||
type={field.type === "number" ? "number" : "text"}
|
type={
|
||||||
{...registerMetadata(field.key)}
|
field.type === "number"
|
||||||
|
? "number"
|
||||||
|
: field.type === "date"
|
||||||
|
? "date"
|
||||||
|
: field.type === "boolean"
|
||||||
|
? "checkbox"
|
||||||
|
: "text"
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
field.type === "boolean" ? "w-auto h-auto" : ""
|
||||||
|
}
|
||||||
|
{...registerMetadata(field)}
|
||||||
/>
|
/>
|
||||||
|
{errors.metadata?.[field.key] && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{(errors.metadata[field.key] as any).message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -482,4 +523,5 @@ function UserCreatePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default UserCreatePage;
|
export default UserCreatePage;
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import { t } from "../../lib/i18n";
|
|||||||
type UserSchemaField = {
|
type UserSchemaField = {
|
||||||
key: string;
|
key: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
type?: "text" | "number" | "boolean";
|
type?: "text" | "number" | "boolean" | "date";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
adminOnly?: boolean;
|
||||||
|
validation?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserFormValues = UserUpdateRequest & { metadata: Record<string, unknown> };
|
type UserFormValues = UserUpdateRequest & { metadata: Record<string, unknown> };
|
||||||
@@ -94,8 +96,24 @@ function UserDetailPage() {
|
|||||||
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const registerMetadata = (key: string) =>
|
const registerMetadata = (field: UserSchemaField) =>
|
||||||
register(`metadata.${key}` as `metadata.${string}`);
|
register(`metadata.${field.key}` as `metadata.${string}`, {
|
||||||
|
required: field.required
|
||||||
|
? t("msg.admin.users.detail.form.field_required", "{{label}}은(는) 필수입니다.", {
|
||||||
|
label: field.label || field.key,
|
||||||
|
})
|
||||||
|
: false,
|
||||||
|
pattern: field.validation
|
||||||
|
? {
|
||||||
|
value: new RegExp(field.validation),
|
||||||
|
message: t(
|
||||||
|
"msg.admin.users.detail.form.field_invalid",
|
||||||
|
"{{label}} 형식이 올바르지 않습니다.",
|
||||||
|
{ label: field.label || field.key },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -139,8 +157,8 @@ function UserDetailPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: UserUpdateRequest) => {
|
const onSubmit = (data: UserFormValues) => {
|
||||||
const payload = { ...data };
|
const payload: UserUpdateRequest = { ...data };
|
||||||
if (!payload.password) {
|
if (!payload.password) {
|
||||||
payload.password = undefined;
|
payload.password = undefined;
|
||||||
}
|
}
|
||||||
@@ -393,13 +411,37 @@ function UserDetailPage() {
|
|||||||
<div key={field.key} className="space-y-2">
|
<div key={field.key} className="space-y-2">
|
||||||
<Label htmlFor={`metadata.${field.key}`}>
|
<Label htmlFor={`metadata.${field.key}`}>
|
||||||
{field.label}
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
{field.adminOnly && (
|
||||||
|
<span className="ml-2 text-[10px] bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded uppercase font-bold tracking-tighter">
|
||||||
|
Admin Only
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id={`metadata.${field.key}`}
|
id={`metadata.${field.key}`}
|
||||||
type={field.type === "number" ? "number" : "text"}
|
type={
|
||||||
{...registerMetadata(field.key)}
|
field.type === "number"
|
||||||
|
? "number"
|
||||||
|
: field.type === "date"
|
||||||
|
? "date"
|
||||||
|
: field.type === "boolean"
|
||||||
|
? "checkbox"
|
||||||
|
: "text"
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
field.type === "boolean" ? "w-auto h-auto" : ""
|
||||||
|
}
|
||||||
|
{...registerMetadata(field)}
|
||||||
/>
|
/>
|
||||||
|
{errors.metadata?.[field.key] && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{(errors.metadata[field.key] as any).message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -410,6 +452,7 @@ function UserDetailPage() {
|
|||||||
<h3 className="mb-4 text-sm font-medium text-muted-foreground">
|
<h3 className="mb-4 text-sm font-medium text-muted-foreground">
|
||||||
{t("ui.admin.users.detail.security.title", "보안 설정")}
|
{t("ui.admin.users.detail.security.title", "보안 설정")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">
|
<Label htmlFor="password">
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import (
|
|||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"baron-sso-backend/internal/utils"
|
"baron-sso-backend/internal/utils"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -302,6 +305,18 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
Attributes: attributes,
|
Attributes: attributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Validation] Based on Tenant Schema
|
||||||
|
if req.CompanyCode != "" && h.TenantService != nil {
|
||||||
|
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode)
|
||||||
|
if err == nil && tenant != nil {
|
||||||
|
if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||||
|
if err := h.validateMetadata(req.Metadata, schema, true); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
identityID, err := h.OryProvider.CreateUser(brokerUser, password)
|
identityID, err := h.OryProvider.CreateUser(brokerUser, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "already exists") {
|
if strings.Contains(err.Error(), "already exists") {
|
||||||
@@ -408,6 +423,24 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Validation] Based on Tenant Schema
|
||||||
|
schemaCompCode := extractTraitString(identity.Traits, "companyCode")
|
||||||
|
if req.CompanyCode != nil {
|
||||||
|
schemaCompCode = *req.CompanyCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if schemaCompCode != "" && h.TenantService != nil {
|
||||||
|
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), schemaCompCode)
|
||||||
|
if err == nil && tenant != nil {
|
||||||
|
if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||||
|
isAdmin := requester != nil && (requester.Role == domain.RoleSuperAdmin || requester.Role == domain.RoleTenantAdmin)
|
||||||
|
if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
traits := identity.Traits
|
traits := identity.Traits
|
||||||
if traits == nil {
|
if traits == nil {
|
||||||
traits = map[string]interface{}{}
|
traits = map[string]interface{}{}
|
||||||
@@ -730,3 +763,68 @@ func normalizePhoneNumber(phone string) string {
|
|||||||
}
|
}
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) validateMetadata(metadata map[string]any, schema []interface{}, checkRequired bool) error {
|
||||||
|
return h.validateMetadataWithAuth(metadata, schema, true, checkRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) validateMetadataWithAuth(metadata map[string]any, schema []interface{}, isAdmin bool, checkRequired bool) error {
|
||||||
|
schemaMap := make(map[string]map[string]interface{})
|
||||||
|
for _, s := range schema {
|
||||||
|
if m, ok := s.(map[string]interface{}); ok {
|
||||||
|
if key, ok := m["key"].(string); ok {
|
||||||
|
schemaMap[key] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check required fields
|
||||||
|
if checkRequired {
|
||||||
|
for key, config := range schemaMap {
|
||||||
|
required, _ := config["required"].(bool)
|
||||||
|
val, exists := metadata[key]
|
||||||
|
if required && (!exists || val == nil || val == "") {
|
||||||
|
return errors.New("field " + key + " is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check each field in metadata
|
||||||
|
for key, val := range metadata {
|
||||||
|
config, exists := schemaMap[key]
|
||||||
|
if !exists {
|
||||||
|
continue // Ignore fields not in schema or allow? Let's allow for now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin Only check
|
||||||
|
adminOnly, _ := config["adminOnly"].(bool)
|
||||||
|
if adminOnly && !isAdmin {
|
||||||
|
return errors.New("field " + key + " is admin only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex validation
|
||||||
|
if regexStr, ok := config["validation"].(string); ok && regexStr != "" {
|
||||||
|
strVal := ""
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
strVal = v
|
||||||
|
case float64:
|
||||||
|
strVal = fmt.Sprintf("%v", v)
|
||||||
|
case int:
|
||||||
|
strVal = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strVal != "" {
|
||||||
|
matched, err := regexp.MatchString(regexStr, strVal)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid regex pattern for field " + key)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return errors.New("field " + key + " does not match validation pattern")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user