17 KiB
Customer Domain Refactor Plan
Moving User Entity Types from Auth to Customer Domain
Overview
Problem: Auth domain currently contains both authentication mechanisms AND user entity types, which mixes concerns.
Solution:
- Move all user entity types (
PortalUser,UserProfile) fromauth/tocustomer/domain - Add Portal (Prisma) as a provider alongside WHMCS
- Keep auth domain focused ONLY on authentication mechanisms
- Consolidate schemas and contracts into single files where appropriate
Architecture Vision
packages/domain/customer/
├── index.ts # Main exports
├── types.ts # ALL types in one file (schemas + inferred types)
│ ├── Schemas (Zod)
│ │ ├── customerAddressSchema
│ │ ├── portalUserSchema (NEW - from auth)
│ │ ├── customerProfileSchema
│ │ └── userProfileSchema (NEW - from auth)
│ │
│ └── Types (inferred from schemas)
│ ├── CustomerAddress
│ ├── PortalUser (NEW - auth state from portal DB)
│ ├── CustomerProfile (WHMCS profile data)
│ └── UserProfile (NEW - PortalUser + CustomerProfile)
│
├── providers/
│ ├── index.ts
│ │
│ ├── portal/ (NEW - Portal DB provider)
│ │ ├── index.ts
│ │ ├── types.ts # PrismaUserRaw
│ │ └── mapper.ts # Prisma → PortalUser
│ │
│ ├── whmcs/ (EXISTING)
│ │ ├── index.ts
│ │ ├── types.ts # WHMCS raw types
│ │ └── mapper.ts # WHMCS → CustomerProfile
│ │
│ └── salesforce/ (EXISTING if needed)
│
packages/domain/auth/
├── index.ts
├── types.ts # Authentication mechanism types ONLY
│ ├── Schemas (Zod)
│ │ ├── loginRequestSchema
│ │ ├── signupRequestSchema
│ │ ├── passwordResetSchema
│ │ ├── authTokensSchema
│ │ ├── authResponseSchema (user: UserProfile reference, tokens)
│ │ └── mfaSchemas
│ │
│ └── Types (inferred)
│ ├── LoginRequest
│ ├── SignupRequest
│ ├── AuthTokens
│ └── AuthResponse
│
└── NO user entity types, NO PortalUser, NO UserProfile
Detailed Implementation Plan
Phase 1: Create New Customer Domain Structure
1.1 Create consolidated types file
File: packages/domain/customer/types.ts
Move from multiple files into one:
- From
customer/schema.ts: All existing customer schemas - From
customer/contract.ts: Interface definitions (convert to schema-first) - From
auth/schema.ts:portalUserSchema,userProfileSchema
Structure:
import { z } from "zod";
// ============================================================================
// Common Schemas
// ============================================================================
export const customerAddressSchema = z.object({...});
export const customerEmailPreferencesSchema = z.object({...});
// ... other existing schemas
// ============================================================================
// Portal User Schema (Auth State from Portal DB)
// ============================================================================
/**
* PortalUser represents ONLY auth state stored in portal database
* Provider: Prisma (portal DB)
*/
export const portalUserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(["USER", "ADMIN"]),
emailVerified: z.boolean(),
mfaEnabled: z.boolean(),
lastLoginAt: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
});
// ============================================================================
// Customer Profile Schema (WHMCS Profile Data)
// ============================================================================
/**
* CustomerProfile represents profile data from WHMCS
* Provider: WHMCS
*/
export const customerProfileSchema = z.object({
id: z.string(),
email: z.string(),
firstname: z.string().nullable().optional(),
lastname: z.string().nullable().optional(),
fullname: z.string().nullable().optional(),
companyname: z.string().nullable().optional(),
phonenumber: z.string().nullable().optional(),
address: customerAddressSchema.optional(),
language: z.string().nullable().optional(),
currencyCode: z.string().nullable().optional(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
});
// ============================================================================
// User Profile Schema (Complete User = PortalUser + CustomerProfile)
// ============================================================================
/**
* UserProfile is the complete authenticated user
* Composition: PortalUser (auth state) + CustomerProfile (WHMCS data)
*/
export const userProfileSchema = portalUserSchema.extend({
// Add CustomerProfile fields
firstname: z.string().nullable().optional(),
lastname: z.string().nullable().optional(),
fullname: z.string().nullable().optional(),
companyname: z.string().nullable().optional(),
phonenumber: z.string().nullable().optional(),
address: customerAddressSchema.optional(),
language: z.string().nullable().optional(),
currencyCode: z.string().nullable().optional(),
});
// ============================================================================
// Inferred Types (Schema-First)
// ============================================================================
export type CustomerAddress = z.infer<typeof customerAddressSchema>;
export type PortalUser = z.infer<typeof portalUserSchema>;
export type CustomerProfile = z.infer<typeof customerProfileSchema>;
export type UserProfile = z.infer<typeof userProfileSchema>;
// ... all other types
1.2 Create Portal Provider
Directory: packages/domain/customer/providers/portal/
File: packages/domain/customer/providers/portal/types.ts
import type { UserRole } from "../../types";
/**
* Raw Prisma user data
* This interface matches Prisma schema but doesn't depend on @prisma/client
*/
export interface PrismaUserRaw {
id: string;
email: string;
passwordHash: string | null;
role: UserRole;
mfaSecret: string | null;
emailVerified: boolean;
failedLoginAttempts: number;
lockedUntil: Date | null;
lastLoginAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
File: packages/domain/customer/providers/portal/mapper.ts
import type { PrismaUserRaw } from "./types";
import type { PortalUser } from "../../types";
/**
* Maps raw Prisma user data to PortalUser domain type
*/
export function mapPrismaUserToPortalUser(raw: PrismaUserRaw): PortalUser {
return {
id: raw.id,
email: raw.email,
role: raw.role,
mfaEnabled: !!raw.mfaSecret,
emailVerified: raw.emailVerified,
lastLoginAt: raw.lastLoginAt?.toISOString(),
createdAt: raw.createdAt.toISOString(),
updatedAt: raw.updatedAt.toISOString(),
};
}
File: packages/domain/customer/providers/portal/index.ts
export * from "./mapper";
export * from "./types";
1.3 Update Customer Domain Index
File: packages/domain/customer/index.ts
/**
* Customer Domain
*
* Contains all user/customer entity types and their providers:
* - PortalUser: Auth state from portal DB (via Prisma)
* - CustomerProfile: Profile data from WHMCS
* - UserProfile: Complete user (PortalUser + CustomerProfile)
*/
// Export all types
export type {
// Core types
PortalUser,
CustomerProfile,
UserProfile,
CustomerAddress,
// ... all other types
} from "./types";
// Export schemas for validation
export {
portalUserSchema,
customerProfileSchema,
userProfileSchema,
customerAddressSchema,
// ... all other schemas
} from "./types";
// Export providers
export * as Providers from "./providers";
1.4 Update Providers Index
File: packages/domain/customer/providers/index.ts
import * as PortalModule from "./portal";
import * as WhmcsModule from "./whmcs";
export const Portal = PortalModule;
export const Whmcs = WhmcsModule;
export { PortalModule, WhmcsModule };
Phase 2: Clean Up Auth Domain
2.1 Remove user entity types from auth
File: packages/domain/auth/types.ts (create new, consolidate schema.ts + contract.ts)
Remove:
portalUserSchema→ moved to customer domainuserProfileSchema→ moved to customer domainPortalUsertype → moved to customer domainUserProfile/AuthenticatedUsertype → moved to customer domain
Keep ONLY:
- Authentication mechanism schemas (login, signup, password reset, MFA)
- Token schemas (authTokensSchema, refreshTokenSchema)
- Auth response schemas (authResponseSchema references UserProfile from customer domain)
Structure:
import { z } from "zod";
import { userProfileSchema } from "../customer/types";
// ============================================================================
// Request Schemas (Authentication Mechanisms)
// ============================================================================
export const loginRequestSchema = z.object({...});
export const signupRequestSchema = z.object({...});
export const passwordResetRequestSchema = z.object({...});
// ... all auth request schemas
// ============================================================================
// Token Schemas
// ============================================================================
export const authTokensSchema = z.object({
accessToken: z.string(),
refreshToken: z.string(),
expiresAt: z.string(),
refreshExpiresAt: z.string(),
tokenType: z.literal("Bearer"),
});
// ============================================================================
// Response Schemas (Reference UserProfile from Customer Domain)
// ============================================================================
export const authResponseSchema = z.object({
user: userProfileSchema, // from customer domain
tokens: authTokensSchema,
});
export const signupResultSchema = z.object({
user: userProfileSchema,
tokens: authTokensSchema,
});
// ============================================================================
// Inferred Types
// ============================================================================
export type LoginRequest = z.infer<typeof loginRequestSchema>;
export type AuthTokens = z.infer<typeof authTokensSchema>;
export type AuthResponse = z.infer<typeof authResponseSchema>;
export type SignupResult = z.infer<typeof signupResultSchema>;
// ... all other auth types
2.2 Delete auth/schema.ts and auth/contract.ts
After consolidating into auth/types.ts, remove:
packages/domain/auth/schema.tspackages/domain/auth/contract.ts
2.3 Delete auth/providers/
Remove packages/domain/auth/providers/ entirely (Portal provider moved to customer domain)
2.4 Update Auth Domain Index
File: packages/domain/auth/index.ts
/**
* Auth Domain
*
* Contains ONLY authentication mechanisms:
* - Login, Signup, Password Management
* - Token Management (JWT)
* - MFA, SSO
*
* User entity types are in customer domain.
*/
export type {
LoginRequest,
SignupRequest,
AuthTokens,
AuthResponse,
SignupResult,
PasswordChangeResult,
// ... all auth mechanism types
} from "./types";
export {
loginRequestSchema,
signupRequestSchema,
authTokensSchema,
authResponseSchema,
// ... all auth schemas
} from "./types";
Phase 3: Update BFF Layer
3.1 Update BFF User Mapper
File: apps/bff/src/infra/mappers/user.mapper.ts
import type { User as PrismaUser } from "@prisma/client";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
import type { PortalUser } from "@customer-portal/domain/customer";
/**
* Adapter: Converts Prisma User to domain PortalUser
*/
export function mapPrismaUserToDomain(user: PrismaUser): PortalUser {
const prismaUserRaw: CustomerProviders.Portal.PrismaUserRaw = {
id: user.id,
email: user.email,
passwordHash: user.passwordHash,
role: user.role,
mfaSecret: user.mfaSecret,
emailVerified: user.emailVerified,
failedLoginAttempts: user.failedLoginAttempts,
lockedUntil: user.lockedUntil,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
return CustomerProviders.Portal.mapPrismaUserToPortalUser(prismaUserRaw);
}
3.2 Update Auth Workflow Services
Update imports in:
apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.tsapps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.tsapps/bff/src/modules/auth/application/auth.facade.ts
Change:
// OLD
import type { UserProfile, SignupResult } from "@customer-portal/domain/auth";
// NEW
import type { UserProfile } from "@customer-portal/domain/customer";
import type { SignupResult } from "@customer-portal/domain/auth";
3.3 Update Users Service
File: apps/bff/src/modules/users/users.service.ts
Construct UserProfile directly (already doing this!):
import {
type PortalUser,
type CustomerProfile,
type UserProfile,
} from "@customer-portal/domain/customer";
async getProfile(userId: string): Promise<UserProfile> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
// Construct UserProfile directly
const profile: UserProfile = {
// Auth state from portal
id: user.id,
email: client.email,
role: user.role,
emailVerified: user.emailVerified,
mfaEnabled: user.mfaSecret !== null,
lastLoginAt: user.lastLoginAt?.toISOString(),
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
// Profile from WHMCS
firstname: client.firstname || null,
lastname: client.lastname || null,
fullname: client.fullname || null,
companyname: client.companyName || null,
phonenumber: client.phoneNumber || null,
address: client.address || undefined,
language: client.language || null,
currencyCode: client.currencyCode || null,
};
return profile;
}
3.4 Update JWT Strategy
File: apps/bff/src/modules/auth/presentation/strategies/jwt.strategy.ts
import type { PortalUser } from "@customer-portal/domain/customer";
async validate(payload: JwtPayload): Promise<PortalUser> {
const user = await this.prisma.user.findUnique({...});
return this.userMapper.mapPrismaUserToDomain(user); // Returns PortalUser
}
Phase 4: Delete Old Files
4.1 Delete from customer domain
packages/domain/customer/schema.ts(consolidated into types.ts)packages/domain/customer/contract.ts(consolidated into types.ts)
4.2 Delete from auth domain
packages/domain/auth/schema.ts(consolidated into types.ts)packages/domain/auth/contract.ts(consolidated into types.ts)packages/domain/auth/providers/(moved to customer domain)
4.3 Delete from BFF (already done in previous cleanup)
- Redundant type files already removed
Phase 5: Verification
5.1 TypeScript Compilation
cd packages/domain && npx tsc --noEmit
cd apps/bff && npx tsc --noEmit
5.2 Import Verification
Verify:
- ✅ No imports of
PortalUserorUserProfilefrom@customer-portal/domain/auth - ✅ All user entity imports come from
@customer-portal/domain/customer - ✅ Auth domain only exports authentication mechanism types
- ✅ BFF constructs UserProfile directly via object creation
5.3 Provider Pattern Consistency
Verify all domains follow provider pattern:
- ✅
customer/providers/portal/- Prisma → PortalUser - ✅
customer/providers/whmcs/- WHMCS → CustomerProfile - ✅ Other domains (billing, subscriptions) have consistent provider structure
Summary of Changes
What's Moving
PortalUsertype:auth/→customer/UserProfiletype:auth/→customer/- Portal (Prisma) provider:
auth/providers/prisma/→customer/providers/portal/ - All schemas:
*/schema.ts+*/contract.ts→*/types.ts(consolidated)
What's Staying
- Auth domain: Only authentication mechanisms (login, signup, tokens, password, MFA)
- Customer domain: All user/customer entity types + providers
New Components
customer/types.ts- Single file with all schemas and types (including PortalUser, UserProfile)customer/providers/portal/- Portal DB provider for PortalUserauth/types.ts- Single file with all auth mechanism types (no user entities)
Benefits
-
Clear Separation of Concerns
- Auth = "How do I authenticate?" (mechanisms)
- Customer = "Who am I?" (entities)
-
Provider Pattern Consistency
- Portal provider (Prisma) alongside WHMCS provider
- Same entity, different data sources
-
Simplified File Structure
- One
types.tsinstead ofschema.ts+contract.ts - Less file navigation
- One
-
Simple & Direct
- No unnecessary abstraction layers
- Direct object construction with type safety
-
Standard DDD
- Customer is the aggregate root
- Auth is a mechanism that references customer