Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
907 lines
24 KiB
Markdown
907 lines
24 KiB
Markdown
# Payload CMS Authentication Migration Guide
|
|
|
|
## Overview
|
|
|
|
This guide provides detailed steps for migrating from the current JWT-based authentication system to Payload CMS's built-in authentication system while maintaining backward compatibility and ensuring zero downtime.
|
|
|
|
## Current Authentication System Analysis
|
|
|
|
### Existing Implementation
|
|
- **Technology**: Custom JWT implementation with bcryptjs
|
|
- **Token Expiry**: 7 days
|
|
- **Storage**: PostgreSQL (User, AdminUser tables)
|
|
- **Roles**: USER, ADMIN, SUPER_ADMIN
|
|
- **Session Management**: Stateless JWT tokens
|
|
|
|
### Current Auth Flow
|
|
```mermaid
|
|
graph LR
|
|
A[User Login] --> B[Validate Credentials]
|
|
B --> C[Generate JWT]
|
|
C --> D[Return Token]
|
|
D --> E[Store in LocalStorage]
|
|
E --> F[Include in Headers]
|
|
F --> G[Verify on Each Request]
|
|
```
|
|
|
|
## Payload Authentication System
|
|
|
|
### Key Features
|
|
- **Cookie-based sessions** with HTTP-only cookies
|
|
- **CSRF protection** built-in
|
|
- **Refresh tokens** for extended sessions
|
|
- **Password reset flow** with email verification
|
|
- **Two-factor authentication** support (optional)
|
|
- **OAuth providers** integration capability
|
|
|
|
### Payload Auth Flow
|
|
```mermaid
|
|
graph LR
|
|
A[User Login] --> B[Validate Credentials]
|
|
B --> C[Create Session]
|
|
C --> D[Set HTTP-only Cookie]
|
|
D --> E[Return User Data]
|
|
E --> F[Auto-include Cookie]
|
|
F --> G[Session Validation]
|
|
```
|
|
|
|
## Migration Strategy
|
|
|
|
### Phase 1: Dual Authentication Support
|
|
|
|
#### Step 1.1: Configure Payload Auth
|
|
```typescript
|
|
// config/auth.config.ts
|
|
export const authConfig = {
|
|
// Enable both JWT and session-based auth
|
|
cookies: {
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'lax' as const,
|
|
domain: process.env.COOKIE_DOMAIN,
|
|
},
|
|
tokenExpiration: 604800, // 7 days (matching current)
|
|
maxLoginAttempts: 5,
|
|
lockTime: 600000, // 10 minutes
|
|
|
|
// Custom JWT for backward compatibility
|
|
jwt: {
|
|
secret: process.env.JWT_SECRET,
|
|
expiresIn: '7d',
|
|
},
|
|
|
|
// Session configuration
|
|
session: {
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
secret: process.env.SESSION_SECRET,
|
|
cookie: {
|
|
maxAge: 604800000, // 7 days in milliseconds
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
#### Step 1.2: Create Compatibility Layer
|
|
```typescript
|
|
// lib/auth/compatibility.ts
|
|
import jwt from 'jsonwebtoken';
|
|
import { PayloadRequest } from 'payload/types';
|
|
|
|
export class AuthCompatibilityLayer {
|
|
/**
|
|
* Validates both old JWT tokens and new Payload sessions
|
|
*/
|
|
static async validateRequest(req: PayloadRequest) {
|
|
// Check for Payload session first
|
|
if (req.user) {
|
|
return { valid: true, user: req.user, method: 'payload' };
|
|
}
|
|
|
|
// Check for legacy JWT token
|
|
const token = this.extractJWT(req);
|
|
if (token) {
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
|
|
const user = await this.getUserFromToken(decoded);
|
|
return { valid: true, user, method: 'jwt' };
|
|
} catch (error) {
|
|
return { valid: false, error: 'Invalid token' };
|
|
}
|
|
}
|
|
|
|
return { valid: false, error: 'No authentication provided' };
|
|
}
|
|
|
|
private static extractJWT(req: PayloadRequest): string | null {
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader?.startsWith('Bearer ')) {
|
|
return authHeader.substring(7);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static async getUserFromToken(decoded: any) {
|
|
// Fetch user from Payload collections
|
|
const user = await payload.findByID({
|
|
collection: 'users',
|
|
id: decoded.userId,
|
|
});
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Generates both JWT (for legacy) and creates Payload session
|
|
*/
|
|
static async createDualAuth(user: any, req: PayloadRequest) {
|
|
// Create Payload session
|
|
const payloadToken = await payload.login({
|
|
collection: 'users',
|
|
data: {
|
|
email: user.email,
|
|
password: user.password,
|
|
},
|
|
req,
|
|
});
|
|
|
|
// Generate legacy JWT
|
|
const jwtToken = jwt.sign(
|
|
{
|
|
userId: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
},
|
|
process.env.JWT_SECRET!,
|
|
{ expiresIn: '7d' }
|
|
);
|
|
|
|
return {
|
|
payloadToken,
|
|
jwtToken, // For backward compatibility
|
|
user,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 2: User Migration
|
|
|
|
#### Step 2.1: User Data Migration Script
|
|
```typescript
|
|
// scripts/migrate-users.ts
|
|
import { PrismaClient } from '@prisma/client';
|
|
import payload from 'payload';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
interface MigrationResult {
|
|
success: number;
|
|
failed: number;
|
|
errors: Array<{ email: string; error: string }>;
|
|
}
|
|
|
|
export async function migrateUsers(): Promise<MigrationResult> {
|
|
const result: MigrationResult = {
|
|
success: 0,
|
|
failed: 0,
|
|
errors: [],
|
|
};
|
|
|
|
// Fetch all users from Prisma
|
|
const users = await prisma.user.findMany({
|
|
include: {
|
|
subscription: true,
|
|
userSettings: true,
|
|
bookmarks: true,
|
|
highlights: true,
|
|
},
|
|
});
|
|
|
|
console.log(`Starting migration of ${users.length} users...`);
|
|
|
|
for (const user of users) {
|
|
try {
|
|
// Check if user already exists in Payload
|
|
const existing = await payload.find({
|
|
collection: 'users',
|
|
where: {
|
|
email: { equals: user.email },
|
|
},
|
|
});
|
|
|
|
if (existing.docs.length > 0) {
|
|
console.log(`User ${user.email} already migrated, skipping...`);
|
|
continue;
|
|
}
|
|
|
|
// Create user in Payload
|
|
const payloadUser = await payload.create({
|
|
collection: 'users',
|
|
data: {
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
|
|
// Password handling - already hashed
|
|
password: user.password,
|
|
_verified: true, // Mark as verified
|
|
|
|
// Custom fields
|
|
stripeCustomerId: user.stripeCustomerId,
|
|
favoriteVersion: user.favoriteVersion || 'VDC',
|
|
|
|
// Settings
|
|
profileSettings: {
|
|
fontSize: user.userSettings?.fontSize || 16,
|
|
theme: user.userSettings?.theme || 'light',
|
|
showVerseNumbers: user.userSettings?.showVerseNumbers ?? true,
|
|
enableNotifications: user.userSettings?.enableNotifications ?? true,
|
|
},
|
|
|
|
// Timestamps
|
|
createdAt: user.createdAt,
|
|
updatedAt: user.updatedAt,
|
|
lastLogin: user.lastLogin,
|
|
},
|
|
});
|
|
|
|
// Migrate related data
|
|
if (user.subscription) {
|
|
await migrateUserSubscription(payloadUser.id, user.subscription);
|
|
}
|
|
|
|
if (user.bookmarks.length > 0) {
|
|
await migrateUserBookmarks(payloadUser.id, user.bookmarks);
|
|
}
|
|
|
|
if (user.highlights.length > 0) {
|
|
await migrateUserHighlights(payloadUser.id, user.highlights);
|
|
}
|
|
|
|
result.success++;
|
|
console.log(`✓ Migrated user: ${user.email}`);
|
|
|
|
} catch (error) {
|
|
result.failed++;
|
|
result.errors.push({
|
|
email: user.email,
|
|
error: error.message,
|
|
});
|
|
console.error(`✗ Failed to migrate user ${user.email}:`, error);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function migrateUserSubscription(userId: string, subscription: any) {
|
|
await payload.create({
|
|
collection: 'subscriptions',
|
|
data: {
|
|
user: userId,
|
|
stripeSubscriptionId: subscription.stripeSubscriptionId,
|
|
planName: subscription.planName,
|
|
status: subscription.status,
|
|
currentPeriodStart: subscription.currentPeriodStart,
|
|
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
conversationCount: subscription.conversationCount,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function migrateUserBookmarks(userId: string, bookmarks: any[]) {
|
|
for (const bookmark of bookmarks) {
|
|
await payload.create({
|
|
collection: 'bookmarks',
|
|
data: {
|
|
user: userId,
|
|
book: bookmark.book,
|
|
chapter: bookmark.chapter,
|
|
verse: bookmark.verse,
|
|
note: bookmark.note,
|
|
createdAt: bookmark.createdAt,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function migrateUserHighlights(userId: string, highlights: any[]) {
|
|
for (const highlight of highlights) {
|
|
await payload.create({
|
|
collection: 'highlights',
|
|
data: {
|
|
user: userId,
|
|
verseId: highlight.verseId,
|
|
color: highlight.color,
|
|
note: highlight.note,
|
|
createdAt: highlight.createdAt,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Step 2.2: Password Migration Strategy
|
|
|
|
Since passwords are already hashed with bcrypt, we have three options:
|
|
|
|
**Option 1: Direct Hash Migration (Recommended)**
|
|
```typescript
|
|
// hooks/auth.hooks.ts
|
|
export const passwordValidationHook = {
|
|
beforeOperation: async ({ args, operation }) => {
|
|
if (operation === 'login') {
|
|
const { email, password } = args.data;
|
|
|
|
// Find user
|
|
const user = await payload.find({
|
|
collection: 'users',
|
|
where: { email: { equals: email } },
|
|
});
|
|
|
|
if (user.docs.length === 0) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
|
|
const userDoc = user.docs[0];
|
|
|
|
// Check if password needs rehashing (migrated user)
|
|
if (userDoc.passwordMigrated) {
|
|
// Use bcrypt directly for migrated passwords
|
|
const valid = await bcrypt.compare(password, userDoc.password);
|
|
if (!valid) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
|
|
// Rehash with Payload's method on successful login
|
|
await payload.update({
|
|
collection: 'users',
|
|
id: userDoc.id,
|
|
data: {
|
|
password, // Will be hashed by Payload
|
|
passwordMigrated: false,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
**Option 2: Password Reset Campaign**
|
|
```typescript
|
|
// scripts/password-reset-campaign.ts
|
|
export async function sendPasswordResetToMigratedUsers() {
|
|
const migratedUsers = await payload.find({
|
|
collection: 'users',
|
|
where: {
|
|
passwordMigrated: { equals: true },
|
|
},
|
|
});
|
|
|
|
for (const user of migratedUsers.docs) {
|
|
const token = await payload.forgotPassword({
|
|
collection: 'users',
|
|
data: { email: user.email },
|
|
disableEmail: false,
|
|
});
|
|
|
|
// Send custom email explaining migration
|
|
await sendMigrationEmail({
|
|
to: user.email,
|
|
token,
|
|
userName: user.name,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 3: API Endpoint Migration
|
|
|
|
#### Step 3.1: Update Frontend API Calls
|
|
```typescript
|
|
// lib/api/auth.ts (Frontend)
|
|
export class AuthAPI {
|
|
private static baseURL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
static async login(email: string, password: string) {
|
|
try {
|
|
// Try new Payload endpoint first
|
|
const response = await fetch(`${this.baseURL}/api/users/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include', // Important for cookies
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Store JWT for backward compatibility if provided
|
|
if (data.token) {
|
|
localStorage.setItem('token', data.token);
|
|
}
|
|
|
|
return { success: true, user: data.user };
|
|
}
|
|
} catch (error) {
|
|
console.error('Payload login failed, trying legacy...', error);
|
|
}
|
|
|
|
// Fallback to legacy endpoint
|
|
return this.legacyLogin(email, password);
|
|
}
|
|
|
|
private static async legacyLogin(email: string, password: string) {
|
|
const response = await fetch(`${this.baseURL}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.token) {
|
|
localStorage.setItem('token', data.token);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
static async logout() {
|
|
// Clear both Payload session and JWT
|
|
await fetch(`${this.baseURL}/api/users/logout`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
|
|
localStorage.removeItem('token');
|
|
}
|
|
|
|
static async getMe() {
|
|
// Try Payload endpoint with cookie
|
|
try {
|
|
const response = await fetch(`${this.baseURL}/api/users/me`, {
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (response.ok) {
|
|
return response.json();
|
|
}
|
|
} catch (error) {
|
|
// Fallback to JWT
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
const response = await fetch(`${this.baseURL}/api/auth/me`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
return response.json();
|
|
}
|
|
}
|
|
|
|
throw new Error('Not authenticated');
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Step 3.2: Update API Middleware
|
|
```typescript
|
|
// middleware/auth.middleware.ts
|
|
import { PayloadRequest } from 'payload/types';
|
|
import { AuthCompatibilityLayer } from '../lib/auth/compatibility';
|
|
|
|
export async function authMiddleware(req: PayloadRequest, res: any, next: any) {
|
|
const auth = await AuthCompatibilityLayer.validateRequest(req);
|
|
|
|
if (!auth.valid) {
|
|
return res.status(401).json({ error: auth.error });
|
|
}
|
|
|
|
// Attach user to request for both auth methods
|
|
req.user = auth.user;
|
|
req.authMethod = auth.method; // Track which auth method was used
|
|
|
|
// Log for monitoring during migration
|
|
console.log(`Auth method: ${auth.method} for user: ${auth.user.email}`);
|
|
|
|
next();
|
|
}
|
|
```
|
|
|
|
### Phase 4: Testing & Validation
|
|
|
|
#### Step 4.1: Authentication Test Suite
|
|
```typescript
|
|
// tests/auth/migration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
import payload from 'payload';
|
|
import { testUser } from '../fixtures/users';
|
|
|
|
describe('Authentication Migration Tests', () => {
|
|
beforeAll(async () => {
|
|
await payload.init({
|
|
local: true,
|
|
secret: 'test-secret',
|
|
});
|
|
});
|
|
|
|
describe('Dual Authentication', () => {
|
|
it('should accept legacy JWT tokens', async () => {
|
|
const token = generateLegacyJWT(testUser);
|
|
const response = await fetch('/api/protected', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it('should accept Payload session cookies', async () => {
|
|
const loginResponse = await fetch('/api/users/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
email: testUser.email,
|
|
password: testUser.password,
|
|
}),
|
|
credentials: 'include',
|
|
});
|
|
|
|
const cookie = loginResponse.headers.get('set-cookie');
|
|
|
|
const response = await fetch('/api/protected', {
|
|
headers: {
|
|
'Cookie': cookie,
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it('should migrate password on first login', async () => {
|
|
const migratedUser = await createMigratedUser();
|
|
|
|
const response = await fetch('/api/users/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
email: migratedUser.email,
|
|
password: 'original-password',
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
// Check that password was rehashed
|
|
const user = await payload.findByID({
|
|
collection: 'users',
|
|
id: migratedUser.id,
|
|
});
|
|
|
|
expect(user.passwordMigrated).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Session Management', () => {
|
|
it('should maintain session across requests', async () => {
|
|
const session = await createAuthSession(testUser);
|
|
|
|
// Make multiple requests with same session
|
|
for (let i = 0; i < 5; i++) {
|
|
const response = await fetch('/api/protected', {
|
|
headers: {
|
|
'Cookie': session.cookie,
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
}
|
|
});
|
|
|
|
it('should refresh token before expiry', async () => {
|
|
const session = await createAuthSession(testUser);
|
|
|
|
// Fast-forward time to near expiry
|
|
jest.advanceTimersByTime(6 * 24 * 60 * 60 * 1000); // 6 days
|
|
|
|
const response = await fetch('/api/users/refresh', {
|
|
headers: {
|
|
'Cookie': session.cookie,
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
const newCookie = response.headers.get('set-cookie');
|
|
expect(newCookie).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('Role-Based Access', () => {
|
|
it('should enforce admin access', async () => {
|
|
const regularUser = await createUser({ role: 'USER' });
|
|
const adminUser = await createUser({ role: 'ADMIN' });
|
|
|
|
const regularSession = await createAuthSession(regularUser);
|
|
const adminSession = await createAuthSession(adminUser);
|
|
|
|
// Regular user should be denied
|
|
const regularResponse = await fetch('/api/admin/users', {
|
|
headers: { 'Cookie': regularSession.cookie },
|
|
});
|
|
expect(regularResponse.status).toBe(403);
|
|
|
|
// Admin should be allowed
|
|
const adminResponse = await fetch('/api/admin/users', {
|
|
headers: { 'Cookie': adminSession.cookie },
|
|
});
|
|
expect(adminResponse.status).toBe(200);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
#### Step 4.2: Migration Validation Script
|
|
```typescript
|
|
// scripts/validate-migration.ts
|
|
export async function validateMigration() {
|
|
const report = {
|
|
users: { total: 0, migrated: 0, failed: [] },
|
|
auth: { jwt: 0, payload: 0, dual: 0 },
|
|
subscriptions: { total: 0, active: 0, cancelled: 0 },
|
|
errors: [],
|
|
};
|
|
|
|
// Check user migration
|
|
const prismaUsers = await prisma.user.count();
|
|
const payloadUsers = await payload.count({ collection: 'users' });
|
|
|
|
report.users.total = prismaUsers;
|
|
report.users.migrated = payloadUsers.totalDocs;
|
|
|
|
// Test authentication methods
|
|
const testResults = await testAuthenticationMethods();
|
|
report.auth = testResults;
|
|
|
|
// Validate subscriptions
|
|
const subscriptions = await validateSubscriptions();
|
|
report.subscriptions = subscriptions;
|
|
|
|
// Generate report
|
|
console.log('Migration Validation Report:');
|
|
console.log('============================');
|
|
console.log(`Users: ${report.users.migrated}/${report.users.total} migrated`);
|
|
console.log(`Auth Methods: JWT: ${report.auth.jwt}, Payload: ${report.auth.payload}`);
|
|
console.log(`Subscriptions: ${report.subscriptions.active} active`);
|
|
|
|
if (report.errors.length > 0) {
|
|
console.log('\nErrors found:');
|
|
report.errors.forEach(error => console.error(error));
|
|
}
|
|
|
|
return report;
|
|
}
|
|
```
|
|
|
|
### Phase 5: Gradual Rollout
|
|
|
|
#### Step 5.1: Feature Flags
|
|
```typescript
|
|
// lib/features/flags.ts
|
|
export const AuthFeatureFlags = {
|
|
USE_PAYLOAD_AUTH: process.env.NEXT_PUBLIC_USE_PAYLOAD_AUTH === 'true',
|
|
DUAL_AUTH_MODE: process.env.NEXT_PUBLIC_DUAL_AUTH === 'true',
|
|
FORCE_PASSWORD_RESET: process.env.FORCE_PASSWORD_RESET === 'true',
|
|
};
|
|
|
|
// Usage in components
|
|
export function LoginForm() {
|
|
const handleSubmit = async (data: LoginData) => {
|
|
if (AuthFeatureFlags.USE_PAYLOAD_AUTH) {
|
|
return payloadLogin(data);
|
|
} else if (AuthFeatureFlags.DUAL_AUTH_MODE) {
|
|
return dualLogin(data);
|
|
} else {
|
|
return legacyLogin(data);
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Step 5.2: A/B Testing
|
|
```typescript
|
|
// lib/ab-testing/auth.ts
|
|
export function getAuthStrategy(userId?: string): 'legacy' | 'payload' | 'dual' {
|
|
// Percentage-based rollout
|
|
const rolloutPercentage = parseInt(process.env.PAYLOAD_AUTH_ROLLOUT || '0');
|
|
|
|
if (!userId) {
|
|
// New users always get Payload auth
|
|
return 'payload';
|
|
}
|
|
|
|
// Consistent assignment based on user ID
|
|
const hash = hashUserId(userId);
|
|
const bucket = hash % 100;
|
|
|
|
if (bucket < rolloutPercentage) {
|
|
return 'payload';
|
|
} else if (process.env.DUAL_AUTH === 'true') {
|
|
return 'dual';
|
|
} else {
|
|
return 'legacy';
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 6: Monitoring & Observability
|
|
|
|
#### Step 6.1: Authentication Metrics
|
|
```typescript
|
|
// lib/monitoring/auth-metrics.ts
|
|
import { metrics } from '@opentelemetry/api-metrics';
|
|
|
|
export class AuthMetrics {
|
|
private meter = metrics.getMeter('auth-migration');
|
|
private loginCounter = this.meter.createCounter('auth_login_total');
|
|
private methodHistogram = this.meter.createHistogram('auth_method_duration');
|
|
private failureCounter = this.meter.createCounter('auth_failure_total');
|
|
|
|
trackLogin(method: 'jwt' | 'payload' | 'dual', success: boolean, duration: number) {
|
|
this.loginCounter.add(1, {
|
|
method,
|
|
success: success.toString(),
|
|
});
|
|
|
|
this.methodHistogram.record(duration, {
|
|
method,
|
|
});
|
|
|
|
if (!success) {
|
|
this.failureCounter.add(1, { method });
|
|
}
|
|
}
|
|
|
|
async generateReport() {
|
|
return {
|
|
totalLogins: await this.getTotalLogins(),
|
|
methodDistribution: await this.getMethodDistribution(),
|
|
failureRate: await this.getFailureRate(),
|
|
avgDuration: await this.getAverageDuration(),
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Step 6.2: Monitoring Dashboard
|
|
```typescript
|
|
// components/admin/AuthMigrationDashboard.tsx
|
|
export function AuthMigrationDashboard() {
|
|
const [metrics, setMetrics] = useState<AuthMetrics>();
|
|
|
|
useEffect(() => {
|
|
const fetchMetrics = async () => {
|
|
const data = await fetch('/api/admin/auth-metrics').then(r => r.json());
|
|
setMetrics(data);
|
|
};
|
|
|
|
fetchMetrics();
|
|
const interval = setInterval(fetchMetrics, 30000); // Update every 30s
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="dashboard">
|
|
<h2>Authentication Migration Status</h2>
|
|
|
|
<div className="metrics-grid">
|
|
<MetricCard
|
|
title="Auth Method Distribution"
|
|
value={
|
|
<PieChart data={[
|
|
{ name: 'JWT', value: metrics?.jwt || 0 },
|
|
{ name: 'Payload', value: metrics?.payload || 0 },
|
|
{ name: 'Dual', value: metrics?.dual || 0 },
|
|
]} />
|
|
}
|
|
/>
|
|
|
|
<MetricCard
|
|
title="Migration Progress"
|
|
value={`${metrics?.migratedUsers || 0} / ${metrics?.totalUsers || 0}`}
|
|
subtitle={`${Math.round((metrics?.migratedUsers / metrics?.totalUsers) * 100)}% complete`}
|
|
/>
|
|
|
|
<MetricCard
|
|
title="Auth Success Rate"
|
|
value={`${metrics?.successRate || 0}%`}
|
|
trend={metrics?.successTrend}
|
|
/>
|
|
|
|
<MetricCard
|
|
title="Active Sessions"
|
|
value={metrics?.activeSessions || 0}
|
|
subtitle={`JWT: ${metrics?.jwtSessions}, Payload: ${metrics?.payloadSessions}`}
|
|
/>
|
|
</div>
|
|
|
|
<div className="recent-issues">
|
|
<h3>Recent Authentication Issues</h3>
|
|
<IssuesList issues={metrics?.recentIssues || []} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Rollback Procedures
|
|
|
|
### Emergency Rollback Script
|
|
```typescript
|
|
// scripts/auth-rollback.ts
|
|
export async function rollbackAuth() {
|
|
console.log('Starting authentication rollback...');
|
|
|
|
// 1. Disable Payload auth endpoints
|
|
await updateEnvironmentVariable('USE_PAYLOAD_AUTH', 'false');
|
|
|
|
// 2. Re-enable legacy endpoints
|
|
await updateEnvironmentVariable('USE_LEGACY_AUTH', 'true');
|
|
|
|
// 3. Clear Payload sessions
|
|
await payload.delete({
|
|
collection: 'sessions',
|
|
where: {},
|
|
});
|
|
|
|
// 4. Notify users
|
|
await sendSystemNotification({
|
|
message: 'Authentication system maintenance in progress',
|
|
type: 'warning',
|
|
});
|
|
|
|
// 5. Monitor legacy auth performance
|
|
startLegacyAuthMonitoring();
|
|
|
|
console.log('Rollback complete. Legacy auth restored.');
|
|
}
|
|
```
|
|
|
|
## Best Practices & Recommendations
|
|
|
|
### Security Considerations
|
|
1. **Never log passwords** in any form
|
|
2. **Use HTTPS only** for production
|
|
3. **Implement rate limiting** on auth endpoints
|
|
4. **Monitor failed login attempts**
|
|
5. **Regular security audits** of auth flows
|
|
|
|
### Performance Optimization
|
|
1. **Cache user sessions** in Redis
|
|
2. **Implement session pooling**
|
|
3. **Use database indexes** on email fields
|
|
4. **Lazy-load user relationships**
|
|
5. **CDN for static auth assets**
|
|
|
|
### User Experience
|
|
1. **Transparent migration** - users shouldn't notice
|
|
2. **Clear error messages** for auth failures
|
|
3. **Password strength indicators**
|
|
4. **Remember me functionality**
|
|
5. **Social login options** (future enhancement)
|
|
|
|
## Conclusion
|
|
|
|
The migration to Payload CMS authentication provides:
|
|
- **Enhanced security** with HTTP-only cookies and CSRF protection
|
|
- **Better session management** with automatic refresh
|
|
- **Simplified codebase** with less custom auth code
|
|
- **Future-proof architecture** for OAuth and 2FA
|
|
|
|
The dual-authentication approach ensures zero downtime and allows for gradual migration with full rollback capability.
|
|
|
|
---
|
|
|
|
*Document Version: 1.0*
|
|
*Last Updated: November 2024*
|
|
*Author: Biblical Guide Development Team* |