// lib/users/service.ts "use server"; import { Otp } from '@/types/user'; import { getAllUsers, createUser, getUserById, updateUser, deleteUser, getUserByEmail, createOtp,getOtpByEmailAndToken, updateOtp, findOtpByEmail ,getOtpByEmailAndCode, findAllRoles, getRoleAssignedUsers} from './repository'; import logger from '@/lib/logger'; import { Role, userRoles, users, userView, type User } from '@/db/schema/users'; import { saveDocument } from '../storage'; import { GetUsersSchema } from '../admin-users/validations'; import { revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; import { filterColumns } from '../filter-columns'; import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm"; import { countUsers, selectUsersWithCompanyAndRoles } from '../admin-users/repository'; import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; interface AssignUsersArgs { roleId: number userIds: number[] } export const fetchAllUsers = async (): Promise => { try { logger.info('Fetching all users'); const users = await getAllUsers(); logger.debug({ count: users.length }, 'Fetched users successfully'); return users; } catch (error) { logger.error({ error }, 'Error fetching all users'); throw new Error('Failed to fetch users'); } }; export const fetchRoleAssignedUserID = async (roleId: number) => { try { logger.info('Fetching all users'); const users = await getRoleAssignedUsers(roleId); logger.debug({ count: users.length }, 'Fetched users successfully'); return users; } catch (error) { logger.error({ error }, 'Error fetching all users'); throw new Error('Failed to fetch users'); } }; export const addNewUser = async (name: string, email: string): Promise => { try { logger.info({ name, email }, 'Creating a new user'); const user = await createUser(name, email); logger.debug({ user }, 'User created successfully'); return user; } catch (error) { logger.error({ error }, 'Error creating a new user'); throw new Error('Failed to create user'); } }; export const findUserById = async (id: number): Promise => { try { logger.info({ id }, 'Fetching user by ID'); const user = await getUserById(id); if (!user) { logger.warn({ id }, 'User not found'); } else { logger.debug({ user }, 'User fetched successfully'); } return user; } catch (error) { logger.error({ error }, 'Error fetching user by ID'); throw new Error('Failed to fetch user'); } }; export const findUserByEmail = async (email: string): Promise => { try { logger.info({ email }, 'Fetching user by Email'); const user = await getUserByEmail(email); if (!user) { logger.warn({ email }, 'User not found'); } else { logger.debug({ user }, 'User fetched successfully'); } return user; } catch (error) { logger.error({ error }, 'Error fetching user by ID'); throw new Error('Failed to fetch user'); } }; export const modifyUser = async (id: number, data: Partial): Promise => { try { logger.info({ id, data }, 'Updating user'); const user = await updateUser(id, data); if (!user) { logger.warn({ id }, 'User not found for update'); } else { logger.debug({ user }, 'User updated successfully'); } return user; } catch (error) { logger.error({ error }, 'Error updating user'); throw new Error('Failed to update user'); } }; export const removeUser = async (id: number): Promise => { try { logger.info({ id }, 'Deleting user'); const success = await deleteUser(id); if (success) { logger.debug({ id }, 'User deleted successfully'); } else { logger.warn({ id }, 'User not found for deletion'); } return success; } catch (error) { logger.error({ error }, 'Error deleting user'); throw new Error('Failed to delete user'); } }; export const addNewOtp = async ( email: string, code: string, createdAt: Date, otpToken: string, otpExpires: Date ): Promise => { try { logger.info({ email }, 'Creating or updating an OTP record'); // 1) 먼저 email로 Otp가 있는지 조회 const existingOtp = await findOtpByEmail(email); // 2) 이미 있으면 update if (existingOtp) { const otp = await updateOtp(email, code, createdAt, otpToken, otpExpires); logger.debug({ otp }, 'OTP updated successfully'); return otp; } // 3) 없으면 새로 생성 else { const otp = await createOtp(email, code, createdAt, otpToken, otpExpires); logger.debug({ otp }, 'OTP created successfully'); return otp; } } catch (error) { logger.error({ error }, 'Error creating or updating OTP'); throw new Error('Failed to create or update OTP'); } }; export const findOtpByEmailandToken = async (email: string, otpToken: string): Promise => { try { logger.info({ email }, 'Fetching otp by Email'); const otp = await getOtpByEmailAndToken(email, otpToken); if (!otp) { logger.warn({ email }, 'Otp not found'); } else { logger.debug({ otp }, 'Otp fetched successfully'); } return otp; } catch (error) { logger.error({ error }, 'Error fetching user by ID'); throw new Error('Failed to fetch user'); } }; export async function findEmailandOtp(email: string, code: string) { try { // 1) otp 조회 const otpRecord: Otp | null = await getOtpByEmailAndCode(email, code) if (!otpRecord) { return null } // 2) 사용자 정보 추가로 조회 const userRecord: User | null = await getUserByEmail(email) if (!userRecord) { return null } // 3) 필요한 형태로 "통합된 객체"를 반환 return { otpExpires: otpRecord.otpExpires, email: userRecord.email, name: userRecord.name, // DB 에서 가져온 실제 이름 id: userRecord.id, // user id imageUrl:userRecord.imageUrl, companyId:userRecord.companyId, domain:userRecord.domain // 기타 필요한 필드... } } catch (error) { // 에러 처리 throw new Error('Failed to fetch user & otp') } } export async function updateUserProfileImage(formData: FormData) { // 1) FormData에서 데이터 꺼내기 const file = formData.get("file") as File | null const userId = Number(formData.get("userId")) const name = formData.get("name") as string const email = formData.get("email") as string // 2) 기본적인 유효성 검증 if (!file) { throw new Error("No file found in the FormData.") } if (!userId) { throw new Error("userId is required.") } try { // 3) 파일 저장 (해시 생성) const directory = './public/profiles' const { hashedFileName } = await saveDocument(file, directory) // 4) DB 업데이트 const imageUrl = hashedFileName const data = { name, email, imageUrl } const user = await updateUser(userId, data) if (!user) { // updateUser가 null을 반환하면, DB 업데이트 실패 혹은 해당 유저가 없음 throw new Error(`User with id=${userId} not found or update failed.`) } // 5) 성공 시 성공 정보 반환 return { success: true, user } } catch (err: any) { // DB 업데이트 중 발생하는 에러나 saveDocument 내부 에러 등을 처리 console.error("[updateUserProfileImage] Error:", err) throw new Error(err.message ?? "Failed to update user profile.") } } export async function getUsersEVCP(input: GetUsersSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // (1) advancedWhere const advancedWhere = filterColumns({ table: userView, filters: input.filters, joinOperator: input.joinOperator, }); // (2) globalWhere let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(userView.user_name, s), ilike(userView.user_email, s), ilike(userView.company_name, s) ); } // (3) 디폴트 domainWhere = eq(userView.domain, "partners") // 다만, 사용자가 이미 domain 필터를 줬다면 적용 X let domainWhere; const hasDomainFilter = input.filters?.some((f) => f.id === "user_domain"); if (!hasDomainFilter) { domainWhere = eq(userView.user_domain, "evcp"); } // (4) 최종 where const finalWhere = and(advancedWhere, globalWhere, domainWhere); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(userView[item.id]) : asc(userView[item.id]) ) : [desc(users.createdAt)]; // ... const { data, total } = await db.transaction(async (tx) => { const data = await selectUsersWithCompanyAndRoles(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); const total = await countUsers(tx, finalWhere); return { data, total }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["users"], } )(); } export async function getAllRoles(): Promise { try { return await findAllRoles(); } catch (err) { throw new Error("Failed to get roles"); } } export async function getUsersAll(input: GetUsersSchema, domain: string) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // (1) advancedWhere const advancedWhere = filterColumns({ table: userView, filters: input.filters, joinOperator: input.joinOperator, }); // (2) globalWhere let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(userView.user_name, s), ilike(userView.user_email, s), ilike(userView.company_name, s) ); } // (3) domainWhere - 무조건 들어가야 하는 domain 조건 const domainWhere = eq(userView.user_domain, domain); // (4) 최종 where // domainWhere과 advancedWhere, globalWhere를 모두 and로 묶는다. // (globalWhere가 존재하지 않을 수 있으니, and() 호출 시 undefined를 자동 무시할 수도 있음) const finalWhere = and(domainWhere, advancedWhere, globalWhere); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(userView[item.id]) : asc(userView[item.id]) ) : [desc(users.createdAt)]; const { data, total } = await db.transaction(async (tx) => { const data = await selectUsersWithCompanyAndRoles(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); const total = await countUsers(tx, finalWhere); return { data, total }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { return { data: [], pageCount: 0 }; } }, // (6) 캐시 종속성 배열에 domain도 추가 [JSON.stringify(input), domain], { revalidate: 3600, tags: ["users"], } )(); } export async function assignUsersToRole(roleId: number, userIds: number[]) { unstable_noStore(); // 캐싱 방지(Next.js 서버 액션용) try{ await db.transaction(async (tx) => { // 1) 기존 userRoles 레코드 삭제 await tx.delete(userRoles).where(eq(userRoles.roleId, roleId)) // 2) 새로 넣기 if (userIds.length > 0) { await tx.insert(userRoles).values( userIds.map((uid) => ({ userId: uid, roleId })) ) } }) revalidateTag("users"); revalidateTag("roles"); return { data: null, error: null }; } catch (err){ return { data: null, error: getErrorMessage(err) }; } }