- Streamlined the README.md for clarity and conciseness. - Deleted outdated documentation files related to Freebit SIM management, SIM management API data flow, and various architectural guides to reduce clutter and improve maintainability. - Updated the last modified date in the README to reflect the latest changes.
271 lines
7.4 KiB
Markdown
271 lines
7.4 KiB
Markdown
# Auth Schema Improvements - Explanation
|
|
|
|
## Problems Identified and Fixed
|
|
|
|
### 1. **`z.unknown()` Type Safety Issue** ❌ → ✅
|
|
|
|
**Problem:**
|
|
|
|
```typescript
|
|
// BEFORE - NO TYPE SAFETY
|
|
export const signupResultSchema = z.object({
|
|
user: z.unknown(), // ❌ Loses all type information and validation
|
|
tokens: authTokensSchema,
|
|
});
|
|
```
|
|
|
|
**Why it was wrong:**
|
|
|
|
- `z.unknown()` provides **zero validation** at runtime
|
|
- TypeScript can't infer proper types from it
|
|
- Defeats the purpose of schema-first architecture
|
|
- Any object could pass validation, even malformed data
|
|
|
|
**Solution:**
|
|
|
|
```typescript
|
|
// AFTER - FULL TYPE SAFETY
|
|
export const userProfileSchema = z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email(),
|
|
role: z.enum(["USER", "ADMIN"]),
|
|
// ... all fields properly typed
|
|
});
|
|
|
|
export const signupResultSchema = z.object({
|
|
user: userProfileSchema, // ✅ Full validation and type inference
|
|
tokens: authTokensSchema,
|
|
});
|
|
```
|
|
|
|
**Benefits:**
|
|
|
|
- Runtime validation of user data structure
|
|
- Proper TypeScript type inference
|
|
- Catches invalid data early
|
|
- Self-documenting API contract
|
|
|
|
---
|
|
|
|
### 2. **Email Validation Issues** ❌ → ✅
|
|
|
|
**Problem:**
|
|
The `z.string().email()` in `checkPasswordNeededResponseSchema` was correct, but using `z.unknown()` elsewhere meant emails weren't being validated in user objects.
|
|
|
|
**Solution:**
|
|
|
|
```typescript
|
|
export const userProfileSchema = z.object({
|
|
email: z.string().email(), // ✅ Validates email format
|
|
// ...
|
|
});
|
|
```
|
|
|
|
Now all user objects have their emails properly validated.
|
|
|
|
---
|
|
|
|
### 3. **UserProfile Alias Confusion** 🤔 → ✅
|
|
|
|
**Problem:**
|
|
|
|
```typescript
|
|
export type UserProfile = AuthenticatedUser;
|
|
```
|
|
|
|
This created confusion about why we have two names for the same thing.
|
|
|
|
**Solution - Added Clear Documentation:**
|
|
|
|
```typescript
|
|
/**
|
|
* UserProfile type alias
|
|
*
|
|
* Note: This is an alias for backward compatibility.
|
|
* Both types represent the same thing: a complete user profile with auth state.
|
|
*
|
|
* Architecture:
|
|
* - PortalUser: Only auth state from portal DB (id, email, role, emailVerified, etc.)
|
|
* - CustomerProfile: Profile data from WHMCS (firstname, lastname, address, etc.)
|
|
* - AuthenticatedUser/UserProfile: CustomerProfile + auth state = complete user
|
|
*/
|
|
export type UserProfile = AuthenticatedUser;
|
|
```
|
|
|
|
**Why the alias exists:**
|
|
|
|
- **Backward compatibility**: Code may use either name
|
|
- **Domain language**: "UserProfile" is more business-friendly than "AuthenticatedUser"
|
|
- **Convention**: Many authentication libraries use "UserProfile"
|
|
|
|
---
|
|
|
|
## Architecture Explanation
|
|
|
|
### Three-Layer User Model
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ AuthenticatedUser / UserProfile │
|
|
│ (Complete User) │
|
|
│ ┌────────────────────────────────────────┐ │
|
|
│ │ CustomerProfile (WHMCS) │ │
|
|
│ │ - firstname, lastname │ │
|
|
│ │ - address, phone │ │
|
|
│ │ - language, currency │ │
|
|
│ └────────────────────────────────────────┘ │
|
|
│ ┌────────────────────────────────────────┐ │
|
|
│ │ PortalUser (Portal DB) │ │
|
|
│ │ - id, email │ │
|
|
│ │ - role, emailVerified, mfaEnabled │ │
|
|
│ │ - lastLoginAt │ │
|
|
│ └────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 1. **PortalUser** (Portal Database)
|
|
|
|
- **Purpose**: Authentication state only
|
|
- **Source**: Portal's Prisma database
|
|
- **Fields**: id, email, role, emailVerified, mfaEnabled, lastLoginAt
|
|
- **Schema**: `portalUserSchema`
|
|
- **Use Cases**: JWT validation, token refresh, auth checks
|
|
|
|
### 2. **CustomerProfile** (WHMCS)
|
|
|
|
- **Purpose**: Business profile data
|
|
- **Source**: WHMCS API (single source of truth for profile data)
|
|
- **Fields**: firstname, lastname, address, phone, language, currency
|
|
- **Schema**: Interface only (external system)
|
|
- **Use Cases**: Displaying user info, profile updates
|
|
|
|
### 3. **AuthenticatedUser / UserProfile** (Combined)
|
|
|
|
- **Purpose**: Complete user representation
|
|
- **Source**: Portal DB + WHMCS (merged)
|
|
- **Fields**: All fields from PortalUser + CustomerProfile
|
|
- **Schema**: `userProfileSchema` (for validation)
|
|
- **Use Cases**: API responses, full user context
|
|
|
|
---
|
|
|
|
## Why This Matters
|
|
|
|
### Before (with `z.unknown()`):
|
|
|
|
```typescript
|
|
// Could pass completely invalid data ❌
|
|
const badData = {
|
|
user: { totally: "wrong", structure: true },
|
|
tokens: {
|
|
/* ... */
|
|
},
|
|
};
|
|
// Would validate successfully! ❌
|
|
```
|
|
|
|
### After (with proper schema):
|
|
|
|
```typescript
|
|
// Validates structure correctly ✅
|
|
const goodData = {
|
|
user: {
|
|
id: "uuid-here",
|
|
email: "valid@email.com",
|
|
role: "USER",
|
|
// ... all required fields
|
|
},
|
|
tokens: {
|
|
/* ... */
|
|
},
|
|
};
|
|
// Validates ✅
|
|
|
|
const badData = {
|
|
user: { wrong: "structure" },
|
|
tokens: {
|
|
/* ... */
|
|
},
|
|
};
|
|
// Throws validation error ✅
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### Schema Definition Location
|
|
|
|
The `userProfileSchema` is defined **before** it's used:
|
|
|
|
```typescript
|
|
// 1. First, define the schema
|
|
export const userProfileSchema = z.object({
|
|
// All fields defined
|
|
});
|
|
|
|
// 2. Then use it in response schemas
|
|
export const authResponseSchema = z.object({
|
|
user: userProfileSchema, // ✅ Now defined
|
|
tokens: authTokensSchema,
|
|
});
|
|
```
|
|
|
|
### Type Inference
|
|
|
|
TypeScript now properly infers types:
|
|
|
|
```typescript
|
|
// Type is inferred from schema
|
|
type InferredUser = z.infer<typeof userProfileSchema>;
|
|
|
|
// Results in proper type:
|
|
// {
|
|
// id: string;
|
|
// email: string;
|
|
// role: "USER" | "ADMIN";
|
|
// emailVerified: boolean;
|
|
// // ... etc
|
|
// }
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Impact
|
|
|
|
### ⚠️ Potential Breaking Change (Security Improvement)
|
|
|
|
Auth endpoints now return **session metadata** instead of **token strings**:
|
|
|
|
- **Before**: `{ user, tokens: { accessToken, refreshToken, expiresAt, refreshExpiresAt } }`
|
|
- **After**: `{ user, session: { expiresAt, refreshExpiresAt } }`
|
|
|
|
Token strings are delivered via **httpOnly cookies** only.
|
|
|
|
### ✅ Improved Type Safety
|
|
|
|
- Better autocomplete in IDEs
|
|
- Compile-time error checking
|
|
- Runtime validation of API responses
|
|
|
|
### ✅ Better Documentation
|
|
|
|
- Schema serves as living documentation
|
|
- Clear separation of concerns
|
|
- Self-describing data structures
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Issue | Before | After |
|
|
| ------------------- | ---------------- | ---------------------- |
|
|
| User validation | `z.unknown()` ❌ | `userProfileSchema` ✅ |
|
|
| Type safety | None | Full |
|
|
| Runtime validation | None | Complete |
|
|
| Documentation | Unclear | Self-documenting |
|
|
| Email validation | Partial | Complete |
|
|
| UserProfile clarity | Confusing | Documented |
|
|
|
|
**Result**: Proper schema-first architecture with full type safety and validation! 🎉
|