Files
dofus-manager/docs/architecture/11-backend-architecture.md
2026-01-19 08:52:38 +01:00

206 lines
5.5 KiB
Markdown

# 11. Backend Architecture
## Server Functions Organization
```
src/server/
├── functions/
│ ├── auth.ts # Authentication functions
│ ├── characters.ts # Character CRUD
│ ├── accounts.ts # Account CRUD
│ ├── teams.ts # Team management
│ ├── progressions.ts # Progression tracking
│ └── dofusdb.ts # DofusDB sync
├── middleware/
│ └── auth.ts # Authentication middleware
└── index.ts # Export all functions
```
## Server Function Example
```typescript
// src/server/functions/characters.ts
import { createServerFn } from '@tanstack/react-start/server';
import { z } from 'zod';
import { db } from '@/lib/server/db';
import { requireAuth } from '@/server/middleware/auth';
// Schemas
const createCharacterSchema = z.object({
name: z.string().min(2).max(50),
level: z.number().int().min(1).max(200),
classId: z.number().int().positive(),
className: z.string(),
serverId: z.number().int().positive(),
serverName: z.string(),
accountId: z.string().cuid(),
});
const characterFiltersSchema = z.object({
search: z.string().optional(),
classIds: z.array(z.number()).optional(),
serverIds: z.array(z.number()).optional(),
accountIds: z.array(z.string()).optional(),
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(20),
});
// Functions
export const getCharacters = createServerFn({ method: 'GET' })
.validator((data: unknown) => characterFiltersSchema.parse(data))
.handler(async ({ data }) => {
const session = await requireAuth();
const { search, classIds, serverIds, accountIds, page, limit } = data;
const where = {
account: { userId: session.userId },
...(search && {
name: { contains: search, mode: 'insensitive' as const },
}),
...(classIds?.length && { classId: { in: classIds } }),
...(serverIds?.length && { serverId: { in: serverIds } }),
...(accountIds?.length && { accountId: { in: accountIds } }),
};
const [characters, total] = await Promise.all([
db.character.findMany({
where,
include: { account: { select: { id: true, name: true } } },
skip: (page - 1) * limit,
take: limit,
orderBy: { name: 'asc' },
}),
db.character.count({ where }),
]);
return {
characters,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
});
export const createCharacter = createServerFn({ method: 'POST' })
.validator((data: unknown) => createCharacterSchema.parse(data))
.handler(async ({ data }) => {
const session = await requireAuth();
// Verify account belongs to user
const account = await db.account.findFirst({
where: { id: data.accountId, userId: session.userId },
});
if (!account) {
throw new Error('Account not found');
}
return db.character.create({ data });
});
export const bulkDeleteCharacters = createServerFn({ method: 'POST' })
.validator((data: unknown) => z.object({ ids: z.array(z.string().cuid()) }).parse(data))
.handler(async ({ data }) => {
const session = await requireAuth();
const result = await db.character.deleteMany({
where: {
id: { in: data.ids },
account: { userId: session.userId },
},
});
return { deleted: result.count };
});
```
## Authentication Middleware
```typescript
// src/server/middleware/auth.ts
import { getWebRequest } from '@tanstack/react-start/server';
import { db } from '@/lib/server/db';
interface Session {
userId: string;
sessionId: string;
}
export async function requireAuth(): Promise<Session> {
const request = getWebRequest();
const sessionId = request.headers.get('cookie')
?.split(';')
.find(c => c.trim().startsWith('session='))
?.split('=')[1];
if (!sessionId) {
throw new Error('Unauthorized');
}
const session = await db.session.findUnique({
where: { id: sessionId },
include: { user: true },
});
if (!session || session.expiresAt < new Date()) {
throw new Error('Session expired');
}
return {
userId: session.userId,
sessionId: session.id,
};
}
export async function getOptionalAuth(): Promise<Session | null> {
try {
return await requireAuth();
} catch {
return null;
}
}
```
## Caching Strategy
```typescript
// src/lib/server/cache.ts
import NodeCache from 'node-cache';
// Different TTLs for different data types
const caches = {
// Reference data (rarely changes)
reference: new NodeCache({ stdTTL: 3600, checkperiod: 600 }), // 1 hour
// User data (changes more frequently)
user: new NodeCache({ stdTTL: 300, checkperiod: 60 }), // 5 minutes
};
export const referenceCache = {
get: <T>(key: string): T | undefined => caches.reference.get(key),
set: <T>(key: string, value: T): boolean => caches.reference.set(key, value),
del: (key: string): number => caches.reference.del(key),
flush: (): void => caches.reference.flushAll(),
};
export const userCache = {
get: <T>(key: string): T | undefined => caches.user.get(key),
set: <T>(key: string, value: T): boolean => caches.user.set(key, value),
del: (key: string | string[]): number => caches.user.del(key),
flush: (): void => caches.user.flushAll(),
// Helper to invalidate all cache for a user
invalidateUser: (userId: string): void => {
const keys = caches.user.keys().filter(k => k.startsWith(`user:${userId}:`));
caches.user.del(keys);
},
};
```
---