using cookie for auth
This commit is contained in:
parent
cd6ceb3f96
commit
8296b813c9
@ -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)
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
115
main.go
115
main.go
@ -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
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
color.Red("🔴 Panic occurred: %v\n", r)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
color.Yellow("🚀 Starting server ...\n")
|
||||||
|
|
||||||
|
// Load .env if needed
|
||||||
if _, exists := os.LookupEnv("MONGO_URI"); !exists {
|
if _, exists := os.LookupEnv("MONGO_URI"); !exists {
|
||||||
err := godotenv.Load()
|
if err := godotenv.Load(); err != nil {
|
||||||
if err != nil {
|
log.Fatal("🔴 Failed to load .env file: ", err)
|
||||||
log.Fatal("Error loading .env file\n")
|
}
|
||||||
|
color.Green("✅ .env file loaded\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Green("✅ .env 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")
|
port := os.Getenv("PORT")
|
||||||
|
|
||||||
if cancel, err := app.Connect(); err != nil {
|
// Connect to Mongo
|
||||||
cancel()
|
dbCancel, err := app.Connect()
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
} else {
|
log.Fatalf("🔴 MongoDB connection error: %v", err)
|
||||||
defer func() {
|
|
||||||
color.Red("❌ Database Connection Closed\n")
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := app.Mongo.Disconnect(context.Background()); err != nil {
|
color.Red("❌ Closing MongoDB connection...\n")
|
||||||
log.Fatal("MogoDB Errors" + err.Error())
|
dbCancel()
|
||||||
|
if err := app.Mongo.Disconnect(ctx); err != nil {
|
||||||
|
log.Fatal("🔴 MongoDB disconnection error: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
color.Green("✅ Connected to MongoDB successfully\n")
|
||||||
|
|
||||||
color.Green("✅ Connected to Database successfully\n")
|
// Load authorization policies using root context
|
||||||
if err := app.LoadAuthorizer(context.Background()); err != nil {
|
if err := app.LoadAuthorizer(ctx); err != nil {
|
||||||
log.Fatal("Authorizer Errors : " + err.Error())
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user