Skip to content

Commit c98f127

Browse files
authored
Merge branch 'refactor/routes' into main
2 parents 2b3bb2c + 217c20b commit c98f127

File tree

42 files changed

+2681
-620
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2681
-620
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "query",
33
"private": true,
44
"version": "1.0.0",
5-
"packageManager": "pnpm@10.25.0",
5+
"packageManager": "pnpm@10.26.1",
66
"engines": {
77
"node": ">=20.16.0",
88
"pnpm": ">=9"

packages/api/package.json

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,33 @@
22
"name": "@query/api",
33
"version": "0.0.0",
44
"private": true,
5-
"main": "./src/index.ts",
6-
"types": "./src/index.ts",
5+
"type": "module",
76
"exports": {
8-
".": "./src/index.ts"
7+
".": "./src/index.ts",
8+
"./client": "./src/client.ts",
9+
"./trpc-server": "./src/trpc-server.ts",
10+
"./server": "./src/index.ts",
11+
"./context": "./src/context.ts",
12+
"./middleware": "./src/middleware.ts",
13+
"./trpc": "./src/trpc.ts"
914
},
1015
"scripts": {
1116
"lint": "eslint .",
12-
"type-check": "tsc --noEmit"
17+
"typecheck": "tsc --noEmit"
1318
},
1419
"dependencies": {
15-
"@trpc/server": "^11.8.0",
16-
"@query/db": "workspace:*",
1720
"@query/auth": "workspace:*",
18-
"superjson": "^2.2.1",
19-
"zod": "^3.22.4"
21+
"@query/db": "workspace:*",
22+
"@trpc/server": "^11.7.2",
23+
"@trpc/client": "^11.7.2",
24+
"@trpc/react-query": "^11.7.2",
25+
"@trpc/next": "^11.7.2",
26+
"@tanstack/react-query": "^5.90.12",
27+
"zod": "^3.23.8",
28+
"superjson": "^2.2.6"
2029
},
2130
"devDependencies": {
2231
"@query/tsconfig": "workspace:*",
23-
"@types/node": "^20",
24-
"typescript": "^5"
32+
"typescript": "^5.6.3"
2533
}
26-
}
34+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { TRPCError } from "@trpc/server";
2+
3+
const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
4+
5+
setInterval(() => {
6+
const now = Date.now();
7+
for (const [key, value] of rateLimitStore.entries()) {
8+
if (now > value.resetAt) {
9+
rateLimitStore.delete(key);
10+
}
11+
}
12+
}, 5 * 60 * 1000);
13+
14+
export function rateLimit(identifier: string, maxRequests: number, windowMs: number): boolean {
15+
const now = Date.now();
16+
const record = rateLimitStore.get(identifier);
17+
18+
if (!record || now > record.resetAt) {
19+
rateLimitStore.set(identifier, {
20+
count: 1,
21+
resetAt: now + windowMs,
22+
});
23+
return true;
24+
}
25+
26+
if (record.count >= maxRequests) {
27+
return false;
28+
}
29+
30+
record.count++;
31+
return true;
32+
}
33+
34+
export function sanitizeInput(input: any): any {
35+
if (input === null || input === undefined) {
36+
return input;
37+
}
38+
39+
if (typeof input === 'string') {
40+
return input
41+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
42+
.replace(/javascript:/gi, '')
43+
.replace(/on\w+\s*=/gi, '')
44+
.replace(/data:text\/html/gi, '')
45+
.replace(/<iframe/gi, '')
46+
.replace(/<embed/gi, '')
47+
.replace(/<object/gi, '')
48+
.trim()
49+
.slice(0, 10000);
50+
}
51+
52+
if (Array.isArray(input)) {
53+
if (input.length > 1000) {
54+
throw new TRPCError({
55+
code: "BAD_REQUEST",
56+
message: "Array too large",
57+
});
58+
}
59+
return input.map(sanitizeInput);
60+
}
61+
62+
if (typeof input === 'object') {
63+
const keys = Object.keys(input);
64+
if (keys.length > 100) {
65+
throw new TRPCError({
66+
code: "BAD_REQUEST",
67+
message: "Object too complex",
68+
});
69+
}
70+
71+
const sanitized: any = {};
72+
for (const [key, value] of Object.entries(input)) {
73+
// Sanitize keys too
74+
const cleanKey = key.replace(/[^\w\-]/g, '').slice(0, 100);
75+
if (cleanKey) {
76+
sanitized[cleanKey] = sanitizeInput(value);
77+
}
78+
}
79+
return sanitized;
80+
}
81+
82+
return input;
83+
}
84+
85+
export function validateEmail(email: string): boolean {
86+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
87+
return emailRegex.test(email) && email.length <= 254;
88+
}
89+
90+
export function validateUrl(url: string): boolean {
91+
try {
92+
const parsed = new URL(url);
93+
return ['http:', 'https:'].includes(parsed.protocol);
94+
} catch {
95+
return false;
96+
}
97+
}
98+
99+
export function validateUUID(uuid: string): boolean {
100+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
101+
return uuidRegex.test(uuid);
102+
}

packages/api/src/root.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { createTRPCRouter } from "./trpc";
22
import { helloRouter } from "./routers/hello";
33
import { userRouter } from "./routers/user";
4+
import { adminRouter } from "./routers/admin";
5+
import { memberRouter } from "./routers/member";
6+
import { hackathonRouter } from "./routers/hackathon";
47

58
export const appRouter = createTRPCRouter({
69
hello: helloRouter,
710
user: userRouter,
11+
admin: adminRouter,
12+
member: memberRouter,
13+
hackathon: hackathonRouter,
814
});
915

1016
export type AppRouter = typeof appRouter;

packages/api/src/routers/admin.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { z } from "zod";
2+
import { TRPCError } from "@trpc/server";
3+
import { createTRPCRouter, protectedProcedure } from "../trpc";
4+
import { admins, users } from "@query/db";
5+
import { eq, and } from "drizzle-orm";
6+
7+
const isAdmin = protectedProcedure.use(async ({ ctx, next }) => {
8+
const admin = await ctx.db.query.admins.findFirst({
9+
where: and(
10+
eq(admins.userId, ctx.userId!),
11+
eq(admins.isActive, true)
12+
),
13+
});
14+
15+
if (!admin) {
16+
throw new TRPCError({
17+
code: "FORBIDDEN",
18+
message: "Admin access required",
19+
});
20+
}
21+
22+
return next({ ctx: { ...ctx, admin } });
23+
});
24+
25+
const isSuperAdmin = isAdmin.use(async ({ ctx, next }) => {
26+
if (ctx.admin.role !== "super_admin") {
27+
throw new TRPCError({
28+
code: "FORBIDDEN",
29+
message: "Super admin access required",
30+
});
31+
}
32+
return next({ ctx });
33+
});
34+
35+
export const adminRouter = createTRPCRouter({
36+
isAdmin: protectedProcedure.query(async ({ ctx }) => {
37+
const admin = await ctx.db.query.admins.findFirst({
38+
where: and(
39+
eq(admins.userId, ctx.userId!),
40+
eq(admins.isActive, true)
41+
),
42+
});
43+
44+
return {
45+
isAdmin: !!admin,
46+
role: admin?.role || null,
47+
permissions: admin?.permissions || [],
48+
};
49+
}),
50+
51+
list: isAdmin.query(async ({ ctx }) => {
52+
const allAdmins = await ctx.db.query.admins.findMany({
53+
with: {
54+
user: {
55+
columns: {
56+
id: true,
57+
name: true,
58+
email: true,
59+
image: true,
60+
},
61+
},
62+
},
63+
orderBy: (admins, { desc }) => [desc(admins.createdAt)],
64+
limit: 100,
65+
});
66+
67+
return allAdmins;
68+
}),
69+
70+
create: isSuperAdmin
71+
.input(
72+
z.object({
73+
userId: z.string().min(1).max(255),
74+
role: z.enum(["super_admin", "admin", "moderator"]),
75+
permissions: z.array(z.string().max(100)).max(50).optional(),
76+
})
77+
)
78+
.mutation(async ({ ctx, input }) => {
79+
const user = await ctx.db.query.users.findFirst({
80+
where: eq(users.id, input.userId),
81+
});
82+
83+
if (!user) {
84+
throw new TRPCError({
85+
code: "NOT_FOUND",
86+
message: "User not found",
87+
});
88+
}
89+
90+
const existingAdmin = await ctx.db.query.admins.findFirst({
91+
where: eq(admins.userId, input.userId),
92+
});
93+
94+
if (existingAdmin) {
95+
throw new TRPCError({
96+
code: "BAD_REQUEST",
97+
message: "User is already an admin",
98+
});
99+
}
100+
101+
const result = await ctx.db
102+
.insert(admins)
103+
.values({
104+
userId: input.userId,
105+
role: input.role,
106+
permissions: input.permissions || [],
107+
})
108+
.returning();
109+
110+
const newAdmin = result[0];
111+
112+
if (!newAdmin) {
113+
throw new TRPCError({
114+
code: "INTERNAL_SERVER_ERROR",
115+
message: "cannot create",
116+
});
117+
}
118+
119+
return newAdmin;
120+
}),
121+
122+
update: isSuperAdmin
123+
.input(
124+
z.object({
125+
adminId: z.string().uuid(),
126+
role: z.enum(["super_admin", "admin", "moderator"]).optional(),
127+
permissions: z.array(z.string().max(100)).max(50).optional(),
128+
isActive: z.boolean().optional(),
129+
})
130+
)
131+
.mutation(async ({ ctx, input }) => {
132+
const targetAdmin = await ctx.db.query.admins.findFirst({
133+
where: eq(admins.id, input.adminId),
134+
});
135+
136+
if (!targetAdmin) {
137+
throw new TRPCError({
138+
code: "NOT_FOUND",
139+
message: "Admin not found",
140+
});
141+
}
142+
if (targetAdmin.userId === ctx.userId && input.isActive === false) {
143+
throw new TRPCError({
144+
code: "BAD_REQUEST",
145+
message: "why?",
146+
});
147+
}
148+
149+
const result = await ctx.db
150+
.update(admins)
151+
.set({
152+
role: input.role,
153+
permissions: input.permissions,
154+
isActive: input.isActive,
155+
updatedAt: new Date(),
156+
})
157+
.where(eq(admins.id, input.adminId))
158+
.returning();
159+
160+
const updatedAdmin = result[0];
161+
162+
if (!updatedAdmin) {
163+
throw new TRPCError({
164+
code: "INTERNAL_SERVER_ERROR",
165+
message: "couldnt update",
166+
});
167+
}
168+
169+
return updatedAdmin;
170+
}),
171+
172+
remove: isSuperAdmin
173+
.input(z.object({ adminId: z.string().uuid() }))
174+
.mutation(async ({ ctx, input }) => {
175+
const targetAdmin = await ctx.db.query.admins.findFirst({
176+
where: eq(admins.id, input.adminId),
177+
});
178+
179+
if (!targetAdmin) {
180+
throw new TRPCError({
181+
code: "NOT_FOUND",
182+
message: "not located",
183+
});
184+
}
185+
186+
if (targetAdmin.userId === ctx.userId) {
187+
throw new TRPCError({
188+
code: "BAD_REQUEST",
189+
message: "why are you trying to do this",
190+
});
191+
}
192+
193+
await ctx.db.delete(admins).where(eq(admins.id, input.adminId));
194+
195+
return { success: true };
196+
}),
197+
});

0 commit comments

Comments
 (0)