All files / app/(authed)/users actions.ts

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

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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215                                                                                                                                                                                                                                                                                                                                                                                                                                             
'use server';

import { revalidatePath } from 'next/cache';
import type { Role } from '@prisma/client';
import { requireSession } from '@/lib/session';
import {
  canCreateUserWithRole,
  canManageUser,
  canResetPassword,
} from '@/lib/permissions';
import * as users from '@/lib/users';
import { verifyPassword } from '@/lib/auth';
import { logActivity } from '@/lib/activity';

export type UserActionState = { error?: string; success?: string };

// ---------- validators ----------

function validateUsername(v: unknown): string | null {
  if (typeof v !== 'string') return null;
  const s = v.trim();
  if (s.length < 3 || s.length > 32) return null;
  if (!/^[A-Za-z0-9_.-]+$/.test(s)) return null;
  return s;
}

function validatePassword(v: unknown): string | null {
  if (typeof v !== 'string') return null;
  if (v.length < 8 || v.length > 128) return null;
  return v;
}

function validateRole(v: unknown): Role | null {
  return v === 'MASTER' || v === 'STAFF' ? v : null;
}

// ---------- CREATE ----------

export async function createUserAction(
  _prev: UserActionState,
  formData: FormData,
): Promise<UserActionState> {
  const session = await requireSession();
  const username = validateUsername(formData.get('username'));
  const password = validatePassword(formData.get('password'));
  const role = validateRole(formData.get('role'));

  if (!username) return { error: 'ชื่อผู้ใช้งานไม่ถูกต้อง (3-32 ตัว, a-z 0-9 . _ -)' };
  if (!password) return { error: 'รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร' };
  if (!role) return { error: 'Role ไม่ถูกต้อง' };

  if (!canCreateUserWithRole(session.role, role)) {
    return { error: 'ไม่มีสิทธิ์สร้างผู้ใช้ระดับ Master Admin' };
  }

  const existing = await users.findUserByUsername(username);
  if (existing) return { error: 'ชื่อผู้ใช้งานนี้ถูกใช้แล้ว' };

  const created = await users.createUser({ username, password, role });
  await logActivity({
    userId: session.sub,
    action: 'user.create',
    detail: `created ${created.username} (${created.role})`,
  });

  revalidatePath('/users');
  return { success: `สร้างผู้ใช้ ${created.username} เรียบร้อย` };
}

// ---------- UPDATE (username / role / status) ----------

export async function updateUserAction(
  _prev: UserActionState,
  formData: FormData,
): Promise<UserActionState> {
  const session = await requireSession();
  const id = String(formData.get('id') ?? '');
  if (!id) return { error: 'Missing user id' };

  const target = await users.getUserById(id);
  if (!target) return { error: 'ไม่พบผู้ใช้ที่ระบุ' };

  if (!canManageUser({ id: session.sub, role: session.role }, { id: target.id, role: target.role })) {
    return { error: 'ไม่มีสิทธิ์จัดการผู้ใช้ Master Admin' };
  }

  const username = validateUsername(formData.get('username'));
  const role = validateRole(formData.get('role'));
  const status = formData.get('status');
  const validStatus =
    status === 'ACTIVE' || status === 'INACTIVE' ? status : target.status;

  if (!username) return { error: 'ชื่อผู้ใช้งานไม่ถูกต้อง' };
  if (!role) return { error: 'Role ไม่ถูกต้อง' };

  // Self-edit guard: actor cannot demote themselves or set themselves inactive
  // — both would lock the account out.  Force these fields to their current
  // values when editing self.  Username change is still allowed.
  const isSelf = id === session.sub;
  const finalRole = isSelf ? target.role : role;
  const finalStatus = isSelf ? target.status : validStatus;

  // Block role change if actor can't assign the new role
  if (finalRole !== target.role && !canCreateUserWithRole(session.role, finalRole)) {
    return { error: 'ไม่มีสิทธิ์เลื่อนระดับเป็น Master Admin' };
  }

  if (username !== target.username) {
    const taken = await users.findUserByUsername(username);
    if (taken && taken.id !== id) return { error: 'ชื่อผู้ใช้งานนี้ถูกใช้แล้ว' };
  }

  await users.updateUser(id, { username, role: finalRole, status: finalStatus });
  await logActivity({
    userId: session.sub,
    action: 'user.update',
    detail: `updated ${target.username} → ${username} (${finalRole}, ${finalStatus})`,
  });

  revalidatePath('/users');
  return { success: 'บันทึกข้อมูลผู้ใช้เรียบร้อย' };
}

// ---------- DELETE ----------

export async function deleteUserAction(
  _prev: UserActionState,
  formData: FormData,
): Promise<UserActionState> {
  const session = await requireSession();
  const id = String(formData.get('id') ?? '');
  if (!id) return { error: 'Missing user id' };

  if (id === session.sub) {
    return { error: 'ไม่สามารถลบบัญชีของตัวเองได้' };
  }

  const target = await users.getUserById(id);
  if (!target) return { error: 'ไม่พบผู้ใช้ที่ระบุ' };

  if (!canManageUser({ id: session.sub, role: session.role }, { id: target.id, role: target.role })) {
    return { error: 'ไม่มีสิทธิ์ลบผู้ใช้ Master Admin' };
  }

  await users.deleteUser(id);
  await logActivity({
    userId: session.sub,
    action: 'user.delete',
    detail: `deleted ${target.username} (${target.role})`,
  });

  revalidatePath('/users');
  return { success: `ลบผู้ใช้ ${target.username} เรียบร้อย` };
}

// ---------- RESET ANOTHER USER'S PASSWORD (Master → Staff) ----------

export async function resetUserPasswordAction(
  _prev: UserActionState,
  formData: FormData,
): Promise<UserActionState> {
  const session = await requireSession();
  const id = String(formData.get('id') ?? '');
  const newPassword = validatePassword(formData.get('newPassword'));
  if (!id) return { error: 'Missing user id' };
  if (!newPassword) return { error: 'รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร' };

  const target = await users.getUserById(id);
  if (!target) return { error: 'ไม่พบผู้ใช้ที่ระบุ' };

  if (!canResetPassword({ id: session.sub, role: session.role }, { id: target.id, role: target.role })) {
    return { error: 'ไม่มีสิทธิ์รีเซ็ตรหัสผ่านของผู้ใช้นี้' };
  }

  await users.setUserPassword(id, newPassword);
  await logActivity({
    userId: session.sub,
    action: 'user.password.reset',
    detail: `reset password for ${target.username}`,
  });

  revalidatePath('/users');
  return { success: `รีเซ็ตรหัสผ่านของ ${target.username} เรียบร้อย` };
}

// ---------- SELF-CHANGE PASSWORD (any role) ----------

export async function changeOwnPasswordAction(
  _prev: UserActionState,
  formData: FormData,
): Promise<UserActionState> {
  const session = await requireSession();
  const current = String(formData.get('currentPassword') ?? '');
  const next = validatePassword(formData.get('newPassword'));
  if (!current) return { error: 'กรุณากรอกรหัสผ่านปัจจุบัน' };
  if (!next) return { error: 'รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร' };
  if (current === next) return { error: 'รหัสผ่านใหม่ต้องไม่ซ้ำกับรหัสเดิม' };

  const user = await users.getUserWithHash(session.sub);
  if (!user) return { error: 'ไม่พบบัญชีของคุณ' };

  if (!(await verifyPassword(current, user.passwordHash))) {
    return { error: 'รหัสผ่านปัจจุบันไม่ถูกต้อง' };
  }

  await users.setUserPassword(session.sub, next);
  await logActivity({
    userId: session.sub,
    action: 'user.password.change',
    detail: 'self password change',
  });

  return { success: 'เปลี่ยนรหัสผ่านเรียบร้อย' };
}