Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.turbo
.DS_Store
.DS_Store
todo.md
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i need to remove this from commit history so bad. ; (

2 changes: 1 addition & 1 deletion apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GOOGLE_REDIRECT_URI="http://localhost:3000/auth/google/callback"
CORS_ORIGIN="http://localhost:5173"

# Frontend
FRONTEND_URL="http://localhost:5000"
FRONTEND_URL="http://localhost:5173"

# Server
PORT="3000"
Expand Down
12 changes: 7 additions & 5 deletions apps/server/src/db/schema/post.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ import { users } from "./user.schema";
export const posts = pgTable(
"post",
{
id: uuid("id").primaryKey().notNull().unique(),
id: uuid("id").primaryKey().notNull().unique().defaultRandom(),

threadId: uuid("thread_id")
.references(() => threads.id)
.notNull(),

vote: integer("vote"),
content: text("content"),
vote: integer("vote").default(0),
content: text("content").notNull(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

It's great that you've made the content field non-nullable. However, the post table is missing a relationship back to the users table for the createdBy field. This is important for data integrity and for easily querying posts by a user.

You've correctly defined createdBy but the foreign key relationship is missing. You should add drizzle-orm's relations to establish this link.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes yes thought of this will add it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ujsquared ye wala kardiyo


createdAt: timestamp("created_at", { mode: "string" }).notNull(),
createdAt: timestamp("created_at", { mode: "string" })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { mode: "string" }),
deletedAt: timestamp("deleted_at", { mode: "string" }),

Expand All @@ -32,7 +34,7 @@ export const posts = pgTable(
updatedBy: uuid("updated_by").references(() => users.id),
deletedBy: uuid("deleted_by").references(() => users.id),

isApproved: boolean("is_approved"),
isApproved: boolean("is_approved").default(false),
},
(table) => [
index("idx_post_created_by").on(table.createdBy),
Expand Down
10 changes: 6 additions & 4 deletions apps/server/src/db/schema/thread.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ import { users } from "./user.schema";
export const threads = pgTable(
"thread",
{
id: uuid("id").primaryKey().notNull().unique(),
id: uuid("id").primaryKey().notNull().unique().defaultRandom(),

topicId: uuid("topic_id")
.references(() => topics.id)
.notNull(),

viewCount: integer("view_count"),
threadTitle: varchar("thread_title", { length: 255 }),
viewCount: integer("view_count").notNull().default(0),
threadTitle: varchar("thread_title", { length: 255 }).notNull(),

createdAt: timestamp("created_at", { mode: "string" }).notNull(),
createdAt: timestamp("created_at", { mode: "string" })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { mode: "string" }),
deletedAt: timestamp("deleted_at", { mode: "string" }),

Expand Down
10 changes: 6 additions & 4 deletions apps/server/src/db/schema/topic.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { users } from "./user.schema";
export const topics = pgTable(
"topic",
{
id: uuid("id").primaryKey().notNull().unique(),
id: uuid("id").primaryKey().notNull().unique().defaultRandom(),

topicName: varchar("topic_name", { length: 255 }),
topicDescription: text("topic_description"),
topicName: varchar("topic_name", { length: 255 }).notNull(),
topicDescription: text("topic_description").notNull(),

createdAt: timestamp("created_at", { mode: "string" }).notNull(),
createdAt: timestamp("created_at", { mode: "string" })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { mode: "string" }),
deletedAt: timestamp("deleted_at", { mode: "string" }),

Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/dto/posts.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from "zod";
import type { posts } from "@/db/schema/post.schema";

export type Post = typeof posts.$inferSelect;

export const createPostSchema = z.object({
threadId: z.string().uuid(),
content: z.string().min(1, "Content is required").max(10_000),
});
export type CreatePostInput = z.infer<typeof createPostSchema>;

export const updatePostSchema = z
.object({
content: z.string().min(1).max(10_000).optional(),
})
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided",
});
export type UpdatePostInput = z.infer<typeof updatePostSchema>;

export const postIdParamsSchema = z.object({
id: z.string().uuid(),
});
29 changes: 29 additions & 0 deletions apps/server/src/dto/threads.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from "zod";
import type { threads } from "@/db/schema/thread.schema";

export type Thread = typeof threads.$inferSelect;

// threadIdParamsSchema already declared above

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment is misleading as threadIdParamsSchema is declared at the end of the file. For better readability and to avoid confusion, it's a good practice to declare shared schemas and types at the top of the file before they are referenced.

export const threadIdParamsSchema = z.object({
	id: z.string().uuid(),
});

// threadIdParamsSchema already declared above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamanishx PTAL


export const createThreadSchema = z.object({
threadTitle: z.string().min(1).max(255),
topicId: z.string().uuid(),
});

export type CreateThreadInput = z.infer<typeof createThreadSchema>;

export const updateThreadSchema = z
.object({
threadTitle: z.string().min(1).max(255).optional(),
// optionally allow moving thread to another topic
topicId: z.string().uuid().optional(),
// lock/pin handled by separate endpoints in many systems, skip for now
})
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided",
});
export type UpdateThreadInput = z.infer<typeof updateThreadSchema>;

export const threadIdParamsSchema = z.object({
id: z.string().uuid(),
});
25 changes: 25 additions & 0 deletions apps/server/src/dto/topics.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from "zod";
import type { topics } from "@/db/schema/topic.schema";

export type Topic = typeof topics.$inferSelect;

export const topicIdParamsSchema = z.object({
id: z.string().uuid(),
});

export const createTopicSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().min(1).max(500),
});

export type CreateTopicInput = z.infer<typeof createTopicSchema>;

export const updateTopicSchema = z
.object({
name: z.string().min(1).max(100).optional(),
description: z.string().min(1).max(500).optional(),
})
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided",
});
export type UpdateTopicInput = z.infer<typeof updateTopicSchema>;
40 changes: 40 additions & 0 deletions apps/server/src/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import type { users } from "@/db/schema/user.schema";

export type User = typeof users.$inferSelect;
export const userDetailsParamsSchema = z.object({
username: z
.string()
.min(3)
.max(32)
.regex(
/^[a-zA-Z0-9_]+$/,
"Only letters, numbers, and underscore are allowed",
),
});
export type UserDetailsParams = z.infer<typeof userDetailsParamsSchema>;

export const userIdParamsSchema = z.object({
id: z.string().uuid(),
});
export type UserIdParams = z.infer<typeof userIdParamsSchema>;
export const userUpdateSchema = z
.object({
username: z
.string()
.min(3)
.max(32)
.regex(
/^[a-zA-Z0-9_]+$/,
"Only letters, numbers, and underscore are allowed",
)
.optional(),
firstName: z.string().max(50).nullable().optional(),
lastName: z.string().max(50).nullable().optional(),
pronouns: z.string().max(50).nullable().optional(),
bio: z.string().max(280).nullable().optional(),
branch: z.string().max(100).nullable().optional(),
passingOutYear: z.number().int().min(2000).max(2100).nullable().optional(),
})
.strict();
export type UserUpdateInput = z.infer<typeof userUpdateSchema>;
2 changes: 1 addition & 1 deletion apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const baseCorsConfig = {

const fastify = Fastify({
logger: true,
ignoreTrailingSlash: true
ignoreTrailingSlash: true,
});

fastify.register(fastifyCookie, {
Expand Down
32 changes: 28 additions & 4 deletions apps/server/src/routers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import crypto from "node:crypto";
import type { FastifyInstance } from "fastify";
import jwt from "jsonwebtoken";
import { users } from "@/db/schema/user.schema";
import { DrizzleClient } from "../db/index";
import { users } from "../db/schema/user.schema";
import {
createUserSchema,
type GoogleTokenInfo,
Expand Down Expand Up @@ -238,8 +238,11 @@ export async function authRoutes(fastify: FastifyInstance) {
);
}

// Authentication middleware
const authenticateUser: AuthenticationMiddleware = async (request, reply) => {
// Authentication middleware - required authentication
export const authenticateUser: AuthenticationMiddleware = async (
request,
reply,
) => {
try {
const token = request.cookies.auth_token;

Expand All @@ -257,9 +260,30 @@ const authenticateUser: AuthenticationMiddleware = async (request, reply) => {
return reply.status(401).send({ error: "Invalid authentication token" });
}

(request as AuthenticatedRequest).userId = jwtValidation.data.userId;
request.userId = jwtValidation.data.userId;
} catch (err) {
request.log.error("Authentication error:", err);
return reply.status(401).send({ error: "Invalid authentication token" });
}
};

// Optional authentication middleware - doesn't block request if no auth
export const optionalAuth: AuthenticationMiddleware = async (
request,
_reply,
) => {
try {
const token = request.cookies.auth_token;
if (!token) return; // No token is fine for optional auth

const decoded = jwt.verify(token, env.JWT_SECRET) as jwt.JwtPayload;
const jwtValidation = jwtPayloadSchema.safeParse(decoded);

if (jwtValidation.success) {
request.userId = jwtValidation.data.userId;
}
} catch (err) {
// Ignore auth errors for optional auth
request.log.debug("Optional authentication failed:", err);
}
};
11 changes: 11 additions & 0 deletions apps/server/src/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { FastifyInstance } from "fastify";
import { authRoutes } from "./auth";
import { postRoutes } from "./posts";
import { threadRoutes } from "./threads";
import { topicRoutes } from "./topics";
import { userRoutes } from "./user";

export async function appRouter(fastify: FastifyInstance) {
// Ensure the request object has a userId property at runtime
// Middleware will assign the real ID when authenticated
fastify.decorateRequest("userId", undefined);
fastify.register(authRoutes, { prefix: "/auth" });
fastify.register(userRoutes, { prefix: "/user" });
fastify.register(topicRoutes);
fastify.register(threadRoutes);
fastify.register(postRoutes);
}
Loading