diff --git a/README.md b/README.md index 062cda0..b402b3f 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,12 @@ It provides ready-to-use user authentication (login, refresh, logout, user info) ``` AccessHandler/ -├── handlers/ -│ ├── access_handler.go # AccessHandler initialization -│ ├── db_handler.go # Database handler using GORM -│ ├── login.go # Login, Refresh, Me, Logout handlers -│ ├── middleware.go # middleware authentification -| ├── role.go # database handling for roles -| ├── user.go # database users for roles +├── access_handler.go # AccessHandler initialization +├── db_handler.go # Database handler using GORM +├── login.go # Login, Refresh, Me, Logout handlers +├── middleware.go # middleware authentification +├── role.go # database handling for roles +├── user.go # database users for roles | ├── models/ │ ├── jsonResponse.go # Json responses model @@ -79,7 +78,7 @@ go get https://gitea.tecamino.com/paadi/tecamino-logger/logging ## 🔑 Authentication Constants -In `handlers/login.go`: +In `login.go`: ```go // ----------------------------- @@ -119,7 +118,6 @@ var REFRESH_SECRET = []byte("*") package main import ( - "AccessHandler/handlers" "gitea.tecamino.com/paadi/tecamino-logger/logging" "github.com/gin-gonic/gin" "log" @@ -127,7 +125,7 @@ import ( func main() { logger, _ := logging.NewLogger("server.log", nil) - accessHandler, err := handlers.NewAccessHandler("access.db", logger) + accessHandler, err := NewAccessHandler("access.db", logger) if err != nil { log.Fatal(err) } diff --git a/db_test.go b/db_test.go index d5ca885..6693127 100644 --- a/db_test.go +++ b/db_test.go @@ -1,4 +1,4 @@ -package accessmanager_test +package AccessHandler import ( "bytes" @@ -7,7 +7,6 @@ import ( "net/http/httptest" "testing" - "gitea.tecamino.com/paadi/AccessHandler/handlers" "gitea.tecamino.com/paadi/AccessHandler/models" "github.com/gin-gonic/gin" @@ -18,7 +17,7 @@ func TestAccesshandlerLogin(t *testing.T) { t.Log("start access handler test") t.Log("initialize accessHandler") - accessHandler, err := handlers.NewAccessHandler("test.db", nil) + accessHandler, err := NewAccessHandler("test.db", nil) if err != nil { t.Fatal(err) } @@ -100,20 +99,20 @@ func TestLoginHandler(t *testing.T) { gin.SetMode(gin.TestMode) // Setup your AccessHandler and router - aH, err := handlers.NewAccessHandler("test.db", nil) + aH, err := NewAccessHandler("test.db", nil) if err != nil { t.Fatal(err) } r := gin.Default() - handlers.SetMiddlewareLogger(r, aH.GetLogger()) + SetMiddlewareLogger(r, aH.GetLogger()) r.POST("/login", aH.Login) r.POST("/login/refresh", aH.Refresh) r.GET("/login/me", aH.Me) r.GET("/logout", aH.Logout) - middleware := r.Group("", handlers.AuthMiddleware()) + middleware := r.Group("", AuthMiddleware()) auth := middleware.Group("/members", aH.AuthorizeRole("")) auth.GET("", func(ctx *gin.Context) { diff --git a/handlers/accessHandler.go b/handlers/accessHandler.go deleted file mode 100644 index a5f5716..0000000 --- a/handlers/accessHandler.go +++ /dev/null @@ -1,125 +0,0 @@ -package handlers - -import "gitea.tecamino.com/paadi/tecamino-logger/logging" - -// -// AccessHandler -// -// Description: -// AccessHandler manages access-related functionality, including -// database operations for users and roles, as well as logging. -// It encapsulates a database handler and a logger so that -// authentication and authorization operations can be performed -// consistently across the application. -// -type AccessHandler struct { - dbHandler *DBHandler // Database handler used for managing users and roles - logger *logging.Logger // Centralized application logger -} - -// -// NewAccessHandler -// -// Description: -// Creates and initializes a new AccessHandler instance. -// -// Behavior: -// 1. If a logger is not provided (nil), it creates a new logger instance -// that writes to "accessHandler.log". -// 2. Initializes the AccessHandler struct. -// 3. Sets up the internal DBHandler with the same logger. -// 4. Automatically creates required database tables and default data: -// - User table -// - Default user(s) -// - Role table -// - Default role(s) -// -// Parameters: -// - dbPath: The file path or connection string for the database. -// - logger: Optional pointer to a logging.Logger instance. If nil, a new one is created. -// -// Returns: -// - aH: A pointer to the fully initialized AccessHandler. -// - err: Any error that occurs during initialization. -// -// Example: -// handler, err := handlers.NewAccessHandler("data/app.db", appLogger) -// if err != nil { -// log.Fatal(err) -// } -// -func NewAccessHandler(dbPath string, logger *logging.Logger) (aH *AccessHandler, err error) { - if logger == nil { - logger, err = logging.NewLogger("accessHandler.log", nil) - if err != nil { - return - } - } - - logger.Debug("NewAccessHandler", "initialize new access handler") - - // Initialize AccessHandler with logger - aH = &AccessHandler{ - logger: logger, - } - - logger.Debug("NewAccessHandler", "initialize db handler") - // Create a new DB handler instance - aH.dbHandler, err = NewDBHandler(dbPath, logger) - if err != nil { - aH.logger.Error("NewAccessHandler", err) - return - } - - logger.Debug("NewAccessHandler", "add user table") - // Add the user table to the database - err = aH.AddUserTable() - if err != nil { - aH.logger.Error("NewAccessHandler", err) - return - } - - logger.Debug("NewAccessHandler", "add default user") - // Add default users to the system - err = aH.AddDefaultUser() - if err != nil { - aH.logger.Error("NewAccessHandler", err) - return - } - - logger.Debug("NewAccessHandler", "add role table") - // Add the role table to the database - err = aH.AddRoleTable() - if err != nil { - aH.logger.Error("NewAccessHandler", err) - return - } - - logger.Debug("NewAccessHandler", "add default role") - // Add default roles to the system - err = aH.AddDefaultRole() - if err != nil { - aH.logger.Error("NewAccessHandler", err) - } - - return -} - -// -// GetLogger -// -// Description: -// Returns the logger associated with this AccessHandler instance. -// Useful when another component or handler needs to reuse the -// same logging instance for consistent log output. -// -// Returns: -// - *logging.Logger: The logger assigned to this AccessHandler. -// -// Example: -// log := accessHandler.GetLogger() -// log.Info("Some event") -// -func (aH *AccessHandler) GetLogger() *logging.Logger { - return aH.logger -} diff --git a/handlers/dbHandler.go b/handlers/dbHandler.go deleted file mode 100644 index 9ac8fb4..0000000 --- a/handlers/dbHandler.go +++ /dev/null @@ -1,300 +0,0 @@ -package handlers - -import ( - "errors" - "fmt" - "reflect" - "strings" - - "gitea.tecamino.com/paadi/tecamino-logger/logging" - "github.com/glebarez/sqlite" - "gorm.io/gorm" -) - -// DBHandler -// -// Description: -// -// Wraps the GORM database connection and provides helper methods for -// common CRUD operations, as well as integrated logging for traceability. -// -// Fields: -// - db: Active GORM database connection. -// - logger: Pointer to a custom logger instance for structured logging. -type DBHandler struct { - db *gorm.DB - logger *logging.Logger -} - -// NewDBHandler -// -// Description: -// -// Creates a new database handler using the specified SQLite database file. -// -// Behavior: -// 1. Opens a GORM connection to the database file at `dbPath`. -// 2. Wraps it in a `DBHandler` struct with logging support. -// -// Parameters: -// - dbPath: Path to the SQLite database file. -// - logger: Logging instance to record DB operations. -// -// Returns: -// - dH: A pointer to the initialized `DBHandler`. -// - err: Any error encountered during database connection. -func NewDBHandler(dbPath string, logger *logging.Logger) (dH *DBHandler, err error) { - dH = &DBHandler{logger: logger} - logger.Debug("NewDBHandler", "open database "+dbPath) - dH.db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - return -} - -// addNewTable -// -// Description: -// -// Uses GORM’s `AutoMigrate` to create or update the database schema -// for the provided model type. -// -// Parameters: -// - model: Struct type representing the database table schema. -// -// Returns: -// - error: Any migration error encountered. -func (dH *DBHandler) addNewTable(model any) error { - return dH.db.AutoMigrate(&model) -} - -// addNewColum -// -// Description: -// -// Inserts a new record into the database table corresponding to `model`. -// -// Parameters: -// - model: Struct instance containing values to be inserted. -// -// Returns: -// - error: Any error encountered during record creation. -func (dH *DBHandler) addNewColum(model any) error { - return dH.db.Create(model).Error -} - -// getById -// -// Description: -// -// Retrieves a record (or all records) from a table by numeric ID. -// -// Behavior: -// - If `id == 0`, returns all records in the table. -// - Otherwise, fetches the record matching the given ID. -// -// Parameters: -// - model: Pointer to a slice or struct to store the result. -// - id: Numeric ID to query by. -// -// Returns: -// - error: Any query error or “not found” message. -func (dH *DBHandler) getById(model any, id uint) error { - dH.logger.Debug("getById", "find id "+fmt.Sprint(id)) - - if id == 0 { - return dH.db.Find(model).Error - } - - err := dH.db.First(model, id).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("no record found for id: %v", id) - } else if err != nil { - return fmt.Errorf("query failed: %w", err) - } - return nil -} - -// getByKey -// -// Description: -// -// Retrieves one or more records matching a key/value pair. -// -// Behavior: -// - If `LikeSearch` is true, performs a SQL LIKE query. -// - Otherwise, performs an exact match query. -// -// Parameters: -// - model: Pointer to a slice or struct to store results. -// - key: Column name (e.g., "email"). -// - value: Value to match or partially match. -// - LikeSearch: If true, replaces '*' with '%' for wildcard matching. -// -// Returns: -// - error: Any database query error. -func (dH *DBHandler) getByKey(model any, key string, value any, LikeSearch bool) error { - if LikeSearch { - value = strings.ReplaceAll(fmt.Sprint(value), "*", "%") - dH.logger.Debug("getByKey", "find like key "+key+" value "+fmt.Sprint(value)) - return dH.db.Where(key+" LIKE ?", value).Find(model).Error - } - - dH.logger.Debug("getByKey", "find equal key "+key+" value "+fmt.Sprint(value)) - return dH.db.Find(model, key+" = ?", value).Error -} - -// updateValuesById -// -// Description: -// -// Updates record fields based on their unique ID. -// -// Behavior: -// 1. Confirms that `model` is a pointer to a struct. -// 2. Fetches the record by ID. -// 3. Updates all non-zero fields using `gorm.Model.Updates`. -// -// Parameters: -// - model: Pointer to struct containing new values. -// - id: Numeric ID of the record to update. -// -// Returns: -// - error: If the model is invalid or query/update fails. -func (dH *DBHandler) updateValuesById(model any, id uint) error { - dH.logger.Debug("updateValuesById", "model"+fmt.Sprint(model)) - modelType := reflect.TypeOf(model) - if modelType.Kind() != reflect.Ptr { - return errors.New("model must be a pointer to struct") - } - - lookUpModel := reflect.New(modelType.Elem()).Interface() - if err := dH.getById(lookUpModel, id); err != nil { - return err - } - return dH.db.Model(lookUpModel).Updates(model).Error -} - -// updateValuesByKey -// -// Description: -// -// Updates records based on a key/value match. -// -// Behavior: -// 1. Confirms model type. -// 2. Fetches the matching record(s) using `getByKey`. -// 3. Updates all non-zero fields. -// -// Parameters: -// - model: Pointer to struct containing updated values. -// - key: Column name to filter by. -// - value: Value to match. -// -// Returns: -// - error: Any query or update error. -func (dH *DBHandler) updateValuesByKey(model any, key string, value any) error { - dH.logger.Debug("updateValuesByKey", "model"+fmt.Sprint(model)) - modelType := reflect.TypeOf(model) - if modelType.Kind() != reflect.Ptr { - return errors.New("model must be a pointer to struct") - } - - lookUpModel := reflect.New(modelType.Elem()).Interface() - if err := dH.getByKey(lookUpModel, key, value, false); err != nil { - return err - } - return dH.db.Model(lookUpModel).Updates(model).Error -} - -// deleteById -// -// Description: -// -// Deletes records by their ID(s). -// -// Behavior: -// - If the first ID == 0, all records in the table are deleted. -// - Otherwise, deletes the provided IDs. -// -// Parameters: -// - model: Model struct type representing the table. -// - id: Variadic list of IDs to delete. -// -// Returns: -// - error: Any deletion error. -func (dH *DBHandler) deleteById(model any, id ...uint) error { - if id[0] == 0 { - dH.logger.Debug("deleteById", "delete all") - return dH.db.Where("1 = 1").Delete(model).Error - } - - dH.logger.Debug("deleteById", "delete ids"+fmt.Sprint(id)) - if err := dH.exists(model, "id", id, false); err != nil { - return err - } - return dH.db.Delete(model, id).Error -} - -// deleteByKey -// -// Description: -// -// Deletes records that match a key/value pair. -// -// Behavior: -// - Supports LIKE queries if `LikeSearch` is true. -// -// Parameters: -// - model: Model struct type representing the table. -// - key: Column name to filter by. -// - value: Value to match. -// - LikeSearch: Whether to use wildcard search. -// -// Returns: -// - error: Any deletion error. -func (dH *DBHandler) deleteByKey(model any, key string, value any, LikeSearch bool) error { - if LikeSearch { - value = strings.ReplaceAll(fmt.Sprint(value), "*", "%") - dH.logger.Debug("deleteByKey", "delete like key "+key+" value "+fmt.Sprint(value)) - return dH.db.Where(key+" LIKE ?", value).Delete(model).Error - } - - dH.logger.Debug("deleteByKey", "delete equal key "+key+" value "+fmt.Sprint(value)) - return dH.db.Where(key+" = ?", value).Delete(model).Error -} - -// exists -// -// Description: -// -// Checks whether a record exists matching the specified key/value filter. -// -// Behavior: -// - Performs a `First` query on the database. -// - If `LikeSearch` is true, performs a LIKE query. -// - Returns an error if the record does not exist or query fails. -// -// Parameters: -// - model: Model struct type to search. -// - key: Column name to filter by. -// - value: Value to match. -// - LikeSearch: Whether to use wildcard search. -// -// Returns: -// - error: “no record found” or DB error. -func (dH *DBHandler) exists(model any, key string, value any, LikeSearch bool) (err error) { - if LikeSearch { - value = strings.ReplaceAll(fmt.Sprint(value), "*", "%") - key = key + " LIKE ?" - } else { - key = key + " = ?" - } - - dH.logger.Debug("exists", "check if exists key "+key+" value "+fmt.Sprint(value)) - err = dH.db.Where(key, value).First(model).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("no record found for %s %v", key[:len(key)-1], value) - } else if err != nil { - return fmt.Errorf("query failed: %w", err) - } - return -} diff --git a/handlers/login.go b/handlers/login.go deleted file mode 100644 index 0499326..0000000 --- a/handlers/login.go +++ /dev/null @@ -1,243 +0,0 @@ -package handlers - -import ( - "fmt" - "log" - "net/http" - "time" - - "gitea.tecamino.com/paadi/AccessHandler/models" - "gitea.tecamino.com/paadi/AccessHandler/utils" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -// ----------------------------- -// 🔐 AUTHENTICATION CONSTANTS -// ----------------------------- - -// JWT secrets (replace "*" with strong random values in production!) -var ACCESS_SECRET = []byte("ShFRprALcXjlosJ2hFCnGYGG3Ce2uRx6") -var REFRESH_SECRET = []byte("pQIjuX6g6Tzf0FEfdScxttT3hlL9NFaa") - -// DOMAIN defines where cookies are valid. Change this in production. -var DOMAIN = "localhost" - -// ACCESS_TOKEN_TIME defines how long access tokens are valid. -var ACCESS_TOKEN_TIME = 15 * time.Minute - -// REFRESH_TOKEN_TIME defines how long refresh tokens are valid. -var REFRESH_TOKEN_TIME = 72 * time.Hour - -// ----------------------------- -// 🧠 HANDLERS -// ----------------------------- - -// Login authenticates a user and returns JWT tokens (access + refresh). -func (aH *AccessHandler) Login(c *gin.Context) { - var user models.User - - aH.logger.Debug("Login", "bind JSON request") - if err := c.BindJSON(&user); err != nil { - aH.logger.Error("Login", err) - c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) - return - } - - // Validate input - if !user.IsValid() { - aH.logger.Error("Login", "user empty") - c.JSON(http.StatusBadRequest, models.NewJsonMessageResponse("user empty")) - return - } - - // Fetch user record from DB - dbRecord, err := aH.GetUserByKey("user_name", user.Name, false) - if err != nil { - aH.logger.Error("Login", err) - c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) - return - } - - if len(dbRecord) > 1 { - log.Println("multiple users found") - aH.logger.Error("Login", "more than one record found") - c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("internal error")) - return - } - - // Check password - if !utils.CheckPassword(user.Password, dbRecord[0].Password) { - aH.logger.Error("Login", "invalid password") - c.JSON(http.StatusUnauthorized, models.NewJsonMessageResponse("invalid credentials")) - return - } - user = dbRecord[0] - - // ----------------------------- - // 🔑 TOKEN CREATION - // ----------------------------- - aH.logger.Debug("Login", "create tokens") - - accessTokenExp := time.Now().Add(ACCESS_TOKEN_TIME) - refreshTokenExp := time.Now().Add(REFRESH_TOKEN_TIME) - - // Create access token - accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": user.Id, - "username": user.Name, - "role": user.Role, - "type": "access", - "exp": accessTokenExp.Unix(), - }) - - // Create refresh token - refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": user.Id, - "username": user.Name, - "role": user.Role, - "type": "refresh", - "exp": refreshTokenExp.Unix(), - }) - - // Sign tokens - accessString, err := accessToken.SignedString(ACCESS_SECRET) - if err != nil { - aH.logger.Error("Login", "failed to sign access token") - c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("could not create access token")) - return - } - - refreshString, err := refreshToken.SignedString(REFRESH_SECRET) - if err != nil { - aH.logger.Error("Login", "failed to sign refresh token") - c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("could not create refresh token")) - return - } - - // ----------------------------- - // 🍪 SET COOKIES - // ----------------------------- - aH.logger.Debug("Login", "set cookies") - - secure := gin.Mode() == gin.ReleaseMode - - c.SetCookie("access_token", accessString, int(time.Until(accessTokenExp).Seconds()), - "/", "", secure, true) - c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()), - "/", "", secure, true) - - aH.logger.Info("Login", "user "+user.Name+" logged in successfully") - - c.JSON(http.StatusOK, gin.H{ - "message": "login successful", - "id": user.Id, - "user": user.Name, - "role": user.Role, - "settings": user.Settings, - }) -} - -// Refresh generates a new access token from a valid refresh token. -func (aH *AccessHandler) Refresh(c *gin.Context) { - aH.logger.Debug("Refresh", "get refresh cookie") - - refreshCookie, err := c.Cookie("refresh_token") - if err != nil { - aH.logger.Error("Refresh", "no refresh token") - c.JSON(http.StatusUnauthorized, gin.H{"message": "no refresh token"}) - return - } - - // Validate token - aH.logger.Debug("Refresh", "parse token") - token, err := jwt.Parse(refreshCookie, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return REFRESH_SECRET, nil - }) - if err != nil || !token.Valid { - aH.logger.Error("Refresh", err) - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid refresh token"}) - return - } - - // Extract claims - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || claims["type"] != "refresh" { - aH.logger.Error("Refresh", "invalid token type") - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token type"}) - return - } - - username := claims["username"].(string) - id := int(claims["id"].(float64)) - role := claims["role"].(string) - - // Create new access token - aH.logger.Debug("Refresh", "create new access token") - accessExp := time.Now().Add(ACCESS_TOKEN_TIME) - newAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": id, - "username": username, - "role": role, - "exp": accessExp.Unix(), - }) - accessString, _ := newAccess.SignedString(ACCESS_SECRET) - - // Set new access cookie - aH.logger.Debug("Refresh", "set new access cookie") - c.SetCookie("access_token", accessString, int(time.Until(accessExp).Seconds()), - "/", DOMAIN, gin.Mode() == gin.ReleaseMode, true) - - c.JSON(http.StatusOK, gin.H{"message": "token refreshed"}) -} - -// Me returns information about the currently authenticated user. -func (aH *AccessHandler) Me(c *gin.Context) { - aH.logger.Debug("Me", "get access cookie") - - cookie, err := c.Cookie("access_token") - if err != nil { - aH.logger.Error("Me", err) - c.JSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) - return - } - - // Parse and validate access token - aH.logger.Debug("Me", "parse token") - token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) - } - return ACCESS_SECRET, nil - }) - if err != nil || !token.Valid { - aH.logger.Error("Me", err) - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) - return - } - - claims := token.Claims.(jwt.MapClaims) - c.JSON(http.StatusOK, gin.H{ - "id": claims["id"], - "user": claims["username"], - "role": claims["role"], - }) -} - -// Logout clears authentication cookies and ends the session. -func (aH *AccessHandler) Logout(c *gin.Context) { - aH.logger.Info("Logout", "logout user") - - secure := gin.Mode() == gin.ReleaseMode - aH.logger.Debug("Logout", fmt.Sprintf("domain=%s secure=%t", DOMAIN, secure)) - - // Clear cookies - c.SetCookie("access_token", "", -1, "/", DOMAIN, secure, true) - c.SetCookie("refresh_token", "", -1, "/", DOMAIN, secure, true) - - c.JSON(http.StatusOK, gin.H{"message": "logged out"}) -} diff --git a/handlers/middleware.go b/handlers/middleware.go deleted file mode 100644 index b49afe8..0000000 --- a/handlers/middleware.go +++ /dev/null @@ -1,165 +0,0 @@ -package handlers - -import ( - "fmt" - "log" - "net/http" - "strings" - - "gitea.tecamino.com/paadi/tecamino-logger/logging" - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -// SetMiddlewareLogger -// -// Description: -// -// Registers a Gin middleware that attaches a custom logger instance -// to every incoming request via the Gin context. This allows other -// middleware and handlers to access the logger using: -// -// logger := c.MustGet("logger").(*logging.Logger) -// -// Parameters: -// - r: The Gin engine to which the middleware is applied. -// - logger: A pointer to the application's custom logger. -// -// Usage: -// -// handlers.SetMiddlewareLogger(router, logger) -func SetMiddlewareLogger(r *gin.Engine, logger *logging.Logger) { - // Add middleware that injects logger into context - r.Use(func(c *gin.Context) { - c.Set("logger", logger) - c.Next() - }) -} - -// AuthMiddleware -// -// Description: -// -// A Gin middleware that performs authentication using a JWT token stored -// in a cookie named "access_token". It validates the token and extracts -// the user's role from the claims, storing it in the Gin context. -// -// Behavior: -// - Requires that SetMiddlewareLogger was used earlier to inject the logger. -// - If the JWT cookie is missing or invalid, it aborts the request with -// an appropriate HTTP error (401 or 500). -// -// Returns: -// -// A Gin handler function that can be used as middleware. -// -// Usage: -// -// r.Use(handlers.AuthMiddleware()) -func AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - // Retrieve logger from Gin context - middlewareLogger, ok := c.Get("logger") - if !ok { - log.Fatal("middleware logger not set — use SetMiddlewareLogger first") - c.AbortWithStatusJSON(http.StatusInternalServerError, http.StatusInternalServerError) - return - } - logger := middlewareLogger.(*logging.Logger) - - // Read access token from cookie - cookie, err := c.Cookie("access_token") - if err != nil { - logger.Error("AuthMiddleware", err) - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) - return - } - - // Parse and validate JWT token - token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { - return ACCESS_SECRET, nil - }) - if err != nil || !token.Valid { - logger.Error("AuthMiddleware", err) - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) - return - } - - // Extract custom claims (role) - if claims, ok := token.Claims.(jwt.MapClaims); ok { - role, _ := claims["role"].(string) - c.Set("role", role) - } - c.Next() - } -} - -// (AccessHandler).AuthorizeRole -// -// Description: -// -// A role-based authorization middleware. It checks whether the authenticated -// user (based on the "role" set in AuthMiddleware) has permission to access -// the given route. The check is performed by comparing the requested URL -// path against the user’s allowed permissions. -// -// Parameters: -// - suffix: A URL prefix to trim from the request path before matching -// permissions (e.g., "/api/v1"). -// -// Behavior: -// - Fetches the user role from Gin context. -// - Uses aH.GetRoleByKey() to retrieve role records and permissions. -// - Grants access (calls c.Next()) if a matching permission is found. -// - Denies access (401 Unauthorized) if no permission matches. -// -// Usage: -// -// router.GET("/secure/:id", aH.AuthorizeRole("/api/v1")) -func (aH *AccessHandler) AuthorizeRole(suffix string) gin.HandlerFunc { - return func(c *gin.Context) { - aH.logger.Debug("AuthorizeRole", "permission path of url path") - permissionPath := strings.TrimPrefix(c.Request.URL.Path, suffix+"/") - - aH.logger.Debug("AuthorizeRole", "get set role") - role, ok := c.Get("role") - if !ok { - aH.logger.Error("AuthorizeRole", "no role set") - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": http.StatusInternalServerError}) - return - } - - // Fetch roles and associated permissions from the database or store - roles, err := aH.GetRoleByKey("role", role, false) - if err != nil { - aH.logger.Error("AuthorizeRole", err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": http.StatusInternalServerError}) - return - } - - // Validate that a role was found - if len(roles) == 0 { - log.Println("not logged in") - aH.logger.Error("AuthorizeRole", "no logged in") - c.JSON(http.StatusUnauthorized, http.StatusUnauthorized) - return - } else if len(roles) > 1 { - aH.logger.Error("AuthorizeRole", "more than one record found") - c.JSON(http.StatusInternalServerError, http.StatusInternalServerError) - return - } - - // Check permissions - for _, permission := range roles[0].Permissions { - fmt.Println(100, permissionPath, permission.Name) - if permission.Name == permissionPath { - c.Next() - return - } - } - - // Access denied - aH.logger.Error("AuthorizeRole", "Forbidden") - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Forbidden"}) - } -} diff --git a/handlers/role.go b/handlers/role.go deleted file mode 100644 index 35d57fc..0000000 --- a/handlers/role.go +++ /dev/null @@ -1,197 +0,0 @@ -package handlers - -import ( - "fmt" - - "gitea.tecamino.com/paadi/AccessHandler/models" -) - -// AddRoleTable -// -// Description: -// -// Creates a new database table for storing role definitions if it does not already exist. -// -// Behavior: -// - Uses the DBHandler to add a table based on the `models.Role` struct. -// - Returns an error if table creation fails. -// -// Returns: -// - error: Any database error encountered. -func (aH *AccessHandler) AddRoleTable() error { - return aH.dbHandler.addNewTable(models.Role{}) -} - -// AddDefaultRole -// -// Description: -// -// Ensures that a default administrative role exists in the database. -// If a role named "admin" is already present, it logs and skips creation. -// -// Behavior: -// 1. Checks for an existing "admin" role. -// 2. If not found, initializes default permissions using -// `models.Permissions.DefaultPermissions()`. -// 3. Creates a new role record with those permissions. -// -// Default Role: -// - Role: "admin" -// - Permissions: all default permissions defined in `models.Permissions`. -// -// Returns: -// - error: Any database or creation error encountered. -func (aH *AccessHandler) AddDefaultRole() (err error) { - role := "admin" - - // Check if a role with this name already exists - if err := aH.dbHandler.exists(&models.Role{}, "role", role, false); err == nil { - // Found a role → skip creation - aH.logger.Debug("AddDefaultRole", "role "+role+" exists already") - return nil - } - - // Initialize default permissions for admin - permissions := models.Permissions{} - aH.logger.Debug("AddDefaultRole", "set default Permissions") - permissions.DefaultPermissions() - - // Create the default admin role - aH.dbHandler.addNewColum(&models.Role{ - Role: role, - Permissions: permissions, - }) - return -} - -// AddNewRole -// -// Description: -// -// Adds a new role with a specific set of permissions to the database. -// -// Behavior: -// 1. Checks whether a role with the same name already exists. -// 2. If it does not exist, creates a new role record. -// -// Parameters: -// - role: The role name (e.g., "manager", "viewer"). -// - permissions: A `models.Permissions` struct defining allowed actions. -// -// Returns: -// - error: If the role already exists or insertion fails. -func (aH *AccessHandler) AddNewRole(role string, permissions models.Permissions) (err error) { - // Check if a role with this name already exists - if err := aH.dbHandler.exists(&models.Role{}, "role", role, false); err == nil { - // Found a role → skip creation - return fmt.Errorf("role with name %s already exists", role) - } - - // Insert new role with provided permissions - aH.dbHandler.addNewColum(&models.Role{ - Role: role, - Permissions: permissions, - }) - return -} - -// GetRoleById -// -// Description: -// -// Retrieves a role record from the database by its numeric ID. -// -// Parameters: -// - id: The unique ID of the role. -// -// Returns: -// - roles: A slice containing the matched role (usually length 1). -// - err: Any database error encountered. -func (aH *AccessHandler) GetRoleById(id uint) (roles []models.Role, err error) { - err = aH.dbHandler.getById(&roles, id) - return -} - -// GetRoleByKey -// -// Description: -// -// Retrieves one or more roles based on a key/value query. -// -// Parameters: -// - key: The column name to search by (e.g., "role"). -// - value: The value to match (e.g., "admin"). -// - likeSearch: Whether to use SQL LIKE for partial matches. -// -// Returns: -// - roles: A list of matched roles. -// - err: Any database error encountered. -func (aH *AccessHandler) GetRoleByKey(key string, value any, likeSearch bool) (roles []models.Role, err error) { - err = aH.dbHandler.getByKey(&roles, key, value, likeSearch) - return -} - -// UpdateRoleById -// -// Description: -// -// Updates a role record identified by its numeric ID. -// -// Parameters: -// - id: The ID of the role to update. -// - role: A struct containing updated role data. -// -// Returns: -// - error: Any database error encountered. -func (aH *AccessHandler) UpdateRoleById(id uint, role models.Role) error { - return aH.dbHandler.updateValuesById(&role, id) -} - -// UpdateRoleByKey -// -// Description: -// -// Updates a role record using a column key/value lookup. -// -// Parameters: -// - role: The updated role data. -// - key: The column name to search by. -// - value: The value to match against the key column. -// -// Returns: -// - error: Any database error encountered. -func (aH *AccessHandler) UpdateRoleByKey(role models.Role, key string, value any) error { - return aH.dbHandler.updateValuesByKey(&role, key, value) -} - -// DeleteRoleById -// -// Description: -// -// Deletes a role record from the database by its numeric ID. -// -// Parameters: -// - id: The ID of the role to delete. -// -// Returns: -// - error: Any database error encountered during deletion. -func (aH *AccessHandler) DeleteRoleById(id uint) (err error) { - return aH.dbHandler.deleteById(&models.Role{}, id) -} - -// DeleteRoleByKey -// -// Description: -// -// Deletes one or more roles from the database matching a given key/value pair. -// -// Parameters: -// - key: The column name used for filtering (e.g., "role"). -// - value: The matching value (e.g., "admin"). -// - likeSearch: If true, performs a LIKE (partial) match. -// -// Returns: -// - error: Any database error encountered. -func (aH *AccessHandler) DeleteRoleByKey(key string, value any, likeSearch bool) (err error) { - return aH.dbHandler.deleteByKey(&models.Role{}, key, value, likeSearch) -} diff --git a/handlers/user.go b/handlers/user.go deleted file mode 100644 index 15378f7..0000000 --- a/handlers/user.go +++ /dev/null @@ -1,218 +0,0 @@ -package handlers - -import ( - "fmt" - - "gitea.tecamino.com/paadi/AccessHandler/models" - "gitea.tecamino.com/paadi/AccessHandler/utils" -) - -// AddUserTable -// -// Description: -// -// Creates a new database table for storing user records if it does not already exist. -// -// Behavior: -// - Uses the DBHandler to add a table based on the `models.User` struct. -// - Returns any error encountered during table creation. -// -// Returns: -// - error: Any database error that occurs while creating the table. -func (aH *AccessHandler) AddUserTable() error { - return aH.dbHandler.addNewTable(models.User{}) -} - -// AddDefaultUser -// -// Description: -// -// Ensures a default administrative user exists in the database. -// If a user with the predefined email already exists, the function logs -// a debug message and exits without making changes. -// -// Behavior: -// 1. Checks if the default user (admin) already exists by email. -// 2. If not found, creates default Quasar UI settings and adds the user. -// -// Default User: -// - Name: "admin" -// - Role: "admin" -// - Email: "zuercher@tecamino.ch" -// - Password: (empty or hashed later) -// -// Returns: -// - error: Any database or creation error encountered. -func (aH *AccessHandler) AddDefaultUser() (err error) { - name := "admin" - role := "admin" - email := "zuercher@tecamino.ch" - - // Check if a user with this email already exists - if err := aH.dbHandler.exists(&models.User{}, "email", email, false); err == nil { - aH.logger.Debug("AddDefaultUser", "user email "+email+" exists already") - // Found a user → skip create - return nil - } - - // Create default settings for the new user - settings := models.Settings{} - aH.logger.Debug("AddDefaultUser", "set default quasar settings") - settings.DefaultQuasarSettings() - - // Insert default admin user into the database - aH.dbHandler.addNewColum(&models.User{ - Name: name, - Role: role, - Email: email, - Password: "$2a$10$sZZOWBP8DSFLrLFQNoXw8OsEEr0tez1B8lPzKCHofaHg6PMNxx1pG", - Settings: settings, - }) - return -} - -// AddNewUser -// -// Description: -// -// Adds a new user record to the database with a hashed password. -// -// Behavior: -// 1. Verifies that the email does not already exist. -// 2. Hashes the password using utils.HashPassword(). -// 3. Inserts the new user record into the database. -// -// Parameters: -// - userName: The user's display name. -// - email: The user's unique email address. -// - password: The user's raw password (will be hashed). -// - role: The role assigned to the user. -// -// Returns: -// - error: If the user already exists or if hashing/insertion fails. -func (aH *AccessHandler) AddNewUser(userName, email, password, role string) (err error) { - // Check if a user with this email already exists - if err := aH.dbHandler.exists(&models.User{}, "email", email, false); err == nil { - // Found a user → skip create - aH.logger.Error("AddNewUser", "user with email "+email+" already exists") - return fmt.Errorf("user with email %s already exists", email) - } - - // Hash the provided password before saving - hash, err := utils.HashPassword(password) - if err != nil { - return err - } - - aH.logger.Debug("AddNewUser", "add new user "+userName+" with role "+role) - - // Insert the new user record - aH.dbHandler.addNewColum(&models.User{ - Name: userName, - Role: role, - Email: email, - Password: hash, - }) - return -} - -// GetUserById -// -// Description: -// -// Retrieves user(s) from the database by their unique ID. -// -// Parameters: -// - id: The numeric user ID. -// -// Returns: -// - users: A slice containing the matched user (usually length 1). -// - err: Any database error encountered. -func (aH *AccessHandler) GetUserById(id uint) (users []models.User, err error) { - err = aH.dbHandler.getById(&users, id) - return -} - -// GetUserByKey -// -// Description: -// -// Queries users based on a given column key and value. -// -// Parameters: -// - key: The column name to search by (e.g., "email"). -// - value: The value to match. -// - likeSearch: If true, performs a LIKE (partial) search. -// -// Returns: -// - users: A list of users that match the search criteria. -// - err: Any database error encountered. -func (aH *AccessHandler) GetUserByKey(key string, value any, likeSearch bool) (users []models.User, err error) { - err = aH.dbHandler.getByKey(&users, key, value, likeSearch) - return -} - -// UpdateUserById -// -// Description: -// -// Updates an existing user record identified by its numeric ID. -// -// Parameters: -// - id: The user ID to update. -// - user: A struct containing updated field values. -// -// Returns: -// - error: Any error encountered during the update. -func (aH *AccessHandler) UpdateUserById(id uint, user models.User) error { - return aH.dbHandler.updateValuesById(&user, id) -} - -// UpdateUserByKey -// -// Description: -// -// Updates a user record based on a specified column key and value. -// -// Parameters: -// - user: The updated user data. -// - key: The column name used for lookup. -// - value: The value to match against the key column. -// -// Returns: -// - error: Any error encountered during the update. -func (aH *AccessHandler) UpdateUserByKey(user models.User, key string, value any) error { - return aH.dbHandler.updateValuesByKey(&user, key, value) -} - -// DeleteUserById -// -// Description: -// -// Deletes a user from the database using their numeric ID. -// -// Parameters: -// - id: The ID of the user to delete. -// -// Returns: -// - error: Any database error encountered during deletion. -func (aH *AccessHandler) DeleteUserById(id uint) (err error) { - return aH.dbHandler.deleteById(&models.User{}, id) -} - -// DeleteUserByKey -// -// Description: -// -// Deletes users matching a specific key/value pair from the database. -// -// Parameters: -// - key: The column name to search by. -// - value: The value to match. -// - likeSearch: Whether to use a partial match (LIKE). -// -// Returns: -// - error: Any database error encountered during deletion. -func (aH *AccessHandler) DeleteUserByKey(key string, value any, likeSearch bool) (err error) { - return aH.dbHandler.deleteByKey(&models.User{}, key, value, likeSearch) -}