@@ -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)
+ }
}