@goplusvn/core 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,45 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleasedplatform foundation extraction
3
+ ## 0.1.8branding wiring + auth/rbac unification + clean typecheck
4
+
5
+ - **Single source for permission checks (security fix).** `@goerp/core/rbac`
6
+ used to ship its own copy of `checkPermission` / `getUserPermissions` /
7
+ `getCrudPermissionsFromSession` / … which had drifted from `@goerp/core/auth`
8
+ — it still allowed `BYPASS_AUTH` in production and denied admins with no
9
+ explicit permission rows. `rbac` now re-exports these from the hardened `auth`
10
+ module (bypass dev-only, admin-before-empty-guard), so the two can't diverge.
11
+
12
+
13
+ - **Branding via `TenantProvider`.** `Logo`/`LogoCompact` now resolve their image,
14
+ company name and tagline from `props → TenantProvider branding → defaults`, so a
15
+ new app re-skins by wrapping with `<TenantProvider tenant={{ branding }}>` (or
16
+ passing `logoSrc`/`companyName`/`tagline` props) instead of hardcoding. Added a
17
+ non-throwing `useOptionalTenant()` hook and a `tagline` field to `TenantBranding`.
18
+ `TenantProvider` also applies `branding.primaryColor` (an HSL triple) to
19
+ `--primary`/`--accent-primary` at runtime, so the brand color is config-driven
20
+ too. Fully backward compatible (no provider/props → previous GoERP defaults).
21
+ - **Core now type-checks clean (0 errors).** Fixed the `system/categories`
22
+ components that imported the consumer alias `@goerp/core/ui` from inside the
23
+ package (unresolvable in core's own tsconfig → cascaded into implicit-any in
24
+ their event handlers); switched to relative imports.
25
+ - **Standard entity configs.** `@goerp/core/configs/entities` now ships canonical
26
+ `EntityConfig`s (starting with `departmentsConfig`, plus the previously
27
+ unexported `materialCategoriesConfig`) so apps import + spread-to-override
28
+ instead of copy-pasting near-identical configs.
29
+
30
+ ## 0.1.7 — auth hardening
31
+
32
+ Hardened `@goerp/core/auth` so the Vinh Hoa app (and others) can consolidate
33
+ their hand-copied RBAC clones onto core without regressing two safety behaviors:
34
+
35
+ - **Bypass is dev-only.** `BYPASS_AUTH` now requires `NODE_ENV !== "production"`,
36
+ so a `BYPASS_AUTH=1` env left set on a prod deploy can no longer disable all
37
+ permission checks.
38
+ - **Admin-before-empty-permissions.** `checkPermission` now evaluates the
39
+ admin-role bypass BEFORE the empty-permissions guard, so an admin who relies on
40
+ their role (no explicit permission rows) is no longer denied everything.
41
+
42
+ ## 0.1.6 — platform foundation extraction
4
43
 
5
44
  Promoted the cross-cutting platform layer out of the Vinh Hoa reference app into
6
45
  core so every app inherits the same design system + infrastructure and only
package/PLATFORM.md CHANGED
@@ -31,6 +31,53 @@ Use semantic tokens, never the raw Tailwind palette: `text-muted-foreground` not
31
31
  `text-slate-500`, `bg-card` not `bg-white`, `text-destructive` not `text-red-600`.
32
32
  Editing `base.css` reskins every app at once.
33
33
 
34
+ ## Branding (logo / company name)
35
+
36
+ Wrap the app shell with `TenantProvider` — `Logo`/`LogoCompact` (used by the
37
+ sidebar/header) then render the app's logo, name and tagline from config instead
38
+ of the GoERP defaults. No provider → defaults (backward compatible).
39
+
40
+ ```tsx
41
+ import { TenantProvider } from "@goerp/core/providers"
42
+
43
+ <TenantProvider
44
+ tenant={{
45
+ id: "tanloc",
46
+ name: "Tấn Lộc",
47
+ branding: {
48
+ logo: "/logo.png",
49
+ companyName: "Tấn Lộc",
50
+ tagline: "Suất ăn CN",
51
+ primaryColor: "262 83% 58%", // HSL triple → re-skins --primary at runtime
52
+ favicon: "/favicon.ico",
53
+ },
54
+ currency: "VND",
55
+ }}
56
+ >
57
+ <MainLayout>{children}</MainLayout>
58
+ </TenantProvider>
59
+ ```
60
+
61
+ `primaryColor` (an HSL triple, no `hsl()` wrapper) reskins the whole app's brand
62
+ token at runtime — no globals edit needed. `favicon` is data the app reads in its
63
+ metadata. Read branding anywhere via `useOptionalTenant()` (non-throwing); for a
64
+ one-off, `Logo` also accepts `logoSrc` / `companyName` / `tagline` props.
65
+
66
+ ## Standard entity configs
67
+
68
+ Core ships canonical configs for org entities so apps don't copy-paste them.
69
+ Import and override by spreading:
70
+
71
+ ```ts
72
+ import { departmentsConfig } from "@goerp/core/configs/entities"
73
+
74
+ // use as-is, or tweak:
75
+ export const departments = { ...departmentsConfig, apiEndpoint: "/api/org/departments" }
76
+ ```
77
+
78
+ These drive the core CRUD engine (`@goerp/core/crud`) — declare an `EntityConfig`
79
+ (fields/filters/permissions/features) and you get table/form/import/export.
80
+
34
81
  ## Errors
35
82
 
36
83
  Bind the db-injected factories once, then use `serverError` in every catch block.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.6",
4
+ "version": "0.1.8",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -49,6 +49,7 @@
49
49
  "./utils/date-utils": "./src/utils/date-utils.ts",
50
50
  "./utils/cccd-parser": "./src/utils/cccd-parser.ts",
51
51
  "./configs/status": "./src/configs/status.ts",
52
+ "./configs/entities": "./src/configs/entities/index.ts",
52
53
  "./package.json": "./package.json"
53
54
  },
54
55
  "peerDependencies": {
package/src/auth/index.ts CHANGED
@@ -43,8 +43,11 @@ export interface CrudPermissionResult {
43
43
  // ============================================================================
44
44
 
45
45
  const ADMIN_ROLE_CODE = "admin";
46
+ // Auth bypass is a DEV-ONLY escape hatch — never allow it in production, even if
47
+ // the env var is accidentally left set on a prod deploy.
46
48
  const BYPASS_AUTH =
47
- process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1";
49
+ process.env.NODE_ENV !== "production" &&
50
+ (process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1");
48
51
 
49
52
  // Action code mapping
50
53
  const ACTION_CODES: Record<string, string> = {
@@ -186,11 +189,8 @@ export function checkPermission(
186
189
  if (!session?.user) return false;
187
190
  const user = session.user as ExtendedUser;
188
191
 
189
- if (!user.permissions || user.permissions.length === 0) {
190
- return false;
191
- }
192
-
193
- // Admin role bypass
192
+ // Admin role bypass — checked BEFORE the empty-permissions guard so an admin
193
+ // who relies on their role (no explicit permission rows) is not locked out.
194
194
  if (
195
195
  user.roles?.includes(ADMIN_ROLE_CODE) ||
196
196
  user.roles?.includes("SUPER_ADMIN")
@@ -198,6 +198,10 @@ export function checkPermission(
198
198
  return true;
199
199
  }
200
200
 
201
+ if (!user.permissions || user.permissions.length === 0) {
202
+ return false;
203
+ }
204
+
201
205
  return user.permissions.some(
202
206
  (p) => p.resourceCode === resourceCode && p.actionCode === actionCode,
203
207
  );
@@ -0,0 +1,140 @@
1
+ import { Building2 } from "lucide-react";
2
+
3
+ import type { EntityConfig } from "../../types";
4
+
5
+ import { STATUS_OPTIONS } from "../status";
6
+
7
+ /**
8
+ * Canonical "Phòng ban" (department) entity config — the org-structure entity
9
+ * every business app needs. Apps import this instead of copy-pasting it, and
10
+ * override fields by spreading: `{ ...departmentsConfig, apiEndpoint: "..." }`.
11
+ */
12
+ export const departmentsConfig: EntityConfig = {
13
+ name: "department",
14
+ permissionResource: "department",
15
+ label: "Phòng ban",
16
+ pluralLabel: "Phòng ban",
17
+ icon: Building2,
18
+ description: "Quản lý cơ cấu tổ chức phòng ban",
19
+ apiEndpoint: "/api/departments",
20
+ idField: "id",
21
+ displayField: "name",
22
+
23
+ fields: [
24
+ {
25
+ name: "id",
26
+ label: "ID",
27
+ type: "text",
28
+ hideInForm: true,
29
+ hideInTable: true,
30
+ },
31
+ {
32
+ name: "code",
33
+ label: "Mã phòng ban",
34
+ type: "text",
35
+ required: true,
36
+ placeholder: "VD: IT, HR, SALES",
37
+ filterable: true,
38
+ width: 120,
39
+ },
40
+ {
41
+ name: "name",
42
+ label: "Tên phòng ban",
43
+ type: "text",
44
+ required: true,
45
+ placeholder: "Nhập tên phòng ban",
46
+ filterable: true,
47
+ },
48
+ {
49
+ name: "description",
50
+ label: "Mô tả",
51
+ type: "textarea",
52
+ placeholder: "Nhập mô tả phòng ban",
53
+ hideInTable: true,
54
+ },
55
+ {
56
+ name: "parentId",
57
+ label: "Phòng ban cha",
58
+ type: "select",
59
+ placeholder: "Chọn phòng ban cha (nếu có)",
60
+ dataSource: {
61
+ type: "api",
62
+ endpoint: "/api/departments",
63
+ labelField: "name",
64
+ valueField: "id",
65
+ searchable: true,
66
+ },
67
+ hideInTable: true,
68
+ },
69
+ {
70
+ name: "parentName",
71
+ label: "Phòng ban cha",
72
+ type: "text",
73
+ hideInForm: true,
74
+ isDisplayOnly: true,
75
+ },
76
+ {
77
+ name: "order",
78
+ label: "Thứ tự",
79
+ type: "number",
80
+ defaultValue: 0,
81
+ placeholder: "Thứ tự hiển thị",
82
+ width: 80,
83
+ },
84
+ {
85
+ name: "status",
86
+ label: "Trạng thái",
87
+ type: "switch",
88
+ required: true,
89
+ defaultValue: "active",
90
+ filterable: true,
91
+ options: STATUS_OPTIONS,
92
+ },
93
+ {
94
+ name: "createdAt",
95
+ label: "Ngày tạo",
96
+ type: "datetime",
97
+ hideInForm: true,
98
+ width: 120,
99
+ renderCell: (value) =>
100
+ new Date(value as string).toLocaleDateString("vi-VN"),
101
+ },
102
+ ],
103
+
104
+ filters: [
105
+ {
106
+ name: "search",
107
+ label: "Tìm kiếm",
108
+ type: "text",
109
+ field: "name",
110
+ operator: "contains",
111
+ },
112
+ {
113
+ name: "status",
114
+ label: "Trạng thái",
115
+ type: "select",
116
+ field: "status",
117
+ operator: "eq",
118
+ options: [
119
+ { label: "Hoạt động", value: "active" },
120
+ { label: "Vô hiệu hóa", value: "inactive" },
121
+ ],
122
+ },
123
+ ],
124
+
125
+ permissions: {
126
+ create: true,
127
+ read: true,
128
+ update: true,
129
+ delete: true,
130
+ export: true,
131
+ import: true,
132
+ },
133
+
134
+ features: {
135
+ search: true,
136
+ bulkActions: true,
137
+ export: true,
138
+ import: true,
139
+ },
140
+ };
@@ -0,0 +1,4 @@
1
+ // Canonical entity configs shipped by core. Apps import these instead of
2
+ // copy-pasting, and override by spreading: `{ ...departmentsConfig, ... }`.
3
+ export { departmentsConfig } from "./departments.config";
4
+ export { materialCategoriesConfig } from "./material-categories.config";
@@ -4,7 +4,14 @@ import React, { createContext, useContext } from "react";
4
4
  export interface TenantBranding {
5
5
  logo?: string;
6
6
  companyName: string;
7
+ /** Subtitle shown under the company name in the logo (e.g. "ERP System"). */
8
+ tagline?: string;
9
+ /**
10
+ * Brand color as an HSL triple, e.g. "262 83% 58%" (no `hsl(...)` wrapper).
11
+ * Applied at runtime to `--primary` / `--accent-primary` by TenantProvider.
12
+ */
7
13
  primaryColor?: string;
14
+ /** Favicon URL — apps read `tenant.branding.favicon` in their metadata. */
8
15
  favicon?: string;
9
16
  }
10
17
 
@@ -29,8 +36,18 @@ interface TenantProviderProps {
29
36
  }
30
37
 
31
38
  export function TenantProvider({ children, tenant }: TenantProviderProps) {
39
+ const { primaryColor } = tenant.branding;
32
40
  return (
33
41
  <TenantContext.Provider value={{ tenant }}>
42
+ {primaryColor && (
43
+ // Re-skin the brand token at runtime so the whole app follows the
44
+ // tenant color. Rendered after the imported base.css → wins the cascade.
45
+ <style
46
+ dangerouslySetInnerHTML={{
47
+ __html: `:root{--primary:${primaryColor};--accent-primary:${primaryColor};}`,
48
+ }}
49
+ />
50
+ )}
34
51
  {children}
35
52
  </TenantContext.Provider>
36
53
  );
@@ -43,3 +60,12 @@ export function useTenantContext() {
43
60
  }
44
61
  return context;
45
62
  }
63
+
64
+ /**
65
+ * Non-throwing variant: returns the tenant config if inside a TenantProvider,
66
+ * otherwise `undefined`. Lets shared components (e.g. Logo) read branding
67
+ * without forcing every app to mount a provider.
68
+ */
69
+ export function useOptionalTenant(): TenantConfig | undefined {
70
+ return useContext(TenantContext)?.tenant;
71
+ }
package/src/rbac/index.ts CHANGED
@@ -116,179 +116,21 @@ interface ExtendedUser {
116
116
  permissions?: Permission[];
117
117
  }
118
118
 
119
- export function getUserPermissions(session: Session | null): Permission[] {
120
- if (!session?.user) return [];
121
- const user = session.user as ExtendedUser;
119
+ // Permission checks now live in a single hardened source: @goerp/core/auth
120
+ // (bypass is dev-only; the admin role is honored BEFORE the empty-permissions
121
+ // guard). Re-exported here so existing @goerp/core/rbac consumers are unchanged
122
+ // and can never diverge from auth again.
123
+ export {
124
+ getUserPermissions,
125
+ getCrudPermissionsFromSession,
126
+ checkPermission,
127
+ hasPermission,
128
+ requirePermission,
129
+ hasRole,
130
+ hasAnyRole,
131
+ isAdmin,
132
+ } from "../auth";
122
133
 
123
- if (!user.permissions) {
124
- return [];
125
- }
126
- return user.permissions;
127
- }
128
-
129
- const BYPASS_AUTH =
130
- process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1";
131
-
132
- const ADMIN_ROLE_CODES = ["admin", "SUPER_ADMIN"];
133
-
134
- export function getCrudPermissionsFromSession(
135
- session: Session | null,
136
- entity: string,
137
- ): {
138
- create: boolean;
139
- view: boolean;
140
- update: boolean;
141
- delete: boolean;
142
- export: boolean;
143
- import: boolean;
144
- approve: boolean;
145
- reject: boolean;
146
- } {
147
- if (BYPASS_AUTH) {
148
- return {
149
- create: true,
150
- view: true,
151
- update: true,
152
- delete: true,
153
- export: true,
154
- import: true,
155
- approve: true,
156
- reject: true,
157
- };
158
- }
159
-
160
- if (!session?.user) {
161
- return {
162
- create: false,
163
- view: false,
164
- update: false,
165
- delete: false,
166
- export: false,
167
- import: false,
168
- approve: false,
169
- reject: false,
170
- };
171
- }
172
-
173
- const user = session.user as ExtendedUser;
174
-
175
- if (!user.id) {
176
- return {
177
- create: false,
178
- view: false,
179
- update: false,
180
- delete: false,
181
- export: false,
182
- import: false,
183
- approve: false,
184
- reject: false,
185
- };
186
- }
187
-
188
- const isAdmin = user.roles?.some((role) => ADMIN_ROLE_CODES.includes(role));
189
- if (isAdmin) {
190
- return {
191
- create: true,
192
- view: true,
193
- update: true,
194
- delete: true,
195
- export: true,
196
- import: true,
197
- approve: true,
198
- reject: true,
199
- };
200
- }
201
-
202
- const permissions = user.permissions || [];
203
- const permissionKeys = new Set(
204
- permissions.map((p) => `${p.resourceCode}:${p.actionCode}`),
205
- );
206
-
207
- const hasPermission = (action: string) => {
208
- const key = `${entity}:${action}`;
209
- return permissionKeys.has(key);
210
- };
211
-
212
- return {
213
- create: hasPermission(getActionCode("create")),
214
- view: hasPermission(getActionCode("view")),
215
- update: hasPermission(getActionCode("update")),
216
- delete: hasPermission(getActionCode("delete")),
217
- export: hasPermission(getActionCode("export")),
218
- import: hasPermission(getActionCode("import")),
219
- approve: hasPermission(getActionCode("approve")),
220
- reject: hasPermission(getActionCode("reject")),
221
- };
222
- }
223
-
224
- export function checkPermission(
225
- session: Session | null,
226
- resourceCode: string,
227
- actionCode: string,
228
- ): boolean {
229
- if (BYPASS_AUTH) {
230
- return true;
231
- }
232
-
233
- if (!session?.user) return false;
234
- const user = session.user as ExtendedUser;
235
-
236
- if (!user.permissions || user.permissions.length === 0) {
237
- return false;
238
- }
239
-
240
- if (user.roles?.some((role) => ADMIN_ROLE_CODES.includes(role))) {
241
- return true;
242
- }
243
-
244
- const permissionKey = `${resourceCode}:${actionCode}`;
245
- return user.permissions.some(
246
- (p) => p.resourceCode === resourceCode && p.actionCode === actionCode,
247
- );
248
- }
249
-
250
- export function hasPermission(
251
- session: Session | null,
252
- resourceCode: string,
253
- actionCode: string,
254
- ): boolean {
255
- return checkPermission(session, resourceCode, actionCode);
256
- }
257
-
258
- export function requirePermission(
259
- session: Session | null,
260
- resourceCode: string,
261
- actionCode: string,
262
- ): void {
263
- if (!checkPermission(session, resourceCode, actionCode)) {
264
- throw new Error(
265
- `Unauthorized: User does not have permission ${actionCode} on ${resourceCode}`,
266
- );
267
- }
268
- }
269
-
270
- export function hasRole(session: Session | null, roleCode: string): boolean {
271
- if (!session?.user) return false;
272
- const user = session.user as ExtendedUser;
273
-
274
- if (!user.roles) {
275
- return false;
276
- }
277
- return user.roles.includes(roleCode);
278
- }
279
-
280
- export function hasAnyRole(
281
- session: Session | null,
282
- roleCodes: string[],
283
- ): boolean {
284
- if (!session?.user) return false;
285
- const user = session.user as ExtendedUser;
286
-
287
- if (!user.roles) {
288
- return false;
289
- }
290
- return roleCodes.some((role) => user.roles!.includes(role));
291
- }
292
134
  export * from "./permission-service";
293
135
  export * from "./role-service";
294
136
  export * from "./resource-service";
@@ -43,7 +43,7 @@ import {
43
43
  SelectValue,
44
44
  Switch,
45
45
  Checkbox,
46
- } from "@goerp/core/ui";
46
+ } from "../../../../ui";
47
47
 
48
48
  interface CategoryListProps {
49
49
  group: SystemCategoryGroup;
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  import { Search } from "lucide-react";
5
5
 
6
6
  import type { SystemCategoryGroup } from "../../../types";
7
- import { Input } from "@goerp/core/ui";
7
+ import { Input } from "../../../../ui";
8
8
  import { GroupSidebar } from "./group-sidebar";
9
9
  import { CategoryList } from "./category-list";
10
10
 
@@ -11,7 +11,7 @@ import {
11
11
  DropdownMenuItem,
12
12
  DropdownMenuTrigger,
13
13
  ScrollArea,
14
- } from "@goerp/core/ui";
14
+ } from "../../../../ui";
15
15
  import { cn } from "../../../../utils";
16
16
  // Assuming we'll implement these dialogs or use generic ones
17
17
  // For now, I'll assume we might use a generic form or specific dialogs
@@ -34,7 +34,7 @@ import {
34
34
  FormMessage,
35
35
  Input as FormInput,
36
36
  Textarea,
37
- } from "@goerp/core/ui";
37
+ } from "../../../../ui";
38
38
  import { systemCategoryGroupSchema } from "../../../schemas/system-category-group.schema";
39
39
 
40
40
  interface GroupSidebarProps {
@@ -2,13 +2,24 @@
2
2
 
3
3
  import Image from "next/image";
4
4
 
5
+ import { useOptionalTenant } from "../../providers/tenant-provider";
5
6
  import { cn } from "../../utils";
6
7
 
8
+ // Defaults used when neither props nor a TenantProvider supply branding — keeps
9
+ // the component working out of the box (and backward compatible).
10
+ const DEFAULT_LOGO_SRC = "/images/logos/goeat_logo.png";
11
+ const DEFAULT_COMPANY = "GoERP";
12
+ const DEFAULT_TAGLINE = "ERP System";
13
+
7
14
  interface LogoProps {
8
15
  className?: string;
9
16
  size?: "sm" | "md" | "lg" | "xl" | "2xl";
10
17
  showText?: boolean;
11
18
  collapsed?: boolean;
19
+ /** Direct overrides. Otherwise read from TenantProvider branding, else defaults. */
20
+ logoSrc?: string;
21
+ companyName?: string;
22
+ tagline?: string;
12
23
  }
13
24
 
14
25
  const sizeMap = {
@@ -24,14 +35,21 @@ export function Logo({
24
35
  size = "md",
25
36
  showText = true,
26
37
  collapsed = false,
38
+ logoSrc,
39
+ companyName,
40
+ tagline,
27
41
  }: LogoProps) {
42
+ const branding = useOptionalTenant()?.branding;
43
+ const src = logoSrc ?? branding?.logo ?? DEFAULT_LOGO_SRC;
44
+ const name = companyName ?? branding?.companyName ?? DEFAULT_COMPANY;
45
+ const sub = tagline ?? branding?.tagline ?? DEFAULT_TAGLINE;
28
46
  const dimensions = sizeMap[size];
29
47
 
30
48
  return (
31
49
  <div className={cn("flex items-center gap-2", className)}>
32
50
  <Image
33
- src="/images/logos/goeat_logo.png"
34
- alt="GoERP Logo"
51
+ src={src}
52
+ alt={`${name} Logo`}
35
53
  width={dimensions.width}
36
54
  height={dimensions.height}
37
55
  className="object-contain"
@@ -39,20 +57,34 @@ export function Logo({
39
57
  />
40
58
  {showText && !collapsed && (
41
59
  <div className="flex flex-col leading-none">
42
- <span className="text-base font-bold text-foreground">GoERP</span>
43
- <span className="text-[11px] text-muted-foreground">ERP System</span>
60
+ <span className="text-base font-bold text-foreground">{name}</span>
61
+ {sub && (
62
+ <span className="text-[11px] text-muted-foreground">{sub}</span>
63
+ )}
44
64
  </div>
45
65
  )}
46
66
  </div>
47
67
  );
48
68
  }
49
69
 
50
- export function LogoCompact({ className }: { className?: string }) {
70
+ export function LogoCompact({
71
+ className,
72
+ logoSrc,
73
+ companyName,
74
+ }: {
75
+ className?: string;
76
+ logoSrc?: string;
77
+ companyName?: string;
78
+ }) {
79
+ const branding = useOptionalTenant()?.branding;
80
+ const src = logoSrc ?? branding?.logo ?? DEFAULT_LOGO_SRC;
81
+ const name = companyName ?? branding?.companyName ?? DEFAULT_COMPANY;
82
+
51
83
  return (
52
84
  <div className={cn("flex items-center justify-center", className)}>
53
85
  <Image
54
- src="/images/logos/goeat_logo.png"
55
- alt="GoERP"
86
+ src={src}
87
+ alt={name}
56
88
  width={32}
57
89
  height={32}
58
90
  className="object-contain"