init bootstrap

This commit is contained in:
Fran Jurmanović
2025-07-06 15:02:09 +02:00
commit 016728532c
47 changed files with 8894 additions and 0 deletions

271
local/model/audit_log.go Normal file
View File

@@ -0,0 +1,271 @@
package model
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// AuditLog represents an audit log entry in the system
type AuditLog struct {
BaseModel
UserID string `json:"userId" gorm:"type:varchar(36);index"`
Action string `json:"action" gorm:"not null;type:varchar(100);index"`
Resource string `json:"resource" gorm:"not null;type:varchar(100);index"`
ResourceID string `json:"resourceId" gorm:"type:varchar(36);index"`
Details map[string]interface{} `json:"details" gorm:"type:text"`
IPAddress string `json:"ipAddress" gorm:"type:varchar(45)"`
UserAgent string `json:"userAgent" gorm:"type:text"`
Success bool `json:"success" gorm:"default:true;index"`
ErrorMsg string `json:"errorMsg,omitempty" gorm:"type:text"`
Duration int64 `json:"duration,omitempty"` // Duration in milliseconds
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// AuditLogCreateRequest represents the request to create a new audit log
type AuditLogCreateRequest struct {
UserID string `json:"userId"`
Action string `json:"action" validate:"required,max=100"`
Resource string `json:"resource" validate:"required,max=100"`
ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress" validate:"max=45"`
UserAgent string `json:"userAgent"`
Success bool `json:"success"`
ErrorMsg string `json:"errorMsg"`
Duration int64 `json:"duration"`
SessionID string `json:"sessionId"`
RequestID string `json:"requestId"`
}
// AuditLogInfo represents public audit log information
type AuditLogInfo struct {
ID string `json:"id"`
UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
Success bool `json:"success"`
ErrorMsg string `json:"errorMsg,omitempty"`
Duration int64 `json:"duration,omitempty"`
SessionID string `json:"sessionId,omitempty"`
RequestID string `json:"requestId,omitempty"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating an audit log
func (al *AuditLog) BeforeCreate(tx *gorm.DB) error {
al.BaseModel.BeforeCreate()
// Normalize fields
al.Action = strings.ToLower(strings.TrimSpace(al.Action))
al.Resource = strings.ToLower(strings.TrimSpace(al.Resource))
al.IPAddress = strings.TrimSpace(al.IPAddress)
al.UserAgent = strings.TrimSpace(al.UserAgent)
return nil
}
// SetDetails sets the details field from a map
func (al *AuditLog) SetDetails(details map[string]interface{}) error {
al.Details = details
return nil
}
// GetDetails returns the details as a map
func (al *AuditLog) GetDetails() map[string]interface{} {
if al.Details == nil {
return make(map[string]interface{})
}
return al.Details
}
// SetDetailsFromJSON sets the details field from a JSON string
func (al *AuditLog) SetDetailsFromJSON(jsonStr string) error {
if jsonStr == "" {
al.Details = make(map[string]interface{})
return nil
}
var details map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
return err
}
al.Details = details
return nil
}
// GetDetailsAsJSON returns the details as a JSON string
func (al *AuditLog) GetDetailsAsJSON() (string, error) {
if al.Details == nil || len(al.Details) == 0 {
return "{}", nil
}
bytes, err := json.Marshal(al.Details)
if err != nil {
return "", err
}
return string(bytes), nil
}
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
info := AuditLogInfo{
ID: al.ID,
UserID: al.UserID,
Action: al.Action,
Resource: al.Resource,
ResourceID: al.ResourceID,
Details: al.GetDetails(),
IPAddress: al.IPAddress,
UserAgent: al.UserAgent,
Success: al.Success,
ErrorMsg: al.ErrorMsg,
Duration: al.Duration,
SessionID: al.SessionID,
RequestID: al.RequestID,
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Include user information if available
if al.User != nil {
info.UserEmail = al.User.Email
info.UserName = al.User.Name
}
return info
}
// AddDetail adds a single detail to the details map
func (al *AuditLog) AddDetail(key string, value interface{}) {
if al.Details == nil {
al.Details = make(map[string]interface{})
}
al.Details[key] = value
}
// GetDetail gets a single detail from the details map
func (al *AuditLog) GetDetail(key string) (interface{}, bool) {
if al.Details == nil {
return nil, false
}
value, exists := al.Details[key]
return value, exists
}
// Common audit log actions
const (
AuditActionCreate = "create"
AuditActionRead = "read"
AuditActionUpdate = "update"
AuditActionDelete = "delete"
AuditActionLogin = "login"
AuditActionLogout = "logout"
AuditActionAccess = "access"
AuditActionExport = "export"
AuditActionImport = "import"
AuditActionConfig = "config"
)
// Common audit log resources
const (
AuditResourceUser = "user"
AuditResourceRole = "role"
AuditResourcePermission = "permission"
AuditResourceSystemConfig = "system_config"
AuditResourceAuth = "auth"
AuditResourceAPI = "api"
AuditResourceFile = "file"
AuditResourceDatabase = "database"
AuditResourceSystem = "system"
)
// CreateAuditLog creates a new audit log entry
func CreateAuditLog(userID, action, resource, resourceID string, success bool) *AuditLog {
auditLog := &AuditLog{
UserID: userID,
Action: action,
Resource: resource,
ResourceID: resourceID,
Success: success,
Details: make(map[string]interface{}),
}
auditLog.Init()
return auditLog
}
// CreateAuditLogWithDetails creates a new audit log entry with details
func CreateAuditLogWithDetails(userID, action, resource, resourceID string, success bool, details map[string]interface{}) *AuditLog {
auditLog := CreateAuditLog(userID, action, resource, resourceID, success)
auditLog.Details = details
return auditLog
}
// CreateAuditLogWithError creates a new audit log entry for an error
func CreateAuditLogWithError(userID, action, resource, resourceID, errorMsg string) *AuditLog {
auditLog := CreateAuditLog(userID, action, resource, resourceID, false)
auditLog.ErrorMsg = errorMsg
return auditLog
}
// SetRequestInfo sets request-related information
func (al *AuditLog) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
al.IPAddress = ipAddress
al.UserAgent = userAgent
al.SessionID = sessionID
al.RequestID = requestID
}
// SetDuration sets the operation duration
func (al *AuditLog) SetDuration(start time.Time) {
al.Duration = time.Since(start).Milliseconds()
}
// IsSuccess returns whether the audit log represents a successful operation
func (al *AuditLog) IsSuccess() bool {
return al.Success
}
// IsFailure returns whether the audit log represents a failed operation
func (al *AuditLog) IsFailure() bool {
return !al.Success
}
// GetActionDescription returns a human-readable description of the action
func (al *AuditLog) GetActionDescription() string {
switch al.Action {
case AuditActionCreate:
return "Created " + al.Resource
case AuditActionRead:
return "Viewed " + al.Resource
case AuditActionUpdate:
return "Updated " + al.Resource
case AuditActionDelete:
return "Deleted " + al.Resource
case AuditActionLogin:
return "Logged in"
case AuditActionLogout:
return "Logged out"
case AuditActionAccess:
return "Accessed " + al.Resource
case AuditActionExport:
return "Exported " + al.Resource
case AuditActionImport:
return "Imported " + al.Resource
case AuditActionConfig:
return "Configured " + al.Resource
default:
return strings.Title(al.Action) + " " + al.Resource
}
}

110
local/model/base.go Normal file
View File

@@ -0,0 +1,110 @@
package model
import (
"time"
"github.com/google/uuid"
)
// BaseModel provides common fields for all database models
type BaseModel struct {
ID string `json:"id" gorm:"primary_key;type:varchar(36)"`
DateCreated time.Time `json:"dateCreated" gorm:"not null"`
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"`
}
// Init initializes base model with DateCreated, DateUpdated, and ID values
func (bm *BaseModel) Init() {
now := time.Now().UTC()
bm.ID = uuid.NewString()
bm.DateCreated = now
bm.DateUpdated = now
}
// UpdateTimestamp updates the DateUpdated field
func (bm *BaseModel) UpdateTimestamp() {
bm.DateUpdated = time.Now().UTC()
}
// BeforeCreate is a GORM hook that runs before creating a record
func (bm *BaseModel) BeforeCreate() error {
if bm.ID == "" {
bm.Init()
}
return nil
}
// BeforeUpdate is a GORM hook that runs before updating a record
func (bm *BaseModel) BeforeUpdate() error {
bm.UpdateTimestamp()
return nil
}
// FilteredResponse represents a paginated response
type FilteredResponse struct {
Items interface{} `json:"items"`
Params
}
// MessageResponse represents a simple message response
type MessageResponse struct {
Message string `json:"message"`
Success bool `json:"success"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Details map[string]interface{} `json:"details,omitempty"`
Code int `json:"code,omitempty"`
}
// Params represents pagination and filtering parameters
type Params struct {
SortBy string `json:"sortBy" query:"sortBy"`
SortOrder string `json:"sortOrder" query:"sortOrder"`
Page int `json:"page" query:"page"`
Limit int `json:"limit" query:"limit"`
Search string `json:"search" query:"search"`
Filter string `json:"filter" query:"filter"`
TotalRecords int64 `json:"totalRecords"`
TotalPages int `json:"totalPages"`
}
// DefaultParams returns default pagination parameters
func DefaultParams() Params {
return Params{
Page: 1,
Limit: 10,
SortBy: "dateCreated",
SortOrder: "desc",
}
}
// Validate validates and sets default values for parameters
func (p *Params) Validate() {
if p.Page < 1 {
p.Page = 1
}
if p.Limit < 1 || p.Limit > 100 {
p.Limit = 10
}
if p.SortBy == "" {
p.SortBy = "dateCreated"
}
if p.SortOrder != "asc" && p.SortOrder != "desc" {
p.SortOrder = "desc"
}
}
// Offset calculates the offset for database queries
func (p *Params) Offset() int {
return (p.Page - 1) * p.Limit
}
// CalculateTotalPages calculates total pages based on total records
func (p *Params) CalculateTotalPages() {
if p.Limit > 0 {
p.TotalPages = int((p.TotalRecords + int64(p.Limit) - 1) / int64(p.Limit))
}
}

View File

@@ -0,0 +1,228 @@
package model
import (
"gorm.io/gorm"
)
// MembershipFilter represents filters for membership-related queries
type MembershipFilter struct {
Params
// User-specific filters
Username string `json:"username" query:"username"`
Email string `json:"email" query:"email"`
Active *bool `json:"active" query:"active"`
EmailVerified *bool `json:"emailVerified" query:"emailVerified"`
RoleID string `json:"roleId" query:"roleId"`
RoleName string `json:"roleName" query:"roleName"`
// Role-specific filters
RoleActive *bool `json:"roleActive" query:"roleActive"`
RoleSystem *bool `json:"roleSystem" query:"roleSystem"`
// Permission-specific filters
PermissionName string `json:"permissionName" query:"permissionName"`
PermissionCategory string `json:"permissionCategory" query:"permissionCategory"`
PermissionActive *bool `json:"permissionActive" query:"permissionActive"`
PermissionSystem *bool `json:"permissionSystem" query:"permissionSystem"`
// Date range filters
CreatedAfter string `json:"createdAfter" query:"createdAfter"`
CreatedBefore string `json:"createdBefore" query:"createdBefore"`
UpdatedAfter string `json:"updatedAfter" query:"updatedAfter"`
UpdatedBefore string `json:"updatedBefore" query:"updatedBefore"`
// Relationship filters
WithRoles bool `json:"withRoles" query:"withRoles"`
WithPermissions bool `json:"withPermissions" query:"withPermissions"`
WithUsers bool `json:"withUsers" query:"withUsers"`
}
// ApplyFilter applies the filter conditions to a GORM query
func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
if f == nil {
return query
}
// Apply search across multiple fields
if f.Search != "" {
query = query.Where("username LIKE ? OR email LIKE ? OR name LIKE ?",
"%"+f.Search+"%", "%"+f.Search+"%", "%"+f.Search+"%")
}
// User-specific filters
if f.Username != "" {
query = query.Where("username = ?", f.Username)
}
if f.Email != "" {
query = query.Where("email = ?", f.Email)
}
if f.Active != nil {
query = query.Where("active = ?", *f.Active)
}
if f.EmailVerified != nil {
query = query.Where("email_verified = ?", *f.EmailVerified)
}
if f.RoleID != "" {
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
Where("user_roles.role_id = ?", f.RoleID)
}
if f.RoleName != "" {
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
Joins("JOIN roles ON user_roles.role_id = roles.id").
Where("roles.name = ?", f.RoleName)
}
// Role-specific filters
if f.RoleActive != nil {
query = query.Where("active = ?", *f.RoleActive)
}
if f.RoleSystem != nil {
query = query.Where("system = ?", *f.RoleSystem)
}
// Permission-specific filters
if f.PermissionName != "" {
query = query.Where("name = ?", f.PermissionName)
}
if f.PermissionCategory != "" {
query = query.Where("category = ?", f.PermissionCategory)
}
if f.PermissionActive != nil {
query = query.Where("active = ?", *f.PermissionActive)
}
if f.PermissionSystem != nil {
query = query.Where("system = ?", *f.PermissionSystem)
}
// Date range filters
if f.CreatedAfter != "" {
query = query.Where("date_created >= ?", f.CreatedAfter)
}
if f.CreatedBefore != "" {
query = query.Where("date_created <= ?", f.CreatedBefore)
}
if f.UpdatedAfter != "" {
query = query.Where("date_updated >= ?", f.UpdatedAfter)
}
if f.UpdatedBefore != "" {
query = query.Where("date_updated <= ?", f.UpdatedBefore)
}
// Relationship preloading
if f.WithRoles {
query = query.Preload("Roles")
}
if f.WithPermissions {
query = query.Preload("Permissions")
}
if f.WithUsers {
query = query.Preload("Users")
}
return query
}
// Pagination returns the offset and limit for pagination
func (f *MembershipFilter) Pagination() (offset, limit int) {
if f == nil {
return 0, 10
}
f.Validate()
return f.Offset(), f.Limit
}
// GetSorting returns the sorting field and direction
func (f *MembershipFilter) GetSorting() (field string, desc bool) {
if f == nil {
return "date_created", true
}
f.Validate()
// Map common sort fields to database column names
switch f.SortBy {
case "dateCreated":
field = "date_created"
case "dateUpdated":
field = "date_updated"
case "username":
field = "username"
case "email":
field = "email"
case "name":
field = "name"
case "active":
field = "active"
default:
field = "date_created"
}
desc = f.SortOrder == "desc"
return field, desc
}
// NewMembershipFilter creates a new MembershipFilter with default values
func NewMembershipFilter() *MembershipFilter {
return &MembershipFilter{
Params: DefaultParams(),
}
}
// SetUserFilters sets user-specific filters
func (f *MembershipFilter) SetUserFilters(username, email string, active, emailVerified *bool) *MembershipFilter {
f.Username = username
f.Email = email
f.Active = active
f.EmailVerified = emailVerified
return f
}
// SetRoleFilters sets role-specific filters
func (f *MembershipFilter) SetRoleFilters(roleID, roleName string, roleActive, roleSystem *bool) *MembershipFilter {
f.RoleID = roleID
f.RoleName = roleName
f.RoleActive = roleActive
f.RoleSystem = roleSystem
return f
}
// SetPermissionFilters sets permission-specific filters
func (f *MembershipFilter) SetPermissionFilters(name, category string, active, system *bool) *MembershipFilter {
f.PermissionName = name
f.PermissionCategory = category
f.PermissionActive = active
f.PermissionSystem = system
return f
}
// SetDateRangeFilters sets date range filters
func (f *MembershipFilter) SetDateRangeFilters(createdAfter, createdBefore, updatedAfter, updatedBefore string) *MembershipFilter {
f.CreatedAfter = createdAfter
f.CreatedBefore = createdBefore
f.UpdatedAfter = updatedAfter
f.UpdatedBefore = updatedBefore
return f
}
// SetPreloads sets which relationships to preload
func (f *MembershipFilter) SetPreloads(withRoles, withPermissions, withUsers bool) *MembershipFilter {
f.WithRoles = withRoles
f.WithPermissions = withPermissions
f.WithUsers = withUsers
return f
}

276
local/model/permission.go Normal file
View File

@@ -0,0 +1,276 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Permission represents a permission in the system
type Permission struct {
BaseModel
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
Category string `json:"category" gorm:"type:varchar(50)"`
Active bool `json:"active" gorm:"default:true"`
System bool `json:"system" gorm:"default:false"` // System permissions cannot be deleted
Roles []Role `json:"-" gorm:"many2many:role_permissions;"`
}
// PermissionCreateRequest represents the request to create a new permission
type PermissionCreateRequest struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"max=500"`
Category string `json:"category" validate:"required,max=50"`
}
// PermissionUpdateRequest represents the request to update a permission
type PermissionUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
Category *string `json:"category,omitempty" validate:"omitempty,max=50"`
Active *bool `json:"active,omitempty"`
}
// PermissionInfo represents public permission information
type PermissionInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Active bool `json:"active"`
System bool `json:"system"`
RoleCount int64 `json:"roleCount"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a permission
func (p *Permission) BeforeCreate(tx *gorm.DB) error {
p.BaseModel.BeforeCreate()
// Normalize fields
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
p.Description = strings.TrimSpace(p.Description)
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
return p.Validate()
}
// BeforeUpdate is called before updating a permission
func (p *Permission) BeforeUpdate(tx *gorm.DB) error {
p.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if p.Name != "" {
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
}
if p.Description != "" {
p.Description = strings.TrimSpace(p.Description)
}
if p.Category != "" {
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
}
return p.Validate()
}
// BeforeDelete is called before deleting a permission
func (p *Permission) BeforeDelete(tx *gorm.DB) error {
if p.System {
return errors.New("system permissions cannot be deleted")
}
// Check if permission is assigned to any roles
var roleCount int64
if err := tx.Model(&Role{}).Where("permissions.id = ?", p.ID).Joins("JOIN role_permissions ON roles.id = role_permissions.role_id").Count(&roleCount).Error; err != nil {
return err
}
if roleCount > 0 {
return errors.New("cannot delete permission that is assigned to roles")
}
return nil
}
// Validate validates permission data
func (p *Permission) Validate() error {
if p.Name == "" {
return errors.New("permission name is required")
}
if len(p.Name) < 3 || len(p.Name) > 100 {
return errors.New("permission name must be between 3 and 100 characters")
}
if !isValidPermissionName(p.Name) {
return errors.New("permission name must follow the format 'resource:action' (e.g., 'user:create')")
}
if p.Category == "" {
return errors.New("permission category is required")
}
if len(p.Category) > 50 {
return errors.New("permission category must not exceed 50 characters")
}
if len(p.Description) > 500 {
return errors.New("permission description must not exceed 500 characters")
}
return nil
}
// ToPermissionInfo converts Permission to PermissionInfo (public information)
func (p *Permission) ToPermissionInfo() PermissionInfo {
return PermissionInfo{
ID: p.ID,
Name: p.Name,
Description: p.Description,
Category: p.Category,
Active: p.Active,
System: p.System,
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
}
}
// GetResource extracts the resource part from a permission name (e.g., "user:create" -> "user")
func (p *Permission) GetResource() string {
parts := strings.Split(p.Name, ":")
if len(parts) > 0 {
return parts[0]
}
return ""
}
// GetAction extracts the action part from a permission name (e.g., "user:create" -> "create")
func (p *Permission) GetAction() string {
parts := strings.Split(p.Name, ":")
if len(parts) > 1 {
return parts[1]
}
return ""
}
// isValidPermissionName validates permission name format
func isValidPermissionName(name string) bool {
// Permission names should follow the format "resource:action"
parts := strings.Split(name, ":")
if len(parts) != 2 {
return false
}
resource := parts[0]
action := parts[1]
// Validate resource part
if len(resource) < 2 || len(resource) > 50 {
return false
}
// Validate action part
if len(action) < 2 || len(action) > 50 {
return false
}
// Check if both parts contain only valid characters
return isValidIdentifier(resource) && isValidIdentifier(action)
}
// isValidIdentifier checks if a string is a valid identifier (letters, numbers, underscores)
func isValidIdentifier(str string) bool {
for _, char := range str {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_') {
return false
}
}
return true
}
// Common permission categories
const (
PermissionCategoryUser = "user"
PermissionCategoryRole = "role"
PermissionCategorySystem = "system"
PermissionCategoryContent = "content"
PermissionCategoryReport = "report"
)
// Common permission patterns
const (
PermissionCreate = "create"
PermissionRead = "read"
PermissionUpdate = "update"
PermissionDelete = "delete"
PermissionManage = "manage"
PermissionAdmin = "admin"
)
// GetStandardPermissions returns a list of standard permissions for a resource
func GetStandardPermissions(resource string) []Permission {
return []Permission{
{
Name: resource + ":" + PermissionCreate,
Description: "Create new " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionRead,
Description: "Read " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionUpdate,
Description: "Update " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionDelete,
Description: "Delete " + resource + " records",
Category: resource,
Active: true,
},
}
}
// Common system permissions
const (
ServerView = "server:view"
ServerUpdate = "server:update"
ServerStart = "server:start"
ServerStop = "server:stop"
ConfigView = "config:view"
ConfigUpdate = "config:update"
)
// AllPermissions returns all available permissions in the system
var AllPermissions = []string{
"user:create",
"user:read",
"user:update",
"user:delete",
"role:create",
"role:read",
"role:update",
"role:delete",
"permission:create",
"permission:read",
"permission:update",
"permission:delete",
ServerView,
ServerUpdate,
ServerStart,
ServerStop,
ConfigView,
ConfigUpdate,
"audit:read",
"system:admin",
}

182
local/model/role.go Normal file
View File

@@ -0,0 +1,182 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Role represents a role in the system
type Role struct {
BaseModel
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
Active bool `json:"active" gorm:"default:true"`
System bool `json:"system" gorm:"default:false"` // System roles cannot be deleted
Users []User `json:"-" gorm:"many2many:user_roles;"`
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}
// RoleCreateRequest represents the request to create a new role
type RoleCreateRequest struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"max=500"`
PermissionIDs []string `json:"permissionIds"`
}
// RoleUpdateRequest represents the request to update a role
type RoleUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
Active *bool `json:"active,omitempty"`
PermissionIDs []string `json:"permissionIds,omitempty"`
}
// RoleInfo represents public role information
type RoleInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Active bool `json:"active"`
System bool `json:"system"`
Permissions []PermissionInfo `json:"permissions"`
UserCount int64 `json:"userCount"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a role
func (r *Role) BeforeCreate(tx *gorm.DB) error {
r.BaseModel.BeforeCreate()
// Normalize name
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
r.Description = strings.TrimSpace(r.Description)
return r.Validate()
}
// BeforeUpdate is called before updating a role
func (r *Role) BeforeUpdate(tx *gorm.DB) error {
r.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if r.Name != "" {
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
}
if r.Description != "" {
r.Description = strings.TrimSpace(r.Description)
}
return r.Validate()
}
// BeforeDelete is called before deleting a role
func (r *Role) BeforeDelete(tx *gorm.DB) error {
if r.System {
return errors.New("system roles cannot be deleted")
}
// Check if role is assigned to any users
var userCount int64
if err := tx.Model(&User{}).Where("roles.id = ?", r.ID).Joins("JOIN user_roles ON users.id = user_roles.user_id").Count(&userCount).Error; err != nil {
return err
}
if userCount > 0 {
return errors.New("cannot delete role that is assigned to users")
}
return nil
}
// Validate validates role data
func (r *Role) Validate() error {
if r.Name == "" {
return errors.New("role name is required")
}
if len(r.Name) < 3 || len(r.Name) > 100 {
return errors.New("role name must be between 3 and 100 characters")
}
if !isValidRoleName(r.Name) {
return errors.New("role name can only contain letters, numbers, underscores, and hyphens")
}
if len(r.Description) > 500 {
return errors.New("role description must not exceed 500 characters")
}
return nil
}
// ToRoleInfo converts Role to RoleInfo (public information)
func (r *Role) ToRoleInfo() RoleInfo {
roleInfo := RoleInfo{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Active: r.Active,
System: r.System,
Permissions: make([]PermissionInfo, len(r.Permissions)),
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Convert permissions
for i, permission := range r.Permissions {
roleInfo.Permissions[i] = permission.ToPermissionInfo()
}
return roleInfo
}
// HasPermission checks if the role has a specific permission
func (r *Role) HasPermission(permissionName string) bool {
for _, permission := range r.Permissions {
if permission.Name == permissionName {
return true
}
}
return false
}
// AddPermission adds a permission to the role
func (r *Role) AddPermission(permission Permission) {
if !r.HasPermission(permission.Name) {
r.Permissions = append(r.Permissions, permission)
}
}
// RemovePermission removes a permission from the role
func (r *Role) RemovePermission(permissionName string) {
for i, permission := range r.Permissions {
if permission.Name == permissionName {
r.Permissions = append(r.Permissions[:i], r.Permissions[i+1:]...)
break
}
}
}
// GetPermissionNames returns a slice of permission names
func (r *Role) GetPermissionNames() []string {
names := make([]string, len(r.Permissions))
for i, permission := range r.Permissions {
names[i] = permission.Name
}
return names
}
// isValidRoleName validates role name format
func isValidRoleName(name string) bool {
// Allow letters, numbers, underscores, and hyphens
for _, char := range name {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_' || char == '-') {
return false
}
}
return true
}

View File

@@ -0,0 +1,367 @@
package model
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// SecurityEvent represents a security event in the system
type SecurityEvent struct {
BaseModel
EventType string `json:"eventType" gorm:"not null;type:varchar(100);index"`
Severity string `json:"severity" gorm:"not null;type:varchar(20);index"`
UserID string `json:"userId" gorm:"type:varchar(36);index"`
IPAddress string `json:"ipAddress" gorm:"type:varchar(45);index"`
UserAgent string `json:"userAgent" gorm:"type:text"`
Resource string `json:"resource" gorm:"type:varchar(100)"`
Action string `json:"action" gorm:"type:varchar(100)"`
Success bool `json:"success" gorm:"index"`
Blocked bool `json:"blocked" gorm:"default:false;index"`
Message string `json:"message" gorm:"type:text"`
Details map[string]interface{} `json:"details" gorm:"type:text"`
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
CountryCode string `json:"countryCode,omitempty" gorm:"type:varchar(2)"`
City string `json:"city,omitempty" gorm:"type:varchar(100)"`
Resolved bool `json:"resolved" gorm:"default:false;index"`
ResolvedBy string `json:"resolvedBy,omitempty" gorm:"type:varchar(36)"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty" gorm:"type:text"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Resolver *User `json:"resolver,omitempty" gorm:"foreignKey:ResolvedBy"`
}
// SecurityEventCreateRequest represents the request to create a new security event
type SecurityEventCreateRequest struct {
EventType string `json:"eventType" validate:"required,max=100"`
Severity string `json:"severity" validate:"required,oneof=low medium high critical"`
UserID string `json:"userId"`
IPAddress string `json:"ipAddress" validate:"max=45"`
UserAgent string `json:"userAgent"`
Resource string `json:"resource" validate:"max=100"`
Action string `json:"action" validate:"max=100"`
Success bool `json:"success"`
Blocked bool `json:"blocked"`
Message string `json:"message" validate:"required"`
Details map[string]interface{} `json:"details"`
SessionID string `json:"sessionId"`
RequestID string `json:"requestId"`
CountryCode string `json:"countryCode" validate:"max=2"`
City string `json:"city" validate:"max=100"`
}
// SecurityEventUpdateRequest represents the request to update a security event
type SecurityEventUpdateRequest struct {
Resolved *bool `json:"resolved,omitempty"`
ResolvedBy *string `json:"resolvedBy,omitempty"`
Notes *string `json:"notes,omitempty"`
}
// SecurityEventInfo represents public security event information
type SecurityEventInfo struct {
ID string `json:"id"`
EventType string `json:"eventType"`
Severity string `json:"severity"`
UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
Resource string `json:"resource"`
Action string `json:"action"`
Success bool `json:"success"`
Blocked bool `json:"blocked"`
Message string `json:"message"`
Details map[string]interface{} `json:"details"`
SessionID string `json:"sessionId,omitempty"`
RequestID string `json:"requestId,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
City string `json:"city,omitempty"`
Resolved bool `json:"resolved"`
ResolvedBy string `json:"resolvedBy,omitempty"`
ResolverEmail string `json:"resolverEmail,omitempty"`
ResolverName string `json:"resolverName,omitempty"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a security event
func (se *SecurityEvent) BeforeCreate(tx *gorm.DB) error {
se.BaseModel.BeforeCreate()
// Normalize fields
se.EventType = strings.ToLower(strings.TrimSpace(se.EventType))
se.Severity = strings.ToLower(strings.TrimSpace(se.Severity))
se.IPAddress = strings.TrimSpace(se.IPAddress)
se.UserAgent = strings.TrimSpace(se.UserAgent)
se.Resource = strings.ToLower(strings.TrimSpace(se.Resource))
se.Action = strings.ToLower(strings.TrimSpace(se.Action))
se.Message = strings.TrimSpace(se.Message)
return se.Validate()
}
// BeforeUpdate is called before updating a security event
func (se *SecurityEvent) BeforeUpdate(tx *gorm.DB) error {
se.BaseModel.BeforeUpdate()
// If resolving the event, set resolved timestamp
if se.Resolved && se.ResolvedAt == nil {
now := time.Now()
se.ResolvedAt = &now
}
return nil
}
// Validate validates security event data
func (se *SecurityEvent) Validate() error {
validSeverities := []string{"low", "medium", "high", "critical"}
isValidSeverity := false
for _, severity := range validSeverities {
if se.Severity == severity {
isValidSeverity = true
break
}
}
if !isValidSeverity {
return gorm.ErrInvalidValue
}
return nil
}
// SetDetails sets the details field from a map
func (se *SecurityEvent) SetDetails(details map[string]interface{}) error {
se.Details = details
return nil
}
// GetDetails returns the details as a map
func (se *SecurityEvent) GetDetails() map[string]interface{} {
if se.Details == nil {
return make(map[string]interface{})
}
return se.Details
}
// SetDetailsFromJSON sets the details field from a JSON string
func (se *SecurityEvent) SetDetailsFromJSON(jsonStr string) error {
if jsonStr == "" {
se.Details = make(map[string]interface{})
return nil
}
var details map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
return err
}
se.Details = details
return nil
}
// GetDetailsAsJSON returns the details as a JSON string
func (se *SecurityEvent) GetDetailsAsJSON() (string, error) {
if se.Details == nil || len(se.Details) == 0 {
return "{}", nil
}
bytes, err := json.Marshal(se.Details)
if err != nil {
return "", err
}
return string(bytes), nil
}
// ToSecurityEventInfo converts SecurityEvent to SecurityEventInfo (public information)
func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
info := SecurityEventInfo{
ID: se.ID,
EventType: se.EventType,
Severity: se.Severity,
UserID: se.UserID,
IPAddress: se.IPAddress,
UserAgent: se.UserAgent,
Resource: se.Resource,
Action: se.Action,
Success: se.Success,
Blocked: se.Blocked,
Message: se.Message,
Details: se.GetDetails(),
SessionID: se.SessionID,
RequestID: se.RequestID,
CountryCode: se.CountryCode,
City: se.City,
Resolved: se.Resolved,
ResolvedBy: se.ResolvedBy,
ResolvedAt: se.ResolvedAt,
Notes: se.Notes,
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Include user information if available
if se.User != nil {
info.UserEmail = se.User.Email
info.UserName = se.User.Name
}
// Include resolver information if available
if se.Resolver != nil {
info.ResolverEmail = se.Resolver.Email
info.ResolverName = se.Resolver.Name
}
return info
}
// AddDetail adds a single detail to the details map
func (se *SecurityEvent) AddDetail(key string, value interface{}) {
if se.Details == nil {
se.Details = make(map[string]interface{})
}
se.Details[key] = value
}
// GetDetail gets a single detail from the details map
func (se *SecurityEvent) GetDetail(key string) (interface{}, bool) {
if se.Details == nil {
return nil, false
}
value, exists := se.Details[key]
return value, exists
}
// Resolve resolves the security event
func (se *SecurityEvent) Resolve(resolverID, notes string) {
se.Resolved = true
se.ResolvedBy = resolverID
se.Notes = notes
now := time.Now()
se.ResolvedAt = &now
}
// Unresolve unresolves the security event
func (se *SecurityEvent) Unresolve() {
se.Resolved = false
se.ResolvedBy = ""
se.ResolvedAt = nil
}
// IsResolved returns whether the security event is resolved
func (se *SecurityEvent) IsResolved() bool {
return se.Resolved
}
// IsCritical returns whether the security event is critical
func (se *SecurityEvent) IsCritical() bool {
return se.Severity == SecuritySeverityCritical
}
// IsHigh returns whether the security event is high severity
func (se *SecurityEvent) IsHigh() bool {
return se.Severity == SecuritySeverityHigh
}
// IsBlocked returns whether the security event was blocked
func (se *SecurityEvent) IsBlocked() bool {
return se.Blocked
}
// Security event types
const (
SecurityEventLoginAttempt = "login_attempt"
SecurityEventLoginFailure = "login_failure"
SecurityEventLoginSuccess = "login_success"
SecurityEventBruteForce = "brute_force"
SecurityEventAccountLockout = "account_lockout"
SecurityEventUnauthorizedAccess = "unauthorized_access"
SecurityEventPrivilegeEscalation = "privilege_escalation"
SecurityEventSuspiciousActivity = "suspicious_activity"
SecurityEventRateLimitExceeded = "rate_limit_exceeded"
SecurityEventInvalidToken = "invalid_token"
SecurityEventTokenExpired = "token_expired"
SecurityEventPasswordChange = "password_change"
SecurityEventEmailVerification = "email_verification"
SecurityEventTwoFactorAuth = "two_factor_auth"
SecurityEventDataExfiltration = "data_exfiltration"
SecurityEventMaliciousRequest = "malicious_request"
SecurityEventSystemAccess = "system_access"
SecurityEventConfigChange = "config_change"
SecurityEventFileAccess = "file_access"
SecurityEventDatabaseAccess = "database_access"
)
// Security severity levels
const (
SecuritySeverityLow = "low"
SecuritySeverityMedium = "medium"
SecuritySeverityHigh = "high"
SecuritySeverityCritical = "critical"
)
// CreateSecurityEvent creates a new security event
func CreateSecurityEvent(eventType, severity, message string) *SecurityEvent {
event := &SecurityEvent{
EventType: eventType,
Severity: severity,
Message: message,
Details: make(map[string]interface{}),
}
event.Init()
return event
}
// CreateSecurityEventWithUser creates a new security event with user information
func CreateSecurityEventWithUser(eventType, severity, message, userID string) *SecurityEvent {
event := CreateSecurityEvent(eventType, severity, message)
event.UserID = userID
return event
}
// CreateSecurityEventWithDetails creates a new security event with details
func CreateSecurityEventWithDetails(eventType, severity, message string, details map[string]interface{}) *SecurityEvent {
event := CreateSecurityEvent(eventType, severity, message)
event.Details = details
return event
}
// SetRequestInfo sets request-related information
func (se *SecurityEvent) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
se.IPAddress = ipAddress
se.UserAgent = userAgent
se.SessionID = sessionID
se.RequestID = requestID
}
// SetLocationInfo sets location-related information
func (se *SecurityEvent) SetLocationInfo(countryCode, city string) {
se.CountryCode = countryCode
se.City = city
}
// SetResourceAction sets resource and action information
func (se *SecurityEvent) SetResourceAction(resource, action string) {
se.Resource = resource
se.Action = action
}
// MarkAsBlocked marks the security event as blocked
func (se *SecurityEvent) MarkAsBlocked() {
se.Blocked = true
}
// MarkAsSuccess marks the security event as successful
func (se *SecurityEvent) MarkAsSuccess() {
se.Success = true
}
// MarkAsFailure marks the security event as failed
func (se *SecurityEvent) MarkAsFailure() {
se.Success = false
}

View File

@@ -0,0 +1,280 @@
package model
import (
"errors"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
// SystemConfig represents a system configuration setting
type SystemConfig struct {
BaseModel
Key string `json:"key" gorm:"unique;not null;type:varchar(255)"`
Value string `json:"value" gorm:"type:text"`
DefaultValue string `json:"defaultValue" gorm:"type:text"`
Description string `json:"description" gorm:"type:text"`
Category string `json:"category" gorm:"type:varchar(100)"`
DataType string `json:"dataType" gorm:"type:varchar(50)"` // string, integer, boolean, json
IsEditable bool `json:"isEditable" gorm:"default:true"`
IsSecret bool `json:"isSecret" gorm:"default:false"` // For sensitive values
DateModified string `json:"dateModified" gorm:"type:varchar(50)"`
}
// SystemConfigCreateRequest represents the request to create a new system config
type SystemConfigCreateRequest struct {
Key string `json:"key" validate:"required,min=3,max=255"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description" validate:"max=1000"`
Category string `json:"category" validate:"required,max=100"`
DataType string `json:"dataType" validate:"required,oneof=string integer boolean json"`
IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"`
}
// SystemConfigUpdateRequest represents the request to update a system config
type SystemConfigUpdateRequest struct {
Value *string `json:"value,omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
Category *string `json:"category,omitempty" validate:"omitempty,max=100"`
DataType *string `json:"dataType,omitempty" validate:"omitempty,oneof=string integer boolean json"`
IsEditable *bool `json:"isEditable,omitempty"`
IsSecret *bool `json:"isSecret,omitempty"`
}
// SystemConfigInfo represents public system config information
type SystemConfigInfo struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value,omitempty"` // Omitted if secret
DefaultValue string `json:"defaultValue,omitempty"`
Description string `json:"description"`
Category string `json:"category"`
DataType string `json:"dataType"`
IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"`
DateCreated string `json:"dateCreated"`
DateModified string `json:"dateModified"`
}
// BeforeCreate is called before creating a system config
func (sc *SystemConfig) BeforeCreate(tx *gorm.DB) error {
sc.BaseModel.BeforeCreate()
// Normalize key and category
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
sc.Description = strings.TrimSpace(sc.Description)
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
return sc.Validate()
}
// BeforeUpdate is called before updating a system config
func (sc *SystemConfig) BeforeUpdate(tx *gorm.DB) error {
sc.BaseModel.BeforeUpdate()
// Update modification timestamp
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
// Normalize fields if they're being updated
if sc.Key != "" {
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
}
if sc.Category != "" {
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
}
if sc.Description != "" {
sc.Description = strings.TrimSpace(sc.Description)
}
return sc.Validate()
}
// Validate validates system config data
func (sc *SystemConfig) Validate() error {
if sc.Key == "" {
return errors.New("configuration key is required")
}
if len(sc.Key) < 3 || len(sc.Key) > 255 {
return errors.New("configuration key must be between 3 and 255 characters")
}
if !isValidConfigKey(sc.Key) {
return errors.New("configuration key can only contain letters, numbers, dots, underscores, and hyphens")
}
if sc.Category == "" {
return errors.New("configuration category is required")
}
if len(sc.Category) > 100 {
return errors.New("configuration category must not exceed 100 characters")
}
if sc.DataType == "" {
return errors.New("configuration data type is required")
}
if !isValidDataType(sc.DataType) {
return errors.New("invalid data type, must be one of: string, integer, boolean, json")
}
if len(sc.Description) > 1000 {
return errors.New("configuration description must not exceed 1000 characters")
}
// Validate value according to data type
if sc.Value != "" {
if err := sc.ValidateValue(sc.Value); err != nil {
return err
}
}
return nil
}
// ValidateValue validates the configuration value according to its data type
func (sc *SystemConfig) ValidateValue(value string) error {
switch sc.DataType {
case "integer":
if _, err := strconv.Atoi(value); err != nil {
return errors.New("value must be a valid integer")
}
case "boolean":
if _, err := strconv.ParseBool(value); err != nil {
return errors.New("value must be a valid boolean (true/false)")
}
case "json":
// Basic JSON validation - check if it starts with { or [
trimmed := strings.TrimSpace(value)
if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
return errors.New("value must be valid JSON")
}
}
return nil
}
// ToSystemConfigInfo converts SystemConfig to SystemConfigInfo (public information)
func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
info := SystemConfigInfo{
ID: sc.ID,
Key: sc.Key,
DefaultValue: sc.DefaultValue,
Description: sc.Description,
Category: sc.Category,
DataType: sc.DataType,
IsEditable: sc.IsEditable,
IsSecret: sc.IsSecret,
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
DateModified: sc.DateModified,
}
// Only include value if it's not a secret
if !sc.IsSecret {
info.Value = sc.Value
}
return info
}
// GetStringValue returns the configuration value as a string
func (sc *SystemConfig) GetStringValue() string {
if sc.Value != "" {
return sc.Value
}
return sc.DefaultValue
}
// GetIntValue returns the configuration value as an integer
func (sc *SystemConfig) GetIntValue() (int, error) {
value := sc.GetStringValue()
return strconv.Atoi(value)
}
// GetBoolValue returns the configuration value as a boolean
func (sc *SystemConfig) GetBoolValue() (bool, error) {
value := sc.GetStringValue()
return strconv.ParseBool(value)
}
// GetFloatValue returns the configuration value as a float64
func (sc *SystemConfig) GetFloatValue() (float64, error) {
value := sc.GetStringValue()
return strconv.ParseFloat(value, 64)
}
// SetValue sets the configuration value with type validation
func (sc *SystemConfig) SetValue(value string) error {
if err := sc.ValidateValue(value); err != nil {
return err
}
sc.Value = value
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
return nil
}
// ResetToDefault resets the configuration value to its default
func (sc *SystemConfig) ResetToDefault() {
sc.Value = sc.DefaultValue
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
}
// isValidConfigKey validates configuration key format
func isValidConfigKey(key string) bool {
// Allow letters, numbers, dots, underscores, and hyphens
for _, char := range key {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' || char == '_' || char == '-') {
return false
}
}
return true
}
// isValidDataType validates the data type
func isValidDataType(dataType string) bool {
validTypes := []string{"string", "integer", "boolean", "json"}
for _, validType := range validTypes {
if dataType == validType {
return true
}
}
return false
}
// Common system configuration categories
const (
ConfigCategoryGeneral = "general"
ConfigCategorySecurity = "security"
ConfigCategoryEmail = "email"
ConfigCategoryAPI = "api"
ConfigCategoryLogging = "logging"
ConfigCategoryStorage = "storage"
ConfigCategoryCache = "cache"
)
// Common system configuration keys
const (
ConfigKeyAppName = "app.name"
ConfigKeyAppVersion = "app.version"
ConfigKeyAppDescription = "app.description"
ConfigKeyJWTExpiryHours = "security.jwt_expiry_hours"
ConfigKeyPasswordMinLength = "security.password_min_length"
ConfigKeyMaxLoginAttempts = "security.max_login_attempts"
ConfigKeyLockoutDurationMinutes = "security.lockout_duration_minutes"
ConfigKeySessionTimeoutMinutes = "security.session_timeout_minutes"
ConfigKeyRateLimitRequests = "security.rate_limit_requests"
ConfigKeyRateLimitWindow = "security.rate_limit_window_minutes"
ConfigKeyLogLevel = "logging.level"
ConfigKeyLogRetentionDays = "logging.retention_days"
ConfigKeyMaxFileUploadSize = "storage.max_file_upload_size_mb"
ConfigKeyCacheEnabled = "cache.enabled"
ConfigKeyCacheTTLMinutes = "cache.ttl_minutes"
)

318
local/model/user.go Normal file
View File

@@ -0,0 +1,318 @@
package model
import (
"errors"
"omega-server/local/utl/password"
"regexp"
"strings"
"time"
"gorm.io/gorm"
)
// User represents a user in the system
type User struct {
BaseModel
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
Name string `json:"name" gorm:"not null;type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:text"`
Active bool `json:"active" gorm:"default:true"`
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetExpires *time.Time `json:"-"`
LastLogin *time.Time `json:"lastLogin"`
LoginAttempts int `json:"-" gorm:"default:0"`
LockedUntil *time.Time `json:"-"`
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
}
// UserCreateRequest represents the request to create a new user
type UserCreateRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Name string `json:"name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"`
RoleIDs []string `json:"roleIds"`
}
// UserUpdateRequest represents the request to update a user
type UserUpdateRequest struct {
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
Active *bool `json:"active,omitempty"`
RoleIDs []string `json:"roleIds,omitempty"`
}
// UserLoginRequest represents a login request
type UserLoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// UserLoginResponse represents a login response
type UserLoginResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refreshToken"`
ExpiresAt time.Time `json:"expiresAt"`
User UserInfo `json:"user"`
}
// UserInfo represents public user information
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
Name string `json:"name"`
Active bool `json:"active"`
EmailVerified bool `json:"emailVerified"`
LastLogin *time.Time `json:"lastLogin"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
DateCreated time.Time `json:"dateCreated"`
}
// ChangePasswordRequest represents a password change request
type ChangePasswordRequest struct {
CurrentPassword string `json:"currentPassword" validate:"required"`
NewPassword string `json:"newPassword" validate:"required,min=8"`
}
// ResetPasswordRequest represents a password reset request
type ResetPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
// ResetPasswordConfirmRequest represents a password reset confirmation
type ResetPasswordConfirmRequest struct {
Token string `json:"token" validate:"required"`
NewPassword string `json:"newPassword" validate:"required,min=8"`
}
// BeforeCreate is called before creating a user
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.BaseModel.BeforeCreate()
// Normalize email and username
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
u.Name = strings.TrimSpace(u.Name)
return u.Validate()
}
// BeforeUpdate is called before updating a user
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if u.Email != "" {
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
}
if u.Username != "" {
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
}
if u.Name != "" {
u.Name = strings.TrimSpace(u.Name)
}
return u.Validate()
}
// Validate validates user data
func (u *User) Validate() error {
if u.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(u.Email) {
return errors.New("invalid email format")
}
if u.Username == "" {
return errors.New("username is required")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("username must be between 3 and 50 characters")
}
if !isValidUsername(u.Username) {
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
}
if u.Name == "" {
return errors.New("name is required")
}
if len(u.Name) < 2 || len(u.Name) > 100 {
return errors.New("name must be between 2 and 100 characters")
}
return nil
}
// SetPassword sets the user's password hash
func (u *User) SetPassword(plainPassword string) error {
if err := validatePassword(plainPassword); err != nil {
return err
}
hash, err := password.HashPassword(plainPassword)
if err != nil {
return err
}
u.PasswordHash = hash
return nil
}
// CheckPassword verifies the user's password
func (u *User) CheckPassword(plainPassword string) bool {
return password.CheckPasswordHash(plainPassword, u.PasswordHash)
}
// VerifyPassword verifies the user's password (alias for CheckPassword)
func (u *User) VerifyPassword(plainPassword string) bool {
return u.CheckPassword(plainPassword)
}
// IsLocked checks if the user account is locked
func (u *User) IsLocked() bool {
if u.LockedUntil == nil {
return false
}
return time.Now().Before(*u.LockedUntil)
}
// Lock locks the user account for the specified duration
func (u *User) Lock(duration time.Duration) {
lockUntil := time.Now().Add(duration)
u.LockedUntil = &lockUntil
}
// Unlock unlocks the user account
func (u *User) Unlock() {
u.LockedUntil = nil
u.LoginAttempts = 0
}
// IncrementLoginAttempts increments the login attempt counter
func (u *User) IncrementLoginAttempts() {
u.LoginAttempts++
}
// ResetLoginAttempts resets the login attempt counter
func (u *User) ResetLoginAttempts() {
u.LoginAttempts = 0
}
// UpdateLastLogin updates the last login timestamp
func (u *User) UpdateLastLogin() {
now := time.Now()
u.LastLogin = &now
}
// ToUserInfo converts User to UserInfo (public information)
func (u *User) ToUserInfo() UserInfo {
userInfo := UserInfo{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Name: u.Name,
Active: u.Active,
EmailVerified: u.EmailVerified,
LastLogin: u.LastLogin,
DateCreated: u.DateCreated,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
}
// Convert roles and collect permissions
permissionSet := make(map[string]bool)
for i, role := range u.Roles {
userInfo.Roles[i] = role.ToRoleInfo()
for _, permission := range role.Permissions {
permissionSet[permission.Name] = true
}
}
// Convert permission set to slice
for permission := range permissionSet {
userInfo.Permissions = append(userInfo.Permissions, permission)
}
return userInfo
}
// HasRole checks if the user has a specific role
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
if role.Name == roleName {
return true
}
}
return false
}
// HasPermission checks if the user has a specific permission
func (u *User) HasPermission(permissionName string) bool {
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if permission.Name == permissionName {
return true
}
}
}
return false
}
// isValidEmail validates email format
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// isValidUsername validates username format
func isValidUsername(username string) bool {
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
return usernameRegex.MatchString(username)
}
// validatePassword validates password strength
func validatePassword(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters long")
}
if len(password) > 128 {
return errors.New("password must not exceed 128 characters")
}
// Check for at least one lowercase letter
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
return errors.New("password must contain at least one lowercase letter")
}
// Check for at least one uppercase letter
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
return errors.New("password must contain at least one uppercase letter")
}
// Check for at least one digit
if matched, _ := regexp.MatchString(`\d`, password); !matched {
return errors.New("password must contain at least one digit")
}
// Check for at least one special character
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
return errors.New("password must contain at least one special character")
}
return nil
}