feat(backend): unified response helpers, refresh token, security hardening

- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
This commit is contained in:
Logan.Li committed 2026-10-26 17:21:00 +08:00
1 parent d5b8b8116c
commit 16f1f7d65f
26 files changed
+916 -963

No files matched your search

+190 -165
View File
@@ -2,6 +2,7 @@ package model
import (
"database/sql"
"encoding/json"
"time"
"gorm.io/gorm"
@@ -10,21 +11,21 @@ import (
// ==================== User ====================
type User struct {
ID int `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
DisplayName string `gorm:"size:100;not null" json:"display_name"`
Email *string `gorm:"size:100" json:"email"`
Phone *string `gorm:"size:20" json:"phone"`
HashedPassword string `gorm:"size:200;not null" json:"-"`
AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap
Role string `gorm:"size:20;not null" json:"role"` // admin, dba, developer, auditor
IsActive *bool `gorm:"default:true" json:"is_active"`
Preferences *string `gorm:"type:text" json:"preferences"` // JSON: {"theme":"system","login_style":"split"}
LastLogin *time.Time `json:"last_login"`
FailedLoginAttempts int `gorm:"default:0" json:"failed_login_attempts"`
LockedUntil *time.Time `json:"locked_until"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
ID int `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
DisplayName string `gorm:"size:100;not null" json:"display_name"`
Email *string `gorm:"size:100" json:"email"`
Phone *string `gorm:"size:20" json:"phone"`
HashedPassword string `gorm:"size:200;not null" json:"-"`
AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap
Role string `gorm:"size:20;not null" json:"role"` // admin, dba, developer, auditor
IsActive *bool `gorm:"default:true" json:"is_active"`
Preferences *string `gorm:"type:text" json:"preferences"` // JSON: {"theme":"system","login_style":"split"}
LastLogin *time.Time `json:"last_login"`
FailedLoginAttempts int `gorm:"default:0" json:"failed_login_attempts"`
LockedUntil *time.Time `json:"locked_until"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
}
func (User) TableName() string { return "users" }
@@ -32,57 +33,66 @@ func (User) TableName() string { return "users" }
// ==================== Instance ====================
type Instance struct {
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
DBType string `gorm:"size:20;not null" json:"db_type"`
Host string `gorm:"size:200;not null" json:"host"`
Port int `gorm:"not null" json:"port"`
Username string `gorm:"size:100;not null" json:"username"`
Password string `gorm:"size:500;not null" json:"password,omitempty"`
Database *string `gorm:"size:100" json:"database"`
Env *string `gorm:"size:20" json:"env"`
Description *string `gorm:"type:text" json:"description"`
Status *string `gorm:"size:20" json:"status"`
Version *string `gorm:"size:50" json:"version"`
ClusterMode *bool `gorm:"default:false" json:"cluster_mode"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
AccessMode *string `gorm:"size:20;default:readwrite" json:"access_mode"`
Workflow *string `gorm:"type:text" json:"workflow"`
ExcludedDatabases *string `gorm:"type:text" json:"excluded_databases"`
MaskFields *string `gorm:"type:text" json:"mask_fields"`
GroupID *int `json:"group_id"`
WorkflowID *int `json:"workflow_id"`
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
DBType string `gorm:"size:20;not null" json:"db_type"`
Host string `gorm:"size:200;not null" json:"host"`
Port int `gorm:"not null" json:"port"`
Username string `gorm:"size:100;not null" json:"username"`
Password string `gorm:"size:500;not null" json:"password,omitempty"`
Database *string `gorm:"size:100" json:"database"`
Env *string `gorm:"size:20" json:"env"`
Description *string `gorm:"type:text" json:"description"`
Status *string `gorm:"size:20" json:"status"`
Version *string `gorm:"size:50" json:"version"`
ClusterMode *bool `gorm:"default:false" json:"cluster_mode"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
AccessMode *string `gorm:"size:20;default:readwrite" json:"access_mode"`
Workflow *string `gorm:"type:text" json:"workflow"`
ExcludedDatabases *string `gorm:"type:text" json:"excluded_databases"`
MaskFields *string `gorm:"type:text" json:"mask_fields"`
GroupID *int `json:"group_id"`
WorkflowID *int `json:"workflow_id"`
}
func (Instance) TableName() string { return "instances" }
// MarshalJSON 保证实例密文密码在任何响应中都不出站,
// 消除各 handler 手动遮蔽遗漏的隐患(入站绑定不受影响)。
func (i Instance) MarshalJSON() ([]byte, error) {
type instanceAlias Instance
a := instanceAlias(i)
a.Password = ""
return json.Marshal(a)
}
// ==================== Ticket ====================
type Ticket struct {
ID int `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title"`
DBType string `gorm:"size:20;not null" json:"db_type"`
InstanceID int `gorm:"not null" json:"instance_id"`
TargetDatabase string `gorm:"size:100;not null" json:"target_database"`
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
RollbackSQL *string `gorm:"type:text" json:"rollback_sql"`
Status *string `gorm:"size:20" json:"status"`
Priority *string `gorm:"size:10" json:"priority"`
ApplicantID int `gorm:"not null" json:"applicant_id"`
ApproverID *int `json:"approver_id"`
Reason *string `gorm:"type:text" json:"reason"`
AuditResult *string `gorm:"type:text" json:"audit_result"`
ExecuteResult *string `gorm:"type:text" json:"execute_result"`
CreateTime time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"create_time"`
ReviewTime *time.Time `json:"review_time"`
ExecuteTime *time.Time `json:"execute_time"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
TicketNo *string `gorm:"size:20" json:"ticket_no"`
ScheduledAt sql.NullTime `json:"scheduled_at"`
WorkflowID *int `json:"workflow_id"`
RiskLevel *string `gorm:"size:20;default:low" json:"risk_level"`
ExecStrategy *string `gorm:"size:20;default:stop_on_error" json:"exec_strategy"`
ID int `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title"`
DBType string `gorm:"size:20;not null" json:"db_type"`
InstanceID int `gorm:"not null" json:"instance_id"`
TargetDatabase string `gorm:"size:100;not null" json:"target_database"`
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
RollbackSQL *string `gorm:"type:text" json:"rollback_sql"`
Status *string `gorm:"size:20" json:"status"`
Priority *string `gorm:"size:10" json:"priority"`
ApplicantID int `gorm:"not null" json:"applicant_id"`
ApproverID *int `json:"approver_id"`
Reason *string `gorm:"type:text" json:"reason"`
AuditResult *string `gorm:"type:text" json:"audit_result"`
ExecuteResult *string `gorm:"type:text" json:"execute_result"`
CreateTime time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"create_time"`
ReviewTime *time.Time `json:"review_time"`
ExecuteTime *time.Time `json:"execute_time"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
TicketNo *string `gorm:"size:20" json:"ticket_no"`
ScheduledAt sql.NullTime `json:"scheduled_at"`
WorkflowID *int `json:"workflow_id"`
RiskLevel *string `gorm:"size:20;default:low" json:"risk_level"`
ExecStrategy *string `gorm:"size:20;default:stop_on_error" json:"exec_strategy"`
}
func (Ticket) TableName() string { return "tickets" }
@@ -90,24 +100,24 @@ func (Ticket) TableName() string { return "tickets" }
// ==================== TicketApproval ====================
type TicketApproval struct {
ID int `gorm:"primaryKey" json:"id"`
TicketID int `gorm:"not null" json:"ticket_id"`
WorkflowID *int `json:"workflow_id"`
VersionID *int `json:"version_id"`
StepNumber int `gorm:"not null" json:"step_number"`
StepName *string `gorm:"size:100" json:"step_name"`
StepType *string `gorm:"size:20" json:"step_type"`
ApproverID *int `json:"approver_id"`
ApproverRole *string `gorm:"size:20" json:"approver_role"`
ActualApproverID *int `json:"actual_approver_id"`
Action *string `gorm:"size:20" json:"action"`
Comment *string `gorm:"type:text" json:"comment"`
TransferFrom *int `json:"transfer_from"`
TransferReason *string `gorm:"type:text" json:"transfer_reason"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
CompletedAt *time.Time `json:"completed_at"`
IsTimeout *bool `json:"is_timeout"`
ID int `gorm:"primaryKey" json:"id"`
TicketID int `gorm:"not null" json:"ticket_id"`
WorkflowID *int `json:"workflow_id"`
VersionID *int `json:"version_id"`
StepNumber int `gorm:"not null" json:"step_number"`
StepName *string `gorm:"size:100" json:"step_name"`
StepType *string `gorm:"size:20" json:"step_type"`
ApproverID *int `json:"approver_id"`
ApproverRole *string `gorm:"size:20" json:"approver_role"`
ActualApproverID *int `json:"actual_approver_id"`
Action *string `gorm:"size:20" json:"action"`
Comment *string `gorm:"type:text" json:"comment"`
TransferFrom *int `json:"transfer_from"`
TransferReason *string `gorm:"type:text" json:"transfer_reason"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
CompletedAt *time.Time `json:"completed_at"`
IsTimeout *bool `json:"is_timeout"`
TimeoutHandledAt *time.Time `json:"timeout_handled_at"`
}
@@ -116,18 +126,18 @@ func (TicketApproval) TableName() string { return "ticket_approvals" }
// ==================== TicketSQLItem ====================
type TicketSQLItem struct {
ID int `gorm:"primaryKey" json:"id"`
TicketID int `gorm:"not null" json:"ticket_id"`
Seq int `gorm:"not null" json:"seq"`
SQLText string `gorm:"type:text;not null" json:"sql_text"`
SQLType *string `gorm:"size:20" json:"sql_type"`
Status *string `gorm:"size:20;default:pending" json:"status"`
AffectedRows *int `gorm:"default:0" json:"affected_rows"`
DurationMs *int `gorm:"default:0" json:"duration_ms"`
ErrorMessage *string `gorm:"type:text" json:"error_message"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
ID int `gorm:"primaryKey" json:"id"`
TicketID int `gorm:"not null" json:"ticket_id"`
Seq int `gorm:"not null" json:"seq"`
SQLText string `gorm:"type:text;not null" json:"sql_text"`
SQLType *string `gorm:"size:20" json:"sql_type"`
Status *string `gorm:"size:20;default:pending" json:"status"`
AffectedRows *int `gorm:"default:0" json:"affected_rows"`
DurationMs *int `gorm:"default:0" json:"duration_ms"`
ErrorMessage *string `gorm:"type:text" json:"error_message"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
}
func (TicketSQLItem) TableName() string { return "ticket_sql_items" }
@@ -204,37 +214,37 @@ func (QueryHistory) TableName() string { return "query_histories" }
// ==================== Workflow ====================
type WorkflowDefinition struct {
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
Category *string `gorm:"size:50" json:"category"`
IsDefault *bool `json:"is_default"`
IsActive *bool `json:"is_active"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
CreatedBy *int `json:"created_by"`
FlowConfig *string `gorm:"type:json" json:"flow_config"`
TriggerConditions *string `gorm:"type:json" json:"trigger_conditions"`
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
Category *string `gorm:"size:50" json:"category"`
IsDefault *bool `json:"is_default"`
IsActive *bool `json:"is_active"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
CreatedBy *int `json:"created_by"`
FlowConfig *string `gorm:"type:json" json:"flow_config"`
TriggerConditions *string `gorm:"type:json" json:"trigger_conditions"`
}
func (WorkflowDefinition) TableName() string { return "workflow_definitions" }
type WorkflowStep struct {
ID int `gorm:"primaryKey" json:"id"`
WorkflowID int `gorm:"not null" json:"workflow_id"`
StepNumber int `gorm:"not null" json:"step_number"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
StepType string `gorm:"size:20;not null" json:"step_type"`
ApproverRole *string `gorm:"size:20" json:"approver_role"`
ApproverIDs *string `gorm:"type:json" json:"approver_ids"`
ApproverAssignment *string `gorm:"type:json" json:"approver_assignment"`
IsRequired *bool `json:"is_required"`
TimeoutHours *int `json:"timeout_hours"`
AutoApproveOnTimeout *bool `json:"auto_approve_on_timeout"`
EntryConditions *string `gorm:"type:json" json:"entry_conditions"`
TransitionConditions *string `gorm:"type:json" json:"transition_conditions"`
FieldPermissions *string `gorm:"type:json" json:"field_permissions"`
ID int `gorm:"primaryKey" json:"id"`
WorkflowID int `gorm:"not null" json:"workflow_id"`
StepNumber int `gorm:"not null" json:"step_number"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
StepType string `gorm:"size:20;not null" json:"step_type"`
ApproverRole *string `gorm:"size:20" json:"approver_role"`
ApproverIDs *string `gorm:"type:json" json:"approver_ids"`
ApproverAssignment *string `gorm:"type:json" json:"approver_assignment"`
IsRequired *bool `json:"is_required"`
TimeoutHours *int `json:"timeout_hours"`
AutoApproveOnTimeout *bool `json:"auto_approve_on_timeout"`
EntryConditions *string `gorm:"type:json" json:"entry_conditions"`
TransitionConditions *string `gorm:"type:json" json:"transition_conditions"`
FieldPermissions *string `gorm:"type:json" json:"field_permissions"`
}
func (WorkflowStep) TableName() string { return "workflow_steps" }
@@ -359,15 +369,15 @@ func (NotificationTemplate) TableName() string { return "notification_templates"
// ==================== Other Models ====================
type SystemSetting struct {
ID int `gorm:"primaryKey" json:"id"`
Key string `gorm:"size:100;not null" json:"key"`
Value string `gorm:"type:text;not null" json:"value"`
ValueType string `gorm:"size:20;not null" json:"value_type"`
Group string `gorm:"size:50;not null" json:"group"`
Label string `gorm:"size:100;not null" json:"label"`
Desc *string `gorm:"type:text" json:"description"`
DefaultValue *string `gorm:"type:text" json:"default_value"`
SortOrder int `gorm:"not null" json:"sort_order"`
ID int `gorm:"primaryKey" json:"id"`
Key string `gorm:"size:100;not null" json:"key"`
Value string `gorm:"type:text;not null" json:"value"`
ValueType string `gorm:"size:20;not null" json:"value_type"`
Group string `gorm:"size:50;not null" json:"group"`
Label string `gorm:"size:100;not null" json:"label"`
Desc *string `gorm:"type:text" json:"description"`
DefaultValue *string `gorm:"type:text" json:"default_value"`
SortOrder int `gorm:"not null" json:"sort_order"`
UpdatedAt *time.Time `json:"updated_at"`
}
@@ -420,15 +430,15 @@ type Backup struct {
func (Backup) TableName() string { return "backups" }
type SchemaRevision struct {
ID int `gorm:"primaryKey" json:"id"`
InstanceID int `gorm:"not null" json:"instance_id"`
Database string `gorm:"size:100;not null" json:"database"`
Revision string `gorm:"size:64;not null" json:"revision"`
DDLContent *string `gorm:"type:text" json:"ddl_content"`
Checksum string `gorm:"size:64;not null" json:"checksum"`
AppliedBy *int `json:"applied_by"`
AppliedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"applied_at"`
Status *string `gorm:"size:20" json:"status"`
ID int `gorm:"primaryKey" json:"id"`
InstanceID int `gorm:"not null" json:"instance_id"`
Database string `gorm:"size:100;not null" json:"database"`
Revision string `gorm:"size:64;not null" json:"revision"`
DDLContent *string `gorm:"type:text" json:"ddl_content"`
Checksum string `gorm:"size:64;not null" json:"checksum"`
AppliedBy *int `json:"applied_by"`
AppliedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"applied_at"`
Status *string `gorm:"size:20" json:"status"`
}
func (SchemaRevision) TableName() string { return "schema_revisions" }
@@ -450,50 +460,64 @@ type SlowLog struct {
func (SlowLog) TableName() string { return "slow_logs" }
type ScheduledTask struct {
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
TaskSQLType string `gorm:"size:20;not null" json:"task_sql_type"`
InstanceID int `gorm:"not null" json:"instance_id"`
TargetDB *string `gorm:"size:100" json:"target_db"`
TargetTable *string `gorm:"size:100" json:"target_table"`
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
CronExpression string `gorm:"size:50;not null" json:"cron_expression"`
IsEnabled *bool `json:"is_enabled"`
LastRunAt *time.Time `json:"last_run_at"`
LastRunStatus *string `gorm:"size:20" json:"last_run_status"`
LastRunMessage *string `gorm:"type:text" json:"last_run_message"`
NextRunAt *time.Time `json:"next_run_at"`
RunCount *int `json:"run_count"`
CreatedBy *int `json:"created_by"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description *string `gorm:"type:text" json:"description"`
TaskSQLType string `gorm:"size:20;not null" json:"task_sql_type"`
InstanceID int `gorm:"not null" json:"instance_id"`
TargetDB *string `gorm:"size:100" json:"target_db"`
TargetTable *string `gorm:"size:100" json:"target_table"`
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
CronExpression string `gorm:"size:50;not null" json:"cron_expression"`
IsEnabled *bool `json:"is_enabled"`
LastRunAt *time.Time `json:"last_run_at"`
LastRunStatus *string `gorm:"size:20" json:"last_run_status"`
LastRunMessage *string `gorm:"type:text" json:"last_run_message"`
NextRunAt *time.Time `json:"next_run_at"`
RunCount *int `json:"run_count"`
CreatedBy *int `json:"created_by"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
}
func (ScheduledTask) TableName() string { return "scheduled_tasks" }
type ScheduledTaskLog struct {
ID int `gorm:"primaryKey" json:"id"`
TaskID int `gorm:"not null" json:"task_id"`
Status string `gorm:"size:20;not null" json:"status"`
SQLContent *string `gorm:"type:text" json:"sql_content"`
AffectedRows *int `json:"affected_rows"`
DurationMs *int `json:"duration_ms"`
ErrorMessage *string `gorm:"type:text" json:"error_message"`
StartedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
ID int `gorm:"primaryKey" json:"id"`
TaskID int `gorm:"not null" json:"task_id"`
Status string `gorm:"size:20;not null" json:"status"`
SQLContent *string `gorm:"type:text" json:"sql_content"`
AffectedRows *int `json:"affected_rows"`
DurationMs *int `json:"duration_ms"`
ErrorMessage *string `gorm:"type:text" json:"error_message"`
StartedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
}
func (ScheduledTaskLog) TableName() string { return "scheduled_task_logs" }
type UserGroup struct {
UserID int `gorm:"primaryKey" json:"user_id"`
GroupID int `gorm:"primaryKey" json:"group_id"`
UserID int `gorm:"primaryKey" json:"user_id"`
GroupID int `gorm:"primaryKey" json:"group_id"`
Permissions *string `gorm:"type:text" json:"permissions"`
}
func (UserGroup) TableName() string { return "user_groups" }
// ==================== RefreshToken ====================
// RefreshToken 持久化 refresh token 的 SHA-256 摘要(明文不落库),支持过期与撤销。
type RefreshToken struct {
ID int `gorm:"primaryKey" json:"id"`
UserID int `gorm:"index;not null" json:"user_id"`
TokenHash string `gorm:"size:64;uniqueIndex;not null" json:"-"`
ExpiresAt time.Time `json:"expires_at"`
RevokedAt *time.Time `json:"revoked_at"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
}
func (RefreshToken) TableName() string { return "refresh_tokens" }
// ==================== DTO ====================
type LoginRequest struct {
@@ -502,9 +526,10 @@ type LoginRequest struct {
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
User User `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type"`
User User `json:"user"`
}
type TicketStatistics struct {