import { GraphQLError } from 'graphql'; import { IResolvers } from 'mercurius'; import { MutationResolvers, QueryResolvers } from '../generated/graphql'; import { withAuth, hashPassword, verifyPassword } from '../utils'; export const userResolvers: IResolvers = { Query: { me: withAuth(async (_, __, { prisma, jwt }) => prisma.user.findUnique({ where: { id: jwt.sub }, }) ), users: withAuth(async (_, __, { prisma }) => prisma.user.findMany() ), user: withAuth(async (_, { id }, { prisma }) => prisma.user.findUnique({ where: { id } }) ), getProfilePicUploadUrl: withAuth( async (_, { fileExtension }, { jwt, minio, prisma }) => { const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; const normalizedExtension = fileExtension .toLowerCase() .replace('.', ''); if (!validExtensions.includes(normalizedExtension)) { throw new GraphQLError('Invalid file extension', { extensions: { code: 'BAD_USER_INPUT', }, }); } const objectName = `${jwt.sub}.${normalizedExtension}`; const [url] = await Promise.all([ minio.generateUploadUrl(objectName), prisma.user.update({ where: { id: jwt.sub }, data: { s3ProfilePicObjectKey: objectName }, }), ]); return { url, expiresIn: 60 * 60, // 1 hour in seconds }; } ), getProfilePicUrl: withAuth( async (_, __, { jwt, minio, prisma }) => { const user = await prisma.user.findUnique({ where: { id: jwt.sub }, select: { s3ProfilePicObjectKey: true }, }); const { s3ProfilePicObjectKey } = user ?? {}; if (!s3ProfilePicObjectKey) return null; const exists = await minio.objectExists(s3ProfilePicObjectKey); if (!exists) return null; return minio.generateDownloadUrl(s3ProfilePicObjectKey); } ), }, Mutation: { register: async ( _, { email, username, password, deviceId }, { prisma, token, reply } ) => { const existingUser = await prisma.user.findFirst({ where: { OR: [{ email }, { username }], }, }); if (existingUser) { throw new GraphQLError('User already exists', { extensions: { code: 'USER_ALREADY_EXISTS', }, }); } const hashedPassword = hashPassword(password); const { user, accessToken, refreshToken } = await prisma.$transaction( async (tx) => { const user = await tx.user.create({ data: { email, username, password: hashedPassword, }, }); const tokens = await token.generateTokens({ userId: user.id, role: 'user', deviceId, txn: tx, }); return { user, ...tokens }; } ); reply.setCookie('refreshToken', refreshToken); return { accessToken, refreshToken, user, }; }, login: async ( _, { email, password, deviceId }, { prisma, token, reply } ) => { const user = await prisma.user.findUnique({ where: { email }, }); if (!user || !verifyPassword(password, user.password)) { throw new GraphQLError('Invalid credentials', { extensions: { code: 'INVALID_CREDENTIALS', }, }); } const { accessToken, refreshToken } = await token.generateTokens({ userId: user.id, role: 'user', deviceId, }); reply.setCookie('refreshToken', refreshToken); return { user, accessToken, refreshToken, }; }, logout: async (_, { deviceId }, { token, jwt, reply }) => { jwt && (await token.revokeTokensByDevice({ userId: jwt.sub, deviceId, })); reply.clearCookie('refreshToken'); return true; }, logoutAllDevices: withAuth( async (_, __, { token, jwt, reply }) => { await token.revokeAllTokens(jwt.sub); reply.clearCookie('refreshToken'); return true; } ), refreshToken: withAuth( async (_, { deviceId }, { token, jwt, reply }) => { const { accessToken, refreshToken } = await token.rotateRefreshToken({ userId: jwt.sub, oldToken: jwt.jti, deviceId, }); reply.setCookie('refreshToken', refreshToken); return { accessToken, refreshToken, }; } ), deleteProfilePic: withAuth( async (_, __, { jwt, prisma, minio }) => { const user = await prisma.user.findUnique({ where: { id: jwt.sub! }, select: { s3ProfilePicObjectKey: true }, }); await Promise.all([ user?.s3ProfilePicObjectKey && minio.deleteObject(user.s3ProfilePicObjectKey), prisma.user.update({ where: { id: jwt.sub! }, data: { s3ProfilePicObjectKey: null }, }), ]); return true; } ), }, User: { messages: async (user, _, { prisma }) => prisma.user .findUnique({ where: { id: user.id }, }) .messages(), rooms: async (user, _, { prisma }) => prisma.room.findMany({ where: { OR: [{ ownerId: user.id }, { members: { some: { id: user.id } } }], }, }), ownedRooms: async (user, _, { prisma }) => prisma.user .findUnique({ where: { id: user.id }, }) .rooms(), }, };