Урок 05

Аутентификация в API

Токен в заголовке

export async function GET(request: Request) {
    const auth = request.headers.get('authorization');

    if (!auth?.startsWith('Bearer ')) {
        return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const token = auth.slice(7);
    const user = await verifyToken(token);

    if (!user) {
        return Response.json({ error: 'Invalid token' }, { status: 401 });
    }

    return Response.json({ user });
}

Cookies

import { cookies } from 'next/headers';

export async function GET() {
    const cookieStore = await cookies();
    const token = cookieStore.get('token')?.value;

    if (!token) {
        return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const user = await verifyToken(token);
    if (!user) {
        return Response.json({ error: 'Invalid token' }, { status: 401 });
    }

    return Response.json({ user });
}

JWT

npm install jose
import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function createToken(payload: { userId: number }) {
    return new SignJWT(payload)
        .setProtectedHeader({ alg: 'HS256' })
        .setIssuedAt()
        .setExpirationTime('7d')
        .sign(secret);
}

export async function verifyToken(token: string) {
    try {
        const { payload } = await jwtVerify(token, secret);
        return payload;
    } catch {
        return null;
    }
}

jose работает в Edge Runtime, в отличие от jsonwebtoken.

Вспомогательная функция

// lib/auth.ts
import { cookies } from 'next/headers';

export async function getCurrentUser() {
    const cookieStore = await cookies();
    const token = cookieStore.get('token')?.value;

    if (!token) return null;

    const payload = await verifyToken(token);
    if (!payload) return null;

    return db.user.findUnique({ where: { id: payload.userId } });
}

Использование

export async function GET() {
    const user = await getCurrentUser();

    if (!user) {
        return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const posts = await db.post.findMany({ where: { authorId: user.id } });
    return Response.json(posts);
}

Проверка ролей

export async function DELETE(
    request: Request,
    { params }: { params: Promise<{ id: string }> }
) {
    const user = await getCurrentUser();
    if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });

    const { id } = await params;
    const post = await db.post.findUnique({ where: { id: Number(id) } });

    if (!post) return Response.json({ error: 'Not found' }, { status: 404 });

    if (post.authorId !== user.id && user.role !== 'admin') {
        return Response.json({ error: 'Forbidden' }, { status: 403 });
    }

    await db.post.delete({ where: { id: Number(id) } });
    return new Response(null, { status: 204 });
}

API-ключи

export async function GET(request: Request) {
    const apiKey = request.headers.get('x-api-key');

    const client = await db.client.findUnique({ where: { apiKey } });
    if (!client) {
        return Response.json({ error: 'Invalid API key' }, { status: 401 });
    }

    return Response.json({ client });
}

Rate limiting по ключу

const limits = new Map<string, number[]>();

export async function GET(request: Request) {
    const apiKey = request.headers.get('x-api-key') ?? 'anonymous';
    const now = Date.now();
    const window = 60_000;
    const limit = 100;

    const timestamps = (limits.get(apiKey) ?? []).filter((t) => now - t < window);

    if (timestamps.length >= limit) {
        return Response.json(
            { error: 'Rate limit exceeded' },
            {
                status: 429,
                headers: { 'Retry-After': '60' },
            }
        );
    }

    timestamps.push(now);
    limits.set(apiKey, timestamps);

    // обработка
}

Установка cookie

export async function POST(request: Request) {
    const { email, password } = await request.json();

    const user = await db.user.findUnique({ where: { email } });
    if (!user || !(await verifyPassword(password, user.password))) {
        return Response.json({ error: 'Invalid credentials' }, { status: 401 });
    }

    const token = await createToken({ userId: user.id });

    const response = Response.json({ user: { id: user.id, email: user.email } });
    response.headers.set(
        'Set-Cookie',
        `token=${token}; HttpOnly; Path=/; Max-Age=604800; SameSite=Lax${
            process.env.NODE_ENV === 'production' ? '; Secure' : ''
        }`
    );

    return response;
}

Выход

export async function POST() {
    const response = Response.json({ ok: true });
    response.headers.set('Set-Cookie', 'token=; HttpOnly; Path=/; Max-Age=0');
    return response;
}

Итоги

  • Токен в заголовке или куке
  • jose для JWT в Edge
  • Вспомогательная функция для пользователя
  • Проверка прав на каждой операции
  • API-ключи в заголовке
  • Rate limiting по ключу