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
7 changes: 7 additions & 0 deletions src/client/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,10 @@
.ombre-text {
@apply inline-block bg-gradient-to-r from-saseBlue to-saseGreen bg-clip-text text-transparent;
}

/* Hide EmbedSocial branding in the Instagram embed container. */
.embedsocial-hashtag .feed-powered-by-es,
.embedsocial-hashtag .es-widget-branding,
.embedsocial-hashtag a[href*="embedsocial.com/instagram-widget"] {
display: none !important;
}
22 changes: 21 additions & 1 deletion src/client/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { OmbreDivider } from "@components/custom_ui/OmbreDivider";
import { useIsMobile } from "@hooks/useIsMobile";
import { PIEBoard } from "@information/People";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { useEffect, useState } from "react";
import { seo } from "../utils/seo";

export const Route = createFileRoute("/")({
Expand All @@ -29,6 +29,20 @@ export const Route = createFileRoute("/")({
const isMobile = useIsMobile();
const [expanded, setExpanded] = useState(false);

useEffect(() => {
const scriptId = "EmbedSocialHashtagScript";

if (document.getElementById(scriptId)) {
return;
}

const script = document.createElement("script");
script.id = scriptId;
script.src = "https://embedsocial.com/cdn/ht.js";
script.defer = true;
document.head.appendChild(script);
}, []);

return (
<div className="flex flex-col items-center">
{/* Title & Image Section */}
Expand Down Expand Up @@ -242,6 +256,12 @@ export const Route = createFileRoute("/")({
)}
</div>
</div>
{/* Instagram Section */}
<div className="flex w-full flex-col items-center bg-white px-1 py-10 dark:bg-gray-700 md:px-4">
<div className="w-full max-w-none">
<div className="embedsocial-hashtag w-full" data-ref="adf8a2d91dfce8da76c2225e0335b55d3ce7afa2" />
</div>
</div>
</div>
);
},
Expand Down
26 changes: 5 additions & 21 deletions src/server/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { db } from "@/server/db/db";
import { VerificationTemplate } from "@/server/email/verification-template";
import { requireSession } from "@/server/middleware/auth";
import { createErrorResponse, createSuccessResponse, emailRegex, passwordRegex } from "@/shared/utils";
import { oauthAccounts, pendingVerifications, professionalInfo, sessions, userRoleRelationship, users } from "@db/tables";
import { SERVER_ENV } from "@server/env";
import { generateCodeVerifier, generateState, Google } from "arctic";
import bcrypt from "bcryptjs";
import { and, eq } from "drizzle-orm";
import { Hono } from "hono";
import { getCookie } from "hono/cookie";
import { generateIdFromEntropySize } from "lucia";
import { Resend } from "resend";

Expand Down Expand Up @@ -245,14 +247,13 @@ authRoutes.post("/auth/login", async (c) => {

// Logout route
authRoutes.post("/auth/logout", async (c) => {
const sessionId = c.req.header("Cookie")?.match(/sessionId=([^;]*)/)?.[1];
const sessionId = getCookie(c, "sessionId");

if (!sessionId) {
return createErrorResponse(c, "NO_SESSION", "No active session found", 401);
}

try {
// delete the session id row from the table
await db.delete(sessions).where(eq(sessions.id, sessionId));

return createSuccessResponse(c, null, "Successfully logged out");
Expand All @@ -263,26 +264,9 @@ authRoutes.post("/auth/logout", async (c) => {
});

// used for validating sessions
authRoutes.get("/auth/session", async (c) => {
const sessionId = c.req.header("Cookie")?.match(/sessionId=([^;]*)/)?.[1];

if (!sessionId) {
return createErrorResponse(c, "NO_SESSION", "No active session", 401);
}

authRoutes.get("/auth/session", requireSession, async (c) => {
try {
const session = await db.select().from(sessions).where(eq(sessions.id, sessionId)).get();

if (!session) {
return createErrorResponse(c, "SESSION_NOT_FOUND", "Session not found", 401);
}

if (session.expiresAt < Date.now()) {
await db.delete(sessions).where(eq(sessions.id, sessionId));
// maybe renew session?
return createErrorResponse(c, "SESSION_EXPIRED", "Session expired", 401);
}

const session = c.get("session");
const user = await db.select({ id: users.id, username: users.username }).from(users).where(eq(users.id, session.userId)).get();

if (!user) {
Expand Down
39 changes: 13 additions & 26 deletions src/server/api/profile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isAdmin } from "@/server/api/roles";
import { db } from "@/server/db/db";
import { requireSession } from "@/server/middleware/auth";
import { createErrorResponse, createSuccessResponse } from "@/shared/utils";
import * as Schema from "@db/tables";
import { updateUserSchema } from "@schema/userSchema";
Expand Down Expand Up @@ -32,26 +32,19 @@ const profileSelection = {
graduationSemester: Schema.professionalInfo.graduationSemester,
};

profileRoutes.get("/profile", async (c) => {
profileRoutes.get("/profile", requireSession, async (c) => {
try {
const cookie = c.req.header("Cookie") || "";
const sessionIDMatch = cookie.match(/sessionId=([^;]*)/);
if (!sessionIDMatch) {
return createErrorResponse(c, "INVALID_SESSION", "Missing or invalid session ID", 400);
}
const sessionID = sessionIDMatch[1];
const session = c.get("session");

const result = await db
.select(profileSelection)
.from(Schema.users)
.innerJoin(Schema.sessions, eq(Schema.users.id, Schema.sessions.userId))
.innerJoin(Schema.professionalInfo, eq(Schema.users.id, Schema.professionalInfo.userId))
.where(eq(Schema.sessions.id, sessionID));
.where(eq(Schema.users.id, session.userId));

if (result.length === 1) {
return createSuccessResponse(c, result[0], "Profile retrieved successfully");
} else if (result.length === 0) {
console.log(sessionID);
return createErrorResponse(c, "NO_USER_FOUND", "No user found", 404);
} else {
return createErrorResponse(c, "MULTIPLE_USERS", "Multiple users", 500);
Expand All @@ -63,22 +56,16 @@ profileRoutes.get("/profile", async (c) => {
});

// update user information only (professional info updates go through /api/users/professional/:id)
profileRoutes.patch("/profile", async (c) => {
profileRoutes.patch("/profile", requireSession, async (c) => {
try {
const cookie = c.req.header("Cookie") || "";
const sessionIDMatch = cookie.match(/sessionId=([^;]*)/);
if (!sessionIDMatch) {
return createErrorResponse(c, "INVALID_SESSION", "Missing or invalid session ID", 400);
}
const sessionID = sessionIDMatch[1];
const adminPerms = await isAdmin(sessionID);
const result = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionID));

if (result.length === 0) {
return createErrorResponse(c, "INVALID_SESSION", "Invalid session", 400);
}

const userID = result[0].userId;
const session = c.get("session");
const userID = session.userId;
const userRoles = await db
.select({ role: Schema.userRoleRelationship.role })
.from(Schema.userRoleRelationship)
.where(eq(Schema.userRoleRelationship.userId, userID))
.all();
const adminPerms = userRoles.some((r) => r.role === "admin" || r.role === "board");
const body = await c.req.json();

// separate user fields from roles fields
Expand Down
37 changes: 3 additions & 34 deletions src/server/api/roles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { db } from "@/server/db/db";
import { requireRoles, requireSession } from "@/server/middleware/auth";
import { createErrorResponse, createSuccessResponse } from "@/shared/utils";
import * as Schema from "@db/tables";
import { and, eq } from "drizzle-orm";
Expand All @@ -25,19 +26,9 @@ roleRoutes.get("/roles/:userID", async (c) => {
}
});

roleRoutes.post("/roles/assign", async (c) => {
roleRoutes.post("/roles/assign", requireSession, requireRoles(["admin", "board"]), async (c) => {
try {
const { role, userId } = await c.req.json();
const cookie = c.req.header("Cookie") || "";
console.log(cookie);
const sessionIDMatch = cookie.match(/sessionId=([^;]*)/);
if (!sessionIDMatch) {
return createErrorResponse(c, "INVALID_SESSION", "Missing or invalid session ID", 400);
}
const sessionID = sessionIDMatch[1];
if (!(await isAdmin(sessionID))) {
return createErrorResponse(c, "ASSIGN_ACTION_UNAUTHORIZED", "Assigning unauthorized: Admin role required", 403);
}

const roleExist = await db.select().from(Schema.roles).where(eq(Schema.roles.name, role)).get();
if (!roleExist) {
Expand Down Expand Up @@ -65,18 +56,9 @@ roleRoutes.post("/roles/assign", async (c) => {
}
});

roleRoutes.post("/roles/delete", async (c) => {
roleRoutes.post("/roles/delete", requireSession, requireRoles(["admin", "board"]), async (c) => {
try {
const { role, userId } = await c.req.json();
const cookie = c.req.header("Cookie") || "";
const sessionIDMatch = cookie.match(/sessionId=([^;]*)/);
if (!sessionIDMatch) {
return createErrorResponse(c, "INVALID_SESSION", "Missing or invalid session ID", 400);
}
const sessionID = sessionIDMatch[1];
if (!(await isAdmin(sessionID))) {
return createErrorResponse(c, "USER_NOT_ADMIN", "Deleting unauthorized: Admin role required", 403);
}

const roleExist = await db.select().from(Schema.roles).where(eq(Schema.roles.name, role)).get();
if (!roleExist) {
Expand All @@ -103,17 +85,4 @@ roleRoutes.post("/roles/delete", async (c) => {
}
});

export async function isAdmin(sessionId: string) {
const session = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionId)).get();
if (!session) return false;

const userRoles = await db
.select({ role: Schema.userRoleRelationship.role })
.from(Schema.userRoleRelationship)
.where(eq(Schema.userRoleRelationship.userId, session.userId))
.all();

if (userRoles.some((r) => r.role === "admin" || r.role === "board")) return true;
}

export default roleRoutes;
71 changes: 71 additions & 0 deletions src/server/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { db } from "@/server/db/db";
import { createErrorResponse } from "@/shared/utils";
import * as Schema from "@db/tables";
import { eq } from "drizzle-orm";
import { getCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory";

export type Env = {
Variables: {
session: {
id: string;
userId: string;
expiresAt: number;
};
};
};

export const requireSession = createMiddleware<Env>(async (c, next) => {
const sessionId = getCookie(c, "sessionId");

if (!sessionId) {
return createErrorResponse(c, "NO_SESSION", "No active session", 401);
}

try {
const session = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionId)).get();

if (!session) {
return createErrorResponse(c, "SESSION_NOT_FOUND", "Session not found", 401);
}

if (session.expiresAt < Date.now()) {
await db.delete(Schema.sessions).where(eq(Schema.sessions.id, sessionId));
return createErrorResponse(c, "SESSION_EXPIRED", "Session expired", 401);
}

c.set("session", session);
await next();
} catch (error) {
console.error("Session middleware error:", error);
return createErrorResponse(c, "SESSION_CHECK_ERROR", "Error checking session", 500);
}
});

export const requireRoles = (allowedRoles: Array<string>) =>
createMiddleware<Env>(async (c, next) => {
const session = c.get("session");

if (!session) {
return createErrorResponse(c, "UNAUTHORIZED", "Unauthorized: No user session found", 401);
}

try {
const userRoles = await db
.select({ role: Schema.userRoleRelationship.role })
.from(Schema.userRoleRelationship)
.where(eq(Schema.userRoleRelationship.userId, session.userId))
.all();

const hasRole = userRoles.some((r) => allowedRoles.includes(r.role));

if (!hasRole) {
return createErrorResponse(c, "FORBIDDEN", `Forbidden: Requires one of roles [${allowedRoles.join(", ")}]`, 403);
}

await next();
} catch (error) {
console.error("Role check error:", error);
return createErrorResponse(c, "ROLE_CHECK_ERROR", "Error checking roles", 500);
}
});
Loading