initial: standalone repo from monorepo split
This commit is contained in:
16
packages/shared/src/constants/index.ts
Normal file
16
packages/shared/src/constants/index.ts
Normal 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';
|
||||
224
packages/shared/src/constants/permissions.ts
Normal file
224
packages/shared/src/constants/permissions.ts
Normal 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 [];
|
||||
}
|
||||
36
packages/shared/src/constants/roles.ts
Normal file
36
packages/shared/src/constants/roles.ts
Normal 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);
|
||||
}
|
||||
67
packages/shared/src/constants/statuses.ts
Normal file
67
packages/shared/src/constants/statuses.ts
Normal 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];
|
||||
3
packages/shared/src/index.ts
Normal file
3
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './constants';
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
25
packages/shared/src/schemas/auth.ts
Normal file
25
packages/shared/src/schemas/auth.ts
Normal 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>;
|
||||
5
packages/shared/src/schemas/index.ts
Normal file
5
packages/shared/src/schemas/index.ts
Normal 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';
|
||||
27
packages/shared/src/schemas/user.ts
Normal file
27
packages/shared/src/schemas/user.ts
Normal 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>;
|
||||
36
packages/shared/src/types/api.ts
Normal file
36
packages/shared/src/types/api.ts
Normal 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;
|
||||
}
|
||||
7
packages/shared/src/types/index.ts
Normal file
7
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type {
|
||||
ApiResponse,
|
||||
PaginationMeta,
|
||||
PaginationQuery,
|
||||
JwtPayload,
|
||||
TokenResponse,
|
||||
} from './api';
|
||||
Reference in New Issue
Block a user