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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ function App() {
<Route path="/centre-head/forgot-password" element={<CentreHeadForgotPassword />} />
<Route path="/account/reset-password" element={<AccountResetPass />} />
<Route path="/staff/login" element={<GuestRoute><StaffLogin /></GuestRoute>} />
<Route path="/faculty/post" element={<FacultyPost />} />
<Route path="/warden/post" element={<WardenPost />} />
<Route path="/centre-head/post" element={<CentreHeadPost />} />
<Route path="/post/:role/:post_id" element={<PostView />} />
<Route path="/faculty/posts" element={<FacultyPost />} />
<Route path="/warden/posts" element={<WardenPost />} />
<Route path="/centre-head/posts" element={<CentreHeadPost />} />
<Route path="/posts/:role/:post_id" element={<PostView />} />
<Route path="/account/verify" element={<VerifyAccount />} />
<Route path="/profile" element={<Profile />} />
<Route path="/admin/xen" element={<XENPostView />} />
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ComplaintCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function ComplaintCard({

return (
<div
onClick={() => navigate(`/post/${role}/${post.id}`)}
onClick={() => navigate(`/posts/${role}/${post.id}`)}
className={`${theme.cardBg} relative border border-gray-200/80 rounded-xl shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 cursor-pointer group flex min-h-[140px]`}
>
{/* Left accent bar (Linear / Vercel style) */}
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Link, useNavigate } from 'react-router-dom';
type Profile = { department?: string; hostel?: string; building?: string } | null;

function getPostRoute(profile: NonNullable<Profile>): string {
if ('department' in profile) return '/faculty/post';
if ('hostel' in profile) return '/warden/post';
return '/centre-head/post';
if ('department' in profile) return '/faculty/posts';
if ('hostel' in profile) return '/warden/posts';
return '/centre-head/posts';
}

export function Footer() {
Expand Down
6 changes: 3 additions & 3 deletions app/src/pages/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { MainLayout } from '../components/layout/MainLayout';
type Profile = { department?: string; hostel?: string; building?: string } | null;

function getPostRoute(profile: NonNullable<Profile>): string {
if ('department' in profile) return '/faculty/post';
if ('hostel' in profile) return '/warden/post';
return '/centre-head/post';
if ('department' in profile) return '/faculty/posts';
if ('hostel' in profile) return '/warden/posts';
return '/centre-head/posts';
}

export function Landing() {
Expand Down
9 changes: 8 additions & 1 deletion app/src/pages/admin/AdminPostView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,16 @@ export function AdminPostView() {
})
.catch((err: Error & { status?: number }) => {
if (!silent) {
// Not an admin — this is just the non-admin viewer of the same
// post, bounce straight to the plain (non-admin) post view instead
// of showing an access-denied screen.
if (err.status === 403) {
navigate(`/posts/${role}/${post_id}`, { replace: true });
return;
}
setError({ message: err.message, status: err.status });
setLoading(false);
if (err.status === 401 || err.status === 403) setTimeout(() => navigate('/'), 4000);
if (err.status === 401) setTimeout(() => navigate('/'), 4000);
}
});
}, [role, post_id, navigate]);
Expand Down
4 changes: 2 additions & 2 deletions app/src/pages/post/CentreHeadPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function CentreHeadPost() {
setMessage('');

try {
const response = await fetch('/api/post/centrehead', {
const response = await fetch('/api/posts/centrehead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
Expand All @@ -43,7 +43,7 @@ export function CentreHeadPost() {
setMessage(data.success || 'Complaint submitted successfully!');
setFormData({ type_of_post: '', title: '', description: '' });
if (data.post?.id) {
navigate(`/post/centrehead/${data.post.id}`);
navigate(`/posts/centrehead/${data.post.id}`);
}
} else {
setStatus('error');
Expand Down
4 changes: 2 additions & 2 deletions app/src/pages/post/FacultyPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function FacultyPost() {
setMessage('');

try {
const response = await fetch('/api/post/faculty', {
const response = await fetch('/api/posts/faculty', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
Expand All @@ -44,7 +44,7 @@ export function FacultyPost() {
setMessage(data.success || 'Complaint submitted successfully!');
setFormData({ place: '', type_of_post: '', title: '', description: '' });
if (data.post?.id) {
navigate(`/post/faculty/${data.post.id}`);
navigate(`/posts/faculty/${data.post.id}`);
}
} else {
setStatus('error');
Expand Down
8 changes: 4 additions & 4 deletions app/src/pages/post/PostView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function PostView() {

const fetchPost = useCallback(async () => {
try {
const res = await fetch(`/api/post/${role}/${post_id}`, { credentials: 'include' });
const res = await fetch(`/api/posts/${role}/${post_id}`, { credentials: 'include' });
if (!res.ok) {
throw new Error(`Failed to fetch post details (${res.status})`);
}
Expand Down Expand Up @@ -189,8 +189,8 @@ export function PostView() {
const comments = post.comments ?? [];
const editExpired = isEditWindowExpired(post.created_at);

const editBase = isFaculty ? '/api/post/faculty/edit' : isWarden ? '/api/post/warden/edit' : '/api/post/centrehead/edit';
const deleteBase = isFaculty ? '/api/post/faculty/delete' : isWarden ? '/api/post/warden/delete' : '/api/post/centrehead/delete';
const editBase = isFaculty ? '/api/posts/faculty/edit' : isWarden ? '/api/posts/warden/edit' : '/api/posts/centrehead/edit';
const deleteBase = isFaculty ? '/api/posts/faculty/delete' : isWarden ? '/api/posts/warden/delete' : '/api/posts/centrehead/delete';

function startEdit() {
if (!post) return;
Expand Down Expand Up @@ -261,7 +261,7 @@ export function PostView() {
setCommentSubmitting(true);
setCommentError(null);
try {
const res = await fetch(`/api/post/${role}/comment/${post.id}`, {
const res = await fetch(`/api/posts/${role}/comment/${post.id}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
Expand Down
4 changes: 2 additions & 2 deletions app/src/pages/post/WardenPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function WardenPost() {
setMessage('');

try {
const response = await fetch('/api/post/warden', {
const response = await fetch('/api/posts/warden', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
Expand All @@ -44,7 +44,7 @@ export function WardenPost() {
setMessage(data.success || 'Complaint submitted successfully!');
setFormData({ room_number: '', type_of_post: '', title: '', description: '' });
if (data.post?.id) {
navigate(`/post/warden/${data.post.id}`);
navigate(`/posts/warden/${data.post.id}`);
}
} else {
setStatus('error');
Expand Down
16 changes: 8 additions & 8 deletions app/src/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export function Profile() {
const fetchPosts = useCallback((silent = false) => {
if (!profile) return;
let endpoint: string;
if ('department' in profile) endpoint = '/api/post/faculty';
else if ('hostel' in profile) endpoint = '/api/post/warden';
else if ('building' in profile) endpoint = '/api/post/centrehead';
if ('department' in profile) endpoint = '/api/posts/faculty';
else if ('hostel' in profile) endpoint = '/api/posts/warden';
else if ('building' in profile) endpoint = '/api/posts/centrehead';
else return;

if (!silent) setPostsLoading(true);
Expand Down Expand Up @@ -116,12 +116,12 @@ export function Profile() {
let roleLabel = 'User';
let registerRoute = '/';
let role: Role = 'centrehead';
if (isFaculty) { roleLabel = 'Faculty Member'; registerRoute = '/faculty/post'; role = 'faculty'; }
else if (isWarden) { roleLabel = 'Hostel Warden'; registerRoute = '/warden/post'; role = 'warden'; }
else if (isCentreHead) { roleLabel = 'Centre Head'; registerRoute = '/centre-head/post'; role = 'centrehead'; }
if (isFaculty) { roleLabel = 'Faculty Member'; registerRoute = '/faculty/posts'; role = 'faculty'; }
else if (isWarden) { roleLabel = 'Hostel Warden'; registerRoute = '/warden/posts'; role = 'warden'; }
else if (isCentreHead) { roleLabel = 'Centre Head'; registerRoute = '/centre-head/posts'; role = 'centrehead'; }

const editBase = isFaculty ? '/api/post/faculty/edit' : isWarden ? '/api/post/warden/edit' : '/api/post/centrehead/edit';
const deleteBase = isFaculty ? '/api/post/faculty/delete' : isWarden ? '/api/post/warden/delete' : '/api/post/centrehead/delete';
const editBase = isFaculty ? '/api/posts/faculty/edit' : isWarden ? '/api/posts/warden/edit' : '/api/posts/centrehead/edit';
const deleteBase = isFaculty ? '/api/posts/faculty/delete' : isWarden ? '/api/posts/warden/delete' : '/api/posts/centrehead/delete';

const handleLogout = async () => {
try { await fetch('/api/auth/logout', { method: 'POST' }); } catch { /* ignore */ }
Expand Down
32 changes: 16 additions & 16 deletions routes/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@ import (

func PostRoute(e *gin.Engine, h *handlers.PostHandler) {
// APIs for new post
e.POST("/api/post/faculty", middleware.IsAuthenticated(), h.FacultyPost)
e.POST("/api/post/warden", middleware.IsAuthenticated(), h.WardenPost)
e.POST("/api/post/centrehead", middleware.IsAuthenticated(), h.CentreheadPost)
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/post/faculty/edit/:post_id", middleware.IsAuthenticated(), h.FacultyPostEdit)
e.PATCH("/api/post/warden/edit/:post_id", middleware.IsAuthenticated(), h.WardenPostEdit)
e.PATCH("/api/post/centrehead/edit/:post_id", middleware.IsAuthenticated(), h.CentreheadPostEdit)
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/post/faculty/delete/:post_id", middleware.IsAuthenticated(), h.FacultyPostDelete)
e.DELETE("/api/post/warden/delete/:post_id", middleware.IsAuthenticated(), h.WardenPostDelete)
e.DELETE("/api/post/centrehead/delete/:post_id", middleware.IsAuthenticated(), h.CentreheadPostDelete)
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/post/faculty", middleware.IsAuthenticated(), h.GetFacultyPosts)
e.GET("/api/post/warden", middleware.IsAuthenticated(), h.GetWardenPosts)
e.GET("/api/post/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts)
e.GET("/api/post/:role/:post_id", middleware.IsAuthenticated(), h.GetPostByID)
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/post/faculty/comment/:post_id", middleware.IsAuthenticated() ,h.FacultyPostComment)
e.POST("/api/post/warden/comment/:post_id", middleware.IsAuthenticated(), h.WardenPostComment)
e.POST("/api/post/centrehead/comment/:post_id", middleware.IsAuthenticated(), h.CentreheadPostComment)
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)
}
34 changes: 17 additions & 17 deletions test/centrehead_post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestCentreheadPost_Create_Success(t *testing.T) {
"title": "Broken door",
"description": "Main entrance door of LHC is jammed",
}
rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead", body)
rec := doRequest(t, e, http.MethodPost, "/api/posts/centrehead", body)

assertStatus(t, rec, 201)

Expand All @@ -35,23 +35,23 @@ func TestCentreheadPost_Create_Unauthenticated(t *testing.T) {
db := newTestDB(t)
e := newPostRouter(db, noAuth())
body := map[string]any{"type_of_post": "Civil", "title": "x", "description": "y"}
rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead", body)
rec := doRequest(t, e, http.MethodPost, "/api/posts/centrehead", body)
assertStatus(t, rec, 401)
}

func TestCentreheadPost_Create_InvalidBody(t *testing.T) {
db := newTestDB(t)
ch := seedCentrehead(t, db, "ch.badbody@iit.ac.in")
e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead", []string{"bad"})
rec := doRequest(t, e, http.MethodPost, "/api/posts/centrehead", []string{"bad"})
assertStatus(t, rec, 400)
}

func TestCentreheadPost_Create_UserNotFound(t *testing.T) {
db := newTestDB(t)
e := newPostRouter(db, authAs(999, "ghost.ch@iit.ac.in"))
body := map[string]any{"type_of_post": "Civil", "title": "x", "description": "y"}
rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead", body)
rec := doRequest(t, e, http.MethodPost, "/api/posts/centrehead", body)
assertStatus(t, rec, 401)
}

Expand All @@ -64,7 +64,7 @@ func TestCentreheadPostEdit_Success(t *testing.T) {
db.Create(&post)

e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodPatch, "/api/post/centrehead/edit/1", map[string]any{
rec := doRequest(t, e, http.MethodPatch, "/api/posts/centrehead/edit/1", map[string]any{
"title": "new title",
"description": "new desc",
})
Expand All @@ -82,7 +82,7 @@ func TestCentreheadPostEdit_NotFound(t *testing.T) {
db := newTestDB(t)
ch := seedCentrehead(t, db, "ch.editnf@iit.ac.in")
e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodPatch, "/api/post/centrehead/edit/999", map[string]any{"title": "x"})
rec := doRequest(t, e, http.MethodPatch, "/api/posts/centrehead/edit/999", map[string]any{"title": "x"})
assertStatus(t, rec, 404)
}

Expand All @@ -94,14 +94,14 @@ func TestCentreheadPostEdit_WrongAuthor(t *testing.T) {
db.Create(&post)

e := newPostRouter(db, authAs(other.ID, other.Email))
rec := doRequest(t, e, http.MethodPatch, "/api/post/centrehead/edit/1", map[string]any{"title": "hijack"})
rec := doRequest(t, e, http.MethodPatch, "/api/posts/centrehead/edit/1", map[string]any{"title": "hijack"})
assertStatus(t, rec, 403)
}

func TestCentreheadPostEdit_Unauthenticated(t *testing.T) {
db := newTestDB(t)
e := newPostRouter(db, noAuth())
rec := doRequest(t, e, http.MethodPatch, "/api/post/centrehead/edit/1", map[string]any{"title": "x"})
rec := doRequest(t, e, http.MethodPatch, "/api/posts/centrehead/edit/1", map[string]any{"title": "x"})
assertStatus(t, rec, 401)
}

Expand All @@ -113,7 +113,7 @@ func TestCentreheadPostEdit_ExpiredWindow(t *testing.T) {
db.Model(&post).Update("created_at", time.Now().Add(-31*time.Minute))

e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodPatch, "/api/post/centrehead/edit/1", map[string]any{"title": "new"})
rec := doRequest(t, e, http.MethodPatch, "/api/posts/centrehead/edit/1", map[string]any{"title": "new"})
assertStatus(t, rec, 403)
}

Expand All @@ -126,7 +126,7 @@ func TestCentreheadPostDelete_Success(t *testing.T) {
db.Create(&post)

e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodDelete, "/api/post/centrehead/delete/1", nil)
rec := doRequest(t, e, http.MethodDelete, "/api/posts/centrehead/delete/1", nil)

assertStatus(t, rec, 200)

Expand All @@ -141,7 +141,7 @@ func TestCentreheadPostDelete_NotFound(t *testing.T) {
db := newTestDB(t)
ch := seedCentrehead(t, db, "ch.delnf@iit.ac.in")
e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodDelete, "/api/post/centrehead/delete/77", nil)
rec := doRequest(t, e, http.MethodDelete, "/api/posts/centrehead/delete/77", nil)
assertStatus(t, rec, 404)
}

Expand All @@ -153,14 +153,14 @@ func TestCentreheadPostDelete_WrongAuthor(t *testing.T) {
db.Create(&post)

e := newPostRouter(db, authAs(other.ID, other.Email))
rec := doRequest(t, e, http.MethodDelete, "/api/post/centrehead/delete/1", nil)
rec := doRequest(t, e, http.MethodDelete, "/api/posts/centrehead/delete/1", nil)
assertStatus(t, rec, 403)
}

func TestCentreheadPostDelete_Unauthenticated(t *testing.T) {
db := newTestDB(t)
e := newPostRouter(db, noAuth())
rec := doRequest(t, e, http.MethodDelete, "/api/post/centrehead/delete/1", nil)
rec := doRequest(t, e, http.MethodDelete, "/api/posts/centrehead/delete/1", nil)
assertStatus(t, rec, 401)
}

Expand All @@ -172,7 +172,7 @@ func TestCentreheadPostDelete_ExpiredWindow(t *testing.T) {
db.Model(&post).Update("created_at", time.Now().Add(-31*time.Minute))

e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodDelete, "/api/post/centrehead/delete/1", nil)
rec := doRequest(t, e, http.MethodDelete, "/api/posts/centrehead/delete/1", nil)
assertStatus(t, rec, 403)
}

Expand All @@ -185,7 +185,7 @@ func TestGetCentreheadPosts_Success(t *testing.T) {
db.Create(&models.CentreheadPost{CentreheadID: ch.ID, TypeOfPost: models.TypeElectrical, Title: "b", Description: "d"})

e := newPostRouter(db, authAs(ch.ID, ch.Email))
rec := doRequest(t, e, http.MethodGet, "/api/post/centrehead", nil)
rec := doRequest(t, e, http.MethodGet, "/api/posts/centrehead", nil)

assertStatus(t, rec, 200)
out := decodeBody(t, rec)
Expand All @@ -203,7 +203,7 @@ func TestGetCentreheadPosts_OnlyOwn(t *testing.T) {
db.Create(&models.CentreheadPost{CentreheadID: theirs.ID, TypeOfPost: models.TypeCivil, Title: "theirs", Description: "d"})

e := newPostRouter(db, authAs(mine.ID, mine.Email))
rec := doRequest(t, e, http.MethodGet, "/api/post/centrehead", nil)
rec := doRequest(t, e, http.MethodGet, "/api/posts/centrehead", nil)

assertStatus(t, rec, 200)
out := decodeBody(t, rec)
Expand All @@ -215,6 +215,6 @@ func TestGetCentreheadPosts_OnlyOwn(t *testing.T) {
func TestGetCentreheadPosts_Unauthenticated(t *testing.T) {
db := newTestDB(t)
e := newPostRouter(db, noAuth())
rec := doRequest(t, e, http.MethodGet, "/api/post/centrehead", nil)
rec := doRequest(t, e, http.MethodGet, "/api/posts/centrehead", nil)
assertStatus(t, rec, 401)
}
Loading
Loading