using cookie for auth

This commit is contained in:
Nimer Farahty 2025-06-07 16:06:35 +03:00
parent cd6ceb3f96
commit 8296b813c9
7 changed files with 125 additions and 59 deletions

View File

@ -3,6 +3,7 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"git.farahty.com/nimer/go-mongo/models" "git.farahty.com/nimer/go-mongo/models"
) )
@ -16,8 +17,16 @@ var (
StatusKey = &contextKey{"status"} StatusKey = &contextKey{"status"}
ExpiryKey = &contextKey{"expiry"} ExpiryKey = &contextKey{"expiry"}
LoadersKey = &contextKey{"dataloaders"} LoadersKey = &contextKey{"dataloaders"}
WriterKye = &contextKey{"writer"}
) )
func WriterFor(ctx context.Context) (*http.ResponseWriter, error) {
if writer, ok := ctx.Value(WriterKye).(*http.ResponseWriter); ok {
return writer, nil
}
return nil, fmt.Errorf("no writer found in context")
}
// Retrieves the current user from the context // Retrieves the current user from the context
func CurrentUser(ctx context.Context) (*models.UserJWT, error) { func CurrentUser(ctx context.Context) (*models.UserJWT, error) {
user, _ := ctx.Value(UserKey).(*models.UserJWT) user, _ := ctx.Value(UserKey).(*models.UserJWT)

View File

@ -30,7 +30,7 @@ func getTokenFromHeader(r *http.Request) (string, error) {
func getTokenFromCookie(r *http.Request) (string, error) { func getTokenFromCookie(r *http.Request) (string, error) {
cookie, err := r.Cookie("app-access-token") cookie, err := r.Cookie("access_token")
if err != nil { if err != nil {
return "", fmt.Errorf("there is no authorization cookie provided") return "", fmt.Errorf("there is no authorization cookie provided")

View File

@ -3,6 +3,7 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
@ -17,18 +18,33 @@ func ExpiryMiddleware(ctx context.Context, next graphql.ResponseHandler) *graphq
return next(ctx) return next(ctx)
} }
// add response writer to context for GraphQL resolvers
func WriterMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), WriterKye, &rw)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
// AuthMiddleware parses JWT token and injects user context for HTTP requests // AuthMiddleware parses JWT token and injects user context for HTTP requests
func AuthMiddleware(next http.Handler) http.Handler { func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
tokenStr, err := getTokenFromHeader(r) headerToken, headerErr := getTokenFromHeader(r)
if err != nil { cookieToken, cookieErr := getTokenFromCookie(r)
ctx := SetStatus(r.Context(), err.Error())
if headerErr != nil && cookieErr != nil {
ctx := SetStatus(r.Context(), headerErr.Error())
next.ServeHTTP(rw, r.WithContext(ctx)) next.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
user, err := getUserFromToken(tokenStr) token := headerToken
if token == "" {
token = cookieToken
}
user, err := getUserFromToken(token)
ctx := r.Context() ctx := r.Context()
if err != nil { if err != nil {

129
main.go
View File

@ -2,98 +2,119 @@ package main
import ( import (
"context" "context"
"log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"time" "time"
"log"
"net/http"
"git.farahty.com/nimer/go-mongo/app" "git.farahty.com/nimer/go-mongo/app"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
func main() { func main() {
// Setup cancelable root context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
color.Yellow("Starting server ...\n") // Panic recovery
if _, exists := os.LookupEnv("MONGO_URI"); !exists {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file\n")
}
color.Green("✅ .env loaded\n")
}
port := os.Getenv("PORT")
if cancel, err := app.Connect(); err != nil {
cancel()
log.Fatal(err)
} else {
defer func() {
color.Red("❌ Database Connection Closed\n")
cancel()
}()
}
defer func() { defer func() {
if err := app.Mongo.Disconnect(context.Background()); err != nil { if r := recover(); r != nil {
log.Fatal("MogoDB Errors" + err.Error()) color.Red("🔴 Panic occurred: %v\n", r)
os.Exit(1)
} }
}() }()
color.Green("✅ Connected to Database successfully\n") color.Yellow("🚀 Starting server ...\n")
if err := app.LoadAuthorizer(context.Background()); err != nil {
log.Fatal("Authorizer Errors : " + err.Error()) // Load .env if needed
if _, exists := os.LookupEnv("MONGO_URI"); !exists {
if err := godotenv.Load(); err != nil {
log.Fatal("🔴 Failed to load .env file: ", err)
}
color.Green("✅ .env file loaded\n")
} }
// Validate environment variables
requiredEnvs := []string{"PORT", "MONGO_URI", "REDIS_HOST", "REDIS_PORT"}
for _, key := range requiredEnvs {
if os.Getenv(key) == "" {
log.Fatalf("🔴 Required environment variable %s is missing", key)
}
}
port := os.Getenv("PORT")
// Connect to Mongo
dbCancel, err := app.Connect()
if err != nil {
log.Fatalf("🔴 MongoDB connection error: %v", err)
}
defer func() {
color.Red("❌ Closing MongoDB connection...\n")
dbCancel()
if err := app.Mongo.Disconnect(ctx); err != nil {
log.Fatal("🔴 MongoDB disconnection error: ", err)
}
}()
color.Green("✅ Connected to MongoDB successfully\n")
// Load authorization policies using root context
if err := app.LoadAuthorizer(ctx); err != nil {
log.Fatal("🔴 Authorizer error: ", err)
}
color.Green("✅ Authorization policies loaded successfully\n") color.Green("✅ Authorization policies loaded successfully\n")
// Redis
redisClient := redis.NewClient(&redis.Options{ redisClient := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"),
Password: os.Getenv("REDIS_PASSWORD"), // no password set Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
}) })
if _, err := redisClient.Ping(context.Background()).Result(); err != nil { if _, err := redisClient.Ping(ctx).Result(); err != nil {
log.Fatal("Redis Error : " + err.Error()) log.Fatal("🔴 Redis connection error: ", err)
} }
defer func() {
defer redisClient.Close() color.Red("❌ Closing Redis connection...\n")
_ = redisClient.Close()
}()
color.Green("✅ Connected to Redis cache successfully\n") color.Green("✅ Connected to Redis cache successfully\n")
// Create GraphQL server
graphqlServer := createGraphqlServer(redisClient) graphqlServer := createGraphqlServer(redisClient)
color.Green("🚀 Server Started at http://localhost:" + port + "\n") // Start HTTP server
//http.ListenAndServe(":"+port, createRouter(graphqlServer))
server := &http.Server{ server := &http.Server{
Addr: ":" + port, Addr: ":" + port,
WriteTimeout: time.Second * 30,
ReadTimeout: time.Second * 30,
IdleTimeout: time.Second * 30,
Handler: createRouter(graphqlServer), Handler: createRouter(graphqlServer),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 30 * time.Second,
} }
go server.ListenAndServe() go func() {
color.Green("🌐 Server listening at http://localhost:%s\n", port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("🔴 Server failed: %v", err)
}
}()
// Wait for interrupt signal to gracefully shut down the server with // Graceful shutdown
// a timeout of 15 seconds.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
<-quit <-quit
color.Yellow(" 🎬 Start Shutdown Signal ... ")
ctx, cancelShutdown := context.WithTimeout(context.Background(), 15*time.Second) color.Yellow("🟡 Shutdown signal received, initiating cleanup...")
defer cancelShutdown()
if err := server.Shutdown(ctx); err != nil { // Cancel root context and wait for graceful shutdown
log.Fatal("Server Shutdown:", err) shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("🔴 Server forced to shutdown: %v", err)
} }
color.Red("❌ Server Exiting")
color.Green("✅ Server shutdown completed gracefully")
} }

View File

@ -14,6 +14,7 @@ import (
// Login is the resolver for the login field. // Login is the resolver for the login field.
func (r *mutationResolver) Login(ctx context.Context, input models.LoginInput) (*models.LoginResponse, error) { func (r *mutationResolver) Login(ctx context.Context, input models.LoginInput) (*models.LoginResponse, error) {
return authService.Login(ctx, &input) return authService.Login(ctx, &input)
} }

View File

@ -41,6 +41,8 @@ func createRouter(graphqlServer http.Handler) chi.Router {
// Custom middleware for Auth // Custom middleware for Auth
router.Use(app.AuthMiddleware) router.Use(app.AuthMiddleware)
router.Use(app.WriterMiddleware)
// REST routes // REST routes
router.Mount("/users", controllers.UserRouter()) router.Mount("/users", controllers.UserRouter())

View File

@ -3,6 +3,8 @@ package authService
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"net/http"
"os" "os"
"git.farahty.com/nimer/go-mongo/app" "git.farahty.com/nimer/go-mongo/app"
@ -61,6 +63,21 @@ func successLogin(ctx context.Context, user *models.User) (*models.LoginResponse
return nil, err return nil, err
} }
w, err := app.WriterFor(ctx)
if err != nil {
return nil, err
}
http.SetCookie(*w, &http.Cookie{
Name: "access_token",
Value: *accessToken,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
return &models.LoginResponse{ return &models.LoginResponse{
AccessToken: *accessToken, AccessToken: *accessToken,
RefreshToken: *refreshToken, RefreshToken: *refreshToken,