Skip to content
Open
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: 7 additions & 1 deletion backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/egeuysall/cove/internal/api"
supabase "github.com/egeuysall/cove/internal/supabase"
generated "github.com/egeuysall/cove/internal/supabase/generated"
"github.com/egeuysall/cove/internal/websocket"
"github.com/joho/godotenv"
)

Expand All @@ -26,7 +27,12 @@ func main() {

utils.Init(queries)

router := api.Router()
// Initialize WebSocket hub
hub := websocket.NewHub()
go hub.Run()
utils.SetHub(hub)

router := api.Router(hub)

portStr := os.Getenv("PORT")

Expand Down
1 change: 1 addition & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (

require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
Expand Down
7 changes: 6 additions & 1 deletion backend/internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (

"github.com/egeuysall/cove/internal/handlers"
appmid "github.com/egeuysall/cove/internal/middleware"
"github.com/egeuysall/cove/internal/websocket"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
)

func Router() *chi.Mux {
func Router(hub *websocket.Hub) *chi.Mux {
r := chi.NewRouter()

// Global middleware
Expand All @@ -33,6 +34,10 @@ func Router() *chi.Mux {
r.Route("/v1", func(r chi.Router) {
r.Use(appmid.RequireAuth())

// WebSocket
wsHandler := websocket.NewHandler(hub)
r.Get("/ws", wsHandler.ServeWs)

// Groups
r.Post("/groups", handlers.HandleCreateGroup)
r.Get("/groups", handlers.HandleGetGroupsByUser)
Expand Down
41 changes: 39 additions & 2 deletions backend/internal/handlers/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,14 @@ func HandleCreateLink(w http.ResponseWriter, r *http.Request) {
return
}

utils.SendJson(w, convertToResponse(link), http.StatusCreated)
linkResponse := convertToResponse(link)

// Broadcast to WebSocket clients
if utils.Hub != nil {
utils.Hub.BroadcastToGroup(req.GroupID, "link_created", linkResponse)
}

utils.SendJson(w, linkResponse, http.StatusCreated)
}

func HandleGetLinkById(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -263,7 +270,14 @@ func HandleUpdateLinkComment(w http.ResponseWriter, r *http.Request) {
return
}

utils.SendJson(w, convertToResponse(updatedLink), http.StatusOK)
linkResponse := convertToResponse(updatedLink)

// Broadcast to WebSocket clients
if utils.Hub != nil {
utils.Hub.BroadcastToGroup(utils.UUIDToString(updatedLink.GroupID), "link_updated", linkResponse)
}

utils.SendJson(w, linkResponse, http.StatusOK)
}

func HandleDeleteLink(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -291,6 +305,22 @@ func HandleDeleteLink(w http.ResponseWriter, r *http.Request) {
return
}

// Get link before deleting to broadcast group ID
link, err := utils.Queries.GetLinkByID(r.Context(), linkId)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
utils.SendError(w, "Link not found", http.StatusNotFound)
return
}
utils.SendError(w, "Failed to get link", http.StatusInternalServerError)
return
}

if link.UserID != userId {
utils.SendError(w, "Not authorized to delete this link", http.StatusForbidden)
return
}

deleteParams := supabase.DeleteLinkParams{
ID: linkId,
UserID: userId,
Expand All @@ -302,6 +332,13 @@ func HandleDeleteLink(w http.ResponseWriter, r *http.Request) {
return
}

// Broadcast to WebSocket clients
if utils.Hub != nil {
utils.Hub.BroadcastToGroup(utils.UUIDToString(link.GroupID), "link_deleted", map[string]string{
"id": utils.UUIDToString(link.ID),
})
}

utils.SendJson(w, "Link deleted successfully", http.StatusOK)
}

Expand Down
22 changes: 15 additions & 7 deletions backend/internal/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,26 @@ func RequireAuth() func(http.Handler) http.Handler {
return
}

var tokenStr string

// Try to get token from Authorization header first
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
utils.SendError(w, "Unauthorized: missing Authorization header", http.StatusUnauthorized)
return
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
tokenStr = parts[1]
}
}

// If no token in header, try query parameter (for WebSocket connections)
if tokenStr == "" {
tokenStr = r.URL.Query().Get("token")
}

parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
utils.SendError(w, "Unauthorized: invalid Authorization header format", http.StatusUnauthorized)
if tokenStr == "" {
utils.SendError(w, "Unauthorized: missing token", http.StatusUnauthorized)
return
}
tokenStr := parts[1]

// Parse and validate the token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
Expand Down
11 changes: 11 additions & 0 deletions backend/internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,14 @@ type LinkResponse struct {
Comment string `json:"comment,omitempty"`
CreatedAt time.Time `json:"created_at"`
}

// WebSocket message types
type WebSocketMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
}

type SubscribeMessage struct {
Type string `json:"type"`
GroupID string `json:"groupId"`
}
6 changes: 6 additions & 0 deletions backend/internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ package utils
import (
"encoding/json"
generated "github.com/egeuysall/cove/internal/supabase/generated"
"github.com/egeuysall/cove/internal/websocket"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"log"
"net/http"
)

var Queries *generated.Queries
var Hub *websocket.Hub

func Init(q *generated.Queries) {
Queries = q
}

func SetHub(h *websocket.Hub) {
Hub = h
}

func SendJson(w http.ResponseWriter, message interface{}, statusCode int) {
w.WriteHeader(statusCode)

Expand Down
65 changes: 65 additions & 0 deletions backend/internal/websocket/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package websocket

import (
"log"
"net/http"

"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// Allow connections from configured origins
origin := r.Header.Get("Origin")
return origin == "https://www.cove.egeuysal.com" ||
origin == "http://localhost:3000" ||
origin == "http://localhost:3001"
},
}

// Handler handles WebSocket upgrade requests
type Handler struct {
Hub *Hub
}

// NewHandler creates a new WebSocket handler
func NewHandler(hub *Hub) *Handler {
return &Handler{
Hub: hub,
}
}

// ServeWs handles WebSocket requests from clients
func (h *Handler) ServeWs(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by JWT middleware)
userID, ok := r.Context().Value("userID").(string)
if !ok || userID == "" {
log.Printf("No user ID in context")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Failed to upgrade connection: %v", err)
return
}

client := &Client{
Hub: h.Hub,
Conn: conn,
Send: make(chan []byte, 256),
UserID: userID,
GroupIDs: make(map[string]bool),
}

client.Hub.register <- client

// Start goroutines for reading and writing
go client.WritePump()
go client.ReadPump()

log.Printf("WebSocket connection established for user: %s", userID)
}
Loading