init bootstrap
This commit is contained in:
271
local/model/audit_log.go
Normal file
271
local/model/audit_log.go
Normal 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
110
local/model/base.go
Normal 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))
|
||||
}
|
||||
}
|
||||
228
local/model/membership_filter.go
Normal file
228
local/model/membership_filter.go
Normal 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
276
local/model/permission.go
Normal 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
182
local/model/role.go
Normal 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
|
||||
}
|
||||
367
local/model/security_event.go
Normal file
367
local/model/security_event.go
Normal 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
|
||||
}
|
||||
280
local/model/system_config.go
Normal file
280
local/model/system_config.go
Normal 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
318
local/model/user.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user