230 lines
5.7 KiB
TypeScript
230 lines
5.7 KiB
TypeScript
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<typeof opinionInsertSchema>;
|
|
type UserInsertSchema = z.infer<typeof userInsertSchema>;
|
|
|
|
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",
|
|
});
|
|
}
|
|
}
|