Skip to content

Commit 5ceeb46

Browse files
feat(utils): user encryption helpers, db upsert set builder
1 parent 17a259f commit 5ceeb46

File tree

4 files changed

+103
-73
lines changed

4 files changed

+103
-73
lines changed

backend/src/routers/tg/permissions.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ import { DB, SCHEMA } from "@/db"
44
import { ARRAY_USER_ROLE, type TUserRole, USER_ROLE } from "@/db/schema/tg/permissions"
55
import { logger } from "@/logger"
66
import { createTRPCRouter, publicProcedure } from "@/trpc"
7+
import { decryptUser, TgUserSchema } from "@/utils/users"
78

89
const s = SCHEMA.TG
910

1011
const CAN_SELF_ASSIGN: TUserRole[] = [USER_ROLE.PRESIDENT, USER_ROLE.OWNER] as const
1112
const CAN_ASSIGN: TUserRole[] = [USER_ROLE.PRESIDENT, USER_ROLE.OWNER, USER_ROLE.DIRETTIVO] as const
1213
const CAN_ADD_BOT: TUserRole[] = [USER_ROLE.HR, USER_ROLE.OWNER, USER_ROLE.CREATOR, USER_ROLE.DIRETTIVO] as const
1314

15+
const direttivoMember = z.object({
16+
userId: z.number(),
17+
user: TgUserSchema.nullable(),
18+
isPresident: z.boolean(),
19+
})
20+
1421
export default createTRPCRouter({
1522
getRoles: publicProcedure
1623
.input(z.object({ userId: z.number() }))
@@ -37,12 +44,7 @@ export default createTRPCRouter({
3744
getDirettivo: publicProcedure
3845
.output(
3946
z.object({
40-
members: z.array(
41-
z.object({
42-
userId: z.number(),
43-
isPresident: z.boolean(),
44-
})
45-
),
47+
members: z.array(direttivoMember),
4648
error: z.union([
4749
z.null(),
4850
z.enum(["EMPTY", "NOT_ENOUGH_MEMBERS", "TOO_MANY_MEMBERS", "INTERNAL_SERVER_ERROR"]),
@@ -51,11 +53,30 @@ export default createTRPCRouter({
5153
)
5254
.query(async () => {
5355
try {
54-
const res = await DB.select({ userId: s.permissions.userId, roles: s.permissions.roles })
56+
const res = await DB.select({ userId: s.permissions.userId, dbUser: s.users, roles: s.permissions.roles })
5557
.from(s.permissions)
5658
.where(arrayContains(s.permissions.roles, [USER_ROLE.DIRETTIVO]))
57-
58-
const members = res.map((r) => ({ userId: r.userId, isPresident: r.roles.includes(USER_ROLE.PRESIDENT) }))
59+
.leftJoin(s.users, eq(s.permissions.userId, s.users.userId))
60+
61+
const members: z.infer<typeof direttivoMember>[] = await Promise.all(
62+
res
63+
.toSorted((a, b) => {
64+
if (a.roles.includes(USER_ROLE.PRESIDENT) && b.roles.includes(USER_ROLE.PRESIDENT)) return 0
65+
if (a.roles.includes(USER_ROLE.PRESIDENT)) return 1
66+
if (b.roles.includes(USER_ROLE.PRESIDENT)) return -1
67+
return 0
68+
})
69+
.map(async (r) => {
70+
const isPresident = r.roles.includes(USER_ROLE.PRESIDENT)
71+
const user = r.dbUser ? await decryptUser(r.dbUser) : null
72+
73+
return {
74+
user,
75+
userId: r.userId,
76+
isPresident,
77+
}
78+
})
79+
)
5980

6081
if (res.length === 0) return { error: "EMPTY", members }
6182
if (res.length < 3) return { error: "NOT_ENOUGH_MEMBERS", members }

backend/src/routers/tg/users.ts

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,22 @@
1-
import { eq, type SQL, sql } from "drizzle-orm"
1+
import { eq } from "drizzle-orm"
22
import { z } from "zod"
33
import { DB, SCHEMA } from "@/db"
44
import { logger } from "@/logger"
55
import { createTRPCRouter, publicProcedure } from "@/trpc"
6-
import { Cipher, DecryptError } from "@/utils/cipher"
6+
import { DecryptError } from "@/utils/cipher"
7+
import { upsertMultipleSetSql } from "@/utils/db"
8+
import { decryptUser, encryptUser, TgUserSchema } from "@/utils/users"
79

8-
const cipher = new Cipher("tg.users")
910
const s = SCHEMA.TG
10-
11-
const updatableCols: Readonly<(keyof typeof s.users.$inferInsert)[]> = [
12-
"firstName",
13-
"username",
14-
"lastName",
15-
"langCode",
16-
"isBot",
17-
] as const
18-
19-
const addUserUpdateSet = updatableCols.reduce<Partial<Record<(typeof updatableCols)[number], SQL<unknown>>>>(
20-
(acc, curr) => {
21-
acc[curr] = sql.raw(`excluded.${s.users[curr].name}`)
22-
return acc
23-
},
24-
{}
25-
)
26-
//
27-
28-
const UserSchema = z.object({
29-
id: z.number(),
30-
firstName: z.string().max(64),
31-
lastName: z.string().max(64).optional(),
32-
username: z.string().max(32).optional(),
33-
isBot: z.boolean(),
34-
langCode: z.string().max(35).optional(),
35-
})
11+
const upsertSet = upsertMultipleSetSql(s.users, ["firstName", "lastName", "username", "langCode", "isBot"])
3612

3713
export default createTRPCRouter({
3814
get: publicProcedure
3915
.input(z.object({ userId: z.number() }))
4016
.output(
4117
z.union([
4218
z.object({
43-
user: UserSchema,
19+
user: TgUserSchema,
4420
error: z.null(),
4521
}),
4622
z.object({
@@ -54,20 +30,7 @@ export default createTRPCRouter({
5430
const res = await DB.select().from(s.users).where(eq(s.users.userId, input.userId)).limit(1)
5531
if (res.length === 0) return { error: "NOT_FOUND" }
5632

57-
const [firstName, lastName, username] = await Promise.all([
58-
cipher.decrypt(res[0].firstName),
59-
res[0].lastName ? cipher.decrypt(res[0].lastName) : undefined,
60-
res[0].username ? cipher.decrypt(res[0].username) : undefined,
61-
])
62-
63-
const user: z.infer<typeof UserSchema> = {
64-
id: res[0].userId,
65-
firstName,
66-
lastName,
67-
username,
68-
langCode: res[0].langCode ?? undefined,
69-
isBot: res[0].isBot,
70-
}
33+
const user = await decryptUser(res[0])
7134

7235
return {
7336
user,
@@ -84,7 +47,7 @@ export default createTRPCRouter({
8447
}),
8548

8649
add: publicProcedure
87-
.input(z.object({ users: z.array(UserSchema) }))
50+
.input(z.object({ users: z.array(TgUserSchema) }))
8851
.output(
8952
z.union([
9053
z.object({
@@ -94,30 +57,13 @@ export default createTRPCRouter({
9457
)
9558
.mutation(async ({ input }) => {
9659
try {
97-
const users: (typeof s.users.$inferInsert)[] = await Promise.all(
98-
input.users.map(async (u) => {
99-
const [firstName, lastName, username] = await Promise.all([
100-
cipher.encrypt(u.firstName),
101-
u.lastName ? cipher.encrypt(u.lastName) : undefined,
102-
u.username ? cipher.encrypt(u.username) : undefined,
103-
])
104-
105-
return {
106-
userId: u.id,
107-
firstName,
108-
lastName,
109-
username,
110-
isBot: u.isBot,
111-
langCode: u.langCode,
112-
}
113-
})
114-
)
60+
const users = await Promise.all(input.users.map(encryptUser))
11561

11662
const res = await DB.insert(s.users)
11763
.values(users)
11864
.onConflictDoUpdate({
11965
target: s.users.userId,
120-
set: addUserUpdateSet,
66+
set: upsertSet,
12167
})
12268
.returning()
12369

backend/src/utils/db.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type SQL, sql } from "drizzle-orm"
2+
import type { PgTableWithColumns, TableConfig } from "drizzle-orm/pg-core"
3+
4+
type ColNames<TC extends TableConfig> = Readonly<(keyof TC["columns"])[]>
5+
type SetSQL<TC extends TableConfig> = Partial<Record<ColNames<TC>[number], SQL<unknown>>>
6+
7+
export function upsertMultipleSetSql<TC extends TableConfig>(
8+
table: PgTableWithColumns<TC>,
9+
columnsToUpdate: ColNames<TC>
10+
): SetSQL<TC> {
11+
return columnsToUpdate.reduce<SetSQL<TC>>((acc, curr) => {
12+
acc[curr] = sql.raw(`excluded.${table[curr].name}`)
13+
return acc
14+
}, {})
15+
}

backend/src/utils/users.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import z from "zod"
2+
import type { SCHEMA } from "@/db"
3+
import { Cipher } from "./cipher"
4+
5+
const userCipher = new Cipher("tg.users")
6+
7+
export const TgUserSchema = z.object({
8+
id: z.number(),
9+
firstName: z.string().max(64),
10+
lastName: z.string().max(64).optional(),
11+
username: z.string().max(32).optional(),
12+
isBot: z.boolean(),
13+
langCode: z.string().max(35).optional(),
14+
})
15+
16+
export async function decryptUser(dbUser: typeof SCHEMA.TG.users.$inferSelect): Promise<z.infer<typeof TgUserSchema>> {
17+
const [firstName, lastName, username] = await Promise.all([
18+
dbUser.firstName,
19+
dbUser.lastName ? userCipher.decrypt(dbUser.lastName) : undefined,
20+
dbUser.username ? userCipher.decrypt(dbUser.username) : undefined,
21+
])
22+
23+
return {
24+
id: dbUser.userId,
25+
firstName,
26+
lastName,
27+
username,
28+
isBot: dbUser.isBot,
29+
langCode: dbUser.langCode ?? undefined,
30+
}
31+
}
32+
33+
export async function encryptUser(tgUser: z.infer<typeof TgUserSchema>): Promise<typeof SCHEMA.TG.users.$inferInsert> {
34+
const [firstName, lastName, username] = await Promise.all([
35+
tgUser.firstName,
36+
tgUser.lastName ? userCipher.encrypt(tgUser.lastName) : undefined,
37+
tgUser.username ? userCipher.encrypt(tgUser.username) : undefined,
38+
])
39+
40+
return {
41+
firstName,
42+
lastName,
43+
username,
44+
userId: tgUser.id,
45+
langCode: tgUser.langCode ?? null,
46+
isBot: tgUser.isBot,
47+
}
48+
}

0 commit comments

Comments
 (0)