implement graphQL and init postgres

This commit is contained in:
Fran Jurmanović
2025-07-06 19:19:36 +02:00
parent 016728532c
commit 26a0d33592
25 changed files with 1713 additions and 314 deletions

View File

@@ -44,22 +44,22 @@ type AuditLogCreateRequest struct {
// 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"`
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"`
CreatedAt string `json:"created_at"`
}
// BeforeCreate is called before creating an audit log
@@ -122,26 +122,26 @@ func (al *AuditLog) GetDetailsAsJSON() (string, error) {
// 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"),
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,
CreatedAt: al.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Include user information if available
if al.User != nil {
info.UserEmail = al.User.Email
info.UserName = al.User.Name
info.UserName = al.User.FullName
}
return info

View File

@@ -8,22 +8,22 @@ import (
// 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"`
ID string `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
CreatedAt time.Time `json:"created_at" gorm:"not null;default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:now()"`
}
// Init initializes base model with DateCreated, DateUpdated, and ID values
// Init initializes base model with CreatedAt, UpdatedAt, and ID values
func (bm *BaseModel) Init() {
now := time.Now().UTC()
bm.ID = uuid.NewString()
bm.DateCreated = now
bm.DateUpdated = now
bm.CreatedAt = now
bm.UpdatedAt = now
}
// UpdateTimestamp updates the DateUpdated field
// UpdateTimestamp updates the UpdatedAt field
func (bm *BaseModel) UpdateTimestamp() {
bm.DateUpdated = time.Now().UTC()
bm.UpdatedAt = time.Now().UTC()
}
// BeforeCreate is a GORM hook that runs before creating a record
@@ -76,7 +76,7 @@ func DefaultParams() Params {
return Params{
Page: 1,
Limit: 10,
SortBy: "dateCreated",
SortBy: "created_at",
SortOrder: "desc",
}
}
@@ -90,7 +90,7 @@ func (p *Params) Validate() {
p.Limit = 10
}
if p.SortBy == "" {
p.SortBy = "dateCreated"
p.SortBy = "created_at"
}
if p.SortOrder != "asc" && p.SortOrder != "desc" {
p.SortOrder = "desc"

144
local/model/integration.go Normal file
View File

@@ -0,0 +1,144 @@
package model
import (
"encoding/json"
"errors"
"strings"
"gorm.io/gorm"
)
// Integration represents a third-party integration configuration
type Integration struct {
BaseModel
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
Type string `json:"type" gorm:"not null;type:varchar(50)"`
Config json.RawMessage `json:"config" gorm:"type:jsonb;not null"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
}
// IntegrationCreateRequest represents the request to create a new integration
type IntegrationCreateRequest struct {
ProjectID string `json:"project_id" validate:"required,uuid"`
Type string `json:"type" validate:"required,min=1,max=50"`
Config map[string]interface{} `json:"config" validate:"required"`
}
// IntegrationUpdateRequest represents the request to update an integration
type IntegrationUpdateRequest struct {
Config map[string]interface{} `json:"config" validate:"required"`
}
// IntegrationInfo represents public integration information
type IntegrationInfo struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// BeforeCreate is called before creating an integration
func (i *Integration) BeforeCreate(tx *gorm.DB) error {
i.BaseModel.BeforeCreate()
// Normalize type
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
return i.Validate()
}
// BeforeUpdate is called before updating an integration
func (i *Integration) BeforeUpdate(tx *gorm.DB) error {
i.BaseModel.BeforeUpdate()
// Normalize type if it's being updated
if i.Type != "" {
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
}
return i.Validate()
}
// Validate validates integration data
func (i *Integration) Validate() error {
if i.ProjectID == "" {
return errors.New("project_id is required")
}
if i.Type == "" {
return errors.New("type is required")
}
if len(i.Type) > 50 {
return errors.New("type must not exceed 50 characters")
}
if len(i.Config) == 0 {
return errors.New("config is required")
}
// Validate that config is valid JSON
var configMap map[string]interface{}
if err := json.Unmarshal(i.Config, &configMap); err != nil {
return errors.New("config must be valid JSON")
}
return nil
}
// ToIntegrationInfo converts Integration to IntegrationInfo (public information)
func (i *Integration) ToIntegrationInfo() IntegrationInfo {
integrationInfo := IntegrationInfo{
ID: i.ID,
ProjectID: i.ProjectID,
Type: i.Type,
CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: i.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Parse config JSON to map
var configMap map[string]interface{}
if err := json.Unmarshal(i.Config, &configMap); err == nil {
integrationInfo.Config = configMap
}
return integrationInfo
}
// SetConfig sets the configuration from a map
func (i *Integration) SetConfig(config map[string]interface{}) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
i.Config = configJSON
return nil
}
// GetConfig gets the configuration as a map
func (i *Integration) GetConfig() (map[string]interface{}, error) {
var configMap map[string]interface{}
err := json.Unmarshal(i.Config, &configMap)
return configMap, err
}
// GetConfigValue gets a specific configuration value
func (i *Integration) GetConfigValue(key string) (interface{}, error) {
configMap, err := i.GetConfig()
if err != nil {
return nil, err
}
return configMap[key], nil
}
// SetConfigValue sets a specific configuration value
func (i *Integration) SetConfigValue(key string, value interface{}) error {
configMap, err := i.GetConfig()
if err != nil {
return err
}
configMap[key] = value
return i.SetConfig(configMap)
}

View File

@@ -156,10 +156,10 @@ func (f *MembershipFilter) GetSorting() (field string, desc bool) {
// Map common sort fields to database column names
switch f.SortBy {
case "dateCreated":
field = "date_created"
case "dateUpdated":
field = "date_updated"
case "created_at":
field = "created_at"
case "updated_at":
field = "updated_at"
case "username":
field = "username"
case "email":

View File

@@ -42,7 +42,7 @@ type PermissionInfo struct {
Active bool `json:"active"`
System bool `json:"system"`
RoleCount int64 `json:"roleCount"`
DateCreated string `json:"dateCreated"`
CreatedAt string `json:"created_at"`
}
// BeforeCreate is called before creating a permission
@@ -132,7 +132,7 @@ func (p *Permission) ToPermissionInfo() PermissionInfo {
Category: p.Category,
Active: p.Active,
System: p.System,
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}

148
local/model/project.go Normal file
View File

@@ -0,0 +1,148 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Project represents a project in the system
type Project struct {
BaseModel
Name string `json:"name" gorm:"not null;type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`
OwnerID string `json:"owner_id" gorm:"not null;type:uuid;index;references:users(id)"`
TypeID string `json:"type_id" gorm:"not null;type:uuid;index;references:types(id)"`
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID"`
Type Type `json:"type,omitempty" gorm:"foreignKey:TypeID"`
Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:ProjectID"`
Members []User `json:"members,omitempty" gorm:"many2many:project_members;"`
}
// ProjectCreateRequest represents the request to create a new project
type ProjectCreateRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
OwnerID string `json:"owner_id" validate:"required,uuid"`
TypeID string `json:"type_id" validate:"required,uuid"`
}
// ProjectUpdateRequest represents the request to update a project
type ProjectUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
TypeID *string `json:"type_id,omitempty" validate:"omitempty,uuid"`
}
// ProjectInfo represents public project information
type ProjectInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
OwnerID string `json:"owner_id"`
TypeID string `json:"type_id"`
Owner UserInfo `json:"owner,omitempty"`
Type TypeInfo `json:"type,omitempty"`
TaskCount int64 `json:"task_count"`
MemberCount int64 `json:"member_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// ProjectMember represents the many-to-many relationship between projects and users
type ProjectMember struct {
ProjectID string `json:"project_id" gorm:"type:uuid;primaryKey"`
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
RoleID string `json:"role_id" gorm:"type:uuid;not null;references:roles(id)"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID"`
}
// BeforeCreate is called before creating a project
func (p *Project) BeforeCreate(tx *gorm.DB) error {
p.BaseModel.BeforeCreate()
// Normalize name and description
p.Name = strings.TrimSpace(p.Name)
p.Description = strings.TrimSpace(p.Description)
return p.Validate()
}
// BeforeUpdate is called before updating a project
func (p *Project) BeforeUpdate(tx *gorm.DB) error {
p.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if p.Name != "" {
p.Name = strings.TrimSpace(p.Name)
}
if p.Description != "" {
p.Description = strings.TrimSpace(p.Description)
}
return p.Validate()
}
// Validate validates project data
func (p *Project) Validate() error {
if p.Name == "" {
return errors.New("name is required")
}
if len(p.Name) > 255 {
return errors.New("name must not exceed 255 characters")
}
if p.OwnerID == "" {
return errors.New("owner_id is required")
}
if p.TypeID == "" {
return errors.New("type_id is required")
}
return nil
}
// ToProjectInfo converts Project to ProjectInfo (public information)
func (p *Project) ToProjectInfo() ProjectInfo {
projectInfo := ProjectInfo{
ID: p.ID,
Name: p.Name,
Description: p.Description,
OwnerID: p.OwnerID,
TypeID: p.TypeID,
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Add owner info if loaded
if p.Owner.ID != "" {
projectInfo.Owner = p.Owner.ToUserInfo()
}
// Add type info if loaded
if p.Type.ID != "" {
projectInfo.Type = p.Type.ToTypeInfo()
}
return projectInfo
}
// HasMember checks if a user is a member of the project
func (p *Project) HasMember(userID string) bool {
for _, member := range p.Members {
if member.ID == userID {
return true
}
}
return false
}
// IsOwner checks if a user is the owner of the project
func (p *Project) IsOwner(userID string) bool {
return p.OwnerID == userID
}

View File

@@ -42,7 +42,7 @@ type RoleInfo struct {
System bool `json:"system"`
Permissions []PermissionInfo `json:"permissions"`
UserCount int64 `json:"userCount"`
DateCreated string `json:"dateCreated"`
CreatedAt string `json:"created_at"`
}
// BeforeCreate is called before creating a role
@@ -120,7 +120,7 @@ func (r *Role) ToRoleInfo() RoleInfo {
Active: r.Active,
System: r.System,
Permissions: make([]PermissionInfo, len(r.Permissions)),
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Convert permissions

View File

@@ -86,7 +86,7 @@ type SecurityEventInfo struct {
ResolverName string `json:"resolverName,omitempty"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty"`
DateCreated string `json:"dateCreated"`
CreatedAt string `json:"created_at"`
}
// BeforeCreate is called before creating a security event
@@ -202,19 +202,19 @@ func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
ResolvedBy: se.ResolvedBy,
ResolvedAt: se.ResolvedAt,
Notes: se.Notes,
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
CreatedAt: se.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Include user information if available
if se.User != nil {
info.UserEmail = se.User.Email
info.UserName = se.User.Name
info.UserName = se.User.FullName
}
// Include resolver information if available
if se.Resolver != nil {
info.ResolverEmail = se.Resolver.Email
info.ResolverName = se.Resolver.Name
info.ResolverName = se.Resolver.FullName
}
return info

View File

@@ -56,7 +56,7 @@ type SystemConfigInfo struct {
DataType string `json:"dataType"`
IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"`
DateCreated string `json:"dateCreated"`
CreatedAt string `json:"created_at"`
DateModified string `json:"dateModified"`
}
@@ -170,7 +170,7 @@ func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
DataType: sc.DataType,
IsEditable: sc.IsEditable,
IsSecret: sc.IsSecret,
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
CreatedAt: sc.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
DateModified: sc.DateModified,
}

213
local/model/task.go Normal file
View File

@@ -0,0 +1,213 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// TaskStatus represents the status of a task
type TaskStatus string
const (
TaskStatusTodo TaskStatus = "todo"
TaskStatusInProgress TaskStatus = "in_progress"
TaskStatusDone TaskStatus = "done"
TaskStatusCanceled TaskStatus = "canceled"
)
// TaskPriority represents the priority of a task
type TaskPriority string
const (
TaskPriorityLow TaskPriority = "low"
TaskPriorityMedium TaskPriority = "medium"
TaskPriorityHigh TaskPriority = "high"
)
// Task represents a task in the system
type Task struct {
BaseModel
Title string `json:"title" gorm:"not null;type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`
Status TaskStatus `json:"status" gorm:"not null;default:'todo';type:varchar(20)"`
Priority TaskPriority `json:"priority" gorm:"not null;default:'medium';type:varchar(20)"`
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
DueDate *string `json:"due_date" gorm:"type:date"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
Assignees []User `json:"assignees,omitempty" gorm:"many2many:task_assignees;"`
}
// TaskCreateRequest represents the request to create a new task
type TaskCreateRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Status TaskStatus `json:"status" validate:"omitempty,oneof=todo in_progress done canceled"`
Priority TaskPriority `json:"priority" validate:"omitempty,oneof=low medium high"`
ProjectID string `json:"project_id" validate:"required,uuid"`
DueDate *string `json:"due_date"`
AssigneeIDs []string `json:"assignee_ids"`
}
// TaskUpdateRequest represents the request to update a task
type TaskUpdateRequest struct {
Title *string `json:"title,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
Status *TaskStatus `json:"status,omitempty" validate:"omitempty,oneof=todo in_progress done canceled"`
Priority *TaskPriority `json:"priority,omitempty" validate:"omitempty,oneof=low medium high"`
DueDate *string `json:"due_date,omitempty"`
AssigneeIDs []string `json:"assignee_ids,omitempty"`
}
// TaskInfo represents public task information
type TaskInfo struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status TaskStatus `json:"status"`
Priority TaskPriority `json:"priority"`
ProjectID string `json:"project_id"`
DueDate *string `json:"due_date"`
Project ProjectInfo `json:"project,omitempty"`
Assignees []UserInfo `json:"assignees,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// TaskAssignee represents the many-to-many relationship between tasks and users
type TaskAssignee struct {
TaskID string `json:"task_id" gorm:"type:uuid;primaryKey"`
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate is called before creating a task
func (t *Task) BeforeCreate(tx *gorm.DB) error {
t.BaseModel.BeforeCreate()
// Normalize title and description
t.Title = strings.TrimSpace(t.Title)
t.Description = strings.TrimSpace(t.Description)
// Set default values
if t.Status == "" {
t.Status = TaskStatusTodo
}
if t.Priority == "" {
t.Priority = TaskPriorityMedium
}
return t.Validate()
}
// BeforeUpdate is called before updating a task
func (t *Task) BeforeUpdate(tx *gorm.DB) error {
t.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if t.Title != "" {
t.Title = strings.TrimSpace(t.Title)
}
if t.Description != "" {
t.Description = strings.TrimSpace(t.Description)
}
return t.Validate()
}
// Validate validates task data
func (t *Task) Validate() error {
if t.Title == "" {
return errors.New("title is required")
}
if len(t.Title) > 255 {
return errors.New("title must not exceed 255 characters")
}
if t.ProjectID == "" {
return errors.New("project_id is required")
}
// Validate status
if t.Status != "" {
switch t.Status {
case TaskStatusTodo, TaskStatusInProgress, TaskStatusDone, TaskStatusCanceled:
// Valid status
default:
return errors.New("invalid status")
}
}
// Validate priority
if t.Priority != "" {
switch t.Priority {
case TaskPriorityLow, TaskPriorityMedium, TaskPriorityHigh:
// Valid priority
default:
return errors.New("invalid priority")
}
}
return nil
}
// ToTaskInfo converts Task to TaskInfo (public information)
func (t *Task) ToTaskInfo() TaskInfo {
taskInfo := TaskInfo{
ID: t.ID,
Title: t.Title,
Description: t.Description,
Status: t.Status,
Priority: t.Priority,
ProjectID: t.ProjectID,
DueDate: t.DueDate,
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Assignees: make([]UserInfo, len(t.Assignees)),
}
// Add project info if loaded
if t.Project.ID != "" {
taskInfo.Project = t.Project.ToProjectInfo()
}
// Add assignee info if loaded
for i, assignee := range t.Assignees {
taskInfo.Assignees[i] = assignee.ToUserInfo()
}
return taskInfo
}
// IsAssignedTo checks if a user is assigned to this task
func (t *Task) IsAssignedTo(userID string) bool {
for _, assignee := range t.Assignees {
if assignee.ID == userID {
return true
}
}
return false
}
// IsCompleted checks if the task is completed
func (t *Task) IsCompleted() bool {
return t.Status == TaskStatusDone
}
// IsCanceled checks if the task is canceled
func (t *Task) IsCanceled() bool {
return t.Status == TaskStatusCanceled
}
// IsInProgress checks if the task is in progress
func (t *Task) IsInProgress() bool {
return t.Status == TaskStatusInProgress
}
// IsTodo checks if the task is todo
func (t *Task) IsTodo() bool {
return t.Status == TaskStatusTodo
}

95
local/model/type.go Normal file
View File

@@ -0,0 +1,95 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Type represents a project type in the system
type Type struct {
BaseModel
UserID *string `json:"user_id" gorm:"type:uuid;index;references:users(id);onDelete:SET NULL"`
Name string `json:"name" gorm:"not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// TypeCreateRequest represents the request to create a new type
type TypeCreateRequest struct {
UserID *string `json:"user_id"`
Name string `json:"name" validate:"required,min=1,max=100"`
Description string `json:"description" validate:"max=1000"`
}
// TypeUpdateRequest represents the request to update a type
type TypeUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
}
// TypeInfo represents public type information
type TypeInfo struct {
ID string `json:"id"`
UserID *string `json:"user_id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// BeforeCreate is called before creating a type
func (t *Type) BeforeCreate(tx *gorm.DB) error {
t.BaseModel.BeforeCreate()
// Normalize name
t.Name = strings.TrimSpace(t.Name)
t.Description = strings.TrimSpace(t.Description)
return t.Validate()
}
// BeforeUpdate is called before updating a type
func (t *Type) BeforeUpdate(tx *gorm.DB) error {
t.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if t.Name != "" {
t.Name = strings.TrimSpace(t.Name)
}
if t.Description != "" {
t.Description = strings.TrimSpace(t.Description)
}
return t.Validate()
}
// Validate validates type data
func (t *Type) Validate() error {
if t.Name == "" {
return errors.New("name is required")
}
if len(t.Name) > 100 {
return errors.New("name must not exceed 100 characters")
}
if len(t.Description) > 1000 {
return errors.New("description must not exceed 1000 characters")
}
return nil
}
// ToTypeInfo converts Type to TypeInfo (public information)
func (t *Type) ToTypeInfo() TypeInfo {
return TypeInfo{
ID: t.ID,
UserID: t.UserID,
Name: t.Name,
Description: t.Description,
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}

View File

@@ -13,42 +13,98 @@ import (
// 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"`
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:varchar(255)"`
FullName string `json:"full_name" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
}
// 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"`
FullName string `json:"full_name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"`
RoleIDs []string `json:"roleIds"`
}
// ToUser converts UserCreateRequest to User domain model
func (req *UserCreateRequest) ToUser() (*User, error) {
user := &User{
Email: req.Email,
FullName: req.FullName,
}
// Handle password hashing
if err := user.SetPassword(req.Password); err != nil {
return nil, err
}
// Note: Roles will be set by the service layer after validation
return user, nil
}
// Validate validates the UserCreateRequest
func (req *UserCreateRequest) Validate() error {
if req.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(req.Email) {
return errors.New("invalid email format")
}
if len(req.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
if req.FullName == "" {
return errors.New("full name is required")
}
return nil
}
// 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"`
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=2,max=100"`
RoleIDs []string `json:"roleIds,omitempty"`
}
// ApplyToUser applies the UserUpdateRequest to an existing User
func (req *UserUpdateRequest) ApplyToUser(user *User) error {
if req.Email != nil {
user.Email = *req.Email
}
if req.FullName != nil {
user.FullName = *req.FullName
}
// Note: Roles will be handled by the service layer
return nil
}
// Validate validates the UserUpdateRequest
func (req *UserUpdateRequest) Validate() error {
if req.Email != nil && !isValidEmail(*req.Email) {
return errors.New("invalid email format")
}
if req.FullName != nil && len(*req.FullName) == 0 {
return errors.New("full name cannot be empty")
}
if req.FullName != nil && len(*req.FullName) > 255 {
return errors.New("full name must not exceed 255 characters")
}
return nil
}
// UserResponse represents the response when returning user data
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UserLoginRequest represents a login request
type UserLoginRequest struct {
Email string `json:"email" validate:"required,email"`
@@ -65,16 +121,13 @@ type UserLoginResponse struct {
// 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"`
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"full_name"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ChangePasswordRequest represents a password change request
@@ -98,10 +151,9 @@ type ResetPasswordConfirmRequest struct {
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.BaseModel.BeforeCreate()
// Normalize email and username
// Normalize email and full name
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
u.Name = strings.TrimSpace(u.Name)
u.FullName = strings.TrimSpace(u.FullName)
return u.Validate()
}
@@ -114,11 +166,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
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)
if u.FullName != "" {
u.FullName = strings.TrimSpace(u.FullName)
}
return u.Validate()
@@ -134,24 +183,8 @@ func (u *User) Validate() error {
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")
if u.FullName != "" && len(u.FullName) > 255 {
return errors.New("full name must not exceed 255 characters")
}
return nil
@@ -182,55 +215,16 @@ 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{},
ID: u.ID,
Email: u.Email,
FullName: u.FullName,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
}
// Convert roles and collect permissions
@@ -250,6 +244,23 @@ func (u *User) ToUserInfo() UserInfo {
return userInfo
}
// ToResponse converts User to UserResponse (for API responses)
func (u *User) ToResponse() *UserResponse {
roleNames := make([]string, len(u.Roles))
for i, role := range u.Roles {
roleNames[i] = role.Name
}
return &UserResponse{
ID: u.ID,
Email: u.Email,
FullName: u.FullName,
Roles: roleNames,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// HasRole checks if the user has a specific role
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
@@ -278,12 +289,6 @@ func isValidEmail(email string) bool {
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 {
@@ -294,25 +299,5 @@ func validatePassword(password string) error {
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
}