init push
This commit is contained in:
16
internal/domain/auth/contact_constants.go
Normal file
16
internal/domain/auth/contact_constants.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
const (
|
||||
RoleCodePilot = "pilot"
|
||||
RoleCodeDoctor = "doctor"
|
||||
RoleCodeAirRescuer = "air_rescuer"
|
||||
RoleCodeTechnician = "technician"
|
||||
RoleCodeFlightAssistant = "flight_assistant"
|
||||
RoleCodeStaff = "staff"
|
||||
)
|
||||
|
||||
const (
|
||||
PilotCategoryRegular = "regular"
|
||||
PilotCategoryFreelance = "freelance"
|
||||
PilotCategoryDryLease = "dry_lease"
|
||||
)
|
||||
141
internal/domain/auth/contact_profiles.go
Normal file
141
internal/domain/auth/contact_profiles.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package auth
|
||||
|
||||
import "time"
|
||||
|
||||
// PilotProfile stores role-specific data for role=pilot.
|
||||
type PilotProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
PilotCategory string `gorm:"type:varchar(16);not null;index;column:pilot_category"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
LicenseNo string `gorm:"type:varchar(120);index;column:license_no"`
|
||||
LicenseIssuedAt *time.Time `gorm:"column:license_issued_at"`
|
||||
LicenseExpiredAt *time.Time `gorm:"column:license_expired_at"`
|
||||
TechnicianLicenseNo string `gorm:"type:varchar(120);column:technician_license_no"`
|
||||
HasIFRQualification bool `gorm:"not null;default:false;column:has_ifr_qualification"`
|
||||
IsChiefPilot bool `gorm:"not null;default:false;column:is_chief_pilot"`
|
||||
IsDUL bool `gorm:"not null;default:false;column:is_dul"`
|
||||
TotalFlightMinutes int `gorm:"not null;default:0;column:total_flight_minutes"`
|
||||
ResponsiblePilotMinutes int `gorm:"not null;default:0;column:responsible_pilot_minutes"`
|
||||
SecondPilotMinutes int `gorm:"not null;default:0;column:second_pilot_minutes"`
|
||||
DoubleCommandMinutes int `gorm:"not null;default:0;column:double_command_minutes"`
|
||||
FlightInstructorMinutes int `gorm:"not null;default:0;column:flight_instructor_minutes"`
|
||||
NightFlightMinutes int `gorm:"not null;default:0;column:night_flight_minutes"`
|
||||
IFRFlightMinutes int `gorm:"not null;default:0;column:ifr_flight_minutes"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
WeightKG *float64 `gorm:"type:decimal(8,2);column:weight_kg"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (PilotProfile) TableName() string { return "pilot_profiles" }
|
||||
|
||||
// DoctorProfile stores role-specific data for role=doctor.
|
||||
type DoctorProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
Specialization string `gorm:"type:varchar(120);column:specialization"`
|
||||
SubSpecialization string `gorm:"type:varchar(120);column:sub_specialization"`
|
||||
IsChiefPhysician bool `gorm:"not null;default:false;column:is_chief_physician"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (DoctorProfile) TableName() string { return "doctor_profiles" }
|
||||
|
||||
// AirRescuerProfile stores role-specific data for role=air_rescuer.
|
||||
type AirRescuerProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
ResponsibleFlightRescuer bool `gorm:"not null;default:false;column:responsible_flight_rescuer"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (AirRescuerProfile) TableName() string { return "air_rescuer_profiles" }
|
||||
|
||||
// TechnicianProfile stores role-specific data for role=technician.
|
||||
type TechnicianProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
LicenseNo string `gorm:"type:varchar(120);index;column:license_no"`
|
||||
IsChiefTechnician bool `gorm:"not null;default:false;column:is_chief_technician"`
|
||||
IsResponsibleComplaints bool `gorm:"not null;default:false;column:is_responsible_complaints"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (TechnicianProfile) TableName() string { return "technician_profiles" }
|
||||
|
||||
// FlightAssistantProfile stores role-specific data for role=flight_assistant.
|
||||
type FlightAssistantProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (FlightAssistantProfile) TableName() string { return "flight_assistant_profiles" }
|
||||
|
||||
// StaffProfile stores role-specific data for role=staff.
|
||||
type StaffProfile struct {
|
||||
UserID []byte `gorm:"type:binary(16);primaryKey;column:user_id"`
|
||||
ShortName string `gorm:"type:varchar(120);column:short_name"`
|
||||
RoleLabel string `gorm:"type:varchar(120);column:role_label"`
|
||||
Location string `gorm:"type:varchar(255);column:location"`
|
||||
Postcode string `gorm:"type:varchar(32);column:postcode"`
|
||||
StreetLine string `gorm:"type:varchar(255);column:street_line"`
|
||||
Note string `gorm:"type:text;column:note"`
|
||||
PhotoFileID string `gorm:"type:varchar(255);column:photo_file_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (StaffProfile) TableName() string { return "staff_profiles" }
|
||||
34
internal/domain/auth/email_login_otp.go
Normal file
34
internal/domain/auth/email_login_otp.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type UserEmailLoginOTP struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);index;not null;column:user_id"`
|
||||
OTPHash []byte `gorm:"type:binary(32);not null;column:otp_hash"`
|
||||
ExpiresAt time.Time `gorm:"index;not null;column:expires_at"`
|
||||
UsedAt *time.Time `gorm:"index;column:used_at"`
|
||||
AttemptCount int `gorm:"not null;default:0;column:attempt_count"`
|
||||
MaxAttempts int `gorm:"not null;default:5;column:max_attempts"`
|
||||
ResendCount int `gorm:"not null;default:1;column:resend_count"`
|
||||
LastSentAt time.Time `gorm:"index;not null;column:last_sent_at"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (UserEmailLoginOTP) TableName() string { return "user_email_login_otps" }
|
||||
|
||||
func (o *UserEmailLoginOTP) BeforeCreate(_ *gorm.DB) error {
|
||||
if len(o.ID) == 0 {
|
||||
o.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
143
internal/domain/auth/email_outbox.go
Normal file
143
internal/domain/auth/email_outbox.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
"wucher/internal/queue"
|
||||
)
|
||||
|
||||
const (
|
||||
EmailOutboxStatusPending = "pending"
|
||||
EmailOutboxStatusProcessing = "processing"
|
||||
EmailOutboxStatusPublished = "published"
|
||||
EmailOutboxStatusDead = "dead"
|
||||
)
|
||||
|
||||
type EmailOutboxMessage struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
MessageID string `gorm:"type:varchar(64);uniqueIndex;not null;column:message_id"`
|
||||
Kind string `gorm:"type:varchar(64);index;not null;column:kind"`
|
||||
JobType string `gorm:"type:varchar(64);index;not null;column:job_type"`
|
||||
Body []byte `gorm:"type:longblob;not null;column:body"`
|
||||
Attributes []byte `gorm:"type:longblob;column:attributes"`
|
||||
MessageGroupID string `gorm:"type:varchar(128);column:message_group_id"`
|
||||
DeduplicationID string `gorm:"type:varchar(128);column:deduplication_id"`
|
||||
Status string `gorm:"type:varchar(32);index;not null;column:status"`
|
||||
Attempts int `gorm:"not null;default:0;column:attempts"`
|
||||
AvailableAt time.Time `gorm:"index;not null;column:available_at"`
|
||||
LockedAt *time.Time `gorm:"index;column:locked_at"`
|
||||
LockedBy string `gorm:"type:varchar(64);column:locked_by"`
|
||||
PublishedAt *time.Time `gorm:"column:published_at"`
|
||||
LastError string `gorm:"type:text;column:last_error"`
|
||||
CorrelationID string `gorm:"type:varchar(64);index;column:correlation_id"`
|
||||
TraceID string `gorm:"type:varchar(64);index;column:trace_id"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (EmailOutboxMessage) TableName() string { return "email_outbox_messages" }
|
||||
|
||||
func (m *EmailOutboxMessage) BeforeCreate(_ *gorm.DB) error {
|
||||
if len(m.ID) == 0 {
|
||||
m.ID = uuidv7.MustBytes()
|
||||
}
|
||||
if strings.TrimSpace(m.Status) == "" {
|
||||
m.Status = EmailOutboxStatusPending
|
||||
}
|
||||
if m.AvailableAt.IsZero() {
|
||||
m.AvailableAt = time.Now().UTC()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewEmailOutboxMessage(now time.Time, message *queue.OutboundMessage) (*EmailOutboxMessage, error) {
|
||||
if message == nil {
|
||||
return nil, errors.New("outbound message is required")
|
||||
}
|
||||
if strings.TrimSpace(message.ID) == "" {
|
||||
return nil, errors.New("outbound message id is required")
|
||||
}
|
||||
if len(message.Body) == 0 {
|
||||
return nil, errors.New("outbound message body is required")
|
||||
}
|
||||
|
||||
attributes, err := json.Marshal(message.Attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
kind := strings.TrimSpace(message.Attributes["message_kind"])
|
||||
if kind == "" {
|
||||
kind = queue.EmailKind
|
||||
}
|
||||
jobType := strings.TrimSpace(message.Attributes["job_type"])
|
||||
if jobType == "" {
|
||||
jobType = "generic"
|
||||
}
|
||||
|
||||
return &EmailOutboxMessage{
|
||||
MessageID: strings.TrimSpace(message.ID),
|
||||
Kind: kind,
|
||||
JobType: jobType,
|
||||
Body: append([]byte(nil), message.Body...),
|
||||
Attributes: append([]byte(nil), attributes...),
|
||||
MessageGroupID: strings.TrimSpace(message.MessageGroupID),
|
||||
DeduplicationID: strings.TrimSpace(message.DeduplicationID),
|
||||
Status: EmailOutboxStatusPending,
|
||||
AvailableAt: now.UTC(),
|
||||
CorrelationID: strings.TrimSpace(message.Attributes["correlation_id"]),
|
||||
TraceID: strings.TrimSpace(message.Attributes["trace_id"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *EmailOutboxMessage) OutboundMessage() (*queue.OutboundMessage, error) {
|
||||
if m == nil {
|
||||
return nil, errors.New("email outbox message is required")
|
||||
}
|
||||
attributes := map[string]string{}
|
||||
if len(m.Attributes) > 0 {
|
||||
if err := json.Unmarshal(m.Attributes, &attributes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(m.MessageID) == "" {
|
||||
return nil, errors.New("message id is required")
|
||||
}
|
||||
if len(m.Body) == 0 {
|
||||
return nil, errors.New("message body is required")
|
||||
}
|
||||
return &queue.OutboundMessage{
|
||||
ID: m.MessageID,
|
||||
Body: append([]byte(nil), m.Body...),
|
||||
Attributes: attributes,
|
||||
MessageGroupID: strings.TrimSpace(m.MessageGroupID),
|
||||
DeduplicationID: strings.TrimSpace(m.DeduplicationID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type EmailOutboxWriter interface {
|
||||
CreateEmailOutboxMessage(ctx context.Context, message *EmailOutboxMessage) error
|
||||
}
|
||||
|
||||
type EmailOutboxRepository interface {
|
||||
EmailOutboxWriter
|
||||
ClaimPendingEmailOutboxMessages(ctx context.Context, limit int, now time.Time, lockTTL time.Duration, workerID string) ([]EmailOutboxMessage, error)
|
||||
MarkEmailOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error
|
||||
MarkEmailOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error
|
||||
MarkEmailOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error
|
||||
CountPendingEmailOutboxMessages(ctx context.Context, now time.Time) (int64, error)
|
||||
}
|
||||
|
||||
type TransactionalRepository interface {
|
||||
WithTransaction(ctx context.Context, fn func(repo Repository, outbox EmailOutboxWriter) error) error
|
||||
}
|
||||
31
internal/domain/auth/identity.go
Normal file
31
internal/domain/auth/identity.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type UserIdentity struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);uniqueIndex:ux_user_identity_user;not null;column:user_id"`
|
||||
Provider string `gorm:"type:varchar(32);uniqueIndex:ux_provider_subject;not null;column:provider"`
|
||||
ProviderSubject string `gorm:"type:varchar(191);uniqueIndex:ux_provider_subject;not null;column:provider_subject"`
|
||||
Email string `gorm:"type:varchar(191);index;column:email"`
|
||||
Metadata []byte `gorm:"type:json;column:metadata"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (UserIdentity) TableName() string { return "user_identities" }
|
||||
|
||||
func (ui *UserIdentity) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(ui.ID) == 0 {
|
||||
ui.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
40
internal/domain/auth/microsoft_oauth_token.go
Normal file
40
internal/domain/auth/microsoft_oauth_token.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type UserMicrosoftOAuthToken struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);uniqueIndex:ux_user_ms_oauth_user_provider;not null;column:user_id"`
|
||||
Provider string `gorm:"type:varchar(32);uniqueIndex:ux_user_ms_oauth_user_provider;not null;column:provider"`
|
||||
TenantID string `gorm:"type:varchar(128);index;not null;column:tenant_id"`
|
||||
MicrosoftObjectID string `gorm:"type:varchar(191);not null;column:microsoft_object_id"`
|
||||
MicrosoftSubject string `gorm:"type:varchar(191);not null;column:microsoft_subject"`
|
||||
MicrosoftSessionID string `gorm:"type:varchar(191);column:microsoft_session_id"`
|
||||
RefreshTokenEncrypted []byte `gorm:"type:blob;not null;column:refresh_token_encrypted"`
|
||||
AccessTokenEncrypted []byte `gorm:"type:blob;column:access_token_encrypted"`
|
||||
IDTokenEncrypted []byte `gorm:"type:blob;column:id_token_encrypted"`
|
||||
Scope string `gorm:"type:text;column:scope"`
|
||||
TokenType string `gorm:"type:varchar(32);column:token_type"`
|
||||
AccessTokenExpiresAt *time.Time `gorm:"column:access_token_expires_at"`
|
||||
RefreshTokenExpiresAt *time.Time `gorm:"column:refresh_token_expires_at"`
|
||||
RefreshTokenRotatedAt *time.Time `gorm:"column:refresh_token_rotated_at"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (UserMicrosoftOAuthToken) TableName() string { return "user_microsoft_oauth_tokens" }
|
||||
|
||||
func (t *UserMicrosoftOAuthToken) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(t.ID) == 0 {
|
||||
t.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
32
internal/domain/auth/password_reset_token.go
Normal file
32
internal/domain/auth/password_reset_token.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type PasswordResetToken struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);index;not null;column:user_id"`
|
||||
TokenHash []byte `gorm:"type:varbinary(32);uniqueIndex;not null;column:token_hash"`
|
||||
ExpiresAt time.Time `gorm:"index;not null;column:expires_at"`
|
||||
UsedAt *time.Time `gorm:"index;column:used_at"`
|
||||
RequestedIP string `gorm:"type:varchar(128);column:requested_ip"`
|
||||
UserAgent string `gorm:"type:varchar(255);column:user_agent"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (PasswordResetToken) TableName() string { return "password_reset_tokens" }
|
||||
|
||||
func (prt *PasswordResetToken) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(prt.ID) == 0 {
|
||||
prt.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
32
internal/domain/auth/permission.go
Normal file
32
internal/domain/auth/permission.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type Permission struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
Name string `gorm:"type:varchar(64);uniqueIndex;not null;column:name"`
|
||||
Key string `gorm:"type:varchar(64);uniqueIndex;not null;column:key"`
|
||||
Description string `gorm:"type:varchar(255);column:description"`
|
||||
Module string `gorm:"type:varchar(64);index;column:module"`
|
||||
RequiresPIN bool `gorm:"not null;default:false;column:requires_pin"`
|
||||
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (Permission) TableName() string { return "permissions" }
|
||||
|
||||
func (p *Permission) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(p.ID) == 0 {
|
||||
p.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
129
internal/domain/auth/permission_module.go
Normal file
129
internal/domain/auth/permission_module.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package auth
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ModuleUser = "user"
|
||||
ModuleRole = "role"
|
||||
ModuleHelicopter = "helicopter"
|
||||
ModuleHelicopterUsage = "helicopter_usage"
|
||||
ModuleReserveAC = "reserve_ac"
|
||||
ModuleComplaint = "complaint"
|
||||
ModuleActionSignoff = "action_signoff"
|
||||
ModuleMCF = "mcf"
|
||||
ModuleEASARelease = "easa_release"
|
||||
ModuleFlightInspection = "flight_inspection"
|
||||
ModuleAfterFlight = "after_flight"
|
||||
ModuleFlight = "flight"
|
||||
ModuleFlightPrepCheck = "flight_prep_check"
|
||||
ModuleDutyRoster = "duty_roster"
|
||||
ModuleDUL = "dul"
|
||||
ModuleHEMSOperation = "hems_operation"
|
||||
ModuleHEMSBase = "hems_base"
|
||||
ModuleOperation = "operation"
|
||||
ModuleOperationCategory = "operation_category"
|
||||
ModuleForcesPresent = "forces_present"
|
||||
ModuleAirRescuerChecklist = "air_rescuer_checklist"
|
||||
ModuleMedicine = "medicine"
|
||||
ModulePatientData = "patient_data"
|
||||
ModuleInsurancePatientData = "insurance_patient_data"
|
||||
ModuleHealthInsuranceCompanies = "health_insurance_companies"
|
||||
ModuleVocation = "vocation"
|
||||
ModuleBase = "base"
|
||||
ModuleFacility = "facility"
|
||||
ModuleCountry = "country"
|
||||
ModuleFederalState = "federal_state"
|
||||
ModuleICAO = "icao"
|
||||
ModuleNoICAOCode = "no_icao_code"
|
||||
ModuleOPC = "opc"
|
||||
ModuleMasterSettings = "master_settings"
|
||||
ModuleFileManager = "file_manager"
|
||||
ModuleFilesystem = "filesystem"
|
||||
ModuleAudit = "audit"
|
||||
)
|
||||
|
||||
type ModuleInfo struct {
|
||||
Key string
|
||||
Label string
|
||||
}
|
||||
|
||||
var ModuleRegistry = []ModuleInfo{
|
||||
{ModuleUser, "User"},
|
||||
{ModuleRole, "Role"},
|
||||
{ModuleHelicopter, "Helicopter"},
|
||||
{ModuleHelicopterUsage, "Helicopter Usage"},
|
||||
{ModuleReserveAC, "Reserve AC"},
|
||||
{ModuleComplaint, "Complaint"},
|
||||
{ModuleActionSignoff, "Action Sign-off"},
|
||||
{ModuleMCF, "MCF"},
|
||||
{ModuleEASARelease, "EASA Release"},
|
||||
{ModuleFlightInspection, "Flight Inspection"},
|
||||
{ModuleAfterFlight, "After Flight Inspection"},
|
||||
{ModuleFlight, "Flight"},
|
||||
{ModuleFlightPrepCheck, "Flight Prep Check"},
|
||||
{ModuleDutyRoster, "Duty Roster"},
|
||||
{ModuleDUL, "DUL"},
|
||||
{ModuleHEMSOperation, "HEMS Operation"},
|
||||
{ModuleHEMSBase, "HEMS Base"},
|
||||
{ModuleOperation, "Operation"},
|
||||
{ModuleOperationCategory, "Operation Category"},
|
||||
{ModuleForcesPresent, "Forces Present"},
|
||||
{ModuleAirRescuerChecklist, "Air Rescuer Checklist"},
|
||||
{ModuleMedicine, "Medicine"},
|
||||
{ModulePatientData, "Patient Data"},
|
||||
{ModuleInsurancePatientData, "Insurance Patient Data"},
|
||||
{ModuleHealthInsuranceCompanies, "Health Insurance Companies"},
|
||||
{ModuleVocation, "Vocation"},
|
||||
{ModuleBase, "Base"},
|
||||
{ModuleFacility, "Facility"},
|
||||
{ModuleCountry, "Country"},
|
||||
{ModuleFederalState, "Federal State"},
|
||||
{ModuleICAO, "ICAO"},
|
||||
{ModuleNoICAOCode, "No ICAO Code"},
|
||||
{ModuleOPC, "OPC"},
|
||||
{ModuleMasterSettings, "Master Settings"},
|
||||
{ModuleFileManager, "File Manager"},
|
||||
{ModuleFilesystem, "Filesystem"},
|
||||
{ModuleAudit, "Audit"},
|
||||
}
|
||||
|
||||
var moduleOrder = func() map[string]int {
|
||||
m := make(map[string]int, len(ModuleRegistry))
|
||||
for i := range ModuleRegistry {
|
||||
m[ModuleRegistry[i].Key] = i
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// ModuleLabel returns the display label for a module key, falling back to the
|
||||
// key itself when the module is not registered.
|
||||
func ModuleLabel(key string) string {
|
||||
if i, ok := moduleOrder[key]; ok {
|
||||
return ModuleRegistry[i].Label
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// IsRegisteredModule reports whether key is a known module in ModuleRegistry.
|
||||
func IsRegisteredModule(key string) bool {
|
||||
_, ok := moduleOrder[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ModuleSortIndex returns a module's position in ModuleRegistry, or a large
|
||||
// value (sorting unknown modules last) when it is not registered.
|
||||
func ModuleSortIndex(key string) int {
|
||||
if idx, ok := moduleOrder[key]; ok {
|
||||
return idx
|
||||
}
|
||||
return len(ModuleRegistry)
|
||||
}
|
||||
|
||||
// ModuleForKey derives the module from a permission key by taking the segment
|
||||
// before the first dot, e.g. "dul.create" -> "dul", "role.permission.read" -> "role".
|
||||
func ModuleForKey(permissionKey string) string {
|
||||
if i := strings.Index(permissionKey, "."); i >= 0 {
|
||||
return permissionKey[:i]
|
||||
}
|
||||
return permissionKey
|
||||
}
|
||||
49
internal/domain/auth/permission_module_test.go
Normal file
49
internal/domain/auth/permission_module_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package auth
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestModuleRegistryIsConsistent(t *testing.T) {
|
||||
seen := make(map[string]bool, len(ModuleRegistry))
|
||||
for _, m := range ModuleRegistry {
|
||||
if m.Key == "" {
|
||||
t.Errorf("registry has an entry with empty key (label %q)", m.Label)
|
||||
}
|
||||
if m.Label == "" {
|
||||
t.Errorf("module %q has an empty label", m.Key)
|
||||
}
|
||||
if seen[m.Key] {
|
||||
t.Errorf("duplicate module key %q in ModuleRegistry", m.Key)
|
||||
}
|
||||
seen[m.Key] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleForKey(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"dul.create": "dul",
|
||||
"role.permission.read": "role",
|
||||
"file_manager.file.read": "file_manager",
|
||||
"audit.read": "audit",
|
||||
"filesystem": "filesystem",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := ModuleForKey(in); got != want {
|
||||
t.Errorf("ModuleForKey(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleLabelAndRegistration(t *testing.T) {
|
||||
if got := ModuleLabel(ModuleDUL); got != "DUL" {
|
||||
t.Errorf("ModuleLabel(%q) = %q, want %q", ModuleDUL, got, "DUL")
|
||||
}
|
||||
if got := ModuleLabel("unknown_module"); got != "unknown_module" {
|
||||
t.Errorf("ModuleLabel fallback = %q, want the key itself", got)
|
||||
}
|
||||
if !IsRegisteredModule(ModuleComplaint) {
|
||||
t.Errorf("expected %q to be a registered module", ModuleComplaint)
|
||||
}
|
||||
if IsRegisteredModule("unknown_module") {
|
||||
t.Errorf("did not expect %q to be registered", "unknown_module")
|
||||
}
|
||||
}
|
||||
24
internal/domain/auth/pin_policy.go
Normal file
24
internal/domain/auth/pin_policy.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package auth
|
||||
|
||||
import "strings"
|
||||
|
||||
var defaultPINRequiredPermissionKeys = []string{
|
||||
"audit.read",
|
||||
"easa_release.sign",
|
||||
}
|
||||
|
||||
func DefaultPINRequiredPermissionKeys() []string {
|
||||
out := make([]string, len(defaultPINRequiredPermissionKeys))
|
||||
copy(out, defaultPINRequiredPermissionKeys)
|
||||
return out
|
||||
}
|
||||
|
||||
func DefaultPermissionRequiresPIN(key string) bool {
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
for i := range defaultPINRequiredPermissionKeys {
|
||||
if defaultPINRequiredPermissionKeys[i] == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
57
internal/domain/auth/repository.go
Normal file
57
internal/domain/auth/repository.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
// Users
|
||||
CreateUser(ctx context.Context, user *User) error
|
||||
GetUserByID(ctx context.Context, id []byte) (*User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*User, error)
|
||||
GetUserBySSOEmail(ctx context.Context, ssoEmail string) (*User, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (*User, error)
|
||||
UpdateUserTimezone(ctx context.Context, id []byte, timezone string) error
|
||||
SetUserPassword(ctx context.Context, id []byte, passwordHash, salt []byte) error
|
||||
SetUserSecurityPIN(ctx context.Context, id []byte, pinHash []byte, setAt time.Time) error
|
||||
MarkEmailVerified(ctx context.Context, id []byte) error
|
||||
|
||||
// Password resets
|
||||
CreatePasswordResetToken(ctx context.Context, token *PasswordResetToken) error
|
||||
InvalidateActivePasswordResetTokensByUserID(ctx context.Context, userID []byte, usedAt time.Time) error
|
||||
ConsumePasswordResetTokenAndSetPassword(ctx context.Context, tokenHash, passwordHash, salt []byte, usedAt time.Time) ([]byte, bool, error)
|
||||
|
||||
// Security PIN resets
|
||||
CreateSecurityPINResetToken(ctx context.Context, token *SecurityPINResetToken) error
|
||||
InvalidateActiveSecurityPINResetTokensByUserID(ctx context.Context, userID []byte, usedAt time.Time) error
|
||||
GetActiveSecurityPINResetTokenUser(ctx context.Context, tokenHash []byte, now time.Time) (*User, error)
|
||||
ConsumeSecurityPINResetTokenAndSetPIN(ctx context.Context, tokenHash, pinHash []byte, usedAt time.Time) ([]byte, bool, error)
|
||||
|
||||
// Identities (SSO)
|
||||
UpsertIdentity(ctx context.Context, identity *UserIdentity) error
|
||||
CreateIdentity(ctx context.Context, identity *UserIdentity) error
|
||||
GetIdentityByProviderSubject(ctx context.Context, provider, subject string) (*UserIdentity, error)
|
||||
GetIdentityByUserID(ctx context.Context, userID []byte) (*UserIdentity, error)
|
||||
DeleteIdentityByUserIDProvider(ctx context.Context, userID []byte, provider string) (bool, error)
|
||||
|
||||
// TOTP
|
||||
GetUserTOTP(ctx context.Context, userID []byte) (*UserTOTP, error)
|
||||
UpsertUserTOTP(ctx context.Context, totp *UserTOTP) error
|
||||
DisableUserTOTP(ctx context.Context, userID []byte) error
|
||||
DeleteUserTOTPSetup(ctx context.Context, userID []byte) error
|
||||
UpdateUserTOTPLastUsed(ctx context.Context, userID []byte) error
|
||||
|
||||
// Email login OTP
|
||||
GetActiveUserEmailLoginOTP(ctx context.Context, userID []byte, now time.Time) (*UserEmailLoginOTP, error)
|
||||
CountUserEmailLoginOTPSince(ctx context.Context, userID []byte, since time.Time) (int64, error)
|
||||
CreateUserEmailLoginOTP(ctx context.Context, otp *UserEmailLoginOTP) error
|
||||
InvalidateActiveUserEmailLoginOTPs(ctx context.Context, userID []byte, usedAt time.Time) error
|
||||
ConsumeUserEmailLoginOTP(ctx context.Context, userID, otpHash []byte, now time.Time) (bool, int, error)
|
||||
|
||||
// WebAuthn
|
||||
ListUserWebAuthnCredentials(ctx context.Context, userID []byte) ([]UserWebAuthnCredential, error)
|
||||
CreateUserWebAuthnCredential(ctx context.Context, credential *UserWebAuthnCredential) error
|
||||
UpdateUserWebAuthnCredential(ctx context.Context, userID, credentialID, credentialJSON []byte, usedAt time.Time) error
|
||||
DeleteAllUserWebAuthnCredentials(ctx context.Context, userID []byte) error
|
||||
}
|
||||
29
internal/domain/auth/role.go
Normal file
29
internal/domain/auth/role.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
Code string `gorm:"type:varchar(64);uniqueIndex;column:code"`
|
||||
Name string `gorm:"type:varchar(64);uniqueIndex;not null;column:name"`
|
||||
Description string `gorm:"type:varchar(255);column:description"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (Role) TableName() string { return "roles" }
|
||||
|
||||
func (r *Role) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(r.ID) == 0 {
|
||||
r.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
internal/domain/auth/role_permission.go
Normal file
28
internal/domain/auth/role_permission.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type RolePermission struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
RoleID []byte `gorm:"type:binary(16);index;uniqueIndex:ux_role_permission;not null;column:role_id"`
|
||||
PermissionID []byte `gorm:"type:binary(16);index;uniqueIndex:ux_role_permission;not null;column:permission_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (RolePermission) TableName() string { return "role_permissions" }
|
||||
|
||||
func (rp *RolePermission) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(rp.ID) == 0 {
|
||||
rp.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
20
internal/domain/auth/role_repository.go
Normal file
20
internal/domain/auth/role_repository.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package auth
|
||||
|
||||
import "context"
|
||||
|
||||
type RoleRepository interface {
|
||||
CreateRole(ctx context.Context, role *Role) error
|
||||
UpdateRole(ctx context.Context, role *Role) error
|
||||
DeleteRole(ctx context.Context, id []byte) error
|
||||
GetRoleByID(ctx context.Context, id []byte) (*Role, error)
|
||||
GetRoleByName(ctx context.Context, name string) (*Role, error)
|
||||
HasRolePermission(ctx context.Context, roleID []byte, permissionKey string) (bool, error)
|
||||
GetPermissionByID(ctx context.Context, id []byte) (*Permission, error)
|
||||
GetPermissionByKey(ctx context.Context, key string) (*Permission, error)
|
||||
UpdatePermission(ctx context.Context, permission *Permission) error
|
||||
ListPermissionsByRoleID(ctx context.Context, roleID []byte) ([]Permission, error)
|
||||
ListPermissions(ctx context.Context, filter string, sort string, limit, offset int) ([]Permission, int64, error)
|
||||
AssignPermission(ctx context.Context, roleID, permissionID []byte) (*RolePermission, error)
|
||||
RemovePermission(ctx context.Context, roleID, permissionID []byte) error
|
||||
ListRoles(ctx context.Context, filterName string, sort string, limit, offset int) ([]Role, int64, error)
|
||||
}
|
||||
32
internal/domain/auth/security_pin_reset_token.go
Normal file
32
internal/domain/auth/security_pin_reset_token.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type SecurityPINResetToken struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);index;not null;column:user_id"`
|
||||
TokenHash []byte `gorm:"type:varbinary(32);uniqueIndex;not null;column:token_hash"`
|
||||
ExpiresAt time.Time `gorm:"index;not null;column:expires_at"`
|
||||
UsedAt *time.Time `gorm:"index;column:used_at"`
|
||||
RequestedIP string `gorm:"type:varchar(128);column:requested_ip"`
|
||||
UserAgent string `gorm:"type:varchar(255);column:user_agent"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (SecurityPINResetToken) TableName() string { return "security_pin_reset_tokens" }
|
||||
|
||||
func (prt *SecurityPINResetToken) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(prt.ID) == 0 {
|
||||
prt.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
30
internal/domain/auth/totp.go
Normal file
30
internal/domain/auth/totp.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type UserTOTP struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);uniqueIndex;not null;column:user_id"`
|
||||
SecretEncrypted []byte `gorm:"type:varbinary(512);not null;column:secret_encrypted"`
|
||||
Enabled bool `gorm:"type:tinyint(1);not null;default:0;column:enabled"`
|
||||
LastUsedAt *time.Time `gorm:"column:last_used_at"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (UserTOTP) TableName() string { return "user_totp" }
|
||||
|
||||
func (ut *UserTOTP) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(ut.ID) == 0 {
|
||||
ut.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
58
internal/domain/auth/user.go
Normal file
58
internal/domain/auth/user.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
// Note: UUIDv7 should be generated at application layer and stored as BINARY(16).
|
||||
type User struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
RoleID []byte `gorm:"type:binary(16);index;column:role_id"`
|
||||
RoleIDs [][]byte `gorm:"-"`
|
||||
Roles []Role `gorm:"-"`
|
||||
ImageAttachmentID []byte `gorm:"type:binary(16);index;column:image_attachment_id"`
|
||||
ProfileAttachmentID []byte `gorm:"type:binary(16);index;column:profile_attachment_id"`
|
||||
|
||||
Email string `gorm:"type:varchar(191);uniqueIndex;not null;column:email"`
|
||||
SSOEmail *string `gorm:"type:varchar(191);uniqueIndex;column:sso_email"`
|
||||
Username *string `gorm:"type:varchar(100);index;column:username"`
|
||||
FirstName string `gorm:"type:varchar(100);not null;column:first_name"`
|
||||
LastName string `gorm:"type:varchar(100);not null;column:last_name"`
|
||||
MobilePhone string `gorm:"type:varchar(32);index;column:mobile_phone"`
|
||||
Timezone string `gorm:"type:varchar(64);not null;default:UTC;column:timezone"`
|
||||
SortKey *int `gorm:"index;column:sortkey"`
|
||||
PasswordHash []byte `gorm:"type:varbinary(255);column:password_hash"`
|
||||
SaltValue []byte `gorm:"type:varbinary(255);column:salt_value"`
|
||||
// Stores PIN hash records ("<salt_b64>$<hash_b64>"); legacy rows may still contain encrypted values.
|
||||
SecurityPINHash []byte `gorm:"type:varbinary(512);column:security_pin_hash"`
|
||||
SecurityPINSetAt *time.Time `gorm:"column:security_pin_set_at"`
|
||||
EmailVerifiedAt *time.Time `gorm:"column:email_verified_at"`
|
||||
IsActive bool `gorm:"index;not null;default:true;column:is_active"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
|
||||
PilotProfile *PilotProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
DoctorProfile *DoctorProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
AirRescuerProfile *AirRescuerProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
TechnicianProfile *TechnicianProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
FlightAssistantProfile *FlightAssistantProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
StaffProfile *StaffProfile `gorm:"foreignKey:UserID;references:ID"`
|
||||
ImageAttachment *filemanager.Attachment `gorm:"foreignKey:ImageAttachmentID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
|
||||
ProfileAttachment *filemanager.Attachment `gorm:"foreignKey:ProfileAttachmentID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "users" }
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(u.ID) == 0 {
|
||||
u.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
internal/domain/auth/user_role.go
Normal file
28
internal/domain/auth/user_role.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type UserRole struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);index;uniqueIndex:ux_user_role;not null;column:user_id"`
|
||||
RoleID []byte `gorm:"type:binary(16);index;uniqueIndex:ux_user_role;not null;column:role_id"`
|
||||
CreatedAt time.Time
|
||||
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
|
||||
}
|
||||
|
||||
func (UserRole) TableName() string { return "user_roles" }
|
||||
|
||||
func (ur *UserRole) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(ur.ID) == 0 {
|
||||
ur.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
30
internal/domain/auth/webauthn_credential.go
Normal file
30
internal/domain/auth/webauthn_credential.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
// UserWebAuthnCredential stores a passkey credential bound to a user.
|
||||
type UserWebAuthnCredential struct {
|
||||
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
||||
UserID []byte `gorm:"type:binary(16);index;not null;column:user_id"`
|
||||
Name string `gorm:"type:varchar(100);column:name"`
|
||||
CredentialID []byte `gorm:"type:varbinary(512);uniqueIndex;not null;column:credential_id"`
|
||||
CredentialJSON []byte `gorm:"type:mediumblob;not null;column:credential_json"`
|
||||
LastUsedAt *time.Time `gorm:"column:last_used_at"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (UserWebAuthnCredential) TableName() string { return "user_webauthn_credentials" }
|
||||
|
||||
func (c *UserWebAuthnCredential) BeforeCreate(tx *gorm.DB) error {
|
||||
if len(c.ID) == 0 {
|
||||
c.ID = uuidv7.MustBytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user