feat: Add Groups system - database schema, seed data, and backend handlers
This commit is contained in:
parent
b3abcf9c6e
commit
21a1d1e8ef
561
go-backend/internal/handlers/groups_handler.go
Normal file
561
go-backend/internal/handlers/groups_handler.go
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type GroupsHandler struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewGroupsHandler(db *pgxpool.Pool) *GroupsHandler {
|
||||
return &GroupsHandler{db: db}
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
BannerURL *string `json:"banner_url"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
MemberCount int `json:"member_count"`
|
||||
PostCount int `json:"post_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UserRole *string `json:"user_role,omitempty"`
|
||||
IsMember bool `json:"is_member"`
|
||||
HasPending bool `json:"has_pending_request,omitempty"`
|
||||
}
|
||||
|
||||
type GroupMember struct {
|
||||
ID string `json:"id"`
|
||||
GroupID string `json:"group_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt time.Time `json:"joined_at"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Avatar *string `json:"avatar_url,omitempty"`
|
||||
}
|
||||
|
||||
type JoinRequest struct {
|
||||
ID string `json:"id"`
|
||||
GroupID string `json:"group_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Status string `json:"status"`
|
||||
Message *string `json:"message"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
ReviewedBy *string `json:"reviewed_by"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Avatar *string `json:"avatar_url,omitempty"`
|
||||
}
|
||||
|
||||
// ListGroups returns all groups with optional category filter
|
||||
func (h *GroupsHandler) ListGroups(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
category := c.Query("category")
|
||||
page := c.DefaultQuery("page", "0")
|
||||
limit := c.DefaultQuery("limit", "20")
|
||||
|
||||
query := `
|
||||
SELECT g.id, g.name, g.description, g.category, g.avatar_url, g.banner_url,
|
||||
g.is_private, g.created_by, g.member_count, g.post_count, g.created_at, g.updated_at,
|
||||
gm.role,
|
||||
EXISTS(SELECT 1 FROM group_members WHERE group_id = g.id AND user_id = $1) as is_member,
|
||||
EXISTS(SELECT 1 FROM group_join_requests WHERE group_id = g.id AND user_id = $1 AND status = 'pending') as has_pending
|
||||
FROM groups g
|
||||
LEFT JOIN group_members gm ON g.id = gm.group_id AND gm.user_id = $1
|
||||
WHERE ($2 = '' OR g.category = $2)
|
||||
ORDER BY g.member_count DESC, g.created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`
|
||||
|
||||
rows, err := h.db.Query(c.Request.Context(), query, userID, category, limit, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch groups"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := []Group{}
|
||||
for rows.Next() {
|
||||
var g Group
|
||||
err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Category, &g.AvatarURL, &g.BannerURL,
|
||||
&g.IsPrivate, &g.CreatedBy, &g.MemberCount, &g.PostCount, &g.CreatedAt, &g.UpdatedAt,
|
||||
&g.UserRole, &g.IsMember, &g.HasPending)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"groups": groups})
|
||||
}
|
||||
|
||||
// GetMyGroups returns groups the user is a member of
|
||||
func (h *GroupsHandler) GetMyGroups(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
query := `
|
||||
SELECT g.id, g.name, g.description, g.category, g.avatar_url, g.banner_url,
|
||||
g.is_private, g.created_by, g.member_count, g.post_count, g.created_at, g.updated_at,
|
||||
gm.role
|
||||
FROM groups g
|
||||
JOIN group_members gm ON g.id = gm.group_id
|
||||
WHERE gm.user_id = $1
|
||||
ORDER BY gm.joined_at DESC
|
||||
`
|
||||
|
||||
rows, err := h.db.Query(c.Request.Context(), query, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch groups"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := []Group{}
|
||||
for rows.Next() {
|
||||
var g Group
|
||||
g.IsMember = true
|
||||
err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Category, &g.AvatarURL, &g.BannerURL,
|
||||
&g.IsPrivate, &g.CreatedBy, &g.MemberCount, &g.PostCount, &g.CreatedAt, &g.UpdatedAt,
|
||||
&g.UserRole)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"groups": groups})
|
||||
}
|
||||
|
||||
// GetSuggestedGroups returns suggested groups for the user
|
||||
func (h *GroupsHandler) GetSuggestedGroups(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
limit := c.DefaultQuery("limit", "10")
|
||||
|
||||
query := `SELECT * FROM get_suggested_groups($1, $2)`
|
||||
|
||||
rows, err := h.db.Query(c.Request.Context(), query, userID, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch suggestions"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type SuggestedGroup struct {
|
||||
Group
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
groups := []SuggestedGroup{}
|
||||
for rows.Next() {
|
||||
var sg SuggestedGroup
|
||||
err := rows.Scan(&sg.ID, &sg.Name, &sg.Description, &sg.Category, &sg.AvatarURL,
|
||||
&sg.IsPrivate, &sg.MemberCount, &sg.PostCount, &sg.Reason)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sg.IsMember = false
|
||||
groups = append(groups, sg)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"suggestions": groups})
|
||||
}
|
||||
|
||||
// GetGroup returns a single group by ID
|
||||
func (h *GroupsHandler) GetGroup(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
|
||||
query := `
|
||||
SELECT g.id, g.name, g.description, g.category, g.avatar_url, g.banner_url,
|
||||
g.is_private, g.created_by, g.member_count, g.post_count, g.created_at, g.updated_at,
|
||||
gm.role,
|
||||
EXISTS(SELECT 1 FROM group_members WHERE group_id = g.id AND user_id = $2) as is_member,
|
||||
EXISTS(SELECT 1 FROM group_join_requests WHERE group_id = g.id AND user_id = $2 AND status = 'pending') as has_pending
|
||||
FROM groups g
|
||||
LEFT JOIN group_members gm ON g.id = gm.group_id AND gm.user_id = $2
|
||||
WHERE g.id = $1
|
||||
`
|
||||
|
||||
var g Group
|
||||
err := h.db.QueryRow(c.Request.Context(), query, groupID, userID).Scan(
|
||||
&g.ID, &g.Name, &g.Description, &g.Category, &g.AvatarURL, &g.BannerURL,
|
||||
&g.IsPrivate, &g.CreatedBy, &g.MemberCount, &g.PostCount, &g.CreatedAt, &g.UpdatedAt,
|
||||
&g.UserRole, &g.IsMember, &g.HasPending)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch group"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"group": g})
|
||||
}
|
||||
|
||||
// CreateGroup creates a new group
|
||||
func (h *GroupsHandler) CreateGroup(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Description string `json:"description" binding:"max=300"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
BannerURL *string `json:"banner_url"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize name for uniqueness check
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
|
||||
tx, err := h.db.Begin(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create group"})
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(c.Request.Context())
|
||||
|
||||
// Create group
|
||||
var groupID string
|
||||
err = tx.QueryRow(c.Request.Context(), `
|
||||
INSERT INTO groups (name, description, category, is_private, created_by, avatar_url, banner_url)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`, req.Name, req.Description, req.Category, req.IsPrivate, userID, req.AvatarURL, req.BannerURL).Scan(&groupID)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "A group with this name already exists"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create group"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add creator as owner
|
||||
_, err = tx.Exec(c.Request.Context(), `
|
||||
INSERT INTO group_members (group_id, user_id, role)
|
||||
VALUES ($1, $2, 'owner')
|
||||
`, groupID, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add owner"})
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(c.Request.Context()); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create group"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"group_id": groupID, "message": "Group created successfully"})
|
||||
}
|
||||
|
||||
// JoinGroup allows a user to join a public group or request to join a private group
|
||||
func (h *GroupsHandler) JoinGroup(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
// Check if group exists and is private
|
||||
var isPrivate bool
|
||||
err := h.db.QueryRow(c.Request.Context(), `SELECT is_private FROM groups WHERE id = $1`, groupID).Scan(&isPrivate)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join group"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already a member
|
||||
var exists bool
|
||||
err = h.db.QueryRow(c.Request.Context(), `
|
||||
SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = $1 AND user_id = $2)
|
||||
`, groupID, userID).Scan(&exists)
|
||||
if err == nil && exists {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Already a member"})
|
||||
return
|
||||
}
|
||||
|
||||
if isPrivate {
|
||||
// Create join request
|
||||
_, err = h.db.Exec(c.Request.Context(), `
|
||||
INSERT INTO group_join_requests (group_id, user_id, message)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (group_id, user_id, status) DO NOTHING
|
||||
`, groupID, userID, req.Message)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create join request"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Join request sent", "status": "pending"})
|
||||
} else {
|
||||
// Join immediately
|
||||
_, err = h.db.Exec(c.Request.Context(), `
|
||||
INSERT INTO group_members (group_id, user_id, role)
|
||||
VALUES ($1, $2, 'member')
|
||||
`, groupID, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join group"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Joined successfully", "status": "joined"})
|
||||
}
|
||||
}
|
||||
|
||||
// LeaveGroup allows a user to leave a group
|
||||
func (h *GroupsHandler) LeaveGroup(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
|
||||
// Check if user is owner
|
||||
var role string
|
||||
err := h.db.QueryRow(c.Request.Context(), `
|
||||
SELECT role FROM group_members WHERE group_id = $1 AND user_id = $2
|
||||
`, groupID, userID).Scan(&role)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Not a member of this group"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to leave group"})
|
||||
return
|
||||
}
|
||||
|
||||
if role == "owner" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Owner must transfer ownership or delete group before leaving"})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove member
|
||||
_, err = h.db.Exec(c.Request.Context(), `
|
||||
DELETE FROM group_members WHERE group_id = $1 AND user_id = $2
|
||||
`, groupID, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to leave group"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Left group successfully"})
|
||||
}
|
||||
|
||||
// GetGroupMembers returns members of a group
|
||||
func (h *GroupsHandler) GetGroupMembers(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
page := c.DefaultQuery("page", "0")
|
||||
limit := c.DefaultQuery("limit", "50")
|
||||
|
||||
query := `
|
||||
SELECT gm.id, gm.group_id, gm.user_id, gm.role, gm.joined_at,
|
||||
p.username, p.avatar_url
|
||||
FROM group_members gm
|
||||
JOIN profiles p ON gm.user_id = p.user_id
|
||||
WHERE gm.group_id = $1
|
||||
ORDER BY
|
||||
CASE gm.role
|
||||
WHEN 'owner' THEN 1
|
||||
WHEN 'admin' THEN 2
|
||||
WHEN 'moderator' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
gm.joined_at ASC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
rows, err := h.db.Query(c.Request.Context(), query, groupID, limit, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch members"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
members := []GroupMember{}
|
||||
for rows.Next() {
|
||||
var m GroupMember
|
||||
err := rows.Scan(&m.ID, &m.GroupID, &m.UserID, &m.Role, &m.JoinedAt, &m.Username, &m.Avatar)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"members": members})
|
||||
}
|
||||
|
||||
// GetPendingRequests returns pending join requests for a group (admin only)
|
||||
func (h *GroupsHandler) GetPendingRequests(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
|
||||
// Check if user is admin/owner
|
||||
var role string
|
||||
err := h.db.QueryRow(c.Request.Context(), `
|
||||
SELECT role FROM group_members WHERE group_id = $1 AND user_id = $2
|
||||
`, groupID, userID).Scan(&role)
|
||||
|
||||
if err != nil || (role != "owner" && role != "admin") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT jr.id, jr.group_id, jr.user_id, jr.status, jr.message, jr.created_at,
|
||||
jr.reviewed_at, jr.reviewed_by, p.username, p.avatar_url
|
||||
FROM group_join_requests jr
|
||||
JOIN profiles p ON jr.user_id = p.user_id
|
||||
WHERE jr.group_id = $1 AND jr.status = 'pending'
|
||||
ORDER BY jr.created_at ASC
|
||||
`
|
||||
|
||||
rows, err := h.db.Query(c.Request.Context(), query, groupID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch requests"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
requests := []JoinRequest{}
|
||||
for rows.Next() {
|
||||
var jr JoinRequest
|
||||
err := rows.Scan(&jr.ID, &jr.GroupID, &jr.UserID, &jr.Status, &jr.Message, &jr.CreatedAt,
|
||||
&jr.ReviewedAt, &jr.ReviewedBy, &jr.Username, &jr.Avatar)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
requests = append(requests, jr)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"requests": requests})
|
||||
}
|
||||
|
||||
// ApproveJoinRequest approves a join request (admin only)
|
||||
func (h *GroupsHandler) ApproveJoinRequest(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
requestID := c.Param("requestId")
|
||||
|
||||
// Check if user is admin/owner
|
||||
var role string
|
||||
err := h.db.QueryRow(c.Request.Context(), `
|
||||
SELECT role FROM group_members WHERE group_id = $1 AND user_id = $2
|
||||
`, groupID, userID).Scan(&role)
|
||||
|
||||
if err != nil || (role != "owner" && role != "admin") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := h.db.Begin(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve request"})
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(c.Request.Context())
|
||||
|
||||
// Get requester user ID
|
||||
var requesterID string
|
||||
err = tx.QueryRow(c.Request.Context(), `
|
||||
SELECT user_id FROM group_join_requests WHERE id = $1 AND group_id = $2 AND status = 'pending'
|
||||
`, requestID, groupID).Scan(&requesterID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Request not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update request status
|
||||
_, err = tx.Exec(c.Request.Context(), `
|
||||
UPDATE group_join_requests
|
||||
SET status = 'approved', reviewed_at = NOW(), reviewed_by = $1
|
||||
WHERE id = $2
|
||||
`, userID, requestID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add user as member
|
||||
_, err = tx.Exec(c.Request.Context(), `
|
||||
INSERT INTO group_members (group_id, user_id, role)
|
||||
VALUES ($1, $2, 'member')
|
||||
`, groupID, requesterID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add member"})
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(c.Request.Context()); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve request"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Request approved"})
|
||||
}
|
||||
|
||||
// RejectJoinRequest rejects a join request (admin only)
|
||||
func (h *GroupsHandler) RejectJoinRequest(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
groupID := c.Param("id")
|
||||
requestID := c.Param("requestId")
|
||||
|
||||
// Check if user is admin/owner
|
||||
var role string
|
||||
err := h.db.QueryRow(c.Request.Context(), `
|
||||
SELECT role FROM group_members WHERE group_id = $1 AND user_id = $2
|
||||
`, groupID, userID).Scan(&role)
|
||||
|
||||
if err != nil || (role != "owner" && role != "admin") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.db.Exec(c.Request.Context(), `
|
||||
UPDATE group_join_requests
|
||||
SET status = 'rejected', reviewed_at = NOW(), reviewed_by = $1
|
||||
WHERE id = $2 AND group_id = $3 AND status = 'pending'
|
||||
`, userID, requestID, groupID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject request"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Request rejected"})
|
||||
}
|
||||
Loading…
Reference in a new issue