diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index df1d0c21..1c994bed 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -9,6 +9,8 @@ import GlobalOverviewPage from "../features/overview/GlobalOverviewPage"; import TenantCreatePage from "../features/tenants/routes/TenantCreatePage"; import TenantDetailPage from "../features/tenants/routes/TenantDetailPage"; import TenantListPage from "../features/tenants/routes/TenantListPage"; +import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage"; +import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage"; import UserCreatePage from "../features/users/UserCreatePage"; import UserDetailPage from "../features/users/UserDetailPage"; import UserListPage from "../features/users/UserListPage"; @@ -28,7 +30,14 @@ export const router = createBrowserRouter( { path: "users/:id", element: }, { path: "tenants", element: }, { path: "tenants/new", element: }, - { path: "tenants/:id", element: }, + { + path: "tenants/:tenantId", + element: , + children: [ + { index: true, element: }, + { path: "schema", element: }, + ], + }, { path: "api-keys", element: }, { path: "api-keys/new", element: }, ], diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx index 14080889..dccf8a27 100644 --- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx @@ -60,6 +60,16 @@ function TenantDetailPage() { > Federation + + Schema + {/* Outlet for nested routes */} diff --git a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx new file mode 100644 index 00000000..03df8252 --- /dev/null +++ b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx @@ -0,0 +1,152 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Plus, Save, Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { Button } from "../../../components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { Input } from "../../../components/ui/input"; +import { Label } from "../../../components/ui/label"; +import { fetchTenant, updateTenant } from "../../../lib/adminApi"; + +type SchemaField = { + key: string; + label: string; + type: "text" | "number" | "boolean"; + required: boolean; +}; + +export function TenantSchemaPage() { + const { tenantId } = useParams<{ tenantId: string }>(); + const queryClient = useQueryClient(); + + if (!tenantId) return
Tenant ID missing
; + + const tenantQuery = useQuery({ + queryKey: ["tenant", tenantId], + queryFn: () => fetchTenant(tenantId), + }); + + const [fields, setFields] = useState([]); + + useEffect(() => { + if (tenantQuery.data?.config?.userSchema) { + setFields(tenantQuery.data.config.userSchema as SchemaField[]); + } + }, [tenantQuery.data]); + + const updateMutation = useMutation({ + mutationFn: (newFields: SchemaField[]) => + updateTenant(tenantId, { + config: { + ...tenantQuery.data?.config, + userSchema: newFields, + }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] }); + alert("Schema updated successfully"); + }, + onError: (err: AxiosError<{ error?: string }>) => { + alert(err.response?.data?.error || "Failed to update schema"); + }, + }); + + const addField = () => { + setFields([...fields, { key: "", label: "", type: "text", required: false }]); + }; + + const removeField = (index: number) => { + setFields(fields.filter((_, i) => i !== index)); + }; + + const updateField = (index: number, updates: Partial) => { + const newFields = [...fields]; + newFields[index] = { ...newFields[index], ...updates }; + setFields(newFields); + }; + + return ( +
+ + +
+
+ User Schema Extension + + Define custom attributes for users in this tenant. + +
+ +
+
+ + {fields.length === 0 && ( +
+ No custom fields defined. Click "Add Field" to begin. +
+ )} + {fields.map((field, index) => ( +
+
+ + updateField(index, { key: e.target.value })} + placeholder="e.g. employee_id" + /> +
+
+ + updateField(index, { label: e.target.value })} + placeholder="e.g. 사번" + /> +
+
+ + +
+ +
+ ))} +
+
+ +
+ +
+
+ ); +} diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index 536644d7..070401cc 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -38,8 +38,9 @@ function UserCreatePage() { const { register, handleSubmit, + watch, formState: { errors }, - } = useForm({ + } = useForm }>({ defaultValues: { email: "", password: "", @@ -48,9 +49,21 @@ function UserCreatePage() { role: "user", companyCode: "", department: "", + metadata: {}, }, }); + const selectedCompanyCode = watch("companyCode"); + const selectedTenant = tenants.find((t) => t.slug === selectedCompanyCode); + + const { data: tenantDetail } = useQuery({ + queryKey: ["tenant", selectedTenant?.id], + queryFn: () => fetchTenant(selectedTenant!.id), + enabled: !!selectedTenant?.id, + }); + + const userSchema = (tenantDetail?.config?.userSchema as any[]) ?? []; + const mutation = useMutation({ mutationFn: createUser, onSuccess: (data: UserCreateResponse) => { @@ -212,35 +225,109 @@ function UserCreatePage() { -
-
- -
- -
-
+
-
- - -
-
+
-
+ + +
+ + + +
+ +
+ + + +
+ + + + + +
+ +
+ + + + {userSchema.length > 0 && ( + +
+ +

+ + 테넌트 확장 정보 (Custom Fields) + +

+ +
+ + {userSchema.map((field) => ( + +
+ + + + + +
+ + ))} + +
+ +
+ + )} + + + +
- - {tenants.map((t) => ( - ))} - -
-
+
-
- - -
-
+
-
+ + +
+ + + +
+ +
+ + + +
+ + + + + +
+ +
+ + + + {userSchema.length > 0 && ( + +
+ +

+ + 테넌트 확장 정보 (Custom Fields) + +

+ +
+ + {userSchema.map((field) => ( + +
+ + + + + +
+ + ))} + +
+ +
+ + )} + + + +

보안 설정

diff --git a/adminfront/src/lib/adminApi.ts b/adminfront/src/lib/adminApi.ts index 28486211..ddd96095 100644 --- a/adminfront/src/lib/adminApi.ts +++ b/adminfront/src/lib/adminApi.ts @@ -26,6 +26,7 @@ export type TenantSummary = { description: string; status: string; domains?: string[]; + config?: Record; createdAt: string; updatedAt: string; }; @@ -36,6 +37,7 @@ export type TenantCreateRequest = { description?: string; status?: string; domains?: string[]; + config?: Record; }; export type TenantListResponse = { @@ -51,6 +53,7 @@ export type TenantUpdateRequest = { description?: string; status?: string; domains?: string[]; + config?: Record; }; export type ApiKeySummary = { @@ -172,6 +175,7 @@ export type UserSummary = { status: string; companyCode?: string; tenant?: TenantSummary; + metadata?: Record; department?: string; createdAt: string; updatedAt: string; diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2935d298..ecf26a7b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -248,7 +248,7 @@ func main() { tenantHandler := handler.NewTenantHandler(db, tenantService) kratosAdminService := service.NewKratosAdminService() oryAdminProvider := service.NewOryProvider() - userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService) + userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, userRepo) apiKeyHandler := handler.NewApiKeyHandler(db) // 3. Initialize Fiber diff --git a/backend/internal/domain/auth_models.go b/backend/internal/domain/auth_models.go index 7f267ff1..a0cacfcd 100644 --- a/backend/internal/domain/auth_models.go +++ b/backend/internal/domain/auth_models.go @@ -72,9 +72,10 @@ type UserProfileResponse struct { Name string `json:"name"` Phone string `json:"phone"` Department string `json:"department"` - AffiliationType string `json:"affiliationType"` - CompanyCode string `json:"companyCode,omitempty"` - Tenant *Tenant `json:"tenant,omitempty"` + AffiliationType string `json:"affiliationType"` + CompanyCode string `json:"companyCode,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + Tenant *Tenant `json:"tenant,omitempty"` } type UpdateUserRequest struct { diff --git a/backend/internal/domain/json_map.go b/backend/internal/domain/json_map.go new file mode 100644 index 00000000..ccbd9776 --- /dev/null +++ b/backend/internal/domain/json_map.go @@ -0,0 +1,42 @@ +package domain + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +// JSONMap is a custom type for handling map[string]any with PostgreSQL JSONB +type JSONMap map[string]any + +// Value implements the driver.Valuer interface +func (m JSONMap) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + ba, err := json.Marshal(m) + return string(ba), err +} + +// Scan implements the sql.Scanner interface +func (m *JSONMap) Scan(value any) error { + if value == nil { + *m = make(JSONMap) + return nil + } + var bytes []byte + switch v := value.(type) { + case []byte: + bytes = v + case string: + bytes = []byte(v) + default: + return errors.New(fmt.Sprintf("failed to scan JSONMap: %v", value)) + } + + result := make(JSONMap) + err := json.Unmarshal(bytes, &result) + *m = result + return err +} diff --git a/backend/internal/domain/tenant.go b/backend/internal/domain/tenant.go index 52acc9c0..d0d2e13a 100644 --- a/backend/internal/domain/tenant.go +++ b/backend/internal/domain/tenant.go @@ -15,6 +15,7 @@ type Tenant struct { Description string `json:"description"` Status string `gorm:"default:'active'" json:"status"` Domains []TenantDomain `gorm:"foreignKey:TenantID" json:"domains,omitempty"` + Config JSONMap `gorm:"type:jsonb" json:"config,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go index 4ef96583..307fea35 100644 --- a/backend/internal/domain/user.go +++ b/backend/internal/domain/user.go @@ -20,6 +20,7 @@ type User struct { TenantID *string `gorm:"type:uuid;index" json:"tenantId,omitempty"` Tenant *Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"` Department string `json:"department"` + Metadata JSONMap `gorm:"type:jsonb" json:"metadata,omitempty"` Status string `gorm:"default:'active'" json:"status"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 3f4078db..75041fdc 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -2338,6 +2338,7 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error { Department: dept, AffiliationType: affType, CompanyCode: compCode, + Metadata: userResponse.CustomAttributes, } if compCode != "" { @@ -4132,7 +4133,20 @@ func (h *AuthHandler) getKratosProfile(sessionToken string) (*domain.UserProfile Department: dept, AffiliationType: affType, CompanyCode: compCode, + Metadata: make(map[string]any), } + + coreTraits := map[string]bool{ + "email": true, "name": true, "phone_number": true, + "grade": true, "companyCode": true, "department": true, + "affiliationType": true, + } + for k, v := range traits { + if !coreTraits[k] { + profile.Metadata[k] = v + } + } + return profile, nil } @@ -4157,7 +4171,20 @@ func (h *AuthHandler) getKratosProfileWithCookie(cookie string) (*domain.UserPro Department: dept, AffiliationType: affType, CompanyCode: compCode, + Metadata: make(map[string]any), } + + coreTraits := map[string]bool{ + "email": true, "name": true, "phone_number": true, + "grade": true, "companyCode": true, "department": true, + "affiliationType": true, + } + for k, v := range traits { + if !coreTraits[k] { + profile.Metadata[k] = v + } + } + return profile, nil } diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index f9b04ad1..affed786 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -21,14 +21,15 @@ func NewTenantHandler(db *gorm.DB, svc service.TenantService) *TenantHandler { } type tenantSummary struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - Status string `json:"status"` - Domains []string `json:"domains,omitempty"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Status string `json:"status"` + Domains []string `json:"domains,omitempty"` + Config domain.JSONMap `json:"config,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } type tenantListResponse struct { @@ -99,9 +100,10 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { var req struct { Name string `json:"name"` Slug string `json:"slug"` - Description string `json:"description"` - Status string `json:"status"` - Domains []string `json:"domains"` + Description string `json:"description"` + Status string `json:"status"` + Domains []string `json:"domains"` + Config map[string]any `json:"config"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) @@ -134,6 +136,11 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + if req.Config != nil { + tenant.Config = req.Config + h.DB.Save(tenant) + } + return c.Status(fiber.StatusCreated).JSON(mapTenantSummary(*tenant)) } @@ -158,9 +165,10 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { var req struct { Name *string `json:"name"` Slug *string `json:"slug"` - Description *string `json:"description"` - Status *string `json:"status"` - Domains []string `json:"domains"` + Description *string `json:"description"` + Status *string `json:"status"` + Domains []string `json:"domains"` + Config map[string]any `json:"config"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) @@ -198,6 +206,9 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { } tenant.Status = status } + if req.Config != nil { + tenant.Config = req.Config + } if err := h.DB.Save(&tenant).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -262,6 +273,7 @@ func mapTenantSummary(t domain.Tenant) tenantSummary { Description: t.Description, Status: t.Status, Domains: domains, + Config: t.Config, CreatedAt: t.CreatedAt.Format(time.RFC3339), UpdatedAt: t.UpdatedAt.Format(time.RFC3339), } diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index ab3a6f91..8c37408c 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -2,6 +2,7 @@ package handler import ( "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/repository" "baron-sso-backend/internal/service" "baron-sso-backend/internal/utils" "context" @@ -16,13 +17,15 @@ type UserHandler struct { KratosAdmin *service.KratosAdminService OryProvider *service.OryProvider TenantService service.TenantService + UserRepo repository.UserRepository } -func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService) *UserHandler { +func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, userRepo repository.UserRepository) *UserHandler { return &UserHandler{ KratosAdmin: kratosAdmin, OryProvider: oryProvider, TenantService: tenantService, + UserRepo: userRepo, } } @@ -34,6 +37,7 @@ type userSummary struct { Role string `json:"role"` Status string `json:"status"` CompanyCode string `json:"companyCode"` + Metadata domain.JSONMap `json:"metadata,omitempty"` Tenant *domain.Tenant `json:"tenant,omitempty"` Department string `json:"department"` CreatedAt string `json:"createdAt"` @@ -128,13 +132,14 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { } var req struct { - Email string `json:"email"` - Password string `json:"password"` - Name string `json:"name"` - Phone string `json:"phone"` - Role string `json:"role"` - CompanyCode string `json:"companyCode"` - Department string `json:"department"` + Email string `json:"email"` + Password string `json:"password"` + Name string `json:"name"` + Phone string `json:"phone"` + Role string `json:"role"` + CompanyCode string `json:"companyCode"` + Department string `json:"department"` + Metadata map[string]any `json:"metadata"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) @@ -191,6 +196,14 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { "grade": role, } + // Merge custom metadata into attributes + for k, v := range req.Metadata { + // Don't overwrite core fields + if _, exists := attributes[k]; !exists { + attributes[k] = v + } + } + brokerUser := &domain.BrokerUser{ Email: email, Name: name, @@ -206,6 +219,32 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + // [New] Local DB Sync + localUser := &domain.User{ + ID: identityID, + Email: email, + Name: name, + Phone: normalizePhoneNumber(req.Phone), + AffiliationType: "internal", + CompanyCode: req.CompanyCode, + Department: req.Department, + Role: role, + Status: "active", + Metadata: req.Metadata, + } + + if req.CompanyCode != "" && h.TenantService != nil { + if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil { + localUser.TenantID = &tenant.ID + } + } + + if h.UserRepo != nil { + if err := h.UserRepo.Create(c.Context(), localUser); err != nil { + slog.Error("[UserHandler] Failed to sync user to local DB", "email", email, "error", err) + } + } + identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -240,13 +279,14 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { } var req struct { - Password *string `json:"password"` - Name *string `json:"name"` - Phone *string `json:"phone"` - Role *string `json:"role"` - Status *string `json:"status"` - CompanyCode *string `json:"companyCode"` - Department *string `json:"department"` + Password *string `json:"password"` + Name *string `json:"name"` + Phone *string `json:"phone"` + Role *string `json:"role"` + Status *string `json:"status"` + CompanyCode *string `json:"companyCode"` + Department *string `json:"department"` + Metadata map[string]any `json:"metadata"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) @@ -276,12 +316,66 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { traits["grade"] = role } + // [Refined] Metadata synchronization: replace non-core traits with new Metadata + coreTraits := map[string]bool{ + "email": true, "name": true, "phone_number": true, + "grade": true, "companyCode": true, "department": true, + "affiliationType": true, + } + + // 1. Remove existing non-core traits to handle deletions + for k := range traits { + if !coreTraits[k] { + delete(traits, k) + } + } + + // 2. Add new metadata fields + for k, v := range req.Metadata { + if !coreTraits[k] { + traits[k] = v + } + } + state := normalizeKratosState(req.Status) updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), userID, traits, state) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + // [New] Local DB Sync + if h.UserRepo != nil { + if localUser, err := h.UserRepo.FindByID(c.Context(), userID); err == nil && localUser != nil { + if req.Name != nil { + localUser.Name = *req.Name + } + if req.Phone != nil { + localUser.Phone = normalizePhoneNumber(*req.Phone) + } + if req.CompanyCode != nil { + localUser.CompanyCode = *req.CompanyCode + if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil { + localUser.TenantID = &tenant.ID + } + } + if req.Department != nil { + localUser.Department = *req.Department + } + if req.Role != nil { + localUser.Role = *req.Role + } + if req.Status != nil { + localUser.Status = *req.Status + } + if req.Metadata != nil { + localUser.Metadata = req.Metadata + } + if err := h.UserRepo.Update(c.Context(), localUser); err != nil { + slog.Error("[UserHandler] Failed to sync user update to local DB", "userID", userID, "error", err) + } + } + } + if req.Password != nil && *req.Password != "" { if err := h.KratosAdmin.UpdateIdentityPassword(c.Context(), userID, *req.Password); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -326,10 +420,23 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K Status: normalizeStatus(identity.State), CompanyCode: compCode, Department: extractTraitString(traits, "department"), + Metadata: make(domain.JSONMap), CreatedAt: formatTime(identity.CreatedAt), UpdatedAt: formatTime(identity.UpdatedAt), } + // Filter out core traits and put everything else in Metadata + coreTraits := map[string]bool{ + "email": true, "name": true, "phone_number": true, + "grade": true, "companyCode": true, "department": true, + "affiliationType": true, + } + for k, v := range traits { + if !coreTraits[k] { + summary.Metadata[k] = v + } + } + if compCode != "" && h.TenantService != nil { if tenant, err := h.TenantService.GetTenantBySlug(ctx, compCode); err == nil && tenant != nil { summary.Tenant = tenant diff --git a/docker/ory/kratos/identity.schema.json b/docker/ory/kratos/identity.schema.json index d967d074..91ed0358 100644 --- a/docker/ory/kratos/identity.schema.json +++ b/docker/ory/kratos/identity.schema.json @@ -98,7 +98,7 @@ "required": [ "email" ], - "additionalProperties": false + "additionalProperties": true } } } diff --git a/userfront/lib/features/profile/data/models/user_profile_model.dart b/userfront/lib/features/profile/data/models/user_profile_model.dart index ed12c4ba..a12187fd 100644 --- a/userfront/lib/features/profile/data/models/user_profile_model.dart +++ b/userfront/lib/features/profile/data/models/user_profile_model.dart @@ -38,6 +38,7 @@ class UserProfile { final String department; final String affiliationType; final String companyCode; + final Map? metadata; final Tenant? tenant; UserProfile({ @@ -48,6 +49,7 @@ class UserProfile { required this.department, required this.affiliationType, required this.companyCode, + this.metadata, this.tenant, }); @@ -60,6 +62,7 @@ class UserProfile { department: json['department'] ?? '', affiliationType: json['affiliationType'] ?? '', companyCode: json['companyCode'] ?? '', + metadata: json['metadata'] != null ? Map.from(json['metadata']) : null, tenant: json['tenant'] != null ? Tenant.fromJson(json['tenant']) : null, ); } @@ -73,6 +76,7 @@ class UserProfile { 'department': department, 'affiliationType': affiliationType, 'companyCode': companyCode, + 'metadata': metadata, 'tenant': tenant?.toJson(), }; }