[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
237
consent-service/internal/models/consent.go
Normal file
237
consent-service/internal/models/consent.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LegalDocument represents a type of legal document (e.g., Terms, Privacy Policy)
|
||||
type LegalDocument struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Type string `json:"type" db:"type"` // 'terms', 'privacy', 'cookies', 'community'
|
||||
Name string `json:"name" db:"name"`
|
||||
Description *string `json:"description" db:"description"`
|
||||
IsMandatory bool `json:"is_mandatory" db:"is_mandatory"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentVersion represents a specific version of a legal document
|
||||
type DocumentVersion struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
DocumentID uuid.UUID `json:"document_id" db:"document_id"`
|
||||
Version string `json:"version" db:"version"` // Semver: 1.0.0, 1.1.0
|
||||
Language string `json:"language" db:"language"` // ISO 639-1: de, en
|
||||
Title string `json:"title" db:"title"`
|
||||
Content string `json:"content" db:"content"` // HTML or Markdown
|
||||
Summary *string `json:"summary" db:"summary"` // Summary of changes
|
||||
Status string `json:"status" db:"status"` // 'draft', 'review', 'approved', 'scheduled', 'published', 'archived'
|
||||
PublishedAt *time.Time `json:"published_at" db:"published_at"`
|
||||
ScheduledPublishAt *time.Time `json:"scheduled_publish_at" db:"scheduled_publish_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by" db:"created_by"`
|
||||
ApprovedBy *uuid.UUID `json:"approved_by" db:"approved_by"`
|
||||
ApprovedAt *time.Time `json:"approved_at" db:"approved_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// UserConsent represents a user's consent to a document version
|
||||
type UserConsent struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
DocumentVersionID uuid.UUID `json:"document_version_id" db:"document_version_id"`
|
||||
Consented bool `json:"consented" db:"consented"`
|
||||
IPAddress *string `json:"ip_address" db:"ip_address"`
|
||||
UserAgent *string `json:"user_agent" db:"user_agent"`
|
||||
ConsentedAt time.Time `json:"consented_at" db:"consented_at"`
|
||||
WithdrawnAt *time.Time `json:"withdrawn_at" db:"withdrawn_at"`
|
||||
}
|
||||
|
||||
// AuditLog represents an audit trail entry for GDPR compliance
|
||||
type AuditLog struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID *uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Action string `json:"action" db:"action"` // 'consent_given', 'consent_withdrawn', 'data_export', 'data_delete'
|
||||
EntityType *string `json:"entity_type" db:"entity_type"` // 'document', 'cookie_category'
|
||||
EntityID *uuid.UUID `json:"entity_id" db:"entity_id"`
|
||||
Details *string `json:"details" db:"details"` // JSON string
|
||||
IPAddress *string `json:"ip_address" db:"ip_address"`
|
||||
UserAgent *string `json:"user_agent" db:"user_agent"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// DataExportRequest represents a user's request to export their data
|
||||
type DataExportRequest struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Status string `json:"status" db:"status"` // 'pending', 'processing', 'completed', 'failed'
|
||||
DownloadURL *string `json:"download_url" db:"download_url"`
|
||||
ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CompletedAt *time.Time `json:"completed_at" db:"completed_at"`
|
||||
}
|
||||
|
||||
// DataDeletionRequest represents a user's request to delete their data
|
||||
type DataDeletionRequest struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Status string `json:"status" db:"status"` // 'pending', 'processing', 'completed', 'failed'
|
||||
Reason *string `json:"reason" db:"reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
ProcessedAt *time.Time `json:"processed_at" db:"processed_at"`
|
||||
ProcessedBy *uuid.UUID `json:"processed_by" db:"processed_by"`
|
||||
}
|
||||
|
||||
// VersionApproval tracks the approval workflow
|
||||
type VersionApproval struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
VersionID uuid.UUID `json:"version_id" db:"version_id"`
|
||||
ApproverID uuid.UUID `json:"approver_id" db:"approver_id"`
|
||||
Action string `json:"action" db:"action"` // 'submitted_for_review', 'approved', 'rejected', 'published'
|
||||
Comment *string `json:"comment,omitempty" db:"comment"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ConsentDeadline tracks consent deadlines per user
|
||||
type ConsentDeadline struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
DocumentVersionID uuid.UUID `json:"document_version_id" db:"document_version_id"`
|
||||
DeadlineAt time.Time `json:"deadline_at" db:"deadline_at"`
|
||||
ReminderCount int `json:"reminder_count" db:"reminder_count"`
|
||||
LastReminderAt *time.Time `json:"last_reminder_at,omitempty" db:"last_reminder_at"`
|
||||
ConsentGivenAt *time.Time `json:"consent_given_at,omitempty" db:"consent_given_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// AccountSuspension tracks account suspensions
|
||||
type AccountSuspension struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Reason string `json:"reason" db:"reason"` // 'consent_deadline_exceeded'
|
||||
Details *string `json:"details,omitempty" db:"details"` // JSON
|
||||
SuspendedAt time.Time `json:"suspended_at" db:"suspended_at"`
|
||||
LiftedAt *time.Time `json:"lifted_at,omitempty" db:"lifted_at"`
|
||||
LiftedReason *string `json:"lifted_reason,omitempty" db:"lifted_reason"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Consent DTOs
|
||||
// ========================================
|
||||
|
||||
// CreateConsentRequest is the request body for creating a consent
|
||||
type CreateConsentRequest struct {
|
||||
DocumentType string `json:"document_type" binding:"required"`
|
||||
VersionID string `json:"version_id" binding:"required"`
|
||||
Consented bool `json:"consented"`
|
||||
}
|
||||
|
||||
// ConsentCheckResponse is the response for checking consent status
|
||||
type ConsentCheckResponse struct {
|
||||
HasConsent bool `json:"has_consent"`
|
||||
CurrentVersionID *string `json:"current_version_id,omitempty"`
|
||||
ConsentedVersion *string `json:"consented_version,omitempty"`
|
||||
NeedsUpdate bool `json:"needs_update"`
|
||||
ConsentedAt *time.Time `json:"consented_at,omitempty"`
|
||||
}
|
||||
|
||||
// DocumentWithVersion combines document info with its latest published version
|
||||
type DocumentWithVersion struct {
|
||||
Document LegalDocument `json:"document"`
|
||||
LatestVersion *DocumentVersion `json:"latest_version,omitempty"`
|
||||
}
|
||||
|
||||
// ConsentHistory represents a user's consent history for a document
|
||||
type ConsentHistory struct {
|
||||
Document LegalDocument `json:"document"`
|
||||
Version DocumentVersion `json:"version"`
|
||||
Consent UserConsent `json:"consent"`
|
||||
}
|
||||
|
||||
// ConsentStats represents statistics about consents
|
||||
type ConsentStats struct {
|
||||
TotalUsers int `json:"total_users"`
|
||||
ConsentedUsers int `json:"consented_users"`
|
||||
ConsentRate float64 `json:"consent_rate"`
|
||||
RecentConsents int `json:"recent_consents"` // Last 7 days
|
||||
RecentWithdrawals int `json:"recent_withdrawals"`
|
||||
}
|
||||
|
||||
// MyDataResponse represents all data we have about a user
|
||||
type MyDataResponse struct {
|
||||
User User `json:"user"`
|
||||
Consents []ConsentHistory `json:"consents"`
|
||||
CookieConsents []CookieConsent `json:"cookie_consents"`
|
||||
AuditLog []AuditLog `json:"audit_log"`
|
||||
ExportedAt time.Time `json:"exported_at"`
|
||||
}
|
||||
|
||||
// CreateDocumentRequest is the request body for creating a document
|
||||
type CreateDocumentRequest struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description *string `json:"description"`
|
||||
IsMandatory bool `json:"is_mandatory"`
|
||||
}
|
||||
|
||||
// CreateVersionRequest is the request body for creating a document version
|
||||
type CreateVersionRequest struct {
|
||||
DocumentID string `json:"document_id" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
Language string `json:"language" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Summary *string `json:"summary"`
|
||||
}
|
||||
|
||||
// UpdateVersionRequest is the request body for updating a version
|
||||
type UpdateVersionRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Content *string `json:"content"`
|
||||
Summary *string `json:"summary"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
// SubmitForReviewRequest for submitting a version for review
|
||||
type SubmitForReviewRequest struct {
|
||||
Comment *string `json:"comment"`
|
||||
}
|
||||
|
||||
// ApproveVersionRequest for approving a version (DSB)
|
||||
type ApproveVersionRequest struct {
|
||||
Comment *string `json:"comment"`
|
||||
ScheduledPublishAt *string `json:"scheduled_publish_at"` // ISO 8601 datetime for scheduled publishing
|
||||
}
|
||||
|
||||
// RejectVersionRequest for rejecting a version
|
||||
type RejectVersionRequest struct {
|
||||
Comment string `json:"comment" binding:"required"`
|
||||
}
|
||||
|
||||
// VersionCompareResponse for comparing versions
|
||||
type VersionCompareResponse struct {
|
||||
Published *DocumentVersion `json:"published,omitempty"`
|
||||
Draft *DocumentVersion `json:"draft"`
|
||||
Diff *string `json:"diff,omitempty"`
|
||||
Approvals []VersionApproval `json:"approvals"`
|
||||
}
|
||||
|
||||
// PendingConsentResponse for pending consents with deadline info
|
||||
type PendingConsentResponse struct {
|
||||
Document LegalDocument `json:"document"`
|
||||
Version DocumentVersion `json:"version"`
|
||||
DeadlineAt time.Time `json:"deadline_at"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
IsOverdue bool `json:"is_overdue"`
|
||||
}
|
||||
|
||||
// AccountStatusResponse for account status check
|
||||
type AccountStatusResponse struct {
|
||||
Status string `json:"status"` // 'active', 'suspended'
|
||||
PendingConsents []PendingConsentResponse `json:"pending_consents,omitempty"`
|
||||
SuspensionReason *string `json:"suspension_reason,omitempty"`
|
||||
CanAccess bool `json:"can_access"`
|
||||
}
|
||||
118
consent-service/internal/models/cookies_notifications.go
Normal file
118
consent-service/internal/models/cookies_notifications.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CookieCategory represents a category of cookies
|
||||
type CookieCategory struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"` // 'necessary', 'functional', 'analytics', 'marketing'
|
||||
DisplayNameDE string `json:"display_name_de" db:"display_name_de"`
|
||||
DisplayNameEN *string `json:"display_name_en" db:"display_name_en"`
|
||||
DescriptionDE *string `json:"description_de" db:"description_de"`
|
||||
DescriptionEN *string `json:"description_en" db:"description_en"`
|
||||
IsMandatory bool `json:"is_mandatory" db:"is_mandatory"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// CookieConsent represents a user's cookie preferences
|
||||
type CookieConsent struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
CategoryID uuid.UUID `json:"category_id" db:"category_id"`
|
||||
Consented bool `json:"consented" db:"consented"`
|
||||
ConsentedAt time.Time `json:"consented_at" db:"consented_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// CookieConsentRequest is the request body for setting cookie preferences
|
||||
type CookieConsentRequest struct {
|
||||
Categories []CookieCategoryConsent `json:"categories" binding:"required"`
|
||||
}
|
||||
|
||||
// CookieCategoryConsent represents consent for a single cookie category
|
||||
type CookieCategoryConsent struct {
|
||||
CategoryID string `json:"category_id" binding:"required"`
|
||||
Consented bool `json:"consented"`
|
||||
}
|
||||
|
||||
// CookieStats represents statistics about cookie consents
|
||||
type CookieStats struct {
|
||||
Category string `json:"category"`
|
||||
TotalUsers int `json:"total_users"`
|
||||
ConsentedUsers int `json:"consented_users"`
|
||||
ConsentRate float64 `json:"consent_rate"`
|
||||
}
|
||||
|
||||
// CreateCookieCategoryRequest is the request body for creating a cookie category
|
||||
type CreateCookieCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DisplayNameDE string `json:"display_name_de" binding:"required"`
|
||||
DisplayNameEN *string `json:"display_name_en"`
|
||||
DescriptionDE *string `json:"description_de"`
|
||||
DescriptionEN *string `json:"description_en"`
|
||||
IsMandatory bool `json:"is_mandatory"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Notification Models
|
||||
// ========================================
|
||||
|
||||
// Notification represents a user notification
|
||||
type Notification struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Type string `json:"type" db:"type"` // 'new_version', 'consent_reminder', 'account_warning'
|
||||
Channel string `json:"channel" db:"channel"` // 'email', 'in_app', 'push'
|
||||
Title string `json:"title" db:"title"`
|
||||
Body string `json:"body" db:"body"`
|
||||
Data *string `json:"data,omitempty" db:"data"` // JSON string
|
||||
ReadAt *time.Time `json:"read_at,omitempty" db:"read_at"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// PushSubscription for Web Push notifications
|
||||
type PushSubscription struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Endpoint string `json:"endpoint" db:"endpoint"`
|
||||
P256dh string `json:"p256dh" db:"p256dh"`
|
||||
Auth string `json:"auth" db:"auth"`
|
||||
UserAgent *string `json:"user_agent,omitempty" db:"user_agent"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// NotificationPreferences for user notification settings
|
||||
type NotificationPreferences struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
EmailEnabled bool `json:"email_enabled" db:"email_enabled"`
|
||||
PushEnabled bool `json:"push_enabled" db:"push_enabled"`
|
||||
InAppEnabled bool `json:"in_app_enabled" db:"in_app_enabled"`
|
||||
ReminderFrequency string `json:"reminder_frequency" db:"reminder_frequency"` // 'daily', 'weekly', 'never'
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// SubscribePushRequest for subscribing to push notifications
|
||||
type SubscribePushRequest struct {
|
||||
Endpoint string `json:"endpoint" binding:"required"`
|
||||
P256dh string `json:"p256dh" binding:"required"`
|
||||
Auth string `json:"auth" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateNotificationPreferencesRequest for updating preferences
|
||||
type UpdateNotificationPreferencesRequest struct {
|
||||
EmailEnabled *bool `json:"email_enabled"`
|
||||
PushEnabled *bool `json:"push_enabled"`
|
||||
InAppEnabled *bool `json:"in_app_enabled"`
|
||||
ReminderFrequency *string `json:"reminder_frequency"`
|
||||
}
|
||||
403
consent-service/internal/models/dsr.go
Normal file
403
consent-service/internal/models/dsr.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ========================================
|
||||
// DSGVO Betroffenenanfragen (DSR)
|
||||
// Data Subject Request Management
|
||||
// Art. 15, 16, 17, 18, 20 DSGVO
|
||||
// ========================================
|
||||
|
||||
// DSRRequestType defines the GDPR article for the request
|
||||
type DSRRequestType string
|
||||
|
||||
const (
|
||||
DSRTypeAccess DSRRequestType = "access" // Art. 15 - Auskunftsrecht
|
||||
DSRTypeRectification DSRRequestType = "rectification" // Art. 16 - Berichtigungsrecht
|
||||
DSRTypeErasure DSRRequestType = "erasure" // Art. 17 - Löschungsrecht
|
||||
DSRTypeRestriction DSRRequestType = "restriction" // Art. 18 - Einschränkungsrecht
|
||||
DSRTypePortability DSRRequestType = "portability" // Art. 20 - Datenübertragbarkeit
|
||||
)
|
||||
|
||||
// DSRStatus defines the workflow state of a DSR
|
||||
type DSRStatus string
|
||||
|
||||
const (
|
||||
DSRStatusIntake DSRStatus = "intake" // Eingegangen
|
||||
DSRStatusIdentityVerification DSRStatus = "identity_verification" // Identitätsprüfung
|
||||
DSRStatusProcessing DSRStatus = "processing" // In Bearbeitung
|
||||
DSRStatusCompleted DSRStatus = "completed" // Abgeschlossen
|
||||
DSRStatusRejected DSRStatus = "rejected" // Abgelehnt
|
||||
DSRStatusCancelled DSRStatus = "cancelled" // Storniert
|
||||
)
|
||||
|
||||
// DSRPriority defines the priority level of a DSR
|
||||
type DSRPriority string
|
||||
|
||||
const (
|
||||
DSRPriorityNormal DSRPriority = "normal"
|
||||
DSRPriorityExpedited DSRPriority = "expedited" // Art. 16, 17, 18 - beschleunigt
|
||||
DSRPriorityUrgent DSRPriority = "urgent"
|
||||
)
|
||||
|
||||
// DSRSource defines where the request came from
|
||||
type DSRSource string
|
||||
|
||||
const (
|
||||
DSRSourceAPI DSRSource = "api" // Über API/Self-Service
|
||||
DSRSourceAdminPanel DSRSource = "admin_panel" // Manuell im Admin
|
||||
DSRSourceEmail DSRSource = "email" // Per E-Mail
|
||||
DSRSourcePostal DSRSource = "postal" // Per Post
|
||||
)
|
||||
|
||||
// Art. 17(3) Exception Types
|
||||
const (
|
||||
DSRExceptionFreedomExpression = "freedom_expression" // Art. 17(3)(a)
|
||||
DSRExceptionLegalObligation = "legal_obligation" // Art. 17(3)(b)
|
||||
DSRExceptionPublicInterest = "public_interest" // Art. 17(3)(c)
|
||||
DSRExceptionPublicHealth = "public_health" // Art. 17(3)(c)
|
||||
DSRExceptionArchiving = "archiving" // Art. 17(3)(d)
|
||||
DSRExceptionLegalClaims = "legal_claims" // Art. 17(3)(e)
|
||||
)
|
||||
|
||||
// DataSubjectRequest represents a GDPR data subject request
|
||||
type DataSubjectRequest struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty" db:"user_id"`
|
||||
RequestNumber string `json:"request_number" db:"request_number"`
|
||||
RequestType DSRRequestType `json:"request_type" db:"request_type"`
|
||||
Status DSRStatus `json:"status" db:"status"`
|
||||
Priority DSRPriority `json:"priority" db:"priority"`
|
||||
Source DSRSource `json:"source" db:"source"`
|
||||
RequesterEmail string `json:"requester_email" db:"requester_email"`
|
||||
RequesterName *string `json:"requester_name,omitempty" db:"requester_name"`
|
||||
RequesterPhone *string `json:"requester_phone,omitempty" db:"requester_phone"`
|
||||
IdentityVerified bool `json:"identity_verified" db:"identity_verified"`
|
||||
IdentityVerifiedAt *time.Time `json:"identity_verified_at,omitempty" db:"identity_verified_at"`
|
||||
IdentityVerifiedBy *uuid.UUID `json:"identity_verified_by,omitempty" db:"identity_verified_by"`
|
||||
IdentityVerificationMethod *string `json:"identity_verification_method,omitempty" db:"identity_verification_method"`
|
||||
RequestDetails map[string]interface{} `json:"request_details" db:"request_details"`
|
||||
DeadlineAt time.Time `json:"deadline_at" db:"deadline_at"`
|
||||
LegalDeadlineDays int `json:"legal_deadline_days" db:"legal_deadline_days"`
|
||||
ExtendedDeadlineAt *time.Time `json:"extended_deadline_at,omitempty" db:"extended_deadline_at"`
|
||||
ExtensionReason *string `json:"extension_reason,omitempty" db:"extension_reason"`
|
||||
AssignedTo *uuid.UUID `json:"assigned_to,omitempty" db:"assigned_to"`
|
||||
ProcessingNotes *string `json:"processing_notes,omitempty" db:"processing_notes"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
|
||||
CompletedBy *uuid.UUID `json:"completed_by,omitempty" db:"completed_by"`
|
||||
ResultSummary *string `json:"result_summary,omitempty" db:"result_summary"`
|
||||
ResultData map[string]interface{} `json:"result_data,omitempty" db:"result_data"`
|
||||
RejectedAt *time.Time `json:"rejected_at,omitempty" db:"rejected_at"`
|
||||
RejectedBy *uuid.UUID `json:"rejected_by,omitempty" db:"rejected_by"`
|
||||
RejectionReason *string `json:"rejection_reason,omitempty" db:"rejection_reason"`
|
||||
RejectionLegalBasis *string `json:"rejection_legal_basis,omitempty" db:"rejection_legal_basis"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" db:"created_by"`
|
||||
}
|
||||
|
||||
// DSRStatusHistory tracks status changes for audit trail
|
||||
type DSRStatusHistory struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
RequestID uuid.UUID `json:"request_id" db:"request_id"`
|
||||
FromStatus *DSRStatus `json:"from_status,omitempty" db:"from_status"`
|
||||
ToStatus DSRStatus `json:"to_status" db:"to_status"`
|
||||
ChangedBy *uuid.UUID `json:"changed_by,omitempty" db:"changed_by"`
|
||||
Comment *string `json:"comment,omitempty" db:"comment"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty" db:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// DSRCommunication tracks all communications related to a DSR
|
||||
type DSRCommunication struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
RequestID uuid.UUID `json:"request_id" db:"request_id"`
|
||||
Direction string `json:"direction" db:"direction"`
|
||||
Channel string `json:"channel" db:"channel"`
|
||||
CommunicationType string `json:"communication_type" db:"communication_type"`
|
||||
TemplateVersionID *uuid.UUID `json:"template_version_id,omitempty" db:"template_version_id"`
|
||||
Subject *string `json:"subject,omitempty" db:"subject"`
|
||||
BodyHTML *string `json:"body_html,omitempty" db:"body_html"`
|
||||
BodyText *string `json:"body_text,omitempty" db:"body_text"`
|
||||
RecipientEmail *string `json:"recipient_email,omitempty" db:"recipient_email"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
ErrorMessage *string `json:"error_message,omitempty" db:"error_message"`
|
||||
Attachments []map[string]interface{} `json:"attachments,omitempty" db:"attachments"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" db:"created_by"`
|
||||
}
|
||||
|
||||
// DSRTemplate represents a template type for DSR communications
|
||||
type DSRTemplate struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TemplateType string `json:"template_type" db:"template_type"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description *string `json:"description,omitempty" db:"description"`
|
||||
RequestTypes []string `json:"request_types" db:"request_types"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// DSRTemplateVersion represents a versioned template for DSR communications
|
||||
type DSRTemplateVersion struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TemplateID uuid.UUID `json:"template_id" db:"template_id"`
|
||||
Version string `json:"version" db:"version"`
|
||||
Language string `json:"language" db:"language"`
|
||||
Subject string `json:"subject" db:"subject"`
|
||||
BodyHTML string `json:"body_html" db:"body_html"`
|
||||
BodyText string `json:"body_text" db:"body_text"`
|
||||
Status string `json:"status" db:"status"`
|
||||
PublishedAt *time.Time `json:"published_at,omitempty" db:"published_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" db:"created_by"`
|
||||
ApprovedBy *uuid.UUID `json:"approved_by,omitempty" db:"approved_by"`
|
||||
ApprovedAt *time.Time `json:"approved_at,omitempty" db:"approved_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// DSRExceptionCheck tracks Art. 17(3) exception evaluations for erasure requests
|
||||
type DSRExceptionCheck struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
RequestID uuid.UUID `json:"request_id" db:"request_id"`
|
||||
ExceptionType string `json:"exception_type" db:"exception_type"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Applies *bool `json:"applies,omitempty" db:"applies"`
|
||||
CheckedBy *uuid.UUID `json:"checked_by,omitempty" db:"checked_by"`
|
||||
CheckedAt *time.Time `json:"checked_at,omitempty" db:"checked_at"`
|
||||
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DSR DTOs
|
||||
// ========================================
|
||||
|
||||
// CreateDSRRequest for creating a new data subject request
|
||||
type CreateDSRRequest struct {
|
||||
RequestType string `json:"request_type" binding:"required"`
|
||||
RequesterEmail string `json:"requester_email" binding:"required,email"`
|
||||
RequesterName *string `json:"requester_name"`
|
||||
RequesterPhone *string `json:"requester_phone"`
|
||||
Source string `json:"source"`
|
||||
RequestDetails map[string]interface{} `json:"request_details"`
|
||||
Priority string `json:"priority"`
|
||||
}
|
||||
|
||||
// UpdateDSRRequest for updating a DSR
|
||||
type UpdateDSRRequest struct {
|
||||
Status *string `json:"status"`
|
||||
AssignedTo *string `json:"assigned_to"`
|
||||
ProcessingNotes *string `json:"processing_notes"`
|
||||
ExtendDeadline *bool `json:"extend_deadline"`
|
||||
ExtensionReason *string `json:"extension_reason"`
|
||||
RequestDetails map[string]interface{} `json:"request_details"`
|
||||
Priority *string `json:"priority"`
|
||||
}
|
||||
|
||||
// VerifyDSRIdentityRequest for verifying identity of requester
|
||||
type VerifyDSRIdentityRequest struct {
|
||||
Method string `json:"method" binding:"required"`
|
||||
Comment *string `json:"comment"`
|
||||
}
|
||||
|
||||
// CompleteDSRRequest for completing a DSR
|
||||
type CompleteDSRRequest struct {
|
||||
ResultSummary string `json:"result_summary" binding:"required"`
|
||||
ResultData map[string]interface{} `json:"result_data"`
|
||||
}
|
||||
|
||||
// RejectDSRRequest for rejecting a DSR
|
||||
type RejectDSRRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
LegalBasis string `json:"legal_basis" binding:"required"`
|
||||
}
|
||||
|
||||
// ExtendDSRDeadlineRequest for extending a DSR deadline
|
||||
type ExtendDSRDeadlineRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
Days int `json:"days"`
|
||||
}
|
||||
|
||||
// AssignDSRRequest for assigning a DSR to a handler
|
||||
type AssignDSRRequest struct {
|
||||
AssigneeID string `json:"assignee_id" binding:"required"`
|
||||
Comment *string `json:"comment"`
|
||||
}
|
||||
|
||||
// SendDSRCommunicationRequest for sending a communication
|
||||
type SendDSRCommunicationRequest struct {
|
||||
CommunicationType string `json:"communication_type" binding:"required"`
|
||||
TemplateVersionID *string `json:"template_version_id"`
|
||||
CustomSubject *string `json:"custom_subject"`
|
||||
CustomBody *string `json:"custom_body"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
// UpdateDSRExceptionCheckRequest for updating an exception check
|
||||
type UpdateDSRExceptionCheckRequest struct {
|
||||
Applies bool `json:"applies"`
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
// DSRListFilters for filtering DSR list
|
||||
type DSRListFilters struct {
|
||||
Status *string `form:"status"`
|
||||
RequestType *string `form:"request_type"`
|
||||
AssignedTo *string `form:"assigned_to"`
|
||||
Priority *string `form:"priority"`
|
||||
OverdueOnly bool `form:"overdue_only"`
|
||||
FromDate *time.Time `form:"from_date"`
|
||||
ToDate *time.Time `form:"to_date"`
|
||||
Search *string `form:"search"`
|
||||
}
|
||||
|
||||
// DSRDashboardStats for the admin dashboard
|
||||
type DSRDashboardStats struct {
|
||||
TotalRequests int `json:"total_requests"`
|
||||
PendingRequests int `json:"pending_requests"`
|
||||
OverdueRequests int `json:"overdue_requests"`
|
||||
CompletedThisMonth int `json:"completed_this_month"`
|
||||
AverageProcessingDays float64 `json:"average_processing_days"`
|
||||
ByType map[string]int `json:"by_type"`
|
||||
ByStatus map[string]int `json:"by_status"`
|
||||
UpcomingDeadlines []DataSubjectRequest `json:"upcoming_deadlines"`
|
||||
}
|
||||
|
||||
// DSRWithDetails combines DSR with related data
|
||||
type DSRWithDetails struct {
|
||||
Request DataSubjectRequest `json:"request"`
|
||||
StatusHistory []DSRStatusHistory `json:"status_history"`
|
||||
Communications []DSRCommunication `json:"communications"`
|
||||
ExceptionChecks []DSRExceptionCheck `json:"exception_checks,omitempty"`
|
||||
AssigneeName *string `json:"assignee_name,omitempty"`
|
||||
CreatorName *string `json:"creator_name,omitempty"`
|
||||
}
|
||||
|
||||
// DSRTemplateWithVersions combines template with versions
|
||||
type DSRTemplateWithVersions struct {
|
||||
Template DSRTemplate `json:"template"`
|
||||
LatestVersion *DSRTemplateVersion `json:"latest_version,omitempty"`
|
||||
Versions []DSRTemplateVersion `json:"versions,omitempty"`
|
||||
}
|
||||
|
||||
// CreateDSRTemplateVersionRequest for creating a template version
|
||||
type CreateDSRTemplateVersionRequest struct {
|
||||
TemplateID string `json:"template_id" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
Language string `json:"language" binding:"required"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
BodyHTML string `json:"body_html" binding:"required"`
|
||||
BodyText string `json:"body_text" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateDSRTemplateVersionRequest for updating a template version
|
||||
type UpdateDSRTemplateVersionRequest struct {
|
||||
Subject *string `json:"subject"`
|
||||
BodyHTML *string `json:"body_html"`
|
||||
BodyText *string `json:"body_text"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
// PreviewDSRTemplateRequest for previewing a template with variables
|
||||
type PreviewDSRTemplateRequest struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
// DSRTemplatePreviewResponse for template preview
|
||||
type DSRTemplatePreviewResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
BodyHTML string `json:"body_html"`
|
||||
BodyText string `json:"body_text"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DSR Helper Methods
|
||||
// ========================================
|
||||
|
||||
// Label returns German label for request type
|
||||
func (rt DSRRequestType) Label() string {
|
||||
switch rt {
|
||||
case DSRTypeAccess:
|
||||
return "Auskunftsanfrage (Art. 15)"
|
||||
case DSRTypeRectification:
|
||||
return "Berichtigungsanfrage (Art. 16)"
|
||||
case DSRTypeErasure:
|
||||
return "Löschanfrage (Art. 17)"
|
||||
case DSRTypeRestriction:
|
||||
return "Einschränkungsanfrage (Art. 18)"
|
||||
case DSRTypePortability:
|
||||
return "Datenübertragung (Art. 20)"
|
||||
default:
|
||||
return string(rt)
|
||||
}
|
||||
}
|
||||
|
||||
// DeadlineDays returns the legal deadline in days for request type
|
||||
func (rt DSRRequestType) DeadlineDays() int {
|
||||
switch rt {
|
||||
case DSRTypeAccess, DSRTypePortability:
|
||||
return 30 // 1 month
|
||||
case DSRTypeRectification, DSRTypeErasure, DSRTypeRestriction:
|
||||
return 14 // 2 weeks (expedited per BDSG)
|
||||
default:
|
||||
return 30
|
||||
}
|
||||
}
|
||||
|
||||
// IsExpedited returns whether this request type should be processed expeditiously
|
||||
func (rt DSRRequestType) IsExpedited() bool {
|
||||
switch rt {
|
||||
case DSRTypeRectification, DSRTypeErasure, DSRTypeRestriction:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Label returns German label for status
|
||||
func (s DSRStatus) Label() string {
|
||||
switch s {
|
||||
case DSRStatusIntake:
|
||||
return "Eingang"
|
||||
case DSRStatusIdentityVerification:
|
||||
return "Identitätsprüfung"
|
||||
case DSRStatusProcessing:
|
||||
return "In Bearbeitung"
|
||||
case DSRStatusCompleted:
|
||||
return "Abgeschlossen"
|
||||
case DSRStatusRejected:
|
||||
return "Abgelehnt"
|
||||
case DSRStatusCancelled:
|
||||
return "Storniert"
|
||||
default:
|
||||
return string(s)
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidDSRRequestType checks if a string is a valid DSR request type
|
||||
func IsValidDSRRequestType(reqType string) bool {
|
||||
switch DSRRequestType(reqType) {
|
||||
case DSRTypeAccess, DSRTypeRectification, DSRTypeErasure, DSRTypeRestriction, DSRTypePortability:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidDSRStatus checks if a string is a valid DSR status
|
||||
func IsValidDSRStatus(status string) bool {
|
||||
switch DSRStatus(status) {
|
||||
case DSRStatusIntake, DSRStatusIdentityVerification, DSRStatusProcessing,
|
||||
DSRStatusCompleted, DSRStatusRejected, DSRStatusCancelled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
191
consent-service/internal/models/email_templates.go
Normal file
191
consent-service/internal/models/email_templates.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// EmailTemplateType defines the types of transactional emails
|
||||
const (
|
||||
// Auth & Security
|
||||
EmailTypeWelcome = "welcome"
|
||||
EmailTypeEmailVerification = "email_verification"
|
||||
EmailTypePasswordReset = "password_reset"
|
||||
EmailTypePasswordChanged = "password_changed"
|
||||
EmailType2FAEnabled = "2fa_enabled"
|
||||
EmailType2FADisabled = "2fa_disabled"
|
||||
EmailTypeNewDeviceLogin = "new_device_login"
|
||||
EmailTypeSuspiciousActivity = "suspicious_activity"
|
||||
EmailTypeAccountLocked = "account_locked"
|
||||
EmailTypeAccountUnlocked = "account_unlocked"
|
||||
|
||||
// Account Lifecycle
|
||||
EmailTypeDeletionRequested = "deletion_requested"
|
||||
EmailTypeDeletionConfirmed = "deletion_confirmed"
|
||||
EmailTypeDataExportReady = "data_export_ready"
|
||||
EmailTypeEmailChanged = "email_changed"
|
||||
EmailTypeEmailChangeVerify = "email_change_verify"
|
||||
|
||||
// Consent-related
|
||||
EmailTypeNewVersionPublished = "new_version_published"
|
||||
EmailTypeConsentReminder = "consent_reminder"
|
||||
EmailTypeConsentDeadlineWarning = "consent_deadline_warning"
|
||||
EmailTypeAccountSuspended = "account_suspended"
|
||||
)
|
||||
|
||||
// EmailTemplate represents a template for transactional emails (like LegalDocument)
|
||||
type EmailTemplate struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Type string `json:"type" db:"type"` // One of EmailType constants
|
||||
Name string `json:"name" db:"name"` // Human-readable name
|
||||
Description *string `json:"description" db:"description"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// EmailTemplateVersion represents a specific version of an email template (like DocumentVersion)
|
||||
type EmailTemplateVersion struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TemplateID uuid.UUID `json:"template_id" db:"template_id"`
|
||||
Version string `json:"version" db:"version"` // Semver: 1.0.0
|
||||
Language string `json:"language" db:"language"` // ISO 639-1: de, en
|
||||
Subject string `json:"subject" db:"subject"` // Email subject line
|
||||
BodyHTML string `json:"body_html" db:"body_html"` // HTML version
|
||||
BodyText string `json:"body_text" db:"body_text"` // Plain text version
|
||||
Summary *string `json:"summary" db:"summary"` // Change summary
|
||||
Status string `json:"status" db:"status"` // draft, review, approved, published, archived
|
||||
PublishedAt *time.Time `json:"published_at" db:"published_at"`
|
||||
ScheduledPublishAt *time.Time `json:"scheduled_publish_at" db:"scheduled_publish_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by" db:"created_by"`
|
||||
ApprovedBy *uuid.UUID `json:"approved_by" db:"approved_by"`
|
||||
ApprovedAt *time.Time `json:"approved_at" db:"approved_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// EmailTemplateApproval tracks approval workflow for email templates
|
||||
type EmailTemplateApproval struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
VersionID uuid.UUID `json:"version_id" db:"version_id"`
|
||||
ApproverID uuid.UUID `json:"approver_id" db:"approver_id"`
|
||||
Action string `json:"action" db:"action"` // submitted_for_review, approved, rejected, published
|
||||
Comment *string `json:"comment,omitempty" db:"comment"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// EmailSendLog tracks sent emails for audit purposes
|
||||
type EmailSendLog struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty" db:"user_id"`
|
||||
VersionID uuid.UUID `json:"version_id" db:"version_id"`
|
||||
Recipient string `json:"recipient" db:"recipient"` // Email address
|
||||
Subject string `json:"subject" db:"subject"`
|
||||
Status string `json:"status" db:"status"` // queued, sent, delivered, bounced, failed
|
||||
ErrorMsg *string `json:"error_msg,omitempty" db:"error_msg"`
|
||||
Variables *string `json:"variables,omitempty" db:"variables"` // JSON of template variables used
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty" db:"delivered_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// EmailTemplateSettings stores global email settings (logo, signature, etc.)
|
||||
type EmailTemplateSettings struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
LogoURL *string `json:"logo_url" db:"logo_url"`
|
||||
LogoBase64 *string `json:"logo_base64" db:"logo_base64"` // For embedding in emails
|
||||
CompanyName string `json:"company_name" db:"company_name"`
|
||||
SenderName string `json:"sender_name" db:"sender_name"`
|
||||
SenderEmail string `json:"sender_email" db:"sender_email"`
|
||||
ReplyToEmail *string `json:"reply_to_email" db:"reply_to_email"`
|
||||
FooterHTML *string `json:"footer_html" db:"footer_html"`
|
||||
FooterText *string `json:"footer_text" db:"footer_text"`
|
||||
PrimaryColor string `json:"primary_color" db:"primary_color"` // Hex color
|
||||
SecondaryColor string `json:"secondary_color" db:"secondary_color"` // Hex color
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
UpdatedBy *uuid.UUID `json:"updated_by" db:"updated_by"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// E-Mail Template DTOs
|
||||
// ========================================
|
||||
|
||||
// CreateEmailTemplateRequest for creating a new email template type
|
||||
type CreateEmailTemplateRequest struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
// CreateEmailTemplateVersionRequest for creating a new version of an email template
|
||||
type CreateEmailTemplateVersionRequest struct {
|
||||
TemplateID string `json:"template_id" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
Language string `json:"language" binding:"required"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
BodyHTML string `json:"body_html" binding:"required"`
|
||||
BodyText string `json:"body_text" binding:"required"`
|
||||
Summary *string `json:"summary"`
|
||||
}
|
||||
|
||||
// UpdateEmailTemplateVersionRequest for updating a version
|
||||
type UpdateEmailTemplateVersionRequest struct {
|
||||
Subject *string `json:"subject"`
|
||||
BodyHTML *string `json:"body_html"`
|
||||
BodyText *string `json:"body_text"`
|
||||
Summary *string `json:"summary"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateEmailTemplateSettingsRequest for updating global settings
|
||||
type UpdateEmailTemplateSettingsRequest struct {
|
||||
LogoURL *string `json:"logo_url"`
|
||||
LogoBase64 *string `json:"logo_base64"`
|
||||
CompanyName *string `json:"company_name"`
|
||||
SenderName *string `json:"sender_name"`
|
||||
SenderEmail *string `json:"sender_email"`
|
||||
ReplyToEmail *string `json:"reply_to_email"`
|
||||
FooterHTML *string `json:"footer_html"`
|
||||
FooterText *string `json:"footer_text"`
|
||||
PrimaryColor *string `json:"primary_color"`
|
||||
SecondaryColor *string `json:"secondary_color"`
|
||||
}
|
||||
|
||||
// EmailTemplateWithVersion combines template info with its latest published version
|
||||
type EmailTemplateWithVersion struct {
|
||||
Template EmailTemplate `json:"template"`
|
||||
LatestVersion *EmailTemplateVersion `json:"latest_version,omitempty"`
|
||||
}
|
||||
|
||||
// SendTestEmailRequest for sending a test email
|
||||
type SendTestEmailRequest struct {
|
||||
VersionID string `json:"version_id" binding:"required"`
|
||||
Recipient string `json:"recipient" binding:"required,email"`
|
||||
Variables map[string]string `json:"variables"` // Template variable overrides
|
||||
}
|
||||
|
||||
// EmailPreviewResponse for previewing an email
|
||||
type EmailPreviewResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
BodyHTML string `json:"body_html"`
|
||||
BodyText string `json:"body_text"`
|
||||
}
|
||||
|
||||
// EmailTemplateVariables defines available variables for each template type
|
||||
type EmailTemplateVariables struct {
|
||||
TemplateType string `json:"template_type"`
|
||||
Variables []string `json:"variables"`
|
||||
Descriptions map[string]string `json:"descriptions"`
|
||||
}
|
||||
|
||||
// EmailStats represents statistics about email sends
|
||||
type EmailStats struct {
|
||||
TotalSent int `json:"total_sent"`
|
||||
Delivered int `json:"delivered"`
|
||||
Bounced int `json:"bounced"`
|
||||
Failed int `json:"failed"`
|
||||
DeliveryRate float64 `json:"delivery_rate"`
|
||||
RecentSent int `json:"recent_sent"` // Last 7 days
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
103
consent-service/internal/models/oauth.go
Normal file
103
consent-service/internal/models/oauth.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OAuthClient represents a registered OAuth 2.0 client application
|
||||
type OAuthClient struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
ClientSecret string `json:"-" db:"client_secret"` // Never expose in JSON
|
||||
Name string `json:"name" db:"name"`
|
||||
Description *string `json:"description,omitempty" db:"description"`
|
||||
RedirectURIs []string `json:"redirect_uris" db:"redirect_uris"` // JSON array
|
||||
Scopes []string `json:"scopes" db:"scopes"` // Allowed scopes
|
||||
GrantTypes []string `json:"grant_types" db:"grant_types"` // authorization_code, refresh_token
|
||||
IsPublic bool `json:"is_public" db:"is_public"` // Public clients (SPAs) don't have secret
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedBy *uuid.UUID `json:"created_by,omitempty" db:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// OAuthAuthorizationCode represents an authorization code for the OAuth flow
|
||||
type OAuthAuthorizationCode struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Code string `json:"-" db:"code"` // Hashed
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
RedirectURI string `json:"redirect_uri" db:"redirect_uri"`
|
||||
Scopes []string `json:"scopes" db:"scopes"`
|
||||
CodeChallenge *string `json:"-" db:"code_challenge"` // For PKCE
|
||||
CodeChallengeMethod *string `json:"-" db:"code_challenge_method"` // S256 or plain
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// OAuthAccessToken represents an OAuth access token
|
||||
type OAuthAccessToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TokenHash string `json:"-" db:"token_hash"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Scopes []string `json:"scopes" db:"scopes"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty" db:"revoked_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// OAuthRefreshToken represents an OAuth refresh token
|
||||
type OAuthRefreshToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TokenHash string `json:"-" db:"token_hash"`
|
||||
AccessTokenID uuid.UUID `json:"access_token_id" db:"access_token_id"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Scopes []string `json:"scopes" db:"scopes"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty" db:"revoked_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// OAuthAuthorizeRequest for the authorization endpoint
|
||||
type OAuthAuthorizeRequest struct {
|
||||
ResponseType string `form:"response_type" binding:"required"` // Must be "code"
|
||||
ClientID string `form:"client_id" binding:"required"`
|
||||
RedirectURI string `form:"redirect_uri" binding:"required"`
|
||||
Scope string `form:"scope"` // Space-separated scopes
|
||||
State string `form:"state" binding:"required"` // CSRF protection
|
||||
CodeChallenge string `form:"code_challenge"` // PKCE
|
||||
CodeChallengeMethod string `form:"code_challenge_method"` // S256 (recommended) or plain
|
||||
}
|
||||
|
||||
// OAuthTokenRequest for the token endpoint
|
||||
type OAuthTokenRequest struct {
|
||||
GrantType string `form:"grant_type" binding:"required"` // authorization_code or refresh_token
|
||||
Code string `form:"code"` // For authorization_code grant
|
||||
RedirectURI string `form:"redirect_uri"` // For authorization_code grant
|
||||
ClientID string `form:"client_id" binding:"required"`
|
||||
ClientSecret string `form:"client_secret"` // For confidential clients
|
||||
CodeVerifier string `form:"code_verifier"` // For PKCE
|
||||
RefreshToken string `form:"refresh_token"` // For refresh_token grant
|
||||
Scope string `form:"scope"` // For refresh_token grant (optional)
|
||||
}
|
||||
|
||||
// OAuthTokenResponse for successful token requests
|
||||
type OAuthTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"` // Always "Bearer"
|
||||
ExpiresIn int `json:"expires_in"` // Seconds until expiration
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// OAuthErrorResponse for OAuth errors (RFC 6749)
|
||||
type OAuthErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
ErrorURI string `json:"error_uri,omitempty"`
|
||||
}
|
||||
187
consent-service/internal/models/school.go
Normal file
187
consent-service/internal/models/school.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SchoolRole defines roles within the school system
|
||||
const (
|
||||
SchoolRoleTeacher = "teacher"
|
||||
SchoolRoleClassTeacher = "class_teacher"
|
||||
SchoolRoleParent = "parent"
|
||||
SchoolRoleParentRep = "parent_representative"
|
||||
SchoolRoleStudent = "student"
|
||||
SchoolRoleAdmin = "school_admin"
|
||||
SchoolRolePrincipal = "principal"
|
||||
SchoolRoleSecretary = "secretary"
|
||||
)
|
||||
|
||||
// AttendanceStatus defines the status of student attendance
|
||||
const (
|
||||
AttendancePresent = "present"
|
||||
AttendanceAbsent = "absent"
|
||||
AttendanceAbsentExcused = "excused"
|
||||
AttendanceAbsentUnexcused = "unexcused"
|
||||
AttendanceLate = "late"
|
||||
AttendanceLateExcused = "late_excused"
|
||||
AttendancePending = "pending_confirmation"
|
||||
)
|
||||
|
||||
// GradeType defines the type of grade
|
||||
const (
|
||||
GradeTypeExam = "exam" // Klassenarbeit/Klausur
|
||||
GradeTypeTest = "test" // Test/Kurzarbeit
|
||||
GradeTypeOral = "oral" // Mündlich
|
||||
GradeTypeHomework = "homework" // Hausaufgabe
|
||||
GradeTypeProject = "project" // Projekt
|
||||
GradeTypeParticipation = "participation" // Mitarbeit
|
||||
GradeTypeSemester = "semester" // Halbjahres-/Semesternote
|
||||
GradeTypeFinal = "final" // Endnote/Zeugnisnote
|
||||
)
|
||||
|
||||
// School represents a school/educational institution
|
||||
type School struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
ShortName *string `json:"short_name,omitempty" db:"short_name"`
|
||||
Type string `json:"type" db:"type"` // 'grundschule', 'hauptschule', 'realschule', 'gymnasium', 'gesamtschule', 'berufsschule'
|
||||
Address *string `json:"address,omitempty" db:"address"`
|
||||
City *string `json:"city,omitempty" db:"city"`
|
||||
PostalCode *string `json:"postal_code,omitempty" db:"postal_code"`
|
||||
State *string `json:"state,omitempty" db:"state"` // Bundesland
|
||||
Country string `json:"country" db:"country"` // Default: DE
|
||||
Phone *string `json:"phone,omitempty" db:"phone"`
|
||||
Email *string `json:"email,omitempty" db:"email"`
|
||||
Website *string `json:"website,omitempty" db:"website"`
|
||||
MatrixServerName *string `json:"matrix_server_name,omitempty" db:"matrix_server_name"`
|
||||
LogoURL *string `json:"logo_url,omitempty" db:"logo_url"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// SchoolYear represents an academic year
|
||||
type SchoolYear struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
Name string `json:"name" db:"name"` // e.g., "2024/2025"
|
||||
StartDate time.Time `json:"start_date" db:"start_date"`
|
||||
EndDate time.Time `json:"end_date" db:"end_date"`
|
||||
IsCurrent bool `json:"is_current" db:"is_current"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// Class represents a school class
|
||||
type Class struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
SchoolYearID uuid.UUID `json:"school_year_id" db:"school_year_id"`
|
||||
Name string `json:"name" db:"name"` // e.g., "5a", "10b"
|
||||
Grade int `json:"grade" db:"grade"` // Klassenstufe: 1-13
|
||||
Section *string `json:"section,omitempty" db:"section"`
|
||||
Room *string `json:"room,omitempty" db:"room"`
|
||||
MatrixInfoRoom *string `json:"matrix_info_room,omitempty" db:"matrix_info_room"`
|
||||
MatrixRepRoom *string `json:"matrix_rep_room,omitempty" db:"matrix_rep_room"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Subject represents a school subject
|
||||
type Subject struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
Name string `json:"name" db:"name"` // e.g., "Mathematik", "Deutsch"
|
||||
ShortName string `json:"short_name" db:"short_name"` // e.g., "Ma", "De"
|
||||
Color *string `json:"color,omitempty" db:"color"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// Student represents a student
|
||||
type Student struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty" db:"user_id"`
|
||||
StudentNumber *string `json:"student_number,omitempty" db:"student_number"`
|
||||
FirstName string `json:"first_name" db:"first_name"`
|
||||
LastName string `json:"last_name" db:"last_name"`
|
||||
DateOfBirth *time.Time `json:"date_of_birth,omitempty" db:"date_of_birth"`
|
||||
Gender *string `json:"gender,omitempty" db:"gender"`
|
||||
MatrixUserID *string `json:"matrix_user_id,omitempty" db:"matrix_user_id"`
|
||||
MatrixDMRoom *string `json:"matrix_dm_room,omitempty" db:"matrix_dm_room"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Teacher represents a teacher
|
||||
type Teacher struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
TeacherCode *string `json:"teacher_code,omitempty" db:"teacher_code"`
|
||||
Title *string `json:"title,omitempty" db:"title"`
|
||||
FirstName string `json:"first_name" db:"first_name"`
|
||||
LastName string `json:"last_name" db:"last_name"`
|
||||
MatrixUserID *string `json:"matrix_user_id,omitempty" db:"matrix_user_id"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// ClassTeacher assigns teachers to classes (Klassenlehrer)
|
||||
type ClassTeacher struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
IsPrimary bool `json:"is_primary" db:"is_primary"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// TeacherSubject assigns subjects to teachers
|
||||
type TeacherSubject struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// Parent represents a parent/guardian
|
||||
type Parent struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
MatrixUserID *string `json:"matrix_user_id,omitempty" db:"matrix_user_id"`
|
||||
FirstName string `json:"first_name" db:"first_name"`
|
||||
LastName string `json:"last_name" db:"last_name"`
|
||||
Phone *string `json:"phone,omitempty" db:"phone"`
|
||||
EmergencyContact bool `json:"emergency_contact" db:"emergency_contact"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// StudentParent links students to their parents
|
||||
type StudentParent struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
ParentID uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
Relationship string `json:"relationship" db:"relationship"`
|
||||
IsPrimary bool `json:"is_primary" db:"is_primary"`
|
||||
HasCustody bool `json:"has_custody" db:"has_custody"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ParentRepresentative assigns parent representatives to classes
|
||||
type ParentRepresentative struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
ParentID uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
Role string `json:"role" db:"role"` // 'first_rep', 'second_rep', 'substitute'
|
||||
ElectedAt time.Time `json:"elected_at" db:"elected_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
382
consent-service/internal/models/school_operations.go
Normal file
382
consent-service/internal/models/school_operations.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ========================================
|
||||
// Stundenplan / Timetable
|
||||
// ========================================
|
||||
|
||||
// TimetableSlot represents a time slot in the timetable
|
||||
type TimetableSlot struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
SlotNumber int `json:"slot_number" db:"slot_number"` // 1, 2, 3... (Stunde)
|
||||
StartTime string `json:"start_time" db:"start_time"` // "08:00"
|
||||
EndTime string `json:"end_time" db:"end_time"` // "08:45"
|
||||
IsBreak bool `json:"is_break" db:"is_break"` // Pause
|
||||
Name *string `json:"name,omitempty" db:"name"` // e.g., "1. Stunde", "Große Pause"
|
||||
}
|
||||
|
||||
// TimetableEntry represents a single lesson in the timetable
|
||||
type TimetableEntry struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolYearID uuid.UUID `json:"school_year_id" db:"school_year_id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
SlotID uuid.UUID `json:"slot_id" db:"slot_id"`
|
||||
DayOfWeek int `json:"day_of_week" db:"day_of_week"` // 1=Monday, 5=Friday
|
||||
Room *string `json:"room,omitempty" db:"room"`
|
||||
ValidFrom time.Time `json:"valid_from" db:"valid_from"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" db:"valid_until"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// TimetableSubstitution represents a substitution/replacement lesson
|
||||
type TimetableSubstitution struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OriginalEntryID uuid.UUID `json:"original_entry_id" db:"original_entry_id"`
|
||||
Date time.Time `json:"date" db:"date"`
|
||||
SubstituteTeacherID *uuid.UUID `json:"substitute_teacher_id,omitempty" db:"substitute_teacher_id"`
|
||||
SubstituteSubjectID *uuid.UUID `json:"substitute_subject_id,omitempty" db:"substitute_subject_id"`
|
||||
Room *string `json:"room,omitempty" db:"room"`
|
||||
Type string `json:"type" db:"type"` // 'substitution', 'cancelled', 'room_change', 'supervision'
|
||||
Note *string `json:"note,omitempty" db:"note"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Abwesenheit / Attendance
|
||||
// ========================================
|
||||
|
||||
// AttendanceRecord represents a student's attendance for a specific lesson
|
||||
type AttendanceRecord struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
TimetableEntryID *uuid.UUID `json:"timetable_entry_id,omitempty" db:"timetable_entry_id"`
|
||||
Date time.Time `json:"date" db:"date"`
|
||||
SlotID uuid.UUID `json:"slot_id" db:"slot_id"`
|
||||
Status string `json:"status" db:"status"` // AttendanceStatus constants
|
||||
RecordedBy uuid.UUID `json:"recorded_by" db:"recorded_by"`
|
||||
Note *string `json:"note,omitempty" db:"note"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// AbsenceReport represents a full absence report (one or more days)
|
||||
type AbsenceReport struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
StartDate time.Time `json:"start_date" db:"start_date"`
|
||||
EndDate time.Time `json:"end_date" db:"end_date"`
|
||||
Reason *string `json:"reason,omitempty" db:"reason"`
|
||||
ReasonCategory string `json:"reason_category" db:"reason_category"`
|
||||
Status string `json:"status" db:"status"`
|
||||
ReportedBy uuid.UUID `json:"reported_by" db:"reported_by"`
|
||||
ReportedAt time.Time `json:"reported_at" db:"reported_at"`
|
||||
ConfirmedBy *uuid.UUID `json:"confirmed_by,omitempty" db:"confirmed_by"`
|
||||
ConfirmedAt *time.Time `json:"confirmed_at,omitempty" db:"confirmed_at"`
|
||||
MedicalCertificate bool `json:"medical_certificate" db:"medical_certificate"`
|
||||
CertificateUploaded bool `json:"certificate_uploaded" db:"certificate_uploaded"`
|
||||
MatrixNotificationSent bool `json:"matrix_notification_sent" db:"matrix_notification_sent"`
|
||||
EmailNotificationSent bool `json:"email_notification_sent" db:"email_notification_sent"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// AbsenceNotification tracks notifications sent to parents about absences
|
||||
type AbsenceNotification struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AttendanceRecordID uuid.UUID `json:"attendance_record_id" db:"attendance_record_id"`
|
||||
ParentID uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
Channel string `json:"channel" db:"channel"`
|
||||
MessageContent string `json:"message_content" db:"message_content"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
ReadAt *time.Time `json:"read_at,omitempty" db:"read_at"`
|
||||
ResponseReceived bool `json:"response_received" db:"response_received"`
|
||||
ResponseContent *string `json:"response_content,omitempty" db:"response_content"`
|
||||
ResponseAt *time.Time `json:"response_at,omitempty" db:"response_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Notenspiegel / Grades
|
||||
// ========================================
|
||||
|
||||
// GradeScale represents the grading scale used
|
||||
type GradeScale struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
MinValue float64 `json:"min_value" db:"min_value"`
|
||||
MaxValue float64 `json:"max_value" db:"max_value"`
|
||||
PassingValue float64 `json:"passing_value" db:"passing_value"`
|
||||
IsAscending bool `json:"is_ascending" db:"is_ascending"`
|
||||
IsDefault bool `json:"is_default" db:"is_default"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// Grade represents a single grade for a student
|
||||
type Grade struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
SchoolYearID uuid.UUID `json:"school_year_id" db:"school_year_id"`
|
||||
GradeScaleID uuid.UUID `json:"grade_scale_id" db:"grade_scale_id"`
|
||||
Type string `json:"type" db:"type"`
|
||||
Value float64 `json:"value" db:"value"`
|
||||
Weight float64 `json:"weight" db:"weight"`
|
||||
Date time.Time `json:"date" db:"date"`
|
||||
Title *string `json:"title,omitempty" db:"title"`
|
||||
Description *string `json:"description,omitempty" db:"description"`
|
||||
IsVisible bool `json:"is_visible" db:"is_visible"`
|
||||
Semester int `json:"semester" db:"semester"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// GradeComment represents a teacher comment on a student's grade
|
||||
type GradeComment struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
GradeID uuid.UUID `json:"grade_id" db:"grade_id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
Comment string `json:"comment" db:"comment"`
|
||||
IsPrivate bool `json:"is_private" db:"is_private"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Klassenbuch, Meetings, Communication
|
||||
// ========================================
|
||||
|
||||
// ClassDiaryEntry represents an entry in the digital class diary
|
||||
type ClassDiaryEntry struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
Date time.Time `json:"date" db:"date"`
|
||||
SlotID uuid.UUID `json:"slot_id" db:"slot_id"`
|
||||
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
Topic *string `json:"topic,omitempty" db:"topic"`
|
||||
Homework *string `json:"homework,omitempty" db:"homework"`
|
||||
HomeworkDueDate *time.Time `json:"homework_due_date,omitempty" db:"homework_due_date"`
|
||||
Materials *string `json:"materials,omitempty" db:"materials"`
|
||||
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||
IsCancelled bool `json:"is_cancelled" db:"is_cancelled"`
|
||||
CancellationReason *string `json:"cancellation_reason,omitempty" db:"cancellation_reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// ParentMeetingSlot represents available time slots for parent meetings
|
||||
type ParentMeetingSlot struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
||||
Date time.Time `json:"date" db:"date"`
|
||||
StartTime string `json:"start_time" db:"start_time"`
|
||||
EndTime string `json:"end_time" db:"end_time"`
|
||||
Location *string `json:"location,omitempty" db:"location"`
|
||||
IsOnline bool `json:"is_online" db:"is_online"`
|
||||
MeetingLink *string `json:"meeting_link,omitempty" db:"meeting_link"`
|
||||
IsBooked bool `json:"is_booked" db:"is_booked"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// ParentMeeting represents a booked parent-teacher meeting
|
||||
type ParentMeeting struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SlotID uuid.UUID `json:"slot_id" db:"slot_id"`
|
||||
ParentID uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
Topic *string `json:"topic,omitempty" db:"topic"`
|
||||
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CancelledAt *time.Time `json:"cancelled_at,omitempty" db:"cancelled_at"`
|
||||
CancelledBy *uuid.UUID `json:"cancelled_by,omitempty" db:"cancelled_by"`
|
||||
CancelReason *string `json:"cancel_reason,omitempty" db:"cancel_reason"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// MatrixRoom tracks Matrix rooms created for school communication
|
||||
type MatrixRoom struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
MatrixRoomID string `json:"matrix_room_id" db:"matrix_room_id"`
|
||||
Type string `json:"type" db:"type"`
|
||||
ClassID *uuid.UUID `json:"class_id,omitempty" db:"class_id"`
|
||||
StudentID *uuid.UUID `json:"student_id,omitempty" db:"student_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
IsEncrypted bool `json:"is_encrypted" db:"is_encrypted"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// MatrixRoomMember tracks membership in Matrix rooms
|
||||
type MatrixRoomMember struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
MatrixRoomID uuid.UUID `json:"matrix_room_id" db:"matrix_room_id"`
|
||||
MatrixUserID string `json:"matrix_user_id" db:"matrix_user_id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty" db:"user_id"`
|
||||
PowerLevel int `json:"power_level" db:"power_level"`
|
||||
CanWrite bool `json:"can_write" db:"can_write"`
|
||||
JoinedAt time.Time `json:"joined_at" db:"joined_at"`
|
||||
LeftAt *time.Time `json:"left_at,omitempty" db:"left_at"`
|
||||
}
|
||||
|
||||
// ParentOnboardingToken for QR-code based parent onboarding
|
||||
type ParentOnboardingToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
SchoolID uuid.UUID `json:"school_id" db:"school_id"`
|
||||
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
||||
StudentID uuid.UUID `json:"student_id" db:"student_id"`
|
||||
Token string `json:"token" db:"token"`
|
||||
Role string `json:"role" db:"role"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||
UsedByUserID *uuid.UUID `json:"used_by_user_id,omitempty" db:"used_by_user_id"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Schulverwaltung DTOs
|
||||
// ========================================
|
||||
|
||||
// CreateSchoolRequest for creating a new school
|
||||
type CreateSchoolRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ShortName *string `json:"short_name"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Address *string `json:"address"`
|
||||
City *string `json:"city"`
|
||||
PostalCode *string `json:"postal_code"`
|
||||
State *string `json:"state"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Website *string `json:"website"`
|
||||
}
|
||||
|
||||
// CreateClassRequest for creating a new class
|
||||
type CreateClassRequest struct {
|
||||
SchoolYearID string `json:"school_year_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Grade int `json:"grade" binding:"required"`
|
||||
Section *string `json:"section"`
|
||||
Room *string `json:"room"`
|
||||
}
|
||||
|
||||
// CreateStudentRequest for creating a new student
|
||||
type CreateStudentRequest struct {
|
||||
ClassID string `json:"class_id" binding:"required"`
|
||||
StudentNumber *string `json:"student_number"`
|
||||
FirstName string `json:"first_name" binding:"required"`
|
||||
LastName string `json:"last_name" binding:"required"`
|
||||
DateOfBirth *string `json:"date_of_birth"` // ISO 8601
|
||||
Gender *string `json:"gender"`
|
||||
}
|
||||
|
||||
// RecordAttendanceRequest for recording attendance
|
||||
type RecordAttendanceRequest struct {
|
||||
StudentID string `json:"student_id" binding:"required"`
|
||||
Date string `json:"date" binding:"required"` // ISO 8601
|
||||
SlotID string `json:"slot_id" binding:"required"`
|
||||
Status string `json:"status" binding:"required"` // AttendanceStatus
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
|
||||
// ReportAbsenceRequest for parents reporting absence
|
||||
type ReportAbsenceRequest struct {
|
||||
StudentID string `json:"student_id" binding:"required"`
|
||||
StartDate string `json:"start_date" binding:"required"`
|
||||
EndDate string `json:"end_date" binding:"required"`
|
||||
Reason *string `json:"reason"`
|
||||
ReasonCategory string `json:"reason_category" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateGradeRequest for creating a grade
|
||||
type CreateGradeRequest struct {
|
||||
StudentID string `json:"student_id" binding:"required"`
|
||||
SubjectID string `json:"subject_id" binding:"required"`
|
||||
SchoolYearID string `json:"school_year_id" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Value float64 `json:"value" binding:"required"`
|
||||
Weight float64 `json:"weight"`
|
||||
Date string `json:"date" binding:"required"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Semester int `json:"semester" binding:"required"`
|
||||
}
|
||||
|
||||
// StudentGradeOverview provides a summary of all grades for a student in a subject
|
||||
type StudentGradeOverview struct {
|
||||
Student Student `json:"student"`
|
||||
Subject Subject `json:"subject"`
|
||||
Grades []Grade `json:"grades"`
|
||||
Average float64 `json:"average"`
|
||||
OralAverage float64 `json:"oral_average"`
|
||||
ExamAverage float64 `json:"exam_average"`
|
||||
Semester int `json:"semester"`
|
||||
}
|
||||
|
||||
// ClassAttendanceOverview provides attendance summary for a class
|
||||
type ClassAttendanceOverview struct {
|
||||
Class Class `json:"class"`
|
||||
Date time.Time `json:"date"`
|
||||
TotalStudents int `json:"total_students"`
|
||||
PresentCount int `json:"present_count"`
|
||||
AbsentCount int `json:"absent_count"`
|
||||
LateCount int `json:"late_count"`
|
||||
Records []AttendanceRecord `json:"records"`
|
||||
}
|
||||
|
||||
// ParentDashboard provides a parent's view of their children's data
|
||||
type ParentDashboard struct {
|
||||
Children []StudentOverview `json:"children"`
|
||||
UnreadMessages int `json:"unread_messages"`
|
||||
UpcomingMeetings []ParentMeeting `json:"upcoming_meetings"`
|
||||
RecentGrades []Grade `json:"recent_grades"`
|
||||
PendingActions []string `json:"pending_actions"`
|
||||
}
|
||||
|
||||
// StudentOverview provides summary info about a student
|
||||
type StudentOverview struct {
|
||||
Student Student `json:"student"`
|
||||
Class Class `json:"class"`
|
||||
ClassTeacher *Teacher `json:"class_teacher,omitempty"`
|
||||
AttendanceRate float64 `json:"attendance_rate"`
|
||||
UnexcusedAbsences int `json:"unexcused_absences"`
|
||||
GradeAverage float64 `json:"grade_average"`
|
||||
}
|
||||
|
||||
// TimetableView provides a formatted timetable for display
|
||||
type TimetableView struct {
|
||||
Class Class `json:"class"`
|
||||
Week string `json:"week"` // ISO week: "2025-W01"
|
||||
Days []TimetableDayView `json:"days"`
|
||||
}
|
||||
|
||||
// TimetableDayView represents a single day in the timetable
|
||||
type TimetableDayView struct {
|
||||
Date time.Time `json:"date"`
|
||||
DayName string `json:"day_name"`
|
||||
Lessons []TimetableLessonView `json:"lessons"`
|
||||
}
|
||||
|
||||
// TimetableLessonView represents a single lesson in the timetable view
|
||||
type TimetableLessonView struct {
|
||||
Slot TimetableSlot `json:"slot"`
|
||||
Subject *Subject `json:"subject,omitempty"`
|
||||
Teacher *Teacher `json:"teacher,omitempty"`
|
||||
Room *string `json:"room,omitempty"`
|
||||
IsSubstitution bool `json:"is_substitution"`
|
||||
IsCancelled bool `json:"is_cancelled"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
}
|
||||
195
consent-service/internal/models/user.go
Normal file
195
consent-service/internal/models/user.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// User represents a user with full authentication support
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ExternalID *string `json:"external_id,omitempty" db:"external_id"`
|
||||
Email string `json:"email" db:"email"`
|
||||
PasswordHash *string `json:"-" db:"password_hash"` // Never exposed in JSON
|
||||
Name *string `json:"name,omitempty" db:"name"`
|
||||
Role string `json:"role" db:"role"` // 'user', 'admin', 'super_admin', 'data_protection_officer'
|
||||
EmailVerified bool `json:"email_verified" db:"email_verified"`
|
||||
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty" db:"email_verified_at"`
|
||||
AccountStatus string `json:"account_status" db:"account_status"` // 'active', 'suspended', 'locked'
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" db:"last_login_at"`
|
||||
FailedLoginAttempts int `json:"failed_login_attempts" db:"failed_login_attempts"`
|
||||
LockedUntil *time.Time `json:"locked_until,omitempty" db:"locked_until"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// EmailVerificationToken for email verification
|
||||
type EmailVerificationToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Token string `json:"token" db:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// PasswordResetToken for password reset
|
||||
type PasswordResetToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Token string `json:"token" db:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||
IPAddress *string `json:"ip_address,omitempty" db:"ip_address"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// UserSession for session management
|
||||
type UserSession struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
TokenHash string `json:"-" db:"token_hash"`
|
||||
DeviceInfo *string `json:"device_info,omitempty" db:"device_info"`
|
||||
IPAddress *string `json:"ip_address,omitempty" db:"ip_address"`
|
||||
UserAgent *string `json:"user_agent,omitempty" db:"user_agent"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty" db:"revoked_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
LastActivityAt time.Time `json:"last_activity_at" db:"last_activity_at"`
|
||||
}
|
||||
|
||||
// RegisterRequest for user registration
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// LoginRequest for user login
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse after successful login
|
||||
type LoginResponse struct {
|
||||
User User `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"` // seconds
|
||||
}
|
||||
|
||||
// RefreshTokenRequest for token refresh
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
// VerifyEmailRequest for email verification
|
||||
type VerifyEmailRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
}
|
||||
|
||||
// ForgotPasswordRequest for password reset request
|
||||
type ForgotPasswordRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest for password reset
|
||||
type ResetPasswordRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest for changing password
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
// UpdateProfileRequest for profile updates
|
||||
type UpdateProfileRequest struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Two-Factor Authentication (2FA/TOTP)
|
||||
// ========================================
|
||||
|
||||
// UserTOTP stores 2FA TOTP configuration for a user
|
||||
type UserTOTP struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Secret string `json:"-" db:"secret"` // Encrypted TOTP secret
|
||||
Verified bool `json:"verified" db:"verified"` // Has 2FA been verified/activated
|
||||
RecoveryCodes []string `json:"-" db:"recovery_codes"` // Encrypted backup codes
|
||||
EnabledAt *time.Time `json:"enabled_at,omitempty" db:"enabled_at"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty" db:"last_used_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// TwoFactorChallenge represents a pending 2FA challenge during login
|
||||
type TwoFactorChallenge struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
ChallengeID string `json:"challenge_id" db:"challenge_id"` // Temporary token
|
||||
IPAddress *string `json:"ip_address,omitempty" db:"ip_address"`
|
||||
UserAgent *string `json:"user_agent,omitempty" db:"user_agent"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// Setup2FAResponse when initiating 2FA setup
|
||||
type Setup2FAResponse struct {
|
||||
Secret string `json:"secret"` // Base32 encoded secret for manual entry
|
||||
QRCodeDataURL string `json:"qr_code"` // Data URL for QR code image
|
||||
RecoveryCodes []string `json:"recovery_codes"` // One-time backup codes
|
||||
}
|
||||
|
||||
// Verify2FARequest for verifying 2FA setup or login
|
||||
type Verify2FARequest struct {
|
||||
Code string `json:"code" binding:"required"` // 6-digit TOTP code
|
||||
ChallengeID string `json:"challenge_id,omitempty"` // For login flow
|
||||
}
|
||||
|
||||
// TwoFactorLoginResponse when 2FA is required during login
|
||||
type TwoFactorLoginResponse struct {
|
||||
RequiresTwoFactor bool `json:"requires_two_factor"`
|
||||
ChallengeID string `json:"challenge_id"` // Use this to complete 2FA
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Complete2FALoginRequest to complete login with 2FA
|
||||
type Complete2FALoginRequest struct {
|
||||
ChallengeID string `json:"challenge_id" binding:"required"`
|
||||
Code string `json:"code" binding:"required"` // 6-digit TOTP or recovery code
|
||||
}
|
||||
|
||||
// Disable2FARequest for disabling 2FA
|
||||
type Disable2FARequest struct {
|
||||
Password string `json:"password" binding:"required"` // Require password confirmation
|
||||
Code string `json:"code" binding:"required"` // Current TOTP code
|
||||
}
|
||||
|
||||
// RecoveryCodeUseRequest for using a recovery code
|
||||
type RecoveryCodeUseRequest struct {
|
||||
ChallengeID string `json:"challenge_id" binding:"required"`
|
||||
RecoveryCode string `json:"recovery_code" binding:"required"`
|
||||
}
|
||||
|
||||
// TwoFactorStatusResponse for checking 2FA status
|
||||
type TwoFactorStatusResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Verified bool `json:"verified"`
|
||||
EnabledAt *time.Time `json:"enabled_at,omitempty"`
|
||||
RecoveryCodesCount int `json:"recovery_codes_count"`
|
||||
}
|
||||
|
||||
// Verify2FAChallengeRequest for verifying a 2FA challenge during login
|
||||
type Verify2FAChallengeRequest struct {
|
||||
ChallengeID string `json:"challenge_id" binding:"required"`
|
||||
Code string `json:"code,omitempty"` // 6-digit TOTP code
|
||||
RecoveryCode string `json:"recovery_code,omitempty"` // Alternative: recovery code
|
||||
}
|
||||
Reference in New Issue
Block a user