import { router, verifiedPhone, publicProcedure, protectedProcedure, } from "./trpc"; import { db } from "./db"; import { phoneToken, user, userOpinion } from "./schema"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; import { SQL, eq } from "drizzle-orm"; import { Config } from "./config"; import { TRPCError } from "@trpc/server"; import * as jwt from "jsonwebtoken"; const userInsertSchema = createInsertSchema(user); const opinionInsertSchema = createInsertSchema(userOpinion) .omit({ userId: true, }) .array() .default([]); const opinionUpdateSchema = createInsertSchema(userOpinion) .omit({ userId: true, }) .required({ opinionId: true }); type OpinionInsertSchema = z.infer; type UserInsertSchema = z.infer; export const userRoute = router({ getAllUser: publicProcedure .input( z.object({ offset: z.number().default(0), limit: z.number().max(50).default(10), group: z.number().optional(), zone: z.number().optional(), }) ) .query( async ({ input }) => await getAllUser(input.offset, input.limit, input.group, input.zone) ), createUser: verifiedPhone .input( userInsertSchema.omit({ id: true, phone: true }).extend({ opinions: opinionInsertSchema, }) ) .mutation( async ({ input, ctx }) => await createUser({ ...input, phone: ctx.phone }, input.opinions) ), requestOtp: publicProcedure .input(z.object({ phone: z.string().trim().min(5) })) .mutation(async ({ input }) => await requestOtp(input.phone)), verifyOtp: publicProcedure .input( z.object({ pin: z.string().trim(), token: z.string(), }) ) .mutation(async ({ input }) => await verifyOtp(input.token, input.pin)), changeOpinion: protectedProcedure .input(opinionUpdateSchema) .mutation( async ({ input, ctx }) => await changeOpinion(input.opinionId, ctx.user.id, input.choice) ), }); async function getAllUser( offset: number, limit: number, group?: number, zone?: number ) { let users = await db.query.user.findMany({ with: { group: true, opinions: true, zone: { with: { province: true }, }, }, limit, offset, where: (user, { eq, and }) => { let conditions: SQL[] = []; if (group) { conditions.push(eq(user.group, group)); } if (zone) { conditions.push(eq(user.zone, zone)); } return and(...conditions); }, }); return users; } async function createUser( newUser: UserInsertSchema, opinions: OpinionInsertSchema ) { try { let result = ( await db.insert(user).values(newUser).returning({ id: user.id }) )[0]; for (let op of opinions) { await db.insert(userOpinion).values({ ...op, userId: result.id }); } return { status: "OK" }; } catch (e) { console.error(e); throw new Error(`Unable to create new user:\n${e}`); } } async function requestOtp(phone: string) { const _phone = phone.trim(); try { let rs = await fetch(Config.sms_api_request_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ key: Config.sms_api_key, secret: Config.sms_api_secret, msisdn: _phone, }), }).then((res) => res.json()); if (rs.errors) { console.error(rs); throw new TRPCError({ message: `Unable to request OTP`, code: "INTERNAL_SERVER_ERROR", }); } await db.insert(phoneToken).values({ phone: _phone, token: rs.token }); return { status: rs.status as string, token: rs.token as string, refno: rs.refno as string, }; } catch (e) { console.error(e); throw new TRPCError({ message: `Unable to request OTP:\n${e}`, code: "INTERNAL_SERVER_ERROR", }); } } async function verifyOtp(token: string, pin: string) { try { let pt = await db.query.phoneToken.findFirst({ where: (pt, { eq }) => eq(pt.token, token), orderBy: (pt, { desc }) => desc(pt.createdOn), }); if (pt === undefined) { throw new TRPCError({ message: `Invalid token`, code: "BAD_REQUEST", }); } pt; let rs = await fetch(Config.sms_api_verify_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ key: Config.sms_api_key, secret: Config.sms_api_secret, token, pin, }), }).then((res) => res.json()); if (rs.errors) { console.error(rs); throw new TRPCError({ message: `Unable to verify OTP`, code: "BAD_REQUEST", }); } else { await db.delete(phoneToken).where(eq(phoneToken.phone, pt.phone)); const token = jwt.sign({ phone: pt.phone }, Config.jwt_secret, { expiresIn: "3d", }); return token; } } catch (e) { console.error(e); throw new TRPCError({ message: `Unable to verify OTP:\n${e}`, code: "BAD_REQUEST", }); } } async function changeOpinion( opinionId: number, userId: number, opinion: OpinionInsertSchema[0]["choice"] ) { try { db.insert(userOpinion) .values({ opinionId, userId, choice: opinion, }) .onConflictDoUpdate({ target: [userOpinion.userId, userOpinion.opinionId], set: { choice: opinion }, }); } catch (e) { console.error(e); throw new TRPCError({ message: "Error updating schema", code: "INTERNAL_SERVER_ERROR", }); } }