import { prisma } from '@/db'; import { HouseWhereInput } from '@/generated/prisma/models'; import { DB_TABLE, LOG_ACTION, NOTIFICATION_TYPE } from '@/types/enum'; import { auth } from '@lib/auth'; import { parseError } from '@lib/errors'; import { authMiddleware } from '@lib/middleware'; import { createServerFn } from '@tanstack/react-start'; import { getRequestHeaders } from '@tanstack/react-start/server'; import { actionInvitationSchema, baseHouse, houseCreateBESchema, houseEditBESchema, houseForSelectSchema, houseListSchema, invitationCreateBESchema, removeMemberSchema, } from './house.schema'; import { createAuditLog, createNotification } from './repository'; export const getAllHouse = createServerFn({ method: 'GET' }) .middleware([authMiddleware]) .validator(houseListSchema) .handler(async ({ data }) => { try { const { page, limit, keyword } = data; const skip = (page - 1) * limit; const where: HouseWhereInput = { OR: [ { name: { contains: keyword, mode: 'insensitive', }, }, ], }; const [list, total]: [HouseWithMembers[], number] = await Promise.all([ await prisma.house.findMany({ where, orderBy: { createdAt: 'desc' }, include: { members: { select: { role: true, user: { select: { id: true, name: true, email: true, image: true, }, }, }, }, }, take: limit, skip, }), await prisma.house.count({ where }), ]); const totalPage = Math.ceil(+total / limit); return { result: list, pagination: { currentPage: page, totalPage, totalItem: total, }, }; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const getCurrentUserHouses = createServerFn({ method: 'GET' }) .middleware([authMiddleware]) .handler(async ({ context: { user } }) => { try { const houses = await prisma.house.findMany({ where: { members: { some: { userId: user.id } } }, orderBy: { createdAt: 'asc' }, include: { _count: { select: { members: true, }, }, members: { select: { role: true, user: { select: { id: true, name: true, email: true, image: true, }, }, }, }, }, }); return houses; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const getHouseForSelect = createServerFn({ method: 'GET' }) .middleware([authMiddleware]) .validator(houseForSelectSchema) .handler(async ({ data }) => { try { const result = await prisma.house.findMany({ where: { OR: [ { name: { contains: data.keyword, mode: 'insensitive', }, }, ], }, select: { id: true, name: true, color: true, }, orderBy: { createdAt: 'desc' }, take: 5, }); return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const createHouse = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(houseCreateBESchema) .handler(async ({ data, context: { user } }) => { try { const result = await auth.api.createOrganization({ body: data, }); if (!result) throw Error('Failed to create house'); await createAuditLog({ action: LOG_ACTION.CREATE, tableName: DB_TABLE.ORGANIZATION, recordId: result.id, oldValue: '', newValue: JSON.stringify(result), userId: user.id, }); return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const updateHouse = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(houseEditBESchema) .handler(async ({ data, context: { user } }) => { try { const currentHouse = await prisma.house.findUnique({ where: { id: data.id }, }); if (!currentHouse) throw Error('House not found'); const { id, slug, name, color } = data; const result = await prisma.house.update({ where: { id }, data: { name, slug, color, }, }); await createAuditLog({ action: LOG_ACTION.UPDATE, tableName: DB_TABLE.ORGANIZATION, recordId: id, oldValue: JSON.stringify(currentHouse), newValue: JSON.stringify(result), userId: user.id, }); return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const deleteHouse = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(baseHouse) .handler(async ({ data, context: { user } }) => { try { const currentHouse = await prisma.house.findUnique({ where: { id: data.id }, }); if (!currentHouse) throw Error('House not found'); const result = await Promise.all([ prisma.house.delete({ where: { id: data.id }, }), prisma.member.deleteMany({ where: { organizationId: data.id }, }), prisma.invitation.deleteMany({ where: { organizationId: data.id }, }), ]); await createAuditLog({ action: LOG_ACTION.DELETE, tableName: DB_TABLE.ORGANIZATION, recordId: result[0]?.id, oldValue: JSON.stringify(currentHouse), newValue: '', userId: user.id, }); return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const deleteUserHouse = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(baseHouse) .handler(async ({ data, context: { user } }) => { try { const currentHouse = await prisma.house.findUnique({ where: { id: data.id }, }); if (!currentHouse) throw Error('House not found'); const headers = getRequestHeaders(); const result = await auth.api.deleteOrganization({ body: { organizationId: data.id, // required }, headers, }); if (result) { await createAuditLog({ action: LOG_ACTION.DELETE, tableName: DB_TABLE.ORGANIZATION, recordId: result?.id, oldValue: JSON.stringify(currentHouse), newValue: '', userId: user.id, }); } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const invitationMember = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(invitationCreateBESchema) .handler(async ({ data, context: { user } }) => { try { const headers = getRequestHeaders(); const body = { email: data.email, role: data.role, organizationId: data.houseId, }; const cuser = await prisma.user.findUnique({ where: { email: data.email }, }); const chouse = await prisma.house.findUnique({ where: { id: data.houseId }, }); const result = await auth.api.createInvitation({ body, headers, }); if (result && cuser && chouse) { await createNotification({ type: NOTIFICATION_TYPE.INVITATION, userId: cuser.id, title: 'INVITATION_HOUSE', message: 'INVITATION_HOUSE', link: result.id, metadata: JSON.stringify({ house: chouse, }), readAt: null, }); await createAuditLog({ action: LOG_ACTION.CREATE, tableName: DB_TABLE.INVITATION, recordId: result.id, oldValue: '', newValue: JSON.stringify(body), userId: user.id, }); } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const cancelInvitation = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(baseHouse) .handler(async ({ data }) => { try { const headers = getRequestHeaders(); const result = await auth.api.cancelInvitation({ body: { invitationId: data.id, // required }, headers, }); if (result) { const notification = await prisma.notification.findFirst({ where: { link: data.id }, }); if (notification) { await prisma.notification.update({ where: { id: notification.id }, data: { link: null }, }); } } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const acceptInvitation = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(actionInvitationSchema) .handler(async ({ data, context: { user } }) => { try { const result = await prisma.invitation.update({ where: { id: data.id }, data: { status: 'accepted' }, }); if (result) { const notify = await prisma.notification.update({ where: { id: data.notificationId }, data: { link: null }, }); const member = await auth.api.addMember({ body: { userId: notify.userId, organizationId: result.organizationId, role: (result.role as 'admin' | 'owner' | 'member') || 'member', }, }); await createAuditLog({ action: LOG_ACTION.UPDATE, tableName: DB_TABLE.INVITATION, recordId: result.id, oldValue: JSON.stringify(result), newValue: 'Accept Invitation / Đồng ý mời', userId: user.id, }); if (member) { await createAuditLog({ action: LOG_ACTION.CREATE, tableName: DB_TABLE.MEMBER, recordId: member.id, oldValue: '', newValue: JSON.stringify(member), userId: user.id, }); } } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const rejectInvitation = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(actionInvitationSchema) .handler(async ({ data }) => { try { const headers = getRequestHeaders(); const result = await auth.api.rejectInvitation({ body: { invitationId: data.id, }, headers, }); if (result) { await prisma.notification.update({ where: { id: data.notificationId }, data: { link: null }, }); } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const leaveHouse = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(baseHouse) .handler(async ({ data }) => { try { const headers = getRequestHeaders(); const result = await auth.api.leaveOrganization({ body: { organizationId: data.id, }, headers, }); if (result) { await createAuditLog({ action: LOG_ACTION.UPDATE, tableName: DB_TABLE.ORGANIZATION, recordId: result.id, oldValue: JSON.stringify(result), newValue: `${result.user.name} đã rời khỏi nhà (left the house)`, userId: result.user.id, }); } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } }); export const removeMember = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .validator(removeMemberSchema) .handler(async ({ data, context: { user } }) => { try { const headers = getRequestHeaders(); const result = await auth.api.removeMember({ body: { memberIdOrEmail: data.memberId, organizationId: data.houseId, }, headers, }); if (result) { const oldMember = await prisma.user.findUnique({ where: { id: result.member.userId }, }); await createAuditLog({ action: LOG_ACTION.UPDATE, tableName: DB_TABLE.MEMBER, recordId: result.member.id, oldValue: JSON.stringify(result), newValue: `${oldMember?.name} đã bị "mời" rời khỏi nhà (was "asked" to leave the house.)`, userId: user.id, }); } return result; } catch (error) { console.error(error); const { message, code } = parseError(error); throw { message, code }; } });