initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:37:07 +08:00
commit 5382f3b4e5
87 changed files with 5278 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "@fiberops/shared",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"lint": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"zod": "^3.24.0"
},
"devDependencies": {
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,16 @@
export { Role, ALL_ROLES, satisfiesRole } from './roles';
export {
Permission, getPermissionsForRoles,
MODULES, MODULE_LABELS, ACTIONS, ACTION_LABELS, MODULE_ACTIONS,
DEFAULT_ROLE_PERMISSIONS,
} from './permissions';
export type { Module, Action, PermissionRow, PermissionString } from './permissions';
export {
SubscriptionStatus,
SubscriptionType,
InvoiceStatus,
TicketType,
TicketStatus,
PaymentMethod,
AccountStatus,
} from './statuses';

View File

@@ -0,0 +1,224 @@
import { Role } from './roles';
/**
* All modules available in the permission matrix.
* Each module can have: view, create, update, archive, approve, export actions.
*/
export const MODULES = [
'dashboard',
'clients',
'subscriptions',
'invoices',
'payments',
'tickets',
'employees',
'payroll',
'expenses',
'assets',
'accounts',
'fund_transfers',
'accounting',
'reports',
'areas',
'plans',
'settings',
'users',
] as const;
export type Module = (typeof MODULES)[number];
export const MODULE_LABELS: Record<Module, string> = {
dashboard: 'Dashboard',
clients: 'Clients',
subscriptions: 'Subscriptions',
invoices: 'Invoices',
payments: 'Payments',
tickets: 'Tickets',
employees: 'Employees',
payroll: 'Payroll',
expenses: 'Expenses',
assets: 'Assets',
accounts: 'Company Accounts',
fund_transfers: 'Fund Transfers',
accounting: 'Accounting',
reports: 'Reports',
areas: 'Areas',
plans: 'Plans',
settings: 'Settings',
users: 'Users',
};
export const ACTIONS = ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'] as const;
export type Action = (typeof ACTIONS)[number];
export const ACTION_LABELS: Record<Action, string> = {
canView: 'View',
canCreate: 'Create',
canUpdate: 'Update',
canArchive: 'Archive',
canApprove: 'Approve',
canExport: 'Export',
};
/**
* Defines which actions are applicable per module.
* Only these checkboxes should be shown/enforced in the matrix.
*/
export const MODULE_ACTIONS: Record<Module, readonly Action[]> = {
dashboard: ['canView'],
clients: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
subscriptions: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
invoices: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'],
payments: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
tickets: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
employees: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
payroll: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
expenses: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'],
assets: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
accounts: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
fund_transfers: ['canView', 'canCreate', 'canApprove', 'canExport'],
accounting: ['canView', 'canExport'],
reports: ['canView', 'canExport'],
areas: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
plans: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
settings: ['canView', 'canUpdate'],
users: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
};
/**
* Permission matrix type — one row per module with boolean actions.
*/
export interface PermissionRow {
module: Module;
canView: boolean;
canCreate: boolean;
canUpdate: boolean;
canArchive: boolean;
canApprove: boolean;
canExport: boolean;
}
/**
* Default permission matrices for system roles seeded per tenant.
*/
function allTrue(modules: readonly Module[], actions: readonly Action[]): PermissionRow[] {
return MODULES.map((mod) => ({
module: mod,
canView: actions.includes('canView') && modules.includes(mod),
canCreate: actions.includes('canCreate') && modules.includes(mod),
canUpdate: actions.includes('canUpdate') && modules.includes(mod),
canArchive: actions.includes('canArchive') && modules.includes(mod),
canApprove: actions.includes('canApprove') && modules.includes(mod),
canExport: actions.includes('canExport') && modules.includes(mod),
}));
}
const ALL_MODULES = [...MODULES] as Module[];
export const DEFAULT_ROLE_PERMISSIONS: Record<string, PermissionRow[]> = {
tenant_admin: MODULES.map((mod) => ({
module: mod,
canView: true,
canCreate: true,
canUpdate: true,
canArchive: true,
canApprove: true,
canExport: true,
})),
manager: MODULES.map((mod) => {
const noAccess: Module[] = ['users'];
const viewOnly: Module[] = ['dashboard', 'accounting', 'settings'];
if (noAccess.includes(mod)) return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (viewOnly.includes(mod)) return { module: mod, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: mod === 'accounting' };
return {
module: mod,
canView: true,
canCreate: true,
canUpdate: true,
canArchive: true,
canApprove: ['invoices', 'payments', 'expenses', 'payroll', 'fund_transfers'].includes(mod),
canExport: true,
};
}),
technician: MODULES.map((mod) => {
// Technicians can create/update tickets and record payments in the field.
// They can VIEW clients, subscriptions, invoices for context but creating/updating
// those requires manager-level API access (@Roles('manager') on POST/PATCH).
// Assets require manager via class-level @Roles('manager').
// Subscriptions require manager for all endpoints.
const viewOnly: Module[] = ['clients', 'subscriptions', 'invoices', 'dashboard'];
const fullAccess: Module[] = ['tickets', 'payments'];
if (viewOnly.includes(mod)) return { module: mod, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (fullAccess.includes(mod)) return { module: mod, canView: true, canCreate: true, canUpdate: true, canArchive: false, canApprove: false, canExport: false };
return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
}),
collector: MODULES.map((mod) => {
const canWrite: Module[] = ['payments'];
const canView: Module[] = ['dashboard', 'clients', 'invoices', 'payments'];
if (!canView.includes(mod)) return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
return {
module: mod,
canView: true,
canCreate: canWrite.includes(mod),
canUpdate: false,
canArchive: false,
canApprove: false,
canExport: false,
};
}),
};
// ─── Legacy support (for super_admin which uses old UserRole system) ───
export const Permission = {
READ_DASHBOARD: 'read:dashboard',
READ_REPORTS: 'read:reports',
READ_CLIENTS: 'read:clients',
WRITE_CLIENTS: 'write:clients',
DELETE_CLIENTS: 'delete:clients',
READ_SUBSCRIPTIONS: 'read:subscriptions',
WRITE_SUBSCRIPTIONS: 'write:subscriptions',
READ_INVOICES: 'read:invoices',
WRITE_INVOICES: 'write:invoices',
VOID_INVOICES: 'void:invoices',
READ_PAYMENTS: 'read:payments',
WRITE_PAYMENTS: 'write:payments',
APPROVE_REMITTANCES: 'approve:remittances',
READ_TICKETS: 'read:tickets',
WRITE_TICKETS: 'write:tickets',
ASSIGN_TICKETS: 'assign:tickets',
READ_EMPLOYEES: 'read:employees',
WRITE_EMPLOYEES: 'write:employees',
READ_PAYROLL: 'read:payroll',
WRITE_PAYROLL: 'write:payroll',
READ_EXPENSES: 'read:expenses',
WRITE_EXPENSES: 'write:expenses',
APPROVE_EXPENSES: 'approve:expenses',
READ_ASSETS: 'read:assets',
WRITE_ASSETS: 'write:assets',
READ_ACCOUNTS: 'read:accounts',
WRITE_ACCOUNTS: 'write:accounts',
TRANSFER_ACCOUNTS: 'transfer:accounts',
READ_ACCOUNTING: 'read:accounting',
WRITE_ACCOUNTING: 'write:accounting',
READ_SETTINGS: 'read:settings',
WRITE_SETTINGS: 'write:settings',
MANAGE_USERS: 'manage:users',
MANAGE_TENANTS: 'manage:tenants',
} as const;
export type PermissionString = (typeof Permission)[keyof typeof Permission];
/**
* Get permissions for super_admin (all permissions).
*/
export function getPermissionsForRoles(roles: string[]): string[] {
if (roles.includes(Role.SUPER_ADMIN)) {
return Object.values(Permission);
}
// For tenant users, permissions come from TenantRole → RolePermission in DB
return [];
}

View File

@@ -0,0 +1,36 @@
export const Role = {
SUPER_ADMIN: 'super_admin',
TENANT_ADMIN: 'tenant_admin',
MANAGER: 'manager',
TECHNICIAN: 'technician',
VIEWER: 'viewer',
} as const;
export type Role = (typeof Role)[keyof typeof Role];
export const ALL_ROLES: readonly Role[] = Object.values(Role);
/**
* Numeric hierarchy level per role.
* Higher number = more powerful role.
* Used for hierarchy-aware authorization checks.
*/
const ROLE_LEVEL: Record<string, number> = {
[Role.SUPER_ADMIN]: 100,
[Role.TENANT_ADMIN]: 80,
[Role.MANAGER]: 60,
[Role.TECHNICIAN]: 40,
[Role.VIEWER]: 20,
};
/**
* Check if any of the user's roles satisfies the required role level.
* A higher-level role always satisfies a lower-level requirement.
*
* Example: user with ['manager'] satisfies 'technician' because manager(60) >= technician(40).
*/
export function satisfiesRole(userRoles: string[], requiredRole: string): boolean {
const requiredLevel = ROLE_LEVEL[requiredRole];
if (requiredLevel === undefined) return false;
return userRoles.some((r) => (ROLE_LEVEL[r] ?? 0) >= requiredLevel);
}

View File

@@ -0,0 +1,67 @@
export const SubscriptionStatus = {
PENDING: 'pending',
ACTIVE: 'active',
SUSPENDED: 'suspended',
CANCELLED: 'cancelled',
EXPIRED: 'expired',
} as const;
export type SubscriptionStatus =
(typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
export const SubscriptionType = {
PREPAID: 'prepaid',
POSTPAID: 'postpaid',
} as const;
export type SubscriptionType =
(typeof SubscriptionType)[keyof typeof SubscriptionType];
export const InvoiceStatus = {
DRAFT: 'draft',
SENT: 'sent',
PARTIAL: 'partial',
PAID: 'paid',
OVERDUE: 'overdue',
VOID: 'void',
} as const;
export type InvoiceStatus =
(typeof InvoiceStatus)[keyof typeof InvoiceStatus];
export const TicketType = {
INSTALLATION: 'installation',
ACTIVATION: 'activation',
SUPPORT: 'support',
MAINTENANCE: 'maintenance',
} as const;
export type TicketType = (typeof TicketType)[keyof typeof TicketType];
export const TicketStatus = {
OPEN: 'open',
IN_PROGRESS: 'in_progress',
RESOLVED: 'resolved',
CANCELLED: 'cancelled',
} as const;
export type TicketStatus = (typeof TicketStatus)[keyof typeof TicketStatus];
export const PaymentMethod = {
GCASH: 'gcash',
MAYA: 'maya',
CASH: 'cash',
BANK_TRANSFER: 'bank_transfer',
} as const;
export type PaymentMethod =
(typeof PaymentMethod)[keyof typeof PaymentMethod];
export const AccountStatus = {
ACTIVE: 'active',
INACTIVE: 'inactive',
SUSPENDED: 'suspended',
} as const;
export type AccountStatus =
(typeof AccountStatus)[keyof typeof AccountStatus];

View File

@@ -0,0 +1,3 @@
export * from './constants';
export * from './schemas';
export * from './types';

View File

@@ -0,0 +1,25 @@
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export const registerTenantSchema = z.object({
tenantName: z.string().min(2, 'Tenant name must be at least 2 characters'),
slug: z
.string()
.min(2)
.max(50)
.regex(
/^[a-z0-9-]+$/,
'Slug must contain only lowercase letters, numbers, and hyphens',
),
adminEmail: z.string().email('Invalid email address'),
adminPassword: z.string().min(8, 'Password must be at least 8 characters'),
adminFirstName: z.string().min(1, 'First name is required'),
adminLastName: z.string().min(1, 'Last name is required'),
});
export type LoginInput = z.infer<typeof loginSchema>;
export type RegisterTenantInput = z.infer<typeof registerTenantSchema>;

View File

@@ -0,0 +1,5 @@
export { loginSchema, registerTenantSchema } from './auth';
export type { LoginInput, RegisterTenantInput } from './auth';
export { createUserSchema, updateUserSchema } from './user';
export type { CreateUserInput, UpdateUserInput } from './user';

View File

@@ -0,0 +1,27 @@
import { z } from 'zod';
import { Role } from '../constants/roles';
const roleEnum = z.enum([
Role.SUPER_ADMIN,
Role.TENANT_ADMIN,
Role.MANAGER,
Role.TECHNICIAN,
Role.VIEWER,
]);
export const createUserSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
roles: z.array(roleEnum).min(1, 'At least one role is required'),
});
export const updateUserSchema = z.object({
firstName: z.string().min(1).optional(),
lastName: z.string().min(1).optional(),
roles: z.array(roleEnum).min(1).optional(),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;

View File

@@ -0,0 +1,36 @@
export interface ApiResponse<T = unknown> {
success: boolean;
data: T | null;
error: string | null;
meta?: PaginationMeta;
}
export interface PaginationMeta {
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface PaginationQuery {
page?: number;
limit?: number;
search?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface JwtPayload {
sub: string;
tenantId?: string | null;
roles: string[];
permissions: string[];
iat?: number;
exp?: number;
}
export interface TokenResponse {
accessToken: string;
refreshToken: string;
mustChangePassword?: boolean;
}

View File

@@ -0,0 +1,7 @@
export type {
ApiResponse,
PaginationMeta,
PaginationQuery,
JwtPayload,
TokenResponse,
} from './api';

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}