All files / app/login actions.ts

0% Statements 0/88
0% Branches 0/1
0% Functions 0/1
0% Lines 0/88

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89                                                                                                                                                                                 
'use server';

import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { prisma } from '@/lib/db';
import {
  SESSION_COOKIE,
  SESSION_MAX_AGE,
  signSession,
  verifyPassword,
} from '@/lib/auth';
import { checkRateLimit, resetRateLimit } from '@/lib/rate-limit';

export type LoginState = { error?: string };

const LOGIN_MAX_ATTEMPTS = 5;
const LOGIN_WINDOW_SECONDS = 15 * 60;

async function getClientIp(): Promise<string> {
  const h = await headers();
  const xff = h.get('x-forwarded-for');
  if (xff) return xff.split(',')[0].trim();
  return h.get('x-real-ip') ?? 'unknown';
}

export async function loginAction(
  _prev: LoginState,
  formData: FormData,
): Promise<LoginState> {
  const username = String(formData.get('username') ?? '').trim();
  const password = String(formData.get('password') ?? '');
  const next = String(formData.get('next') ?? '/dashboard');

  if (!username || !password) {
    return { error: 'กรุณากรอกข้อมูลให้ครบถ้วน' };
  }

  const ip = await getClientIp();
  const limitKey = `login:${ip}:${username.toLowerCase()}`;
  const rl = await checkRateLimit({
    key: limitKey,
    limit: LOGIN_MAX_ATTEMPTS,
    windowSeconds: LOGIN_WINDOW_SECONDS,
  });
  if (!rl.allowed) {
    return {
      error: `พยายามเข้าสู่ระบบเกินจำนวนที่กำหนด ลองใหม่ในอีก ${Math.ceil(rl.resetIn / 60)} นาที`,
    };
  }

  const user = await prisma.user.findUnique({ where: { username } });
  if (!user || user.status !== 'ACTIVE') {
    return { error: 'ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง' };
  }

  const ok = await verifyPassword(password, user.passwordHash);
  if (!ok) {
    return { error: 'ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง' };
  }

  await prisma.user.update({
    where: { id: user.id },
    data: { lastLoginAt: new Date() },
  });

  await resetRateLimit(limitKey);

  const token = await signSession({
    sub: user.id,
    username: user.username,
    role: user.role,
  });

  (await cookies()).set(SESSION_COOKIE, token, {
    httpOnly: true,
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
    path: '/',
    maxAge: SESSION_MAX_AGE,
  });

  redirect(next.startsWith('/') ? next : '/dashboard');
}

export async function logoutAction(): Promise<void> {
  (await cookies()).delete(SESSION_COOKIE);
  redirect('/login');
}