first commit
CI/CD / CI · Admin API (dotnet build) (push) Successful in 41s
CI/CD / CI · Admin Web (tsc) (push) Failing after 5s
CI/CD / CI · Website (tsc) (push) Failing after 4s
CI/CD / CI · Koja (tsc) (push) Failing after 5s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m13s
CI/CD / CI · Dashboard (tsc) (push) Failing after 2m32s
CI/CD / Deploy · all services (push) Has been skipped

This commit is contained in:
soroush.asadi
2026-05-31 11:06:24 +03:30
parent 51e422272d
commit 345ae0a4b5
69 changed files with 11964 additions and 152 deletions
+56
View File
@@ -0,0 +1,56 @@
import { apiGet, apiPost, apiPatch, apiDelete } from "@/lib/api/client";
import type { AuthTokenResponse } from "@/lib/api/types";
export interface BranchRoleAssignment {
id: string;
branchId: string;
branchName: string;
role: string;
}
export function listBranchRoles(cafeId: string, employeeId: string) {
return apiGet<BranchRoleAssignment[]>(
`/api/cafes/${cafeId}/employees/${employeeId}/branch-roles`
);
}
export function assignBranchRole(
cafeId: string,
employeeId: string,
body: { branchId: string; role: string }
) {
return apiPost<BranchRoleAssignment, typeof body>(
`/api/cafes/${cafeId}/employees/${employeeId}/branch-roles`,
body
);
}
export function updateBranchRole(
cafeId: string,
employeeId: string,
assignmentId: string,
role: string
) {
return apiPatch<BranchRoleAssignment, { role: string }>(
`/api/cafes/${cafeId}/employees/${employeeId}/branch-roles/${assignmentId}`,
{ role }
);
}
export function removeBranchRole(
cafeId: string,
employeeId: string,
assignmentId: string
) {
return apiDelete(
`/api/cafes/${cafeId}/employees/${employeeId}/branch-roles/${assignmentId}`
);
}
/** Re-issue the session token scoped to a branch (null = café-wide, Owner only). */
export function switchBranch(branchId: string | null) {
return apiPost<AuthTokenResponse, { branchId: string | null }>(
`/api/auth/switch-branch`,
{ branchId }
);
}
+14
View File
@@ -11,6 +11,12 @@ export interface CafeMembership {
planTier: string;
}
export interface BranchMembership {
branchId: string;
branchName: string;
role: string;
}
export interface AuthTokenResponse {
accessToken: string;
refreshToken: string;
@@ -23,6 +29,14 @@ export interface AuthTokenResponse {
actor?: string;
branchId?: string | null;
memberships?: CafeMembership[] | null;
/** Display name of the currently active branch (null when café-wide). */
branchName?: string | null;
/** True when the session spans the whole café (Owner, no branch scope). */
isCafeWide?: boolean;
/** Branches this employee may operate as, with their role in each. */
branches?: BranchMembership[] | null;
/** Effective capabilities for the active role — drives page/action gating. */
permissions?: string[] | null;
}
/** Returned (in the data field) when a phone belongs to multiple cafés. */
+13 -2
View File
@@ -1,4 +1,5 @@
import { BRANCH_ONLY_NAV_GROUP, type NavGroupId } from "@/lib/sidebar-nav";
import { BRANCH_ONLY_NAV_GROUP, type NavGroupId, type NavItemKey } from "@/lib/sidebar-nav";
import { NAV_REQUIRED_PERMISSION } from "@/lib/permissions";
/** Cafe owner (HQ) — billing, taxes, branches. */
export function isCafeOwner(role: string | undefined): boolean {
@@ -26,7 +27,8 @@ export function canSeeNavGroup(
export function canSeeNavItem(
key: string,
role: string | undefined,
branchId: string | null | undefined
branchId: string | null | undefined,
permissions?: Set<string> | null
): boolean {
if ((OWNER_ONLY_NAV_KEYS as readonly string[]).includes(key) && !isCafeOwner(role)) {
return false;
@@ -34,5 +36,14 @@ export function canSeeNavItem(
if (key === "branches" && isBranchAccount(branchId)) {
return false;
}
// Permission-based page visibility. `permissions === null` means a legacy
// session with no permission list — fall back to the role/branch rules above
// so those users keep their current access until the next token refresh.
if (permissions) {
const required = NAV_REQUIRED_PERMISSION[key as NavItemKey];
if (required && !permissions.has(required)) {
return false;
}
}
return true;
}
+80
View File
@@ -0,0 +1,80 @@
import { useAuthStore } from "@/lib/stores/auth.store";
import type { NavItemKey } from "@/lib/sidebar-nav";
/**
* Client mirror of the backend `Meezi.Core.Authorization.Permission` enum. The
* server (EnsurePermission) remains the single source of truth — these values
* only drive what the UI *shows* (pages, action buttons). Never rely on them
* for actual security.
*/
export type Permission =
| "ManageCafeSettings"
| "ManageBilling"
| "ManageBranches"
| "ManageStaff"
| "ManageMenu"
| "ManageInventory"
| "ManageExpenses"
| "ManageTaxes"
| "ManageCoupons"
| "ManageReservations"
| "ManageTables"
| "ViewReports"
| "ReviewLeave"
| "ManageSalaries"
| "ManagePrintSettings"
| "ProcessOrders"
| "HandlePayments"
| "OperateRegister"
| "ManageQueue"
| "ViewKitchen"
| "HandleDelivery";
/**
* Permission a nav page requires to be visible. Pages not listed here fall back
* to the existing owner-only / branch-account visibility logic in
* {@link file://./auth-permissions.ts}.
*/
export const NAV_REQUIRED_PERMISSION: Partial<Record<NavItemKey, Permission>> = {
pos: "ProcessOrders",
tables: "ManageTables",
queue: "ManageQueue",
kds: "ViewKitchen",
reservations: "ManageReservations",
menu: "ManageMenu",
inventory: "ManageInventory",
coupons: "ManageCoupons",
reports: "ViewReports",
expenses: "ManageExpenses",
shifts: "OperateRegister",
taxes: "ManageTaxes",
hr: "ManageStaff",
};
/** Read the effective permission set off an auth response (null = legacy session). */
export function permissionsOf(
user: { permissions?: string[] | null } | null | undefined
): Set<string> | null {
if (!user?.permissions) return null;
return new Set(user.permissions);
}
/**
* Whether the user holds a capability. Legacy sessions (no permissions array, e.g.
* issued before this feature shipped) return `true` so the UI degrades gracefully
* until the next token refresh — the server still enforces real access.
*/
export function hasPermission(
user: { permissions?: string[] | null } | null | undefined,
permission: Permission
): boolean {
const set = permissionsOf(user);
if (set === null) return true;
return set.has(permission);
}
/** React hook: does the current user hold the given permission? */
export function useHasPermission(permission: Permission): boolean {
const user = useAuthStore((s) => s.user);
return hasPermission(user, permission);
}