From e469b61bb2f878597f9d20076fc72f712f8b559d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Z=C3=BCrcher?= Date: Fri, 24 Oct 2025 10:43:59 +0200 Subject: [PATCH] move handlers to root folder --- accessHandler.go | 125 ++++++++++++++++++++ dbHandler.go | 300 +++++++++++++++++++++++++++++++++++++++++++++++ login.go | 243 ++++++++++++++++++++++++++++++++++++++ middleware.go | 165 ++++++++++++++++++++++++++ role.go | 197 +++++++++++++++++++++++++++++++ user.go | 218 ++++++++++++++++++++++++++++++++++ 6 files changed, 1248 insertions(+) create mode 100644 accessHandler.go create mode 100644 dbHandler.go create mode 100644 login.go create mode 100644 middleware.go create mode 100644 role.go create mode 100644 user.go diff --git a/accessHandler.go b/accessHandler.go new file mode 100644 index 0000000..b1497aa --- /dev/null +++ b/accessHandler.go @@ -0,0 +1,125 @@ +package AccessHandler + +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 := 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/dbHandler.go b/dbHandler.go new file mode 100644 index 0000000..35330de --- /dev/null +++ b/dbHandler.go @@ -0,0 +1,300 @@ +package AccessHandler + +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/login.go b/login.go new file mode 100644 index 0000000..b3690a8 --- /dev/null +++ b/login.go @@ -0,0 +1,243 @@ +package AccessHandler + +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/middleware.go b/middleware.go new file mode 100644 index 0000000..28495c5 --- /dev/null +++ b/middleware.go @@ -0,0 +1,165 @@ +package AccessHandler + +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: +// +// 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(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/role.go b/role.go new file mode 100644 index 0000000..75a84ab --- /dev/null +++ b/role.go @@ -0,0 +1,197 @@ +package AccessHandler + +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/user.go b/user.go new file mode 100644 index 0000000..165ebdb --- /dev/null +++ b/user.go @@ -0,0 +1,218 @@ +package AccessHandler + +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) +}