diff --git a/drizzle/0001_premium_betty_brant.sql b/drizzle/0001_premium_betty_brant.sql new file mode 100644 index 00000000..789355f9 --- /dev/null +++ b/drizzle/0001_premium_betty_brant.sql @@ -0,0 +1,31 @@ +ALTER TABLE `user` RENAME COLUMN "password_hash" TO "password";--> statement-breakpoint +CREATE TABLE `email_subscriber` ( + `id` text PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `name` text, + `subscribed_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `email_subscriber_email_unique` ON `email_subscriber` (`email`);--> statement-breakpoint +ALTER TABLE `user` ADD `email` text NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint +DROP INDEX IF EXISTS `personal_info_email_unique`;--> statement-breakpoint +DROP INDEX IF EXISTS "blog_tag_name_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "blog_title_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "email_subscriber_email_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "event_name_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "personal_info_phone_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "user_username_unique";--> statement-breakpoint +DROP INDEX IF EXISTS "user_email_unique";--> statement-breakpoint +ALTER TABLE `personal_info` ALTER COLUMN "phone" TO "phone" text;--> statement-breakpoint +CREATE UNIQUE INDEX `blog_tag_name_unique` ON `blog_tag` (`name`);--> statement-breakpoint +CREATE UNIQUE INDEX `blog_title_unique` ON `blog` (`title`);--> statement-breakpoint +CREATE UNIQUE INDEX `event_name_unique` ON `event` (`name`);--> statement-breakpoint +CREATE UNIQUE INDEX `personal_info_phone_unique` ON `personal_info` (`phone`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);--> statement-breakpoint +ALTER TABLE `personal_info` DROP COLUMN `email`;--> statement-breakpoint +ALTER TABLE `event` ADD `slides_url` text;--> statement-breakpoint +ALTER TABLE `blog` DROP COLUMN `last_update_date`;--> statement-breakpoint +ALTER TABLE `blog` DROP COLUMN `tags`;--> statement-breakpoint +ALTER TABLE `sase_info` DROP COLUMN `mentors`;--> statement-breakpoint +ALTER TABLE `sase_info` DROP COLUMN `mentees`; \ No newline at end of file diff --git a/src/client/AuthContext.tsx b/src/client/AuthContext.tsx deleted file mode 100644 index 5d991567..00000000 --- a/src/client/AuthContext.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// AuthContext.js -import React, { createContext, useContext, useEffect, useState } from "react"; -import type { ReactNode } from "react"; - -export interface AuthContextType { - isAuthenticated: boolean; - login: () => void; - logout: () => void; - isLoading: boolean; -} - -// Create AuthContext as undefined values for now: -// const AuthContext = createContext(undefined); - -// Create the AuthContext with default values -const AuthContext = createContext({ - isAuthenticated: false, - login: () => {}, - logout: () => {}, - isLoading: true, -}); - -interface AuthProviderProps { - children: ReactNode; -} -// AuthProvider component that wraps your app and provides auth state -export const AuthProvider: React.FC = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const checkSession = async () => { - try { - const response = await fetch("/api/auth/session", { - credentials: "include", - }); - - if (response.ok) { - setIsAuthenticated(true); - } else { - setIsAuthenticated(false); - } - } catch (error) { - console.error(error); - setIsAuthenticated(false); - } finally { - setIsLoading(false); // userbutton wont render unless loading is false - } - }; - - useEffect(() => { - checkSession(); // validate user session whenever authcontext renders - }, []); - - const login = () => { - setIsAuthenticated(true); - }; - - const logout = () => { - setIsAuthenticated(false); - }; - - return {children}; -}; - -// Custom hook to use the AuthContext -export const useAuth = (): AuthContextType => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; diff --git a/src/client/api/blogs.ts b/src/client/api/blogs.ts index 97113a9e..f1c7dea1 100644 --- a/src/client/api/blogs.ts +++ b/src/client/api/blogs.ts @@ -1,11 +1,13 @@ // libapi/blogs.ts -import type { Blog, BlogSearchResponse, CreateBlog, UpdateBlog } from "@shared/schema/blogSchema"; -import { blogsApiResponseSchema, blogSearchResponseSchema, blogTitleSchema, singleBlogApiResponseSchema } from "@shared/schema/blogSchema"; +import type { Blog, CreateBlog, UpdateBlog } from "@shared/schema/blogSchema"; +import { blogSchema, blogSearchResponseSchema, blogTitleSchema, updateBlogSchema } from "@shared/schema/blogSchema"; import { apiFetch } from "@shared/utils"; +import { z } from "zod"; // Fetch ALL Blogs export const fetchBlogs = async (): Promise> => { - return apiFetch>("/api/blogs/all", { method: "GET" }, blogsApiResponseSchema); + const response = await apiFetch("/api/blogs/all", { method: "GET" }, z.array(blogSchema)); + return response.data; }; // Fetch Blog by ID @@ -13,16 +15,17 @@ export const fetchBlogById = async (blogId: string): Promise => { if (!blogId) { throw new Error("Blog ID is required"); } - return apiFetch(`/api/blogs/${blogId}`, { method: "GET" }, singleBlogApiResponseSchema); + const response = await apiFetch(`/api/blogs/${blogId}`, { method: "GET" }, blogSchema); + return response.data; }; // Search Blogs by Title -export const searchBlogsByTitle = async (title: string): Promise => { +export const searchBlogsByTitle = async (title: string): Promise => { if (!title) { throw new Error("Title is required"); } blogTitleSchema.parse({ title }); - return apiFetch( + const response = await apiFetch( "/api/blogs/search", { method: "GET", @@ -31,28 +34,31 @@ export const searchBlogsByTitle = async (title: string): Promise => { - return apiFetch( + const response = await apiFetch( "/api/blogs/add", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newBlog), }, - singleBlogApiResponseSchema, + blogSchema, ); + return response.data; }; export const updateBlog = async (blog: UpdateBlog): Promise => { - return apiFetch( + const response = await apiFetch( "/api/blogs/update", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(blog), }, - singleBlogApiResponseSchema, + updateBlogSchema, ); + return response.data; }; diff --git a/src/client/api/users.ts b/src/client/api/users.ts index fead2235..5cae14c9 100644 --- a/src/client/api/users.ts +++ b/src/client/api/users.ts @@ -1,84 +1,62 @@ -import type { ApiResponse } from "@shared/schema/responseSchema"; import type { DeleteUser, InsertUser, SelectUser, UpdateUser } from "@shared/schema/userSchema"; import { deleteUserSchema, insertUserSchema, selectUserSchema } from "@shared/schema/userSchema"; +import { apiFetch } from "@shared/utils"; // Fetch ALL Users export const fetchUsers = async (): Promise> => { - const response = await fetch("/api/users"); - const json = (await response.json()) as ApiResponse>; - - if (!response.ok) { - throw new Error(json.error?.message || "Unknown error occured"); - } - - return selectUserSchema.array().parse(json); + const response = await apiFetch("/api/users", { method: "GET" }, selectUserSchema.array()); + return response.data; }; export const fetchUser = async (id: string): Promise => { - const response = await fetch(`/api/users/${id}`, { - method: "GET", - credentials: "include", // Include session credentials (cookies) - headers: { - "Content-Type": "application/json", + const response = await apiFetch( + `/api/users/${id}`, + { + method: "GET", + credentials: "include", + headers: { "Content-Type": "application/json" }, }, - }); - - const json = (await response.json()) as ApiResponse; - - if (!response.ok) { - throw new Error(json.error?.message || "Unknown error occurred"); - } - - return selectUserSchema.parse(json.data); + selectUserSchema, + ); + return response.data; }; export const createUser = async (newUser: InsertUser): Promise => { insertUserSchema.parse(newUser); - const response = await fetch("/api/users", { - method: "POST", - body: JSON.stringify(newUser), - headers: { - "Content-Type": "application/json", + const response = await apiFetch( + "/api/users", + { + method: "POST", + body: JSON.stringify(newUser), + headers: { "Content-Type": "application/json" }, }, - }); - const json = (await response.json()) as ApiResponse; - - if (!response.ok) { - throw new Error(json.error?.message || "Unknown error occured"); - } - - return selectUserSchema.parse(json); + selectUserSchema, + ); + return response.data; }; export const updateUser = async (updatedUser: UpdateUser): Promise => { insertUserSchema.parse(updatedUser); - const response = await fetch(`/api/users/${updatedUser.id}`, { - method: "PATCH", - body: JSON.stringify(updatedUser), - headers: { - "Content-Type": "application/json", + const response = await apiFetch( + `/api/users/${updatedUser.id}`, + { + method: "PATCH", + body: JSON.stringify(updatedUser), + headers: { "Content-Type": "application/json" }, }, - }); - const json = (await response.json()) as ApiResponse; - - if (!response.ok) { - throw new Error(json.error?.message || "Unknown error occured"); - } - - return selectUserSchema.parse(json); + selectUserSchema, + ); + return response.data; }; export const deleteUser = async (userId: number): Promise => { - const response = await fetch(`/api/users/${userId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", + const response = await apiFetch( + `/api/users/${userId}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, }, - }); - const json = (await response.json()) as ApiResponse; - - if (!response.ok) { - throw new Error(json.error?.message || "Unknown error occured"); - } - return deleteUserSchema.parse(json); + deleteUserSchema, + ); + return response.data; }; diff --git a/src/client/components/navigation/Header.tsx b/src/client/components/navigation/Header.tsx index 5307f119..4ba19c77 100644 --- a/src/client/components/navigation/Header.tsx +++ b/src/client/components/navigation/Header.tsx @@ -1,4 +1,4 @@ -import { useAuth } from "@/client/AuthContext"; +import { useAuth } from "@/client/hooks/AuthContext"; import { cn } from "@/shared/utils"; import { DesktopMenu } from "@navigation/DesktopMenu"; import { Logo } from "@navigation/Logo"; diff --git a/src/client/hooks/AuthContext.tsx b/src/client/hooks/AuthContext.tsx new file mode 100644 index 00000000..0d948d5c --- /dev/null +++ b/src/client/hooks/AuthContext.tsx @@ -0,0 +1,93 @@ +import { apiFetch } from "@/shared/utils"; +import React, { createContext, useContext, useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { z } from "zod"; + +export interface AuthContextType { + isAuthenticated: boolean; + login: () => void; + logout: () => Promise; + isLoading: boolean; + id: string; + errorMessage: string; +} + +const AuthContext = createContext({ + isAuthenticated: false, + login: () => {}, + logout: async () => {}, + isLoading: true, + id: "", + errorMessage: "", +}); + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [id, setId] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const checkSession = async () => { + try { + const user = await apiFetch( + "/api/auth/session", + { credentials: "include" }, + z.object({ + id: z.string(), + username: z.string(), + }), + ); + console.log("ID set to ", user.data.id); + setId(user.data.id); + setIsAuthenticated(true); + } catch (error) { + console.log(error); + const errMsg = error instanceof Error ? error.message : "Unknown error occurred"; + setErrorMessage(errMsg); + setIsAuthenticated(false); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + checkSession(); + }, []); + + const login = () => { + setIsAuthenticated(true); + }; + + const logout = async () => { + try { + const response = await fetch("/api/auth/logout", { + method: "POST", + credentials: "include", + }); + if (response.ok) { + setIsAuthenticated(false); + setId(""); + } else { + throw new Error("Logout failed"); + } + } catch (error) { + console.error("Logout error:", error); + const errMsg = error instanceof Error ? error.message : "Unknown error occurred"; + setErrorMessage(errMsg); + } + }; + + return {children}; +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/src/client/hooks/useBlogs.ts b/src/client/hooks/useBlogs.ts index e564d0b3..736b1f4b 100644 --- a/src/client/hooks/useBlogs.ts +++ b/src/client/hooks/useBlogs.ts @@ -1,4 +1,4 @@ -import type { Blog, BlogSearchResponse, CreateBlog, UpdateBlog } from "@shared/schema/blogSchema"; +import type { Blog, CreateBlog, UpdateBlog } from "@shared/schema/blogSchema"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createBlog, fetchBlogById, fetchBlogs, searchBlogsByTitle, updateBlog } from "../api/blogs"; @@ -37,7 +37,7 @@ export const useBlogs = () => { // Search blogs by title const searchBlogsQuery = (title: string) => - useQuery({ + useQuery({ queryKey: ["blogs", "search", title], queryFn: () => searchBlogsByTitle(title), enabled: !!title, diff --git a/src/client/hooks/useProfile.ts b/src/client/hooks/useProfile.ts new file mode 100644 index 00000000..f3b6579d --- /dev/null +++ b/src/client/hooks/useProfile.ts @@ -0,0 +1,14 @@ +import { profileSchema } from "@/shared/schema/profileSchema"; +import type { Profile } from "@/shared/schema/profileSchema"; +import { apiFetch } from "@/shared/utils"; +import { useQuery } from "@tanstack/react-query"; + +export const useProfile = () => { + return useQuery({ + queryKey: ["profile"], + queryFn: async () => { + const response = await apiFetch("/api/profile", { credentials: "include" }, profileSchema); + return response.data; + }, + }); +}; diff --git a/src/client/router.tsx b/src/client/router.tsx index 877cdc95..e2712ac0 100644 --- a/src/client/router.tsx +++ b/src/client/router.tsx @@ -1,9 +1,9 @@ import { dehydrate, hydrate, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { SuperJSON } from "superjson"; -import { AuthProvider } from "./AuthContext"; import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"; import { NotFound } from "./components/NotFound"; +import { AuthProvider } from "./hooks/AuthContext"; import { routeTree } from "./routeTree.gen"; export function createRouter() { diff --git a/src/client/routes/__root.tsx b/src/client/routes/__root.tsx index 8eceabce..7179826f 100644 --- a/src/client/routes/__root.tsx +++ b/src/client/routes/__root.tsx @@ -7,9 +7,9 @@ import { Body, Head, Html, Meta, Scripts } from "@tanstack/start"; import * as React from "react"; import { Toaster } from "react-hot-toast"; import { seo } from "src/client/utils/seo"; -import { AuthProvider } from "../AuthContext"; import Footer from "../components/navigation/Footer"; import Header from "../components/navigation/Header"; +import { AuthProvider } from "../hooks/AuthContext"; import css from "../index.css?url"; export const Route = createRootRoute({ diff --git a/src/client/routes/authed.tsx b/src/client/routes/authed.tsx index 5d0bf3a3..60638694 100644 --- a/src/client/routes/authed.tsx +++ b/src/client/routes/authed.tsx @@ -1,4 +1,4 @@ -import { useAuth } from "@client/AuthContext"; +import { useAuth } from "@/client/hooks/AuthContext"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/authed")({ diff --git a/src/client/routes/blogs.tsx b/src/client/routes/blogs.tsx index 7cf61549..5a046fd1 100644 --- a/src/client/routes/blogs.tsx +++ b/src/client/routes/blogs.tsx @@ -1,4 +1,4 @@ -import { useAuth } from "@client/AuthContext"; +import { useAuth } from "@/client/hooks/AuthContext"; import { Button } from "@components/ui/button"; import { Input } from "@components/ui/input"; import { Textarea } from "@components/ui/textarea"; @@ -15,7 +15,7 @@ export const Route = createFileRoute("/blogs")({ const [newBlogTitle, setNewBlogTitle] = useState(""); const [newBlogContent, setNewBlogContent] = useState(""); const [newBlogTags, setNewBlogTags] = useState(""); - const { isAuthenticated } = useAuth(); + const { id, isAuthenticated } = useAuth(); const [error, setError] = useState(null); const { blogs, createBlog, updateBlog } = useBlogs(); @@ -26,7 +26,7 @@ export const Route = createFileRoute("/blogs")({ title: newBlogTitle, content: newBlogContent, tags: newBlogTags.split(",").map((tag) => tag.trim()), - author_id: "SASE Historian", // TODO: This should be optional + author_id: id, }, { onError: (error: Error) => { diff --git a/src/client/routes/login.tsx b/src/client/routes/login.tsx index b646eb73..e7dcc1b8 100644 --- a/src/client/routes/login.tsx +++ b/src/client/routes/login.tsx @@ -5,8 +5,8 @@ import { useMutation } from "@tanstack/react-query"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { imageUrls } from "../assets/imageUrls"; -import { useAuth } from "../AuthContext"; import ShadowCard from "../components/AuthShadowCard"; +import { useAuth } from "../hooks/AuthContext"; import { seo } from "../utils/seo"; export const Route = createFileRoute("/login")({ diff --git a/src/client/routes/profile.tsx b/src/client/routes/profile.tsx index 9be737e6..da4d55e9 100644 --- a/src/client/routes/profile.tsx +++ b/src/client/routes/profile.tsx @@ -1,15 +1,14 @@ import ProfileNav from "@components/profile/ProfileNav"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; -import { useAuth } from "../AuthContext"; +import { useState } from "react"; import UserInfoBox from "../components/profile/UserInfoBox"; import { Button } from "../components/ui/button"; +import { useAuth } from "../hooks/AuthContext"; +import { useProfile } from "../hooks/useProfile"; export const Route = createFileRoute("/profile")({ component: () => { - const [profile, setProfile] = useState<{ username: string } | null>(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const { data: profile, error, isLoading } = useProfile(); const [activeSection, setActiveSection] = useState({ account: true, userinfo: false, @@ -19,32 +18,15 @@ export const Route = createFileRoute("/profile")({ resources: false, settings: false, }); - const { logout } = useAuth(); + const { errorMessage, logout } = useAuth(); const navigate = useNavigate(); - useEffect(() => { - if (!isLoading) return; - const fetchProfile = async () => { - try { - const response = await fetch("/api/profile", { credentials: "include" }); - if (!response.ok) { - navigate({ to: "/" }); - } - const result = (await response.json()) as { data: { username: string } }; - console.log("Profile API Response:", result); - setProfile(result.data); - } catch (err) { - setError(err instanceof Error ? err.message : "Unknown error"); - } finally { - setIsLoading(false); - } - }; - - fetchProfile(); - }, []); + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; const handleLogout = async () => { try { + // Use a hook-based logout if possible; otherwise, fall back to fetch const response = await fetch("/api/auth/logout", { method: "POST", credentials: "include" }); if (response.ok) { logout(); @@ -67,15 +49,12 @@ export const Route = createFileRoute("/profile")({ resources: false, settings: false, }); - setActiveSection((prev) => ({ ...prev, [section]: true })); }; - if (isLoading) return
Loading...
; - if (error) return
Error: {error}
; - return (
+ {errorMessage &&
{errorMessage}
}
diff --git a/src/client/routes/signup.tsx b/src/client/routes/signup.tsx index 14880575..6c2244f6 100644 --- a/src/client/routes/signup.tsx +++ b/src/client/routes/signup.tsx @@ -4,10 +4,10 @@ import { Page } from "@components/Page"; import { useMutation } from "@tanstack/react-query"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; -import { useAuth } from "../AuthContext"; import AuthForm from "../components/AuthForm"; import ShadowCard from "../components/SignUpShadowCard"; import { SuccessModal } from "../components/SuccessModal"; +import { useAuth } from "../hooks/AuthContext"; import { seo } from "../utils/seo"; export const Route = createFileRoute("/signup")({ diff --git a/src/server/api/auth.ts b/src/server/api/auth.ts index f31a9736..7459d678 100644 --- a/src/server/api/auth.ts +++ b/src/server/api/auth.ts @@ -1,4 +1,5 @@ import { db } from "@/server/db/db"; +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import * as Schema from "@db/tables"; import bcrypt from "bcryptjs"; import { eq } from "drizzle-orm"; @@ -9,6 +10,7 @@ const { compare, genSalt, hash } = bcrypt; const authRoutes = new Hono(); +// Signup route authRoutes.post("/auth/signup", async (c) => { const formData = await c.req.json(); const formUsername = formData["username"]; @@ -18,22 +20,16 @@ authRoutes.post("/auth/signup", async (c) => { //validate username if (!formUsername || typeof formUsername !== "string") { - return new Response("Invalid username!", { - status: 400, - }); + return createErrorResponse(c, "INVALID_USERNAME", "Invalid username!", 400); } //validate password if (!formPassword || typeof formPassword !== "string") { - return new Response("Invalid password!", { - status: 400, - }); + return createErrorResponse(c, "INVALID_PASSWORD", "Invalid password!", 400); } //validate email // add 3rd validation for email using regular expressions if (!formEmail || typeof formEmail !== "string" || !emailRegex.test(formEmail)) { - return new Response("Invalid email!", { - status: 400, - }); + return createErrorResponse(c, "INVALID_EMAIL", "Invalid email!", 400); } const passSalt = await genSalt(10); @@ -67,80 +63,62 @@ authRoutes.post("/auth/signup", async (c) => { graduation_semester: "", }); - return new Response("User successfully created!", { - status: 201, - headers: { - Location: "/", - }, - }); + return createSuccessResponse(c, { userId }, "User successfully created"); } catch (error) { console.log(error); - // db error, email taken, etc - return new Response("Error creating user", { - status: 400, - }); + return createErrorResponse(c, "CREATE_USER_ERROR", "Error creating user", 400); } }); +// Login route authRoutes.post("/auth/login", async (c) => { const formData = await c.req.json(); const formUsername = formData["username"]; const formPassword = formData["password"]; if (!formUsername || typeof formUsername !== "string") { - return new Response("Invalid username!", { - status: 401, - }); + return createErrorResponse(c, "INVALID_USERNAME", "Invalid username!", 401); } if (!formPassword || typeof formPassword !== "string") { - return new Response("Invalid password!", { - status: 401, - }); + return createErrorResponse(c, "INVALID_PASSWORD", "Invalid password!", 401); } const user = await db.select().from(Schema.users).where(eq(Schema.users.username, formUsername)); - if (user.length == 0) return new Response("Invalid username or password!", { status: 401 }); + if (user.length === 0) { + return createErrorResponse(c, "INVALID_CREDENTIALS", "Invalid username or password!", 401); + } const validPassword = await compare(formPassword, user[0].password); if (!validPassword) { - return new Response("Invalid password!", { - status: 401, - }); + return createErrorResponse(c, "INVALID_PASSWORD", "Invalid password!", 401); } else { const session_id = generateIdFromEntropySize(16); createSession(session_id, user[0].id); - return new Response("Successfully logged in", { - status: 200, - headers: { - "Set-Cookie": `sessionId=${session_id}; Path=/; HttpOnly; secure; Max-Age=3600; SameSite=Strict`, - }, - }); + // Set cookie here + c.header("Set-Cookie", `sessionId=${session_id}; Path=/; HttpOnly; Secure; Max-Age=3600; SameSite=Strict`); + return createSuccessResponse(c, { sessionId: session_id }, "Successfully logged in"); } }); +// Logout route authRoutes.post("/auth/logout", async (c) => { const sessionId = c.req.header("Cookie")?.match(/sessionId=([^;]*)/)?.[1]; if (!sessionId) { - return new Response("No active session found", { status: 401 }); + return createErrorResponse(c, "NO_SESSION", "No active session found", 401); } try { // delete the session id row from the table await db.delete(Schema.sessions).where(eq(Schema.sessions.id, sessionId)); - return new Response("Successfully logged out", { - status: 200, - headers: { - "Set-Cookie": "sessionId=; Path=/; HttpOnly; Secure; Max-Age=0; SameSite=Strict", - }, - }); + return createSuccessResponse(c, { success: true }, "Successfully logged out"); } catch (error) { console.log(error); - return new Response("Error logging out", { status: 500 }); + return createErrorResponse(c, "LOGOUT_ERROR", "Error logging out", 500); } }); @@ -149,35 +127,35 @@ authRoutes.get("/auth/session", async (c) => { const sessionId = c.req.header("Cookie")?.match(/sessionId=([^;]*)/)?.[1]; if (!sessionId) { - return new Response("No active session", { status: 401 }); + return createErrorResponse(c, "NO_SESSION", "No active session", 401); } try { const session = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionId)); if (session.length === 0) { - return new Response("Session not found", { status: 401 }); + return createErrorResponse(c, "SESSION_NOT_FOUND", "Session not found", 401); } if (session[0].expires_at < Date.now()) { await db.delete(Schema.sessions).where(eq(Schema.sessions.id, sessionId)); // maybe renew session? - return new Response("Session expired", { status: 401 }); + return createErrorResponse(c, "SESSION_EXPIRED", "Session expired", 401); } - const user = await db.select({ username: Schema.users.username }).from(Schema.users).where(eq(Schema.users.id, session[0].user_id)); + const user = await db + .select({ id: Schema.users.id, username: Schema.users.username }) + .from(Schema.users) + .where(eq(Schema.users.id, session[0].user_id)); if (user.length === 0) { - return new Response("User not found", { status: 401 }); + return createErrorResponse(c, "USER_NOT_FOUND", "User not found", 401); } - return new Response(JSON.stringify({ username: user[0].username }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + return createSuccessResponse(c, { id: user[0].id, username: user[0].username }, "Session valid"); } catch (error) { console.log(error); - return new Response("Error checking session", { status: 500 }); + return createErrorResponse(c, "SESSION_CHECK_ERROR", "Error checking session", 500); } }); @@ -192,4 +170,5 @@ async function createSession(sessionID: string, userID: string) { console.log(error); } } + export default authRoutes; diff --git a/src/server/api/blogs.ts b/src/server/api/blogs.ts index f42609e9..7d473e70 100644 --- a/src/server/api/blogs.ts +++ b/src/server/api/blogs.ts @@ -1,6 +1,6 @@ import { db } from "@/server/db/db"; -import * as Schema from "@/server/db/tables"; -import { createErrorResponse } from "@shared/utils"; +import * as Schema from "@db/tables"; +import { createErrorResponse, createSuccessResponse } from "@shared/utils"; import { eq, like } from "drizzle-orm"; import { Hono } from "hono"; @@ -10,7 +10,7 @@ const blogRoutes = new Hono(); blogRoutes.get("/blogs/all", async (c) => { try { const result = await db.select().from(Schema.blogs); - return c.json({ data: result }); + return createSuccessResponse(c, result, "Blogs retrieved successfully"); } catch (error) { console.log(error); return createErrorResponse(c, "MISSING_BLOG", "Cannot fetch blogs", 400); @@ -28,8 +28,9 @@ blogRoutes.get("/blogs/:blogID", async (c) => { if (result.length === 0) { return createErrorResponse(c, "BLOG_NOT_FOUND", "No Blogs Found", 404); } - return c.json(result[0]); - } catch { + return createSuccessResponse(c, result[0], "Blog retrieved successfully"); + } catch (error) { + console.log(error); return createErrorResponse(c, "FETCH_BLOG_ERROR", "Failed to fetch blog", 500); } }); @@ -44,27 +45,30 @@ blogRoutes.get("/blogs/search/:title", async (c) => { .from(Schema.blogs) .where(like(Schema.blogs.title, `%${search_title}%`)); // Approximate search const blog_ids = result.map((row) => row.res_blog_ids); - return c.json(blog_ids); - } catch { + return createSuccessResponse(c, blog_ids, "Blog IDs retrieved successfully"); + } catch (error) { + console.log(error); return createErrorResponse(c, "SEARCH_BLOGS_ERROR", "Failed to search blogs", 500); } }); +// Add blog blogRoutes.post("/blogs/add", async (c) => { try { const body = await c.req.json(); - const newBlog = await db + const newBlogArray = await db .insert(Schema.blogs) - .values({ - ...body, - }) + .values({ ...body }) .returning(); - return c.json(`Inserted blog with ID: ${newBlog[0].id}`); + const newBlog = newBlogArray[0]; + return createSuccessResponse(c, newBlog, "Blog added successfully"); } catch (error) { - if (error) return createErrorResponse(c, "ADD_BLOG_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "ADD_BLOG_ERROR", "Failed to add blog", 500); } }); +// Update blog blogRoutes.post("/blogs/update", async (c) => { try { const body = await c.req.json(); @@ -74,15 +78,13 @@ blogRoutes.post("/blogs/update", async (c) => { } const updatedBlog = await db .update(Schema.blogs) - .set({ - ...update, - time_updated: new Date(), - }) + .set({ ...update, time_updated: new Date() }) .where(eq(Schema.blogs.id, id)) .returning(); - return c.json(`Updated blog with ID: ${updatedBlog[0].id}`); + return createSuccessResponse(c, `Updated blog with ID: ${updatedBlog[0].id}`, "Blog updated successfully"); } catch (error) { - if (error) return createErrorResponse(c, "UPDATE_BLOG_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "UPDATE_BLOG_ERROR", "Failed to update blog", 500); } }); diff --git a/src/server/api/contact.ts b/src/server/api/contact.ts index c64d5f85..97f23d67 100644 --- a/src/server/api/contact.ts +++ b/src/server/api/contact.ts @@ -1,76 +1,78 @@ +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import { Hono } from "hono"; -//sleep function that behaves synchronously in an async function +// Sleep function that behaves synchronously in an async function const sleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); const contactRoutes = new Hono(); + contactRoutes.post("/contact/submit", async (c) => { - const formData = await c.req.json(); - const formFirstName = formData["firstName"]; - const formLastName = formData["lastName"]; - const formEmail = formData["email"]; - const formMessage = formData["message"]; - const saseEmail = "ufsase.webmaster.shared@gmail.com"; - //validation - if (typeof formFirstName !== "string" || formFirstName.length > 256) { - return new Response("Invalid Name", { - status: 400, - }); - } + try { + const formData = await c.req.json(); + const formFirstName = formData["firstName"]; + const formLastName = formData["lastName"]; + const formEmail = formData["email"]; + const formMessage = formData["message"]; + const saseEmail = "ufsase.webmaster.shared@gmail.com"; - if (typeof formLastName !== "string" || formLastName.length > 256) { - return new Response("Invalid Name", { - status: 400, - }); - } - const regex = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; - if (typeof formEmail !== "string" || formEmail.length > 256 || !regex.test(formEmail)) { - return new Response("Invalid Email", { - status: 400, - }); - } + // Validation + if (typeof formFirstName !== "string" || formFirstName.length > 256) { + return createErrorResponse(c, "INVALID_INPUT", "Invalid Name", 400); + } + if (typeof formLastName !== "string" || formLastName.length > 256) { + return createErrorResponse(c, "INVALID_INPUT", "Invalid Name", 400); + } + const regex = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; + if (typeof formEmail !== "string" || formEmail.length > 256 || !regex.test(formEmail)) { + return createErrorResponse(c, "INVALID_INPUT", "Invalid Email", 400); + } + if (typeof formMessage !== "string" || formMessage.length > 3000) { + return createErrorResponse(c, "INVALID_INPUT", "Message is too long", 400); + } - if (typeof formMessage !== "string" || formMessage.length > 3000) { - return new Response("Message is too long", { - status: 400, - }); - } - //generate email HTML - const formHTML = `

${formMessage}

+ // Generate email HTML + const formHTML = `

${formMessage}

Sender name: ${formFirstName} ${formLastName}

Sender email: ${formEmail}

`; - let resp = await fetch("https://api.resend.com/emails", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.RESEND_API_KEY}`, - }, - body: JSON.stringify({ - from: "UF SASE ", - to: [saseEmail], - subject: "Contact Form Submission", - html: formHTML, - }), - }); - if (resp.status == 429) { - await sleep(1000); - resp = await fetch("https://api.resend.com/emails", { + let resp = await fetch("https://api.resend.com/emails", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.RESEND_API_KEY}`, }, body: JSON.stringify({ - from: "UF SASE ", + from: "UF SASE ", to: [saseEmail], subject: "Contact Form Submission", html: formHTML, }), }); - } - if (resp.ok) return c.json({ message: "Email sent succesfully", c }, 200); - else { - return c.json({ message: "Email not sent successfully", c }, 300); + + if (resp.status === 429) { + await sleep(1000); + resp = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.RESEND_API_KEY}`, + }, + body: JSON.stringify({ + from: "UF SASE ", + to: [saseEmail], + subject: "Contact Form Submission", + html: formHTML, + }), + }); + } + + if (resp.ok) { + return createSuccessResponse(c, { message: "Email sent successfully" }, "Email sent successfully"); + } else { + return createErrorResponse(c, "EMAIL_FAILURE", "Email not sent successfully", 300); + } + } catch (error) { + console.error("Contact form error:", error); + return createErrorResponse(c, "CONTACT_SUBMIT_ERROR", "Failed to submit contact form", 500); } }); diff --git a/src/server/api/email.ts b/src/server/api/email.ts index e00aa02f..f5183da4 100644 --- a/src/server/api/email.ts +++ b/src/server/api/email.ts @@ -1,6 +1,6 @@ import { db } from "@/server/db/db"; import { emailSubscribers } from "@db/tables"; -import { createErrorResponse } from "@shared/utils"; +import { createErrorResponse, createSuccessResponse } from "@shared/utils"; import { Hono } from "hono"; import { Resend } from "resend"; @@ -15,8 +15,7 @@ emailRoutes.get("/email/test", async (c) => { subject: "Test Email from Resend", html: "

This is a test email

If you received this, Resend works!

", }); - - return c.json({ message: "Test email sent successfully", result }, 200); + return createSuccessResponse(c, result, "Test email sent successfully"); } catch (error) { console.error("Test email error:", error); return createErrorResponse(c, "EMAIL_TEST_FAILURE", "Failed to send test email", 500); @@ -25,16 +24,13 @@ emailRoutes.get("/email/test", async (c) => { emailRoutes.post("/email/add", async (c) => { try { - // Parse and validate the request body const { email, name } = await c.req.json(); if (!email) return createErrorResponse(c, "INVALID_INPUT", "Email is required", 400); - await db.insert(emailSubscribers).values({ email, - name: name || null, // Optional name + name: name || null, }); - - return c.json({ message: "Email added successfully" }, 200); + return createSuccessResponse(c, { success: true }, "Email added successfully"); } catch (error) { console.error("Error adding email:", error); return createErrorResponse(c, "EMAIL_ADD_FAILURE", "Failed to add email", 500); @@ -43,13 +39,10 @@ emailRoutes.post("/email/add", async (c) => { emailRoutes.post("/email/send", async (c) => { try { - // Parse and validate the request body const { html, recipientList, subject } = await c.req.json(); if (!recipientList || !subject || !html) { return createErrorResponse(c, "INVALID_INPUT", "Missing required fields", 400); } - - // Loop through recipients and send emails const results = []; for (const recipient of recipientList) { const result = await resend.emails.send({ @@ -60,8 +53,7 @@ emailRoutes.post("/email/send", async (c) => { }); results.push(result); } - - return c.json({ message: "Emails sent successfully", results }, 200); + return createSuccessResponse(c, results, "Emails sent successfully"); } catch (error) { console.error("Email sending error:", error); return createErrorResponse(c, "EMAIL_SEND_FAILURE", "Failed to send emails", 500); @@ -74,7 +66,6 @@ emailRoutes.post("/email/password-reset", async (c) => { if (!email) { return createErrorResponse(c, "INVALID_INPUT", "Email is required", 400); } - const resetPage = "http://ufsase.com/reset-password"; const htmlTemplate = `
@@ -90,16 +81,15 @@ emailRoutes.post("/email/password-reset", async (c) => {

Best regards,
The UF SASE WebDev Team

`; - const result = await resend.emails.send({ from: "UF SASE ", to: [email], subject: "Password Reset Request for UF SASE", html: htmlTemplate, }); - - return c.json({ message: "Password reset email sent successfully", result }, 200); - } catch { + return createSuccessResponse(c, result, "Password reset email sent successfully"); + } catch (error) { + console.error("Password reset email error:", error); return createErrorResponse(c, "PASSWORD_RESET_FAILURE", "Failed to send password reset email", 500); } }); diff --git a/src/server/api/events.ts b/src/server/api/events.ts index 21070479..505cd4a7 100644 --- a/src/server/api/events.ts +++ b/src/server/api/events.ts @@ -1,6 +1,6 @@ import { db } from "@/server/db/db"; -import * as Schema from "@/server/db/tables"; -import { createErrorResponse } from "@shared/utils"; +import * as Schema from "@db/tables"; +import { createErrorResponse, createSuccessResponse } from "@shared/utils"; import { and, eq, gte, like, lte } from "drizzle-orm"; import { Hono } from "hono"; @@ -14,7 +14,7 @@ eventRoutes.get("/events", async (c) => { if (!startDateStr || !endDateStr) { const result = await db.select().from(Schema.events); - return c.json(result); + return createSuccessResponse(c, result, "Events retrieved successfully"); } const startDate = new Date(startDateStr); @@ -28,9 +28,10 @@ eventRoutes.get("/events", async (c) => { .select() .from(Schema.events) .where(and(lte(Schema.events.start_time, endDate), gte(Schema.events.end_time, startDate))); - return c.json(eventsData); + return createSuccessResponse(c, eventsData, "Events retrieved successfully"); } catch (error) { - if (error) return createErrorResponse(c, "MISSING_EVENT", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "MISSING_EVENT", "Failed to fetch events", 500); } }); @@ -61,12 +62,10 @@ eventRoutes.post("/events", async (c) => { }) .returning(); - return c.json({ - message: `Inserted event with ID: ${result[0].id}`, - data: result[0], - }); + return createSuccessResponse(c, result[0], `Inserted event with ID: ${result[0].id}`); } catch (error) { - if (error) return createErrorResponse(c, "ADD_BLOG_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "ADD_EVENT_ERROR", "Failed to add event", 500); } }); @@ -97,12 +96,10 @@ eventRoutes.patch("/events", async (c) => { }) .where(eq(Schema.events.id, id)) .returning(); - return c.json({ - message: `Updated event with ID: ${updatedEvent[0].id}`, - data: updatedEvent[0], - }); + return createSuccessResponse(c, updatedEvent[0], `Updated event with ID: ${updatedEvent[0].id}`); } catch (error) { - if (error) return createErrorResponse(c, "UPDATE_EVENT_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "UPDATE_EVENT_ERROR", "Failed to update event", 500); } }); @@ -111,15 +108,16 @@ eventRoutes.get("/events/:eventID", async (c) => { try { const eventID = c.req.param("eventID"); if (!eventID) { - return createErrorResponse(c, "MISSING_EVENT_ID", "Event Id required", 400); + return createErrorResponse(c, "MISSING_EVENT_ID", "Event ID required", 400); } const result = await db.select().from(Schema.events).where(eq(Schema.events.id, eventID)).limit(1); if (result.length === 0) { return createErrorResponse(c, "EVENT_NOT_FOUND", "Event not found", 404); } - return c.json(result[0]); + return createSuccessResponse(c, result[0], "Event retrieved successfully"); } catch (error) { - if (error) return createErrorResponse(c, "FETCH_EVENT_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "FETCH_EVENT_ERROR", "Failed to fetch event", 500); } }); @@ -132,9 +130,10 @@ eventRoutes.get("/events/search/:name", async (c) => { .from(Schema.events) .where(like(Schema.events.name, `%${searchName}%`)); // Approximate search const eventIDs = result.map((row) => row.resEventIDs); - return c.json(eventIDs); + return createSuccessResponse(c, eventIDs, "Event IDs retrieved successfully"); } catch (error) { - if (error) return createErrorResponse(c, "SEARCH_EVENTS_ERROR", error.toString(), 500); + console.log(error); + return createErrorResponse(c, "SEARCH_EVENTS_ERROR", "Failed to search events", 500); } }); diff --git a/src/server/api/profile.ts b/src/server/api/profile.ts index 66390d85..f73e34dc 100644 --- a/src/server/api/profile.ts +++ b/src/server/api/profile.ts @@ -1,12 +1,17 @@ import { db } from "@/server/db/db"; +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import * as Schema from "@db/tables"; import { eq, getTableColumns } from "drizzle-orm"; import { Hono } from "hono"; const profileRoutes = new Hono(); -const profileSchema = { +const profileSelection = { + id: Schema.users.id, username: Schema.users.username, + email: Schema.users.email, + time_added: Schema.users.time_added, + time_updated: Schema.users.time_updated, first_name: Schema.personalInfo.first_name, last_name: Schema.personalInfo.last_name, phone: Schema.personalInfo.phone, @@ -18,87 +23,121 @@ const profileSchema = { graduation_semester: Schema.professionalInfo.graduation_semester, }; +profileRoutes.get("/profile", 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 result = await db + .select(profileSelection) + .from(Schema.users) + .innerJoin(Schema.sessions, eq(Schema.users.id, Schema.sessions.user_id)) + .innerJoin(Schema.personalInfo, eq(Schema.users.id, Schema.personalInfo.user_id)) + .innerJoin(Schema.professionalInfo, eq(Schema.users.id, Schema.professionalInfo.user_id)) + .where(eq(Schema.sessions.id, sessionID)); + + 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); + } + } catch (err) { + console.error("Error:", err); + return createErrorResponse(c, "FETCH_PROFILE_ERROR", "Internal server error", 500); + } +}); + // Fetch profile, return JSON object of {data: , message: } -//This route expects a session ID in the cookie, make sure a user is signed in when calling this endpoint +// This route expects a session ID in the cookie, make sure a user is signed in when calling this endpoint profileRoutes.get("/profile", async (c) => { try { const cookie = c.req.header("Cookie") || ""; - console.log(cookie); const sessionIDMatch = cookie.match(/sessionId=([^;]*)/); if (!sessionIDMatch) { - return c.json({ error: { code: 400, message: "Missing or invalid session ID" } }, 400); + return createErrorResponse(c, "INVALID_SESSION", "Missing or invalid session ID", 400); } const sessionID = sessionIDMatch[1]; - console.log(cookie, sessionID); const result = await db - .select(profileSchema) + .select(profileSelection) .from(Schema.users) .innerJoin(Schema.sessions, eq(Schema.users.id, Schema.sessions.user_id)) .innerJoin(Schema.personalInfo, eq(Schema.users.id, Schema.personalInfo.user_id)) .innerJoin(Schema.professionalInfo, eq(Schema.users.id, Schema.professionalInfo.user_id)) .where(eq(Schema.sessions.id, sessionID)); - return result.length === 1 - ? c.json({ data: result[0], message: "Profile retrieved successfully" }, 200) - : c.json( - { error: { code: result.length === 0 ? 404 : 500, message: result.length === 0 ? "No user found" : "Multiple users found" } }, - result.length === 0 ? 404 : 500, - ); + 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); + } + } } catch (err) { console.error("Error:", err); - return c.json({ error: { code: 500, message: "Internal server error" } }, 500); + return createErrorResponse(c, "FETCH_PROFILE_ERROR", "Internal server error", 500); } }); profileRoutes.patch("/profile", async (c) => { - const cookie = c.req.header("Cookie") || ""; - const sessionIDMatch = cookie.match(/sessionId=([^;]*)/); - if (!sessionIDMatch) { - return c.json({ error: { code: 400, message: "Missing or invalid session ID" } }, 400); - } - const sessionID = sessionIDMatch[1]; - - const result = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionID)); - if (result.length > 0) { - //Creates a set of column names for personal and professional info respectively - const columnNames = generateColumns(); - const personalColumns = columnNames[0]; - const professionalColumns = columnNames[1]; - const userID = result[0].user_id; - - const body = await c.req.json(); - - const updatePromises = Object.keys(body).map(async (key) => { - if (key in profileSchema) { - const value = body[key]; - - //Update based on the column name and its respective table - if (personalColumns.has(key)) { - await db - .update(Schema.personalInfo) - .set({ [key]: value }) - .where(eq(Schema.personalInfo.user_id, userID)); - } else if (professionalColumns.has(key)) { - await db - .update(Schema.professionalInfo) - .set({ [key]: value }) - .where(eq(Schema.professionalInfo.user_id, userID)); + 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 result = await db.select().from(Schema.sessions).where(eq(Schema.sessions.id, sessionID)); + if (result.length > 0) { + // Creates a set of column names for personal and professional info respectively + const columnNames = generateColumns(); + const personalColumns = columnNames[0]; + const professionalColumns = columnNames[1]; + const userID = result[0].user_id; + + const body = await c.req.json(); + + const updatePromises = Object.keys(body).map(async (key) => { + if (key in profileSelection) { + const value = body[key]; + + // Update based on the column name and its respective table + if (personalColumns.has(key)) { + await db + .update(Schema.personalInfo) + .set({ [key]: value }) + .where(eq(Schema.personalInfo.user_id, userID)); + } else if (professionalColumns.has(key)) { + await db + .update(Schema.professionalInfo) + .set({ [key]: value }) + .where(eq(Schema.professionalInfo.user_id, userID)); + } } - } - }); + }); - try { await Promise.all(updatePromises); - } catch (err) { - console.error("Error:", err); - return c.json({ error: { code: 500, message: "Failed to update info" } }, 500); + return createSuccessResponse(c, {}, "Profile updated successfully"); + } else { + return createErrorResponse(c, "INVALID_SESSION", "Invalid session", 400); } - } else { - return c.json({ error: { code: 400, message: "Invalid session" } }, 400); + } catch (err) { + console.error("Error updating profile:", err); + return createErrorResponse(c, "UPDATE_PROFILE_ERROR", "Failed to update info", 500); } - return c.json({ message: "Profile updated successfully" }, 200); }); + export default profileRoutes; function generateColumns() { diff --git a/src/server/api/saseInfo.ts b/src/server/api/saseInfo.ts index 88cdb7c4..84425714 100644 --- a/src/server/api/saseInfo.ts +++ b/src/server/api/saseInfo.ts @@ -1,4 +1,5 @@ import { db } from "@/server/db/db"; +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import * as Schema from "@db/tables"; import { saseInfoSchema } from "@schema/saseInfoSchema"; import { eq } from "drizzle-orm"; @@ -7,16 +8,25 @@ import { Hono } from "hono"; const saseRoutes = new Hono(); saseRoutes.post("/users/sase", async (c) => { - const saseInfoInsertion = saseInfoSchema.parse(await c.req.json()); - const sase_info = await db.insert(Schema.saseInfo).values(saseInfoInsertion); - return c.json({ sase_info }); + try { + const saseInfoInsertion = saseInfoSchema.parse(await c.req.json()); + const sase_info = await db.insert(Schema.saseInfo).values(saseInfoInsertion); + return createSuccessResponse(c, sase_info, "SASE info created successfully"); + } catch (error) { + console.error("Error creating SASE info:", error); + return createErrorResponse(c, "INSERT_SASE_ERROR", "Failed to create SASE info", 500); + } }); saseRoutes.get("/users/sase/:id", async (c) => { - const user_id = c.req.param("id"); - const sase_info = await db.select().from(Schema.saseInfo).where(eq(Schema.saseInfo.user_id, user_id)); - - return c.json({ sase_info }); + try { + const user_id = c.req.param("id"); + const sase_info = await db.select().from(Schema.saseInfo).where(eq(Schema.saseInfo.user_id, user_id)); + return createSuccessResponse(c, sase_info, "SASE info retrieved successfully"); + } catch (error) { + console.error("Error retrieving SASE info:", error); + return createErrorResponse(c, "FETCH_SASE_ERROR", "Failed to retrieve SASE info", 500); + } }); export default saseRoutes; diff --git a/src/server/api/tags.ts b/src/server/api/tags.ts index d170daee..0f0842a7 100644 --- a/src/server/api/tags.ts +++ b/src/server/api/tags.ts @@ -1,4 +1,5 @@ import { db } from "@/server/db/db"; +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import * as Schema from "@db/tables"; import { blogTagsSchema } from "@schema/blogTagSchema"; import { eq, inArray } from "drizzle-orm"; @@ -7,18 +8,24 @@ import { Hono } from "hono"; const tagRoutes = new Hono(); tagRoutes.post("/search/blogs/tags", async (c) => { - const search_tags = blogTagsSchema.parse(await c.req.json()); - const tagNames = search_tags.map((tag) => tag.name); + try { + const search_tags = blogTagsSchema.parse(await c.req.json()); + const tagNames = search_tags.map((tag) => tag.name); - const result = await db - .select({ res_blog_ids: Schema.blogTagRelationship.blog_id }) - .from(Schema.blogTagRelationship) - .innerJoin(Schema.blogTags, eq(Schema.blogTags.id, Schema.blogTagRelationship.tag_id)) - .where(inArray(Schema.blogTags.name, tagNames)); - // Remove duplicates from the resulting blog IDs - let blog_ids = result.map((row) => row.res_blog_ids); - blog_ids = Array.from(new Set(blog_ids)); - return c.json({ blog_ids }); + const result = await db + .select({ res_blog_ids: Schema.blogTagRelationship.blog_id }) + .from(Schema.blogTagRelationship) + .innerJoin(Schema.blogTags, eq(Schema.blogTags.id, Schema.blogTagRelationship.tag_id)) + .where(inArray(Schema.blogTags.name, tagNames)); + + // Remove duplicates from the resulting blog IDs + let blog_ids = result.map((row) => row.res_blog_ids); + blog_ids = Array.from(new Set(blog_ids)); + return createSuccessResponse(c, blog_ids, "Blog tags search successful"); + } catch (error) { + console.error("Error searching blog tags:", error); + return createErrorResponse(c, "SEARCH_BLOG_TAGS_ERROR", "Failed to search blog tags", 500); + } }); export default tagRoutes; diff --git a/src/server/api/user.ts b/src/server/api/user.ts index 31e3b1b5..874d9746 100644 --- a/src/server/api/user.ts +++ b/src/server/api/user.ts @@ -1,4 +1,5 @@ import { db } from "@/server/db/db"; +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import * as Schema from "@db/tables"; import { updateUserSchema } from "@shared/schema/userSchema"; import bcrypt from "bcryptjs"; @@ -12,15 +13,13 @@ userRoutes.get("/users/:id", async (c) => { try { const userId = c.req.param("id"); const user = await db.select().from(Schema.users).where(eq(Schema.users.id, userId)); - - if (!user) { - return c.json({ error: { code: 404, message: "User not found" } }, 404); + if (!user || user.length === 0) { + return createErrorResponse(c, "USER_NOT_FOUND", "User not found", 404); } - - return c.json({ data: user, message: "User retrieved successfully" }); + return createSuccessResponse(c, user, "User retrieved successfully"); } catch (error) { console.error("Error fetching user:", error); - return c.json({ error: { code: 500, message: "An error occurred while fetching the user" } }, 500); + return createErrorResponse(c, "FETCH_USER_ERROR", "An error occurred while fetching the user", 500); } }); @@ -29,22 +28,18 @@ userRoutes.patch("/users/:id", async (c) => { try { const userId = c.req.param("id"); const userToUpdate = updateUserSchema.parse(await c.req.json()); - // Fetch existing user from the database const existingUser = await db.select().from(Schema.users).where(eq(Schema.users.id, userId)); - if (!existingUser) { - return c.json({ error: { code: 404, message: "User not found" } }, 404); + if (!existingUser || existingUser.length === 0) { + return createErrorResponse(c, "USER_NOT_FOUND", "User not found", 404); } - // Merge updated fields with the existing user data (Important to do this first so types match!) - const mergedUser = { ...existingUser, ...userToUpdate }; - + const mergedUser = { ...existingUser[0], ...userToUpdate }; const updatedUser = await db.update(Schema.users).set(mergedUser).where(eq(Schema.users.id, userId)).returning(); - - return c.json({ data: updatedUser, message: "User updated successfully" }); + return createSuccessResponse(c, updatedUser, "User updated successfully"); } catch (error) { console.error("Error updating user:", error); - return c.json({ error: { code: 400, message: "Failed to update user" } }, 400); + return createErrorResponse(c, "UPDATE_USER_ERROR", "Failed to update user", 400); } }); @@ -53,28 +48,32 @@ userRoutes.delete("/users/:id", async (c) => { try { const userId = c.req.param("id"); await db.delete(Schema.users).where(eq(Schema.users.id, userId)); - return c.json({ data: { success: true }, message: "User deleted successfully" }); + return createSuccessResponse(c, { success: true }, "User deleted successfully"); } catch (error) { console.error("Error deleting user:", error); - return c.json({ error: { code: 400, message: "Failed to delete user" } }, 400); + return createErrorResponse(c, "DELETE_USER_ERROR", "Failed to delete user", 400); } }); // User password update userRoutes.patch("/users/:id/password", async (c) => { - const userId = c.req.param("id"); - const { password: newPassword } = await c.req.json(); - - // Find user - const user = await db.select().from(Schema.users).where(eq(Schema.users.id, userId)); - if (!user) return new Response("User not found", { status: 404 }); - - // Hash the new password - const passSalt = await bcrypt.genSalt(10); - const passwordHash = await bcrypt.hash(newPassword, passSalt); - - await db.update(Schema.users).set({ password: passwordHash }).where(eq(Schema.users.id, userId)); - return new Response("Password updated successfully", { status: 200 }); + try { + const userId = c.req.param("id"); + const { password: newPassword } = await c.req.json(); + // Find user + const user = await db.select().from(Schema.users).where(eq(Schema.users.id, userId)); + if (!user || user.length === 0) { + return createErrorResponse(c, "USER_NOT_FOUND", "User not found", 404); + } + // Hash the new password + const passSalt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(newPassword, passSalt); + await db.update(Schema.users).set({ password: passwordHash }).where(eq(Schema.users.id, userId)); + return createSuccessResponse(c, { success: true }, "Password updated successfully"); + } catch (error) { + console.error("Error updating password:", error); + return createErrorResponse(c, "UPDATE_PASSWORD_ERROR", "Failed to update password", 500); + } }); export default userRoutes; diff --git a/src/server/api/userInfo.ts b/src/server/api/userInfo.ts index 7d5e6bb8..876b4de0 100644 --- a/src/server/api/userInfo.ts +++ b/src/server/api/userInfo.ts @@ -1,3 +1,4 @@ +import { createErrorResponse, createSuccessResponse } from "@/shared/utils"; import { personalInfoInsertSchema, personalInfoUpdateSchema } from "@schema/personalInfoSchema"; import { professionalInfoInsertSchema, professionalInfoUpdateSchema } from "@schema/professionalInfoSchema"; import { eq } from "drizzle-orm"; @@ -7,48 +8,76 @@ import * as Schema from "../db/tables"; const infoRoutes = new Hono(); -//Insert personal info infoRoutes.post("/users/personal", async (c) => { - const personalInfoInsertion = personalInfoInsertSchema.parse(await c.req.json()); - const personal_info = await db.insert(Schema.personalInfo).values(personalInfoInsertion); - return c.json({ personal_info }); + try { + const payload = await c.req.json(); + const personalInfoInsertion = personalInfoInsertSchema.parse(payload); + const personal_info = await db.insert(Schema.personalInfo).values(personalInfoInsertion); + return createSuccessResponse(c, personal_info, "Personal info added successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "INSERT_PERSONAL_ERROR", "Failed to add personal info", 500); + } }); -//Query personal info by user_id infoRoutes.get("/users/personal/:id", async (c) => { - const user_id = c.req.param("id"); - const personal_info = await db.select().from(Schema.personalInfo).where(eq(Schema.personalInfo.user_id, user_id)); - return c.json({ personal_info }); + try { + const user_id = c.req.param("id"); + const personal_info = await db.select().from(Schema.personalInfo).where(eq(Schema.personalInfo.user_id, user_id)); + return createSuccessResponse(c, personal_info, "Personal info retrieved successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "FETCH_PERSONAL_ERROR", "Failed to retrieve personal info", 500); + } }); -//Update personal info by user_id infoRoutes.patch("/users/personal/:id", async (c) => { - const user_id = c.req.param("id"); - const updateInfo = personalInfoUpdateSchema.parse(await c.req.json()); - const personal_info = await db.update(Schema.personalInfo).set(updateInfo).where(eq(Schema.personalInfo.user_id, user_id)); - return c.json({ personal_info }); + try { + const user_id = c.req.param("id"); + const payload = await c.req.json(); + const updateInfo = personalInfoUpdateSchema.parse(payload); + const personal_info = await db.update(Schema.personalInfo).set(updateInfo).where(eq(Schema.personalInfo.user_id, user_id)); + return createSuccessResponse(c, personal_info, "Personal info updated successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "UPDATE_PERSONAL_ERROR", "Failed to update personal info", 500); + } }); -//Insert professional info infoRoutes.post("/users/professional", async (c) => { - const professionalInfoInsertion = professionalInfoInsertSchema.parse(await c.req.json()); - const professional_info = await db.insert(Schema.professionalInfo).values(professionalInfoInsertion); - return c.json({ professional_info }); + try { + const payload = await c.req.json(); + const professionalInfoInsertion = professionalInfoInsertSchema.parse(payload); + const professional_info = await db.insert(Schema.professionalInfo).values(professionalInfoInsertion); + return createSuccessResponse(c, professional_info, "Professional info created successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "INSERT_PROFESSIONAL_ERROR", "Failed to create professional info", 500); + } }); -//Query professional info by user_id infoRoutes.get("/users/professional/:id", async (c) => { - const user_id = c.req.param("id"); - const professional_info = await db.select().from(Schema.professionalInfo).where(eq(Schema.professionalInfo.user_id, user_id)); - return c.json({ professional_info }); + try { + const user_id = c.req.param("id"); + const professional_info = await db.select().from(Schema.professionalInfo).where(eq(Schema.professionalInfo.user_id, user_id)); + return createSuccessResponse(c, professional_info, "Professional info retrieved successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "FETCH_PROFESSIONAL_ERROR", "Failed to retrieve professional info", 500); + } }); -//Update professional info by user_id infoRoutes.patch("/users/professional/:id", async (c) => { - const user_id = c.req.param("id"); - const updateInfo = professionalInfoUpdateSchema.parse(await c.req.json()); - const professional_info = await db.update(Schema.professionalInfo).set(updateInfo).where(eq(Schema.professionalInfo.user_id, user_id)); - return c.json({ professional_info }); + try { + const user_id = c.req.param("id"); + const payload = await c.req.json(); + const updateInfo = professionalInfoUpdateSchema.parse(payload); + const professional_info = await db.update(Schema.professionalInfo).set(updateInfo).where(eq(Schema.professionalInfo.user_id, user_id)); + return createSuccessResponse(c, professional_info, "Professional info updated successfully"); + } catch (error) { + console.log(error); + return createErrorResponse(c, "UPDATE_PROFESSIONAL_ERROR", "Failed to update professional info", 500); + } }); export default infoRoutes; diff --git a/src/shared/schema/blogSchema.ts b/src/shared/schema/blogSchema.ts index 5c86e878..a182f981 100644 --- a/src/shared/schema/blogSchema.ts +++ b/src/shared/schema/blogSchema.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { successResponseSchema } from "./responseSchema"; // Blog schema for full blog details export const blogSchema = z.object({ @@ -32,24 +31,11 @@ export const updateBlogSchema = blogSchema.partial().omit({ published_date: true, }); -// Export TypeScript types export type Blog = z.infer; export type BlogTitle = z.infer; export type CreateBlog = z.infer; export type UpdateBlog = z.infer; -export type BlogSearchResponse = z.infer; -// Specific success schemas -export const blogsApiResponseSchema = successResponseSchema.extend({ - data: z.array(blogSchema), -}); - -export const singleBlogApiResponseSchema = successResponseSchema.extend({ - data: blogSchema, -}); - -export const blogSearchResponseSchema = successResponseSchema.extend({ - data: z.object({ - blog_ids: z.array(z.string().min(1, "Blog ID must be valid.")), - }), +export const blogSearchResponseSchema = z.object({ + blog_ids: z.array(z.string().min(1, "Blog ID must be valid.")), }); diff --git a/src/shared/schema/index.ts b/src/shared/schema/index.ts index c94f724f..68a97758 100644 --- a/src/shared/schema/index.ts +++ b/src/shared/schema/index.ts @@ -8,3 +8,4 @@ export * from "./professionalInfoSchema"; export * from "./saseInfoSchema"; export * from "./userSchema"; export * from "./responseSchema"; +export * from "./profileSchema"; diff --git a/src/shared/schema/profileSchema.ts b/src/shared/schema/profileSchema.ts new file mode 100644 index 00000000..2d164284 --- /dev/null +++ b/src/shared/schema/profileSchema.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const profileSchema = z.object({ + id: z.string().min(1, "User ID is required."), + username: z.string().min(1, "Username is required."), + email: z.string().email("Invalid email address."), + time_added: z.preprocess( + (val) => (typeof val === "string" ? Date.parse(val) : val), + z.number().int().min(0, "Time added must be a valid timestamp."), + ), + time_updated: z.preprocess( + (val) => (typeof val === "string" ? Date.parse(val) : val), + z.number().int().min(0, "Time updated must be a valid timestamp."), + ), + + first_name: z.string(), // allow empty string if needed + last_name: z.string(), // allow empty string if needed + phone: z.string().optional(), + + resume: z.string().optional(), + linkedin: z.string().optional(), + portfolio: z.string().optional(), + majors: z.string().optional(), + minors: z.string().optional(), + graduation_semester: z.string().optional(), +}); + +export type Profile = z.infer; diff --git a/src/shared/schema/responseSchema.ts b/src/shared/schema/responseSchema.ts index efcb3a70..f5a53b8c 100644 --- a/src/shared/schema/responseSchema.ts +++ b/src/shared/schema/responseSchema.ts @@ -22,7 +22,5 @@ export const errorResponseSchema = z.object({ }), }); -export type SuccessResponse = z.infer & { - data: T; -}; +export type SuccessResponse = z.infer; export type ErrorResponse = z.infer; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index bf5f1be1..b2a4e66b 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,46 +1,35 @@ -import { errorResponseSchema } from "@schema/responseSchema"; +import type { SuccessResponse } from "@schema/responseSchema"; +import { errorResponseSchema, successResponseSchema } from "@schema/responseSchema"; import { clsx, type ClassValue } from "clsx"; import type { Context } from "hono"; import type { StatusCode } from "hono/utils/http-status"; import { twMerge } from "tailwind-merge"; -import type { z, ZodTypeAny } from "zod"; +import type { ZodTypeAny } from "zod"; // This is for shad-ci ui https://ui.shadcn.com/docs/installation/manual#add-a-cn-helper export function cn(...inputs: Array) { return twMerge(clsx(inputs)); } -export async function zodFetch(schema: T, input: RequestInfo, init?: RequestInit): Promise> { - const res = await fetch(input, init); - if (!res.ok) { - throw new Error(res.statusText); - } - return schema.parse(await res.json()); -} - -export const formatAndValidateResponse = (result: T, message: string, schema: ZodTypeAny, meta: Record = {}) => { - const responsePayload = { - data: result, - message, - meta, - }; - return schema.parse(responsePayload); +export const createSuccessResponse = (c: Context, result: T, message: string = "Success", meta: Record = {}) => { + return c.json({ data: result, message, meta }); }; export const createErrorResponse = (c: Context, errCode: string, errMsg: string, statusCode: StatusCode = 500) => c.json({ error: { errCode, errMsg } }, statusCode); -export const apiFetch = async (url: string, options: RequestInit = {}, successSchema: ZodTypeAny): Promise => { +export const apiFetch = async (url: string, options: RequestInit = {}, dataSuccessSchema: ZodTypeAny): Promise => { const response = await fetch(url, options); - const data = await response.json(); + const json = await response.json(); if (!response.ok) { - const parsedError = errorResponseSchema.safeParse(data); + const parsedError = errorResponseSchema.safeParse(json); if (parsedError.success) { - throw new Error(parsedError.data.error.errMsg); + const { errCode, errMsg } = parsedError.data.error; + throw new Error(`[${errCode}] ${errMsg}`); } throw new Error("An unknown error occurred"); } - - return successSchema.parse(data).data; + const fullSchema = successResponseSchema.extend({ data: dataSuccessSchema }); + return fullSchema.parse(json); };