diff --git a/app/src/App.tsx b/app/src/App.tsx index 02eaef2..3325a41 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -23,35 +23,38 @@ import { JEPostView } from './pages/admin/JEPostView'; import { AdminPostView } from './pages/admin/AdminPostView'; import { GuestRoute } from './components/GuestRoute'; import { NotFound } from './pages/NotFound'; +import { AuthProvider } from './context/AuthContext'; function App() { return ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); } diff --git a/app/src/components/GuestRoute.tsx b/app/src/components/GuestRoute.tsx index 58d14d1..3a730a2 100644 --- a/app/src/components/GuestRoute.tsx +++ b/app/src/components/GuestRoute.tsx @@ -1,28 +1,16 @@ -import { useEffect, useState } from 'react'; import type { ReactNode } from 'react'; import { Navigate } from 'react-router-dom'; import { Loader } from './Loader'; +import { useAuth } from '../context/auth-context'; interface GuestRouteProps { children: ReactNode; } export function GuestRoute({ children }: GuestRouteProps) { - const [isAuth, setIsAuth] = useState(null); + const { status } = useAuth(); - useEffect(() => { - fetch('/api/profile', { credentials: 'include' }) - .then(res => { - if (!res.ok) { - setIsAuth(false); - return; - } - setIsAuth(true); - }) - .catch(() => setIsAuth(false)); - }, []); - - if (isAuth === null) { + if (status === 'loading') { return (
@@ -30,7 +18,7 @@ export function GuestRoute({ children }: GuestRouteProps) { ); } - if (isAuth) { + if (status === 'authenticated') { return ; } diff --git a/app/src/components/layout/Footer.tsx b/app/src/components/layout/Footer.tsx index 5fae9d6..4c0d48a 100644 --- a/app/src/components/layout/Footer.tsx +++ b/app/src/components/layout/Footer.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Mail, MapPin, ChevronDown } from 'lucide-react'; import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/auth-context'; type Profile = { department?: string; hostel?: string; building?: string } | null; @@ -11,21 +12,11 @@ function getPostRoute(profile: NonNullable): string { } export function Footer() { - const [profile, setProfile] = useState(null); - const [isAuth, setIsAuth] = useState(null); + const { profile, status } = useAuth(); + const isAuth = status === 'authenticated'; const [lodgeOpen, setLodgeOpen] = useState(false); const navigate = useNavigate(); - useEffect(() => { - fetch('/api/profile', { credentials: 'include' }) - .then(res => { - if (!res.ok) { setIsAuth(false); return null; } - return res.json(); - }) - .then(data => { if (data) { setProfile(data); setIsAuth(true); } }) - .catch(() => setIsAuth(false)); - }, []); - function handleLodgeComplaintClick() { if (isAuth && profile) { navigate(getPostRoute(profile)); diff --git a/app/src/components/layout/Navbar.tsx b/app/src/components/layout/Navbar.tsx index a5e1208..7042a3b 100644 --- a/app/src/components/layout/Navbar.tsx +++ b/app/src/components/layout/Navbar.tsx @@ -1,22 +1,15 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { ChevronDown, Menu, X, User, LogOut } from 'lucide-react'; import { Link } from 'react-router-dom'; +import { useAuth } from '../../context/auth-context'; export function Navbar() { const [mobileOpen, setMobileOpen] = useState(false); const [lodgeOpen, setLodgeOpen] = useState(false); const [adminOpen, setAdminOpen] = useState(false); const [loginDropdownOpen, setLoginDropdownOpen] = useState(false); - const [isAuth, setIsAuth] = useState(null); - - useEffect(() => { - fetch('/api/profile', { credentials: 'include' }) - .then(res => { - if (!res.ok) { setIsAuth(false); return; } - setIsAuth(true); - }) - .catch(() => setIsAuth(false)); - }, []); + const { status } = useAuth(); + const isAuth = status === 'loading' ? null : status === 'authenticated'; const closeMobile = () => { setMobileOpen(false); diff --git a/app/src/context/AuthContext.tsx b/app/src/context/AuthContext.tsx new file mode 100644 index 0000000..65de277 --- /dev/null +++ b/app/src/context/AuthContext.tsx @@ -0,0 +1,56 @@ +import { useCallback, useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { AuthContext } from './auth-context'; +import type { AuthStatus, ProfileData } from './auth-context'; + +// AuthProvider fetches /api/profile exactly once for the whole app and shares +// the result via context. Every component that previously fetched /api/profile +// on its own (Navbar, Footer, Landing, GuestRoute, Profile) reads from here +// instead, so a single page load makes one request, not five. +export function AuthProvider({ children }: { children: ReactNode }) { + const [profile, setProfile] = useState(null); + const [status, setStatus] = useState('loading'); + const [errorMessage, setErrorMessage] = useState(null); + + const refetch = useCallback(() => { + setStatus('loading'); + setErrorMessage(null); + fetch('/api/profile', { credentials: 'include' }) + .then(async (res) => { + if (res.status === 429) { + const b = await res.json().catch(() => ({})); + setErrorMessage(b.error ?? "You've hit the rate limit. Please try again in a moment."); + setStatus('rate-limited'); + return null; + } + if (!res.ok) { + setProfile(null); + setStatus('unauthenticated'); + return null; + } + return res.json(); + }) + .then((data) => { + if (data) { + setProfile(data); + setStatus('authenticated'); + } + }) + .catch(() => { + setErrorMessage('Failed to reach the server.'); + setStatus('error'); + }); + }, []); + + const patchProfile = useCallback((patch: Partial) => { + setProfile((prev) => prev ? { ...prev, ...patch } : prev); + }, []); + + useEffect(() => { refetch(); }, [refetch]); + + return ( + + {children} + + ); +} diff --git a/app/src/context/auth-context.ts b/app/src/context/auth-context.ts new file mode 100644 index 0000000..48ecb63 --- /dev/null +++ b/app/src/context/auth-context.ts @@ -0,0 +1,34 @@ +import { createContext, useContext } from 'react'; + +export interface ProfileData { + name?: string; + email?: string; + is_verified?: boolean; + phone_number?: string; + department?: string; + house_number?: string; + block?: string; + type?: string; + hostel?: string; + building?: string; +} + +export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'rate-limited' | 'error'; + +export interface AuthState { + profile: ProfileData | null; + status: AuthStatus; + errorMessage: string | null; + /** Re-fetches /api/profile from scratch. */ + refetch: () => void; + /** Merges a patch into the cached profile without hitting the network again. */ + patchProfile: (patch: Partial) => void; +} + +export const AuthContext = createContext(undefined); + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be used within an AuthProvider'); + return ctx; +} diff --git a/app/src/pages/Landing.tsx b/app/src/pages/Landing.tsx index 091e28a..8ee7fb4 100644 --- a/app/src/pages/Landing.tsx +++ b/app/src/pages/Landing.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { ArrowRight } from 'lucide-react'; import { Link, useNavigate } from 'react-router-dom'; import { MainLayout } from '../components/layout/MainLayout'; +import { useAuth } from '../context/auth-context'; type Profile = { department?: string; hostel?: string; building?: string } | null; @@ -12,24 +13,14 @@ function getPostRoute(profile: NonNullable): string { } export function Landing() { - const [profile, setProfile] = useState(null); - const [isAuth, setIsAuth] = useState(null); + const { profile, status } = useAuth(); + const isAuth = status === 'loading' ? null : status === 'authenticated'; const [showLoginMenu, setShowLoginMenu] = useState(false); const [showSignupMenu, setShowSignupMenu] = useState(false); const menuRef = useRef(null); const signupMenuRef = useRef(null); const navigate = useNavigate(); - useEffect(() => { - fetch('/api/profile', { credentials: 'include' }) - .then(res => { - if (!res.ok) { setIsAuth(false); return null; } - return res.json(); - }) - .then(data => { if (data) { setProfile(data); setIsAuth(true); } }) - .catch(() => setIsAuth(false)); - }, []); - useEffect(() => { function handleClick(e: MouseEvent) { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { diff --git a/app/src/pages/post/PostView.tsx b/app/src/pages/post/PostView.tsx index e3dacda..c728d0c 100644 --- a/app/src/pages/post/PostView.tsx +++ b/app/src/pages/post/PostView.tsx @@ -106,7 +106,8 @@ export function PostView() { try { const res = await fetch(`/api/posts/${role}/${post_id}`, { credentials: 'include' }); if (!res.ok) { - throw new Error(`Failed to fetch post details (${res.status})`); + const b = await res.json().catch(() => ({})); + throw new Error(b.error ?? `Failed to fetch post details (${res.status})`); } const data = await res.json(); setPost(data.post); diff --git a/app/src/pages/profile/Profile.tsx b/app/src/pages/profile/Profile.tsx index 236095c..d8f59e2 100644 --- a/app/src/pages/profile/Profile.tsx +++ b/app/src/pages/profile/Profile.tsx @@ -2,31 +2,17 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { ShieldCheck, LogOut, PlusCircle, AlertCircle, Pencil, - Inbox, ServerCrash, Info, X, + Inbox, ServerCrash, Info, X, Clock, } from 'lucide-react'; import { MainLayout } from '../../components/layout/MainLayout'; import { ComplaintCard } from '../../components/ComplaintCard'; import type { ComplaintPost, EditForm, Role } from '../../components/ComplaintCard'; import { Loader } from '../../components/Loader'; import { BUILDINGS, HOSTELS, DEPARTMENTS, BLOCK_LABELS, BLOCK_TYPES } from '../../constants/models'; - -interface ProfileData { - name?: string; - email?: string; - is_verified?: boolean; - phone_number?: string; - department?: string; - house_number?: string; - block?: string; - type?: string; - hostel?: string; - building?: string; -} +import { useAuth } from '../../context/auth-context'; export function Profile() { - const [profile, setProfile] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const { profile, status, errorMessage, patchProfile } = useAuth(); const navigate = useNavigate(); const [posts, setPosts] = useState([]); @@ -47,19 +33,14 @@ export function Profile() { const [profileSaving, setProfileSaving] = useState(false); const [profileError, setProfileError] = useState(null); + // Only bounce unauthenticated/broken sessions home — a rate-limited response + // isn't a login problem, so it's left for the user to just wait it out. useEffect(() => { - fetch('/api/profile', { credentials: 'include' }) - .then((res) => { - if (!res.ok) throw new Error('Failed to fetch profile. Please login.'); - return res.json(); - }) - .then((data) => { setProfile(data); setLoading(false); }) - .catch((err) => { - setError(err.message); - setLoading(false); - setTimeout(() => navigate('/'), 3000); - }); - }, [navigate]); + if (status === 'unauthenticated' || status === 'error') { + const timer = setTimeout(() => navigate('/'), 3000); + return () => clearTimeout(timer); + } + }, [status, navigate]); const fetchPosts = useCallback((silent = false) => { if (!profile) return; @@ -73,22 +54,25 @@ export function Profile() { setPostsError(null); fetch(endpoint, { credentials: 'include' }) .then(async (res) => { - if (!res.ok) throw new Error(`Server error (${res.status})`); + if (!res.ok) { + const b = await res.json().catch(() => ({})); + throw new Error(b.error ?? `Server error (${res.status})`); + } return res.json(); }) .then((data) => { setPosts(data.posts ?? []); setPostsLoading(false); if (data.name) { - setProfile((prev) => prev ? { ...prev, name: data.name } : null); + patchProfile({ name: data.name }); } }) .catch((err: Error) => { setPostsError(err.message); setPostsLoading(false); }); - }, [profile]); + }, [profile, patchProfile]); useEffect(() => { fetchPosts(); }, [fetchPosts]); - if (loading) { + if (status === 'loading') { return (
@@ -101,14 +85,28 @@ export function Profile() { ); } - if (error) { + if (status === 'rate-limited') { + return ( + +
+
+ +

Slow Down

+

{errorMessage}

+
+
+
+ ); + } + + if (status === 'unauthenticated' || status === 'error') { return (

Access Denied

-

{error}

+

{errorMessage ?? 'Failed to fetch profile. Please login.'}

Redirecting to Homepage…

@@ -238,7 +236,7 @@ export function Profile() { const b = await res.json().catch(() => ({})); throw new Error(b.error ?? `Failed to update profile (${res.status})`); } - setProfile((prev) => prev ? { ...prev, ...body } : null); + patchProfile(body); setIsEditingProfile(false); } catch (err) { setProfileError((err as Error).message); diff --git a/handlers/admin_auth.go b/handlers/admin_auth.go index d9f7da3..97cb8e1 100644 --- a/handlers/admin_auth.go +++ b/handlers/admin_auth.go @@ -2,7 +2,6 @@ package handlers import ( "errors" - "time" "github.com/ayush00git/cms-web/helpers" "github.com/ayush00git/cms-web/models" @@ -11,26 +10,26 @@ import ( "gorm.io/gorm" ) -// // Use AdminSignup only when registering admins to the database. -// // Not to be used as a public API. -func (h *AdminHandler) AdminSignup (c *gin.Context) { - var inputs models.Admin - if err := c.ShouldBindJSON(&inputs); err != nil { - c.JSON(400, gin.H{"error": "invalid request body"}) - return - } +// // // Use AdminSignup only when registering admins to the database. +// // // Not to be used as a public API. +// func (h *AdminHandler) AdminSignup (c *gin.Context) { +// var inputs models.Admin +// if err := c.ShouldBindJSON(&inputs); err != nil { +// c.JSON(400, gin.H{"error": "invalid request body"}) +// return +// } - hashedPass, err := bcrypt.GenerateFromPassword([]byte(inputs.Password), 10) - if err != nil { - c.JSON(500, gin.H{"error": "internal server error"}) - return - } - inputs.Password = string(hashedPass) +// hashedPass, err := bcrypt.GenerateFromPassword([]byte(inputs.Password), 10) +// if err != nil { +// c.JSON(500, gin.H{"error": "internal server error"}) +// return +// } +// inputs.Password = string(hashedPass) - inputs.CreatedAt = time.Now() - _ = h.DB.Create(&inputs) - c.JSON(201, gin.H{"success": "admin registered successfully!"}) -} +// inputs.CreatedAt = time.Now() +// _ = h.DB.Create(&inputs) +// c.JSON(201, gin.H{"success": "admin registered successfully!"}) +// } // Note that for admins as they are pre-set to the service the // isVerified field is always by default true. diff --git a/middleware/auth_limiter.go b/middleware/auth_limiter.go new file mode 100644 index 0000000..0fe1328 --- /dev/null +++ b/middleware/auth_limiter.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// LimitByAuth is the rate limiter middleware for authenticated users. +func (rl *RateLimiter) LimitByAuth() gin.HandlerFunc { + return func(c *gin.Context) { + email, ok := c.Get(EmailKey) + role, okk := c.Get(RoleKey) + + if !ok || !okk { + c.JSON(401, gin.H{"error": "unauthorized access!"}) + return + } + + key := email.(string) + ":" + role.(string) + + bucket := rl.GetBucket(key) + + rl.mu.Lock() + allowed := bucket.Allow() + rl.mu.Unlock() + + if !allowed { + c.JSON(429, gin.H{"error": "you've hit it too many time! try again later"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/public_limiter.go b/middleware/public_limiter.go new file mode 100644 index 0000000..7c6b22e --- /dev/null +++ b/middleware/public_limiter.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// Limit is the middleware which looks for bucket associated with the IP / assigns a new one. +// And returns 429 if rate limit exceeded. +func (rl *RateLimiter) Limit() gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + bucket := rl.GetBucket(ip) + + rl.mu.Lock() + allowed := bucket.Allow() + rl.mu.Unlock() + + if !allowed { + c.JSON(429, gin.H{"error": "you've hit it too many times! get back later"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go new file mode 100644 index 0000000..fa2e505 --- /dev/null +++ b/middleware/rate_limiter.go @@ -0,0 +1,91 @@ +package middleware + +import ( + "sync" + "time" +) + +type TokenBucket struct { + tokens float64 + maxTokens float64 + refillRate float64 + lastRefill time.Time +} + +type RateLimiter struct { + buckets map[string]*TokenBucket + mu sync.Mutex + max float64 + refill float64 +} + +// NewRateLimiter instantiates a new RateLimiter object and start +// a cleanup goroutine which deletes the stale buckets. +func NewRateLimiter(maxTokens, refillRate float64) *RateLimiter { + rl := &RateLimiter{ + buckets: make(map[string]*TokenBucket), + max: maxTokens, + refill: refillRate, + } + + // this goroutine runs every 10 minutes and clean stale + // buckets with inactivity of 30 minutes. + go func() { + for { + time.Sleep(10*time.Minute) + rl.mu.Lock() + for ip, bucket := range rl.buckets { + if time.Since(bucket.lastRefill) > 30*time.Minute { + delete(rl.buckets, ip) + } + } + rl.mu.Unlock() + } + }() + + return rl +} + +// GetBucket initiates a new/existing bucket inside the memory map with key = ip. +// And assigns a new bucket to a newly seen ip. +func (rl *RateLimiter) GetBucket(ip string) *TokenBucket { + rl.mu.Lock() + defer rl.mu.Unlock() + + bucket, exists := rl.buckets[ip] + if !exists { + bucket = &TokenBucket{ + tokens: rl.max, + maxTokens: rl.max, + refillRate: rl.refill, + lastRefill: time.Now(), + } + rl.buckets[ip] = bucket + } + return bucket +} + +// Allow is the main rate limiting logic, it allows/disallows a request +// based on number of tokens left and caps a bucket at maxTokens. +func (b *TokenBucket) Allow() bool { + now := time.Now() + elapsed := now.Sub(b.lastRefill).Seconds() + + // bucket refill logic. + b.tokens += b.refillRate * elapsed + + // cap the bucket at its maximum allowed tokens capacity. + if b.tokens > b.maxTokens { + b.tokens = b.maxTokens + } + b.lastRefill = now + + // consume one token for one request and allow the request. + if b.tokens >= 1 { + b.tokens-- + return true + } + + // otherwise block the request (if token < 1) + return false +} diff --git a/routes/admin.go b/routes/admin.go index 003a130..bdc4403 100644 --- a/routes/admin.go +++ b/routes/admin.go @@ -7,8 +7,12 @@ import ( ) func AdminRoutes (e *gin.Engine, h *handlers.AdminHandler) { - e.POST("/api/auth/admin/signup", h.AdminSignup) // not to be used as an public API - e.POST("/api/auth/admin/login", h.AdminLogin) + // e.POST("/api/auth/admin/signup", h.AdminSignup) // not to be used as an public API + + // refill 1 token every 6 seconds. And allow a maximum of 10 tokens. + adminLoginRateLimiter := middleware.NewRateLimiter(10, 1.0/6.0) + + e.POST("/api/auth/admin/login", adminLoginRateLimiter.Limit(), h.AdminLogin) e.GET("/api/admin/comments", middleware.IsAuthenticated(), h.AdminGetComments) e.POST("/api/admin/comment/:type/:id", middleware.IsAuthenticated(), h.AdminPostComment) diff --git a/routes/auth.go b/routes/auth.go index a5712ac..1cf6d62 100644 --- a/routes/auth.go +++ b/routes/auth.go @@ -7,37 +7,45 @@ import ( ) func AuthRoute (e *gin.Engine, h *handlers.AuthHandler) { + + // maximum of 3 tokens and 1 refill per 15 seconds. + mailRoutesRateLimiter := middleware.NewRateLimiter(3, 1.0/30.0) + standardRateLimiter := middleware.NewRateLimiter(10, 1.0/6.0) + faculty := e.Group("/api/auth/faculty") { - faculty.POST("/signup", h.FacultySignup) - faculty.POST("/login", h.FacultyLogin) - faculty.POST("/forget-password", h.FacultyForgetPassword) - faculty.PATCH("/reset-password", h.FacultyResetPassword) + faculty.POST("/signup", mailRoutesRateLimiter.Limit(), h.FacultySignup) + faculty.POST("/login", standardRateLimiter.Limit(), h.FacultyLogin) + faculty.POST("/forget-password", mailRoutesRateLimiter.Limit(), h.FacultyForgetPassword) + faculty.PATCH("/reset-password", standardRateLimiter.Limit(), h.FacultyResetPassword) } warden := e.Group("/api/auth/warden") { - warden.POST("/signup", h.WardenSignup) - warden.POST("/login", h.WardenLogin) - warden.POST("/forget-password", h.WardenForgetPassword) - warden.PATCH("/reset-password", h.WardenResetPassword) + warden.POST("/signup", mailRoutesRateLimiter.Limit(), h.WardenSignup) + warden.POST("/login", standardRateLimiter.Limit(), h.WardenLogin) + warden.POST("/forget-password", mailRoutesRateLimiter.Limit(), h.WardenForgetPassword) + warden.PATCH("/reset-password", standardRateLimiter.Limit(), h.WardenResetPassword) } centrehead := e.Group("/api/auth/centrehead") { - centrehead.POST("/signup", h.CentreheadSignup) - centrehead.POST("/login", h.CentreheadLogin) - centrehead.POST("/forget-password", h.CentreheadForgetPassword) - centrehead.PATCH("/reset-password", h.CentreheadResetPassword) + centrehead.POST("/signup", mailRoutesRateLimiter.Limit(), h.CentreheadSignup) + centrehead.POST("/login", standardRateLimiter.Limit(), h.CentreheadLogin) + centrehead.POST("/forget-password", mailRoutesRateLimiter.Limit(), h.CentreheadForgetPassword) + centrehead.PATCH("/reset-password", standardRateLimiter.Limit(), h.CentreheadResetPassword) } e.POST("/api/auth/logout", h.Logout) // for account verifications. e.GET("/api/auth/verify", h.VerifyAccount) + // 100 tokens maximum with a refill rate of 1 token per 5 seconds. + profileRoutesRateLimiting := middleware.NewRateLimiter(100, 1.0/5.0) + // for returning the user's profile. - e.GET("/api/profile", middleware.IsAuthenticated(), h.UserProfile) + e.GET("/api/profile", middleware.IsAuthenticated(), profileRoutesRateLimiting.LimitByAuth(), h.UserProfile) // for editing user's profile. - e.PATCH("/api/faculty/profile/edit", middleware.IsAuthenticated(), h.FacultyProfileEdit) - e.PATCH("/api/warden/profile/edit", middleware.IsAuthenticated(), h.WardenProfileEdit) - e.PATCH("/api/centrehead/profile/edit", middleware.IsAuthenticated(), h.CentreheadProfileEdit) + e.PATCH("/api/faculty/profile/edit", middleware.IsAuthenticated(), profileRoutesRateLimiting.LimitByAuth(), h.FacultyProfileEdit) + e.PATCH("/api/warden/profile/edit", middleware.IsAuthenticated(), profileRoutesRateLimiting.LimitByAuth(), h.WardenProfileEdit) + e.PATCH("/api/centrehead/profile/edit", middleware.IsAuthenticated(), profileRoutesRateLimiting.LimitByAuth(), h.CentreheadProfileEdit) } diff --git a/routes/post.go b/routes/post.go index 4e633f5..afc1858 100644 --- a/routes/post.go +++ b/routes/post.go @@ -8,29 +8,35 @@ import ( ) func PostRoute(e *gin.Engine, h *handlers.PostHandler) { - // APIs for new post - e.POST("/api/posts/faculty", middleware.IsAuthenticated(), h.FacultyPost) - e.POST("/api/posts/warden", middleware.IsAuthenticated(), h.WardenPost) - e.POST("/api/posts/centrehead", middleware.IsAuthenticated(), h.CentreheadPost) - - // APIs for updating the post - e.PATCH("/api/posts/faculty/edit/:post_id", middleware.IsAuthenticated(), h.FacultyPostEdit) - e.PATCH("/api/posts/warden/edit/:post_id", middleware.IsAuthenticated(), h.WardenPostEdit) - e.PATCH("/api/posts/centrehead/edit/:post_id", middleware.IsAuthenticated(), h.CentreheadPostEdit) - - // APIs for deleting the post - e.DELETE("/api/posts/faculty/delete/:post_id", middleware.IsAuthenticated(), h.FacultyPostDelete) - e.DELETE("/api/posts/warden/delete/:post_id", middleware.IsAuthenticated(), h.WardenPostDelete) - e.DELETE("/api/posts/centrehead/delete/:post_id", middleware.IsAuthenticated(), h.CentreheadPostDelete) - - // APIs for getting the posts - e.GET("/api/posts/faculty", middleware.IsAuthenticated(), h.GetFacultyPosts) - e.GET("/api/posts/warden", middleware.IsAuthenticated(), h.GetWardenPosts) - e.GET("/api/posts/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts) - e.GET("/api/posts/:role/:post_id", middleware.IsAuthenticated(), h.GetPostByID) - - // APIs for comments on the posts - e.POST("/api/posts/faculty/comment/:post_id", middleware.IsAuthenticated() ,h.FacultyPostComment) - e.POST("/api/posts/warden/comment/:post_id", middleware.IsAuthenticated(), h.WardenPostComment) - e.POST("/api/posts/centrehead/comment/:post_id", middleware.IsAuthenticated(), h.CentreheadPostComment) + + postRoutesRateLimiter := middleware.NewRateLimiter(200, 1.0/10.0) + + posts := e.Group("/api/posts", middleware.IsAuthenticated(), postRoutesRateLimiter.LimitByAuth()) + { + // APIs for new post + posts.POST("/faculty", h.FacultyPost) + posts.POST("/warden", h.WardenPost) + posts.POST("/centrehead", h.CentreheadPost) + + // APIs for updating the post + posts.PATCH("/faculty/edit/:post_id", h.FacultyPostEdit) + posts.PATCH("/warden/edit/:post_id", h.WardenPostEdit) + posts.PATCH("/centrehead/edit/:post_id", h.CentreheadPostEdit) + + // APIs for deleting the post + posts.DELETE("/faculty/delete/:post_id", h.FacultyPostDelete) + posts.DELETE("/warden/delete/:post_id", h.WardenPostDelete) + posts.DELETE("/centrehead/delete/:post_id", h.CentreheadPostDelete) + + // APIs for getting the posts + posts.GET("/faculty", h.GetFacultyPosts) + posts.GET("/warden", h.GetWardenPosts) + posts.GET("/centrehead", h.GetCentreheadPosts) + posts.GET("/:role/:post_id", h.GetPostByID) + + // APIs for comments on the posts + posts.POST("/faculty/comment/:post_id", h.FacultyPostComment) + posts.POST("/warden/comment/:post_id", h.WardenPostComment) + posts.POST("/centrehead/comment/:post_id", h.CentreheadPostComment) + } }