From b5840984b8d9c08424ffbaeac3844a12a4aea3e2 Mon Sep 17 00:00:00 2001 From: "Maurice L." Date: Sun, 22 Feb 2026 14:47:17 +0100 Subject: [PATCH] Started Authentication System --- auth/jwt.go | 45 +++++++++++++++++++++++++++++++++++++++++ auth/middleware.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ auth/password.go | 12 +++++++++++ handlers/account.go | 5 +++-- handlers/expenses.go | 7 +++++++ server/server.go | 14 ++++++++++--- utils/util.go | 33 ------------------------------ 7 files changed, 126 insertions(+), 38 deletions(-) create mode 100644 auth/jwt.go create mode 100644 auth/middleware.go create mode 100644 auth/password.go create mode 100644 handlers/expenses.go diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 0000000..014d1d6 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,45 @@ +package auth + +import ( + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserID string `json:"user_id"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +var secret = os.Getenv("SHAP_JWT_SECRET") + +func GenerateJWT(userID, role string, secret []byte) (string, error) { + claims := Claims{ + UserID: userID, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secret) +} +func ValidateJWT(tokenStr string, secret []byte) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return secret, nil + }) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, err + } + + return claims, nil +} diff --git a/auth/middleware.go b/auth/middleware.go new file mode 100644 index 0000000..c1af8d6 --- /dev/null +++ b/auth/middleware.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "net/http" + "strings" +) + +type contextKey string + +const UserContextKey contextKey = contextKey("user") + +func AuthMiddleware(secret []byte) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Missing token", http.StatusUnauthorized) + return + } + + tokenStr := strings.TrimPrefix(authHeader, "Bearer ") + + claims, err := ValidateJWT(tokenStr, secret) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), UserContextKey, claims) + next.ServeHTTP(w, r.WithContext(ctx)) + + }) + } +} +func RequireRole(role string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims := r.Context().Value(UserContextKey).(*Claims) + if claims.Role != role { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/auth/password.go b/auth/password.go new file mode 100644 index 0000000..d9af44b --- /dev/null +++ b/auth/password.go @@ -0,0 +1,12 @@ +package auth + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/handlers/account.go b/handlers/account.go index 0b71af9..9f9aa83 100644 --- a/handlers/account.go +++ b/handlers/account.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/json" "net/http" + "shap-planner-backend/auth" "shap-planner-backend/models" "shap-planner-backend/storage" "shap-planner-backend/utils" @@ -11,7 +12,7 @@ import ( func Register(w http.ResponseWriter, r *http.Request) { var user models.User _ = json.NewDecoder(r.Body).Decode(&user) - hashed, _ := utils.HashPassword(user.Password) + hashed, _ := auth.HashPassword(user.Password) user.Password = hashed user.ID = utils.GenerateUUID() @@ -36,7 +37,7 @@ func Login(w http.ResponseWriter, r *http.Request) { return } - if !utils.CheckPasswordHash(creds.Password, user.Password) { + if !auth.CheckPasswordHash(creds.Password, user.Password) { http.Error(w, "Wrong password", http.StatusUnauthorized) return } diff --git a/handlers/expenses.go b/handlers/expenses.go new file mode 100644 index 0000000..a2bd67d --- /dev/null +++ b/handlers/expenses.go @@ -0,0 +1,7 @@ +package handlers + +import "net/http" + +func GetExpenses(w http.ResponseWriter, r *http.Request) {} + +func AdminPanel(w http.ResponseWriter, r *http.Request) {} diff --git a/server/server.go b/server/server.go index 5a25b0b..d1bff77 100644 --- a/server/server.go +++ b/server/server.go @@ -4,6 +4,7 @@ import ( "log" "net/http" "os" + "shap-planner-backend/auth" "shap-planner-backend/config" "shap-planner-backend/handlers" ) @@ -42,9 +43,16 @@ func InitServer() *Server { } func (server *Server) Run() { - http.HandleFunc("/register", handlers.Register) - http.HandleFunc("/login", handlers.Login) + mux := http.NewServeMux() + + mux.HandleFunc("/login", handlers.Login) + + protected := auth.AuthMiddleware(server.JWTSecret)(http.HandlerFunc(handlers.GetExpenses)) + mux.Handle("/expenses", protected) + + adminOnly := auth.AuthMiddleware(server.JWTSecret)(auth.RequireRole("admin")(http.HandlerFunc(handlers.AdminPanel))) + mux.Handle("/admin", adminOnly) log.Printf("Listening on port %s", server.Port) - log.Fatal(http.ListenAndServe(":"+server.Port, nil)) + log.Fatal(http.ListenAndServe(":"+server.Port, mux)) } diff --git a/utils/util.go b/utils/util.go index 1126979..bf8ef7f 100644 --- a/utils/util.go +++ b/utils/util.go @@ -1,45 +1,12 @@ package utils import ( - "os" - "time" - "crypto/rand" "encoding/base64" - "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" ) -var secret = []byte(os.Getenv("SHAP_JWT_SECRET")) - -// Password Validation -func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) - return string(bytes), err -} -func CheckPasswordHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -// JWT Validation -func GenerateJWT(userID string) (string, error) { - claims := jwt.MapClaims{ - "userId": userID, - "exp": time.Now().Add(time.Hour * 1).Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(secret) -} -func ValidateJWT(tokenString string) (*jwt.Token, error) { - return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return secret, nil - }) -} - func GenerateUUID() string { return uuid.New().String() }