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
15 changes: 15 additions & 0 deletions db/migrations/00026_add_last_activity.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE newf ADD COLUMN last_activity TIMESTAMP NULL;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE INDEX idx_newf_last_activity ON newf (last_activity);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS idx_newf_last_activity;
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE newf DROP COLUMN last_activity;
-- +goose StatementEnd
6 changes: 6 additions & 0 deletions handlers/auth/auth_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Something went wrong during login"})
}

// Record last activity timestamp (best-effort, does not block login)
if _, err := h.DB.Exec(`UPDATE newf SET last_activity = NOW() WHERE email = $1`, storedNewf.Email); err != nil {
utils.LogMessage(utils.LevelError, "Failed to update last_activity")
utils.LogLineKeyValue(utils.LevelError, "Error", err)
}
Comment thread
luclu7 marked this conversation as resolved.

// Send notification email about new sign-in
if h.EmailService != nil {
// Capture data for the goroutine
Expand Down
6 changes: 3 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,15 @@ func main() {
routes.SetupUserRoutes(app, db, notificationService)
routes.SetupTraqRoutes(app, db)
routes.SetupFileRoutes(app, db, r2Service)
routes.SetupRestaurantRoutes(app, restHandler)
routes.SetupClubRoutes(app, clubsHandler)
routes.SetupRestaurantRoutes(app, db, restHandler)
routes.SetupClubRoutes(app, db, clubsHandler)
routes.SetupPlanningRoutes(app, db)
routes.SetupNotificationRoutes(app, db, notificationService)
routes.SetupStatisticsRoutes(app, db, statisticsService)
routes.SetupWashingMachineRoutes(app)
routes.SetupWeatherRoutes(app, weatherHandler)
routes.SetupBusDepartureRoutes(app, gtfsService)
routes.SetupEventRoutes(app, eventHandler)
routes.SetupEventRoutes(app, db, eventHandler)
routes.SetupReservationRoutes(app, db, reservationDiscordService)
routes.SetupBassineRoutes(app, db)
routes.SetupAdminRoutes(app, db)
Expand Down
86 changes: 55 additions & 31 deletions middlewares/JWT.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,75 @@
package middlewares

import (
"context"
"database/sql"
"strings"
"time"

"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/plugimt/transat-backend/utils"
)

func JWTMiddleware(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
// sémaphore partagé
var activitySem = make(chan struct{}, 50)

utils.LogHeader("📧 JWT Middleware")
func JWTMiddleware(db *sql.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")

if authHeader == "" {
utils.LogMessage(utils.LevelError, "Missing token")
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token"})
}
utils.LogHeader("📧 JWT Middleware")

tokenString := authHeader
if strings.HasPrefix(authHeader, "Bearer ") {
tokenString = authHeader[7:]
}
if authHeader == "" {
utils.LogMessage(utils.LevelError, "Missing token")
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token"})
}

token, err := utils.ValidateJWT(tokenString)
if err != nil {
utils.LogMessage(utils.LevelError, "Invalid token")
utils.LogLineKeyValue(utils.LevelError, "Error", err)
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid token"})
}
tokenString := authHeader
if strings.HasPrefix(authHeader, "Bearer ") {
tokenString = authHeader[7:]
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
utils.LogMessage(utils.LevelError, "Invalid claims")
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid claims"})
}
token, err := utils.ValidateJWT(tokenString)
if err != nil {
utils.LogMessage(utils.LevelError, "Invalid token")
utils.LogLineKeyValue(utils.LevelError, "Error", err)
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid token"})
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
utils.LogMessage(utils.LevelError, "Invalid claims")
utils.LogFooter()
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid claims"})
}

c.Locals("email", claims["email"])
email, _ := claims["email"].(string)
c.Locals("email", email)

// Note: We only store email in context - roles are checked from database for security
// Note: We only store email in context - roles are checked from database for security

utils.LogMessage(utils.LevelInfo, "Token is valid")
utils.LogLineKeyValue(utils.LevelInfo, "Email", claims["email"])
utils.LogFooter()
utils.LogMessage(utils.LevelInfo, "Token is valid")
utils.LogLineKeyValue(utils.LevelInfo, "Email", email)
utils.LogFooter()

return c.Next()
// Update last_activity at most once per day; bounded to 50 concurrent writes.
select {
case activitySem <- struct{}{}:
go func() {
defer func() { <-activitySem }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := db.ExecContext(ctx, `UPDATE newf SET last_activity = NOW() WHERE email = $1 AND (last_activity IS NULL OR last_activity < NOW() - INTERVAL '1 day')`, email); err != nil {
utils.LogMessage(utils.LevelError, "Failed to update last_activity")
utils.LogLineKeyValue(utils.LevelError, "Error", err)
}
}()
default:
}

return c.Next()
}
}
23 changes: 12 additions & 11 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ type Newf struct {
NewPasswordConfirmation string `json:"new_password_confirmation"`
PasswordUpdatedDate string `json:"password_updated_date"`
VerificationCodeData
CreationDate string `json:"creation_date"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
ProfilePicture string `json:"profile_picture"`
NotificationToken string `json:"notification_token"`
GraduationYear *int `json:"graduation_year,omitempty"`
FormationName string `json:"formation_name"`
Campus string `json:"campus"`
TotalUsers int `json:"total_newf"`
Language string `json:"language"`
CreationDate string `json:"creation_date"`
LastActivity *string `json:"last_activity,omitempty"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
ProfilePicture string `json:"profile_picture"`
NotificationToken string `json:"notification_token"`
GraduationYear *int `json:"graduation_year,omitempty"`
FormationName string `json:"formation_name"`
Campus string `json:"campus"`
TotalUsers int `json:"total_newf"`
Language string `json:"language"`
}

// VerificationCodeData holds verification code details.
Expand Down
2 changes: 1 addition & 1 deletion routes/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func SetupAdminRoutes(router fiber.Router, db *sql.DB) {
adminHandler := admin.NewAdminHandler(db)

adminGroup := router.Group("/admin",
middlewares.JWTMiddleware,
middlewares.JWTMiddleware(db),
utils.EnhanceSentryEventWithEmail,
middlewares.AdminAuthMiddleware(db),
)
Expand Down
2 changes: 1 addition & 1 deletion routes/bassine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func SetupBassineRoutes(router fiber.Router, db *sql.DB) {
// Initialize Reservation Handler
bassineHandler := bassine.NewBassineHandler(db)

bassineGroup := router.Group("/bassine", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail)
bassineGroup := router.Group("/bassine", middlewares.JWTMiddleware(db), utils.EnhanceSentryEventWithEmail)

// Root reservation routes
bassineGroup.Patch("", bassineHandler.IncrementBassine) // Increments or decrements the bassine count
Expand Down
6 changes: 4 additions & 2 deletions routes/club.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package routes

import (
"database/sql"

"github.com/plugimt/transat-backend/handlers/club" // Import the club handlers
"github.com/plugimt/transat-backend/middlewares"
"github.com/plugimt/transat-backend/utils"

"github.com/gofiber/fiber/v2"
)

func SetupClubRoutes(router fiber.Router, clubHandler *club.ClubHandler) {
func SetupClubRoutes(router fiber.Router, db *sql.DB, clubHandler *club.ClubHandler) {

clubGroup := router.Group("/club", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail)
clubGroup := router.Group("/club", middlewares.JWTMiddleware(db), utils.EnhanceSentryEventWithEmail)

clubGroup.Get("", clubHandler.GetClub)
clubGroup.Get("/", clubHandler.GetClub)
Expand Down
6 changes: 4 additions & 2 deletions routes/event.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package routes

import (
"database/sql"

"github.com/plugimt/transat-backend/handlers/event" // Import the event handlers
"github.com/plugimt/transat-backend/middlewares"
"github.com/plugimt/transat-backend/utils"

"github.com/gofiber/fiber/v2"
)

func SetupEventRoutes(router fiber.Router, eventHandler *event.EventHandler) {
func SetupEventRoutes(router fiber.Router, db *sql.DB, eventHandler *event.EventHandler) {

eventGroup := router.Group("/event", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail)
eventGroup := router.Group("/event", middlewares.JWTMiddleware(db), utils.EnhanceSentryEventWithEmail)

eventGroup.Get("", eventHandler.GetEvent)
eventGroup.Get("/", eventHandler.GetEvent)
Expand Down
8 changes: 4 additions & 4 deletions routes/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ func SetupFileRoutes(router fiber.Router, db *sql.DB, r2Service *services.R2Serv

// Authenticated routes for managing files
// Placed directly under /api, protected by JWT middleware
router.Post("/upload", middlewares.JWTMiddleware, fileHandler.UploadFile)
router.Get("/files", middlewares.JWTMiddleware, fileHandler.ListUserFiles) // List user's own files
router.Delete("/files/:filename", middlewares.JWTMiddleware, fileHandler.DeleteFile) // Delete user's own file (by original name)
router.Post("/upload", middlewares.JWTMiddleware(db), fileHandler.UploadFile)
router.Get("/files", middlewares.JWTMiddleware(db), fileHandler.ListUserFiles) // List user's own files
router.Delete("/files/:filename", middlewares.JWTMiddleware(db), fileHandler.DeleteFile) // Delete user's own file (by original name)

// Admin route (example - secure appropriately!)
// Requires admin check within handler or a specific admin middleware
router.Get("/all-files", middlewares.JWTMiddleware, fileHandler.ListAllFiles) // List all files on server
router.Get("/all-files", middlewares.JWTMiddleware(db), fileHandler.ListAllFiles) // List all files on server
}
2 changes: 1 addition & 1 deletion routes/notification_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func SetupNotificationRoutes(router fiber.Router, db *sql.DB, notificationServic
notificationGroup := router.Group("/notifications")

// Apply authentication middleware
notificationGroup.Use(middlewares.JWTMiddleware)
notificationGroup.Use(middlewares.JWTMiddleware(db))

// route 1: Send notification to a specific user by email
notificationGroup.Post("/send-to-user", func(c *fiber.Ctx) error {
Expand Down
2 changes: 1 addition & 1 deletion routes/planning.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func SetupPlanningRoutes(router fiber.Router, db *sql.DB) {
planningHandler := planning.NewPlanningHandler(db)

planningGroup := router.Group("/planning", middlewares.JWTMiddleware)
planningGroup := router.Group("/planning", middlewares.JWTMiddleware(db))

// GET /planning/users - List users with only NewfID, FirstName, LastName, PassID
planningGroup.Get("/users", planningHandler.GetUsersWithPassID)
Expand Down
2 changes: 1 addition & 1 deletion routes/reservation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func SetupReservationRoutes(router fiber.Router, db *sql.DB, discordService *ser
// Initialize Reservation Handler
reservationHandler := reservation.NewReservationHandler(db, discordService)

reservationGroup := router.Group("/reservation", middlewares.JWTMiddleware)
reservationGroup := router.Group("/reservation", middlewares.JWTMiddleware(db))

// Root reservation routes
reservationGroup.Get("", reservationHandler.GetReservationItems) // Returns root categories and items TODO: Add filders && add reservations overview
Expand Down
6 changes: 4 additions & 2 deletions routes/restaurant.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package routes

import (
"database/sql"

"github.com/gofiber/fiber/v2"
restaurantHandler "github.com/plugimt/transat-backend/handlers/restaurant"
"github.com/plugimt/transat-backend/middlewares"
"github.com/plugimt/transat-backend/utils"
)

func SetupRestaurantRoutes(router fiber.Router, h *restaurantHandler.RestaurantHandler) {
func SetupRestaurantRoutes(router fiber.Router, db *sql.DB, h *restaurantHandler.RestaurantHandler) {
// Public routes (no authentication required)

restaurant := router.Group("/restaurant", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail)
restaurant := router.Group("/restaurant", middlewares.JWTMiddleware(db), utils.EnhanceSentryEventWithEmail)

restaurant.Get("/", h.GetRestaurantMenu)
restaurant.Get("", h.GetRestaurantMenu)
Expand Down
2 changes: 1 addition & 1 deletion routes/statistics.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ func SetupStatisticsRoutes(router fiber.Router, db *sql.DB, statisticsService *s
statsGroup.Get("/top-users", statsHandler.GetTopUserStatistics)

// Admin endpoints
adminStatsGroup := router.Group("/statistics", middlewares.JWTMiddleware, middlewares.AdminAuthMiddleware(db))
adminStatsGroup := router.Group("/statistics", middlewares.JWTMiddleware(db), middlewares.AdminAuthMiddleware(db))
adminStatsGroup.Get("/dashboard", statsHandler.GetDashboardStatistics)
}
2 changes: 1 addition & 1 deletion routes/traq.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func SetupTraqRoutes(router fiber.Router, db *sql.DB) {
traqGroup := router.Group("/traq")

adminOnly := []fiber.Handler{
middlewares.JWTMiddleware,
middlewares.JWTMiddleware(db),
utils.EnhanceSentryEventWithEmail,
middlewares.AdminAuthMiddleware(db),
}
Expand Down
2 changes: 1 addition & 1 deletion routes/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func SetupUserRoutes(router fiber.Router, db *sql.DB, notifService *services.Not

// Group routes that require JWT authentication
// Changed group name from "/newf" to "/user" for clarity
userGroup := router.Group("/newf", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail)
userGroup := router.Group("/newf", middlewares.JWTMiddleware(db), utils.EnhanceSentryEventWithEmail)

// Profile routes
userGroup.Get("/me", userHandler.GetNewf) // GET /api/user/me
Expand Down