feat: jwt authentication
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/robindittmar/dttmr-api/internal/api/request"
|
||||
"github.com/robindittmar/dttmr-api/internal/api/response"
|
||||
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
AuthService *domain.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *domain.AuthService) *AuthHandler {
|
||||
return &AuthHandler{AuthService: authService}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
payload, err := request.DecodeLogin(r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to decode login payload", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request body")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.AuthService.Login(ctx, payload.Email, payload.Password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to login", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to login")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(ctx, w, http.StatusOK, token)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/robindittmar/dttmr-api/internal/api/response"
|
||||
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||
)
|
||||
|
||||
func WithJWT(authService *domain.AuthService) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
slog.ErrorContext(ctx, "missing authorization header")
|
||||
response.Error(ctx, w, http.StatusUnauthorized, "missing authorization header")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
||||
slog.ErrorContext(ctx, "invalid authorization header")
|
||||
response.Error(ctx, w, http.StatusUnauthorized, "invalid authorization header")
|
||||
return
|
||||
}
|
||||
|
||||
authContext, err := authService.ParseToken(ctx, parts[1])
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "invalid token", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusUnauthorized, "invalid or expired token")
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, domain.AuthContextKey, authContext)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type LoginPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func DecodeLogin(r *http.Request) (LoginPayload, error) {
|
||||
var payload LoginPayload
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
if err := decoder.Decode(&payload); err != nil {
|
||||
return payload, fmt.Errorf("error decoding login payload: %w", err)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Database *sql.DB
|
||||
Database *sql.DB
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func NewMux(cfg Config) http.Handler {
|
||||
@@ -19,18 +20,24 @@ func NewMux(cfg Config) http.Handler {
|
||||
userService := domain.NewUserService(userRepo)
|
||||
userHandler := handler.NewUserHandler(userService)
|
||||
|
||||
authRepo := repository.NewAuthRepo(cfg.Database)
|
||||
authService := domain.NewAuthService(authRepo, []byte(cfg.JWTSecret))
|
||||
authHandler := handler.NewAuthHandler(authService)
|
||||
|
||||
listRepo := repository.NewListRepo(cfg.Database)
|
||||
listService := domain.NewListService(listRepo)
|
||||
listHandler := handler.NewListHandler(listService)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
protected := middleware.WithJWT(authService)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", handler.DefaultHandler)
|
||||
mux.HandleFunc("GET /health", handler.HealthHandler)
|
||||
mux.HandleFunc("POST /login", authHandler.Login)
|
||||
|
||||
mux.HandleFunc("POST /users", userHandler.CreateUser)
|
||||
mux.Handle("POST /users", protected(userHandler.CreateUser))
|
||||
|
||||
mux.HandleFunc("POST /lists", listHandler.CreateList)
|
||||
mux.Handle("POST /lists", protected(listHandler.CreateList))
|
||||
|
||||
var httpHandler http.Handler = mux
|
||||
httpHandler = middleware.WithMaxBytes(1024 * 64)(httpHandler)
|
||||
|
||||
Reference in New Issue
Block a user