211 lines
5.5 KiB
Markdown
211 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);
|
|
},
|
|
};
|
|
```
|
|
|
|
---
|