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

5.5 KiB

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

// 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

// 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

// 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);
  },
};