Initial open source release

This commit is contained in:
Ronni Skansing
2025-08-21 16:14:09 +02:00
commit 11cf01f08e
488 changed files with 97180 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
ALLOW_DENY_TABLE = "allow_denies"
)
// AllowDeny is a gorm data model for allow deny listing
type AllowDeny struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
CompanyID *uuid.UUID `gorm:"uniqueIndex:idx_allow_denies_unique_name_and_company_id;type:uuid"`
Name string `gorm:"not null;uniqueIndex:idx_allow_denies_unique_name_and_company_id;"`
Cidrs string `gorm:"not null;"`
Allowed bool `gorm:"not null;"`
}
func (AllowDeny) TableName() string {
return ALLOW_DENY_TABLE
}
func (e *AllowDeny) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "allow_denies")
}

View File

@@ -0,0 +1,48 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
API_SENDER_TABLE = "api_senders"
)
type APISender struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
Name string `gorm:"not null;uniqueIndex:idx_api_senders_name_company_id;"`
CompanyID *uuid.UUID `gorm:"uniqueIndex:idx_api_senders_name_company_id;type:uuid"`
// Extra fields
APIKey string
CustomField1 string
CustomField2 string
CustomField3 string
CustomField4 string
// Request fields
RequestMethod string
RequestURL string
RequestHeaders string
RequestBody string
// Response fields
ExpectedResponseStatusCode int
ExpectedResponseHeaders string
ExpectedResponseBody string
}
func (e *APISender) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + null company id is unique
return UniqueIndexNameAndNullCompanyID(db, "api_senders")
}
func (APISender) TableName() string {
return API_SENDER_TABLE
}

View File

@@ -0,0 +1,26 @@
package database
import (
"time"
"github.com/google/uuid"
)
type APISenderHeader struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
Key string `gorm:"not null;"`
Value string `gorm:"not null;"`
// IsRequestHeader is true if the header is a request header
// and false if it is a expected response header
IsRequestHeader bool `gorm:"not null;"`
// belongs to
APISenderID *uuid.UUID `gorm:"index;not null;type:uuid"`
}
func (APISenderHeader) TableName() string {
return "api_sender_headers"
}

33
backend/database/asset.go Normal file
View File

@@ -0,0 +1,33 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
ASSET_TABLE = "assets"
)
// Asset is gorm data model
type Asset struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
// has one
DomainID *uuid.UUID `gorm:"index;type:uuid;"`
DomainName string
// can has one
CompanyID *uuid.UUID `gorm:"index;type:uuid;"`
Name string `gorm:";index"`
Description string `gorm:";"`
Path string `gorm:"not null;index"`
}
func (Asset) TableName() string {
return ASSET_TABLE
}

View File

@@ -0,0 +1,33 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
ATTACHMENT_TABLE = "attachments"
)
// Attachment is gorm data model
type Attachment struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
// can has one
CompanyID *uuid.UUID `gorm:"index;type:uuid;"`
// many to many
Mails []Email `gorm:"many2many:message_attachments;"`
Name string `gorm:";index"`
Description string `gorm:";"`
Filename string `gorm:"not null;index"`
EmbeddedContent bool `gorm:"not null;default:false;index"`
}
func (Attachment) TableName() string {
return ATTACHMENT_TABLE
}

View File

@@ -0,0 +1,76 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
CAMPAIGN_TABLE = "campaigns"
)
// Campaign is gorm data model
type Campaign struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
CloseAt *time.Time `gorm:"index;"`
ClosedAt *time.Time `gorm:"index;"`
AnonymizeAt *time.Time `gorm:"index;"`
AnonymizedAt *time.Time `gorm:"index;"`
SortField string `gorm:";"`
SortOrder string `gorm:";"` // 'asc,desc,random'
SendStartAt *time.Time `gorm:"index;"`
SendEndAt *time.Time `gorm:"index;"`
// ConstraintWeekDays is a binary format.
// 0b00000001 = 1 = sunday
// 0b00000010 = 2 = monday
// 0b00000100 = 4 = tuesday
// 0b00001000 = 8 = ...
// 0b00010000 = 16 =
// 0b00100000 = 32 =
// 0b01000000 = 64 =
ConstraintWeekDays *int `gorm:";"`
// hh:mm
ConstraintStartTime *string `gorm:"index;"`
// hh:mm
ConstraintEndTime *string `gorm:"index;"`
SaveSubmittedData bool `gorm:"not null;default:false"`
IsAnonymous bool `gorm:"not null;default:false"`
IsTest bool `gorm:"not null;default:false"`
// has one
CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"`
CampaignTemplate *CampaignTemplate
// can has one
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;uniqueIndex:idx_campaigns_unique_name_and_company_id;"`
Company *Company
DenyPageID *uuid.UUID `gorm:"type:uuid;index;"`
DenyPage *Page `gorm:"foreignKey:DenyPageID;references:ID"`
// NotableEventID notable event for this campaign
NotableEvent *Event `gorm:"foreignKey:NotableEventID;references:ID"`
NotableEventID *uuid.UUID `gorm:"type:uuid;index"`
WebhookID *uuid.UUID `gorm:"type:uuid;index;"`
// has many-to-many
RecipientGroups []*RecipientGroup `gorm:"many2many:campaign_recipient_groups"`
AllowDeny []*AllowDeny `gorm:"many2many:campaign_allow_denies"`
Name string `gorm:"not null;uniqueIndex:idx_campaigns_unique_name_and_company_id"`
}
func (c *Campaign) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "campaigns")
}
func (Campaign) TableName() string {
return CAMPAIGN_TABLE
}

View File

@@ -0,0 +1,22 @@
package database
import (
"github.com/google/uuid"
)
const (
CAMPAIGN_ALLOW_DENY_TABLE = "campaign_allow_denies"
)
// CampaignAllowDeny is a gorm data model
// is a table of those allow deny lists that belong to a campaign
type CampaignAllowDeny struct {
CampaignID *uuid.UUID `gorm:"not null;index;type:uuid;uniqueIndex:idx_campaign_allow_denies;"`
Campaign *Campaign
AllowDenyID *uuid.UUID `gorm:"not null;index;type:uuid;uniqueIndex:idx_campaign_allow_denies;"`
AllowDeny *AllowDeny
}
func (CampaignAllowDeny) TableName() string {
return CAMPAIGN_ALLOW_DENY_TABLE
}

View File

@@ -0,0 +1,52 @@
package database
import (
"reflect"
"time"
"github.com/google/uuid"
)
const (
CAMPAIGN_EVENT_TABLE = "campaign_events"
)
// Campaign is gorm data model
type CampaignEvent struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;"`
// arbitrary data
Data string `gorm:"not null;"`
// has one
CampaignID *uuid.UUID `gorm:"not null;index;type:uuid;"`
EventID *uuid.UUID `gorm:"not null;index;type:uuid;"`
// can has one
UserAgent string `gorm:";"`
IPAddress string `gorm:";"`
// AnonymizedID is set when the recipient has been anonymized
AnonymizedID *uuid.UUID `gorm:"type:uuid;index;"`
// if null either the event has no recipient or the recipient has been anonymized
RecipientID *uuid.UUID `gorm:"index;type:uuid;"`
Recipient *Recipient
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;"`
}
// RecipientCampaignEvent is a aggregated read-only model
type RecipientCampaignEvent struct {
CampaignEvent
Name string // event name
CampaignName string
}
func (CampaignEvent) TableName() string {
return CAMPAIGN_EVENT_TABLE
}
var _ = reflect.TypeOf(RecipientCampaignEvent{})

View File

@@ -0,0 +1,52 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
CAMPAIGN_RECIPIENT_TABLE_NAME = "campaign_recipients"
)
// CampaigReciever is gorm data model
// this model/table is primarily used to keep track of who and when should recieve a campaign
type CampaignRecipient struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
Campaign *Campaign
CampaignID *uuid.UUID `gorm:"not null;type:uuid;uniqueIndex:idx_campaign_recipients_campaign_id_recipient_id;"`
// CancelledAt *time.Time `gorm:"index;"`
CancelledAt *time.Time `gorm:"index;"`
// when it should be send
SendAt *time.Time `gorm:"index;"`
// when it was last attempted send
LastAttemptAt *time.Time `gorm:"index;"`
// when it was sent
SentAt *time.Time `gorm:"index;"`
// self-managed
SelfManaged bool `gorm:"not null;default:false;"`
// AnonymizedID is set when the recipient has been anonymized
AnonymizedID *uuid.UUID `gorm:"type:uuid;"`
Recipient *Recipient
// A null recipientID means that the data has been anonymized
RecipientID *uuid.UUID `gorm:"type:uuid;index;uniqueIndex:idx_campaign_recipients_campaign_id_recipient_id;"`
// NotableEventID is the most notable event for this recipient
NotableEvent *Event `gorm:"foreignKey:NotableEventID;references:ID"`
NotableEventID *uuid.UUID `gorm:"type:uuid;index"`
}
func (CampaignRecipient) TableName() string {
return CAMPAIGN_RECIPIENT_TABLE_NAME
}

View File

@@ -0,0 +1,19 @@
package database
import (
"github.com/google/uuid"
)
// CampaignRecipientGroup is gorm data model
// is a table of those recipient groups that belong to a campaign
type CampaignRecipientGroup struct {
CampaignID *uuid.UUID `gorm:"not null;index;type:uuid;uniqueIndex:idx_campaign_recipient_group;"`
Campaign *Campaign
RecipientGroupID *uuid.UUID `gorm:"not null;index;type:uuid;uniqueIndex:idx_campaign_recipient_group;"`
RecipientGroup *RecipientGroup
}
func (CampaignRecipientGroup) TableName() string {
return "campaign_recipient_groups"
}

View File

@@ -0,0 +1,51 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
CAMPAIGN_STATS_TABLE = "campaign_stats"
)
// CampaignStats is gorm data model for aggregated campaign statistics
type CampaignStats struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid" json:"id"`
CreatedAt *time.Time `gorm:"not null;index;" json:"createdAt"`
UpdatedAt *time.Time `gorm:"not null;" json:"updatedAt"`
// Campaign reference
CampaignID *uuid.UUID `gorm:"not null;unique;index;type:uuid;" json:"campaignId"`
CampaignName string `gorm:"not null;" json:"campaignName"`
CompanyID *uuid.UUID `gorm:"index;type:uuid;" json:"companyId"` // nullable for global campaigns
// Time metrics
CampaignStartDate *time.Time `gorm:"index;" json:"campaignStartDate"`
CampaignEndDate *time.Time `gorm:"index;" json:"campaignEndDate"`
CampaignClosedAt *time.Time `gorm:"index;" json:"campaignClosedAt"`
// Volume metrics
TotalRecipients int `gorm:"not null;default:0" json:"totalRecipients"`
TotalEvents int `gorm:"not null;default:0" json:"totalEvents"`
// Event type breakdowns
EmailsSent int `gorm:"not null;default:0" json:"emailsSent"`
TrackingPixelLoaded int `gorm:"not null;default:0" json:"trackingPixelLoaded"` // Email opens
WebsiteVisits int `gorm:"not null;default:0" json:"websiteVisits"` // Link clicks
DataSubmissions int `gorm:"not null;default:0" json:"dataSubmissions"` // Form submissions
// Success rates (as percentages for quick display)
OpenRate float64 `gorm:"not null;default:0" json:"openRate"`
ClickRate float64 `gorm:"not null;default:0" json:"clickRate"`
SubmissionRate float64 `gorm:"not null;default:0" json:"submissionRate"`
// Campaign metadata
TemplateName string `gorm:"" json:"templateName"`
CampaignType string `gorm:"" json:"campaignType"` // 'scheduled', 'self-managed'
}
func (CampaignStats) TableName() string {
return CAMPAIGN_STATS_TABLE
}

View File

@@ -0,0 +1,72 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
CAMPAIGN_TEMPLATE_TABLE = "campaign_templates"
)
// CampaignTemplate is gorm data model
type CampaignTemplate struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
Name string `gorm:"not null;index;uniqueIndex:idx_campaign_templates_unique_name_and_company_id;"`
URLPath string `gorm:"not null;default:'';index"`
// IsUsable indicates if a template is usable based on if it has all the required
// data such as domainID, landingPage and etc to be used in a campaign
IsUsable bool `gorm:"not null;default:false;index"`
// has-a
LandingPageID *uuid.UUID `gorm:"type:uuid;index;"`
LandingPage *Page `gorm:"references:LandingPage;foreignKey:LandingPageID;references:ID;"`
DomainID *uuid.UUID `gorm:"type:uuid;index;"`
Domain *Domain `gorm:"foreignKey:DomainID"`
URLIdentifierID *uuid.UUID `gorm:"not null;type:uuid;index"`
URLIdentifier *Identifier `gorm:"references:foreignKey:URLIdentifierID;references:ID"`
StateIdentifierID *uuid.UUID `gorm:"type:uuid;index"`
StateIdentifier *Identifier `gorm:"references:foreignKey:StateIdentifierID;references:ID"`
// has-a optional
BeforeLandingPageID *uuid.UUID `gorm:"type:uuid;index"`
BeforeLandingPage *Page `gorm:"foreignkey:BeforeLandingPageID;references:ID"`
AfterLandingPageID *uuid.UUID `gorm:"type:uuid;index"`
AfterLandingPage *Page `gorm:"foreignKey:AfterLandingPageID;references:ID"`
AfterLandingPageRedirectURL string `gorm:"not null;"`
EmailID *uuid.UUID `gorm:"type:uuid;index;"`
Email *Email `gorm:"foreignKey:EmailID;references:ID;"`
SMTPConfigurationID *uuid.UUID `gorm:"type:uuid;index;"`
SMTPConfiguration *SMTPConfiguration `gorm:"foreignKey:SMTPConfigurationID"`
APISenderID *uuid.UUID `gorm:"type:uuid;index;"`
APISender *APISender `gorm:"foreignKey:APISenderID"`
// can belong-to
CompanyID *uuid.UUID `gorm:"type:uuid;index;uniqueIndex:idx_campaign_templates_unique_name_and_company_id"`
Company *Company `gorm:"foreignKey:CompanyID"`
}
func (e *CampaignTemplate) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "campaign_templates")
}
func (CampaignTemplate) TableName() string {
return CAMPAIGN_TEMPLATE_TABLE
}

View File

@@ -0,0 +1,26 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
COMPANY_TABLE = "companies"
)
type Company struct {
ID uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
Name string `gorm:"not null;unique;index"`
// backref: many-to-one
Users []*User //`gorm:"foreignKey:CompanyID;"`
RecipientGroups []*RecipientGroup //`gorm:"foreignKey:CompanyID;"`
}
func (Company) TableName() string {
return COMPANY_TABLE
}

View File

@@ -0,0 +1,32 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
DOMAIN_TABLE = "domains"
)
// Domain is gorm data model
type Domain struct {
ID uuid.UUID `gorm:"primary_key;not null;unique;type:uuid;"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
CompanyID *uuid.UUID `gorm:"index;type:uuid;"`
Name string `gorm:"not null;unique;"`
ManagedTLSCerts bool `gorm:"not null;index;default:false"`
OwnManagedTLS bool `gorm:"not null;index;default:false"`
HostWebsite bool `gorm:"not null;"`
PageContent string
PageNotFoundContent string
RedirectURL string
// could has-one
Company *Company
}
func (Domain) TableName() string {
return DOMAIN_TABLE
}

47
backend/database/email.go Normal file
View File

@@ -0,0 +1,47 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
EMAIL_TABLE = "emails"
)
// Email is a gorm data model
type Email struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
Name string `gorm:"not null;index;uniqueIndex:idx_emails_name_company_id;"`
Content string `gorm:"not null;"`
AddTrackingPixel bool `gorm:"not null;"`
// mail fields
// Envelope header - Bounce / Return-Path
MailFrom string `gorm:"not null;"`
// Mail header
Subject string `gorm:"not null;"`
From string `gorm:"not null;"`
// many to many
Attachments []*Attachment `gorm:"many2many:email_attachments;"`
// can belong to
CompanyID *uuid.UUID `gorm:"index;type:uuid;uniqueIndex:idx_emails_name_company_id;"`
Company *Company
}
func (e *Email) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + null company id is unique
return UniqueIndexNameAndNullCompanyID(db, "emails")
}
func (Email) TableName() string {
return EMAIL_TABLE
}

View File

@@ -0,0 +1,16 @@
package database
import (
"github.com/google/uuid"
)
// EmailAttachment is a gorm data model
// it is a many to many relationship between messages and attachments
type EmailAttachment struct {
EmailID *uuid.UUID `gorm:"primary_key;not null;index;type:uuid;unique_index:idx_message_attachment;"`
AttachmentID *uuid.UUID `gorm:"primary_key;not null;index;type:uuid;unique_index:idx_message_attachment;"`
}
func (EmailAttachment) TableName() string {
return "email_attachments"
}

View File

@@ -0,0 +1,21 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
EVENT_TABLE = "events"
)
type Event struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
Name string `gorm:"not null;index;"`
}
func (Event) TableName() string {
return EVENT_TABLE
}

View File

@@ -0,0 +1,43 @@
package database
import (
"fmt"
"github.com/phishingclub/phishingclub/config"
"github.com/phishingclub/phishingclub/errs"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// FromConfig database factory from config
func FromConfig(conf config.Config) (*gorm.DB, error) {
var db *gorm.DB
switch conf.Database().Engine {
case config.DefaultAdministrationUseSqlite:
var err error
dsn := fmt.Sprintf(
"%s?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_foreign_keys=ON",
conf.Database().DSN,
)
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, errs.Wrap(err)
}
// SetMaxOpenConns sets the maximum number of open connections to the database.
// without this, gorutines doing simultaneous db operations will cause
// "database is locked" error when using sqlite with a high concurrency
// this is because sqlite only allows one write operation at a time
// and locks the whole database for the duration any write operation
innerDB, err := db.DB()
if err != nil {
return nil, errs.Wrap(err)
}
innerDB.SetMaxIdleConns(1)
default:
return nil, config.ErrInvalidDatabase
}
return db, nil
}

View File

@@ -0,0 +1,18 @@
package database
import (
"github.com/google/uuid"
)
const (
IDENTIFIER_TABLE = "identifiers"
)
type Identifier struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
Name string `gorm:"not null;uniqueIndex"`
}
func (Identifier) TableName() string {
return IDENTIFIER_TABLE
}

View File

@@ -0,0 +1,16 @@
package database
import (
"github.com/google/uuid"
)
// Option is a database option (options stored in the database)
type Option struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
Key string `gorm:"not null;unique;index"`
Value string `gorm:"not null;"`
}
func (Option) TableName() string {
return "options"
}

35
backend/database/page.go Normal file
View File

@@ -0,0 +1,35 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
PAGE_TABLE = "pages"
)
// Page is a gorm data model
type Page struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
CompanyID *uuid.UUID `gorm:"index;uniqueIndex:idx_pages_unique_name_and_company_id;type:uuid"`
Name string `gorm:"not null;index;uniqueIndex:idx_pages_unique_name_and_company_id;"`
Content string `gorm:"not null;"`
// could has-one
Company *Company
}
func (e *Page) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "pages")
}
func (Page) TableName() string {
return PAGE_TABLE
}

View File

@@ -0,0 +1,42 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
RECIPIENT_TABLE = "recipients"
)
// Recipient is a gorm data model
type Recipient struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
DeletedAt *time.Time `gorm:"index;"`
Email *string `gorm:";uniqueIndex"`
Phone *string `gorm:";index"`
ExtraIdentifier *string `gorm:";index"`
FirstName string `gorm:";"`
LastName string `gorm:";"`
Position string `gorm:";"`
Department string `gorm:";"`
City string `gorm:";"`
Country string `gorm:";"`
Misc string `gorm:";"`
// can belong to
CompanyID *uuid.UUID `gorm:"type:uuid;index;"`
Company *Company
// many-to-many
Groups []RecipientGroup `gorm:"many2many:recipient_group_recipients;"`
}
func (Recipient) TableName() string {
return RECIPIENT_TABLE
}

View File

@@ -0,0 +1,9 @@
package database
// RecipientCampaignEventView is a view read-only model
type RecipientCampaignEventView struct {
CampaignEvent
Name string // event name
CampaignName string
}

View File

@@ -0,0 +1,38 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
RECIPIENT_GROUP_TABLE = "recipient_groups"
)
// RecipientGroup is a grouping of recipient
type RecipientGroup struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
Name string `gorm:"not null;index;uniqueIndex:idx_recipient_groups_unique_name_and_company_id;"`
// can belong-to
CompanyID *uuid.UUID `gorm:"type:uuid;index;uniqueIndex:idx_recipient_groups_unique_name_and_company_id"`
Company *Company
// many-to-many
Recipients []Recipient `gorm:"many2many:recipient_group_recipients;"`
}
func (e *RecipientGroup) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "recipient_groups")
}
func (RecipientGroup) TableName() string {
return RECIPIENT_GROUP_TABLE
}

View File

@@ -0,0 +1,22 @@
package database
import (
"github.com/google/uuid"
)
const (
RECIPIENT_GROUP_RECIPIENT_TABLE = "recipient_group_recipients"
)
// RecipientGroupRecipient is a grouping of recipients and recipient groups
type RecipientGroupRecipient struct {
Recipient *Recipient
RecipientID *uuid.UUID `gorm:"not null;uniqueIndex:idx_recipient_group"`
RecipientGroup *RecipientGroup
RecipientGroupID *uuid.UUID `gorm:"not null;uniqueIndex:idx_recipient_group"`
}
func (RecipientGroupRecipient) TableName() string {
return RECIPIENT_GROUP_RECIPIENT_TABLE
}

18
backend/database/role.go Normal file
View File

@@ -0,0 +1,18 @@
package database
import (
"github.com/google/uuid"
)
// Role is a role
type Role struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
Name string `gorm:"not null;index;unique;"`
// one-to-many
Users []*User
}
func (Role) TableName() string {
return "roles"
}

View File

@@ -0,0 +1,32 @@
package database
import (
"time"
"github.com/google/uuid"
)
const (
SESSION_TABLE = "sessions"
)
type Session struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index"`
UpdatedAt *time.Time `gorm:"not null;index"`
// IP address of the user when the session was created
IPAddress string `gorm:"not null;index;default:''"`
// the expiresAt is the time when the session will expire, nomatter the maxAgeAt
ExpiresAt *time.Time `gorm:"not null;index"`
// the maxAgeAt is the time when the session will expire, nomatter the expiresAt
MaxAgeAt *time.Time `gorm:"not null;index"`
// has-one
//
// belongs to
UserID string `gorm:";type:uuid;"`
User *User
}
func (Session) TableName() string {
return SESSION_TABLE
}

View File

@@ -0,0 +1,43 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
SMTP_CONFIGURATION_TABLE = "smtp_configurations"
)
// SMTPConfiguration is a page gorm data model
// Simple Mail Transfer Protocol
type SMTPConfiguration struct {
ID uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
Name string `gorm:"not null;uniqueIndex:idx_smtp_configurations_unique_name_and_company_id;"`
Host string `gorm:"not null;"`
Port uint16 `gorm:"not null;"`
Username string `gorm:"not null;"`
Password string `gorm:"not null;"`
IgnoreCertErrors bool `gorm:"not null;"`
// back-reference
Headers []*SMTPHeader
// can belong-to
CompanyID *uuid.UUID `gorm:"uniqueIndex:idx_smtp_configurations_unique_name_and_company_id;"`
Company *Company `gorm:"foreignkey:CompanyID;"`
}
func (e *SMTPConfiguration) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "smtp_configurations")
}
func (SMTPConfiguration) TableName() string {
return SMTP_CONFIGURATION_TABLE
}

View File

@@ -0,0 +1,24 @@
package database
import (
"time"
"github.com/google/uuid"
)
// SMTPHeader is headers sent with specific SMTP configurations
type SMTPHeader struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
Key string `gorm:"not null;"`
Value string `gorm:"not null;"`
// belongs to
SMTPConfigurationID *uuid.UUID `gorm:"index;not null;type:uuid"`
SMTP *SMTPConfiguration `gorm:"foreignKey:SMTPConfigurationID"`
}
func (SMTPHeader) TableName() string {
return "smtp_headers"
}

49
backend/database/user.go Normal file
View File

@@ -0,0 +1,49 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
USER_TABLE = "users"
)
// User is a database user
type User struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index"`
DeletedAt gorm.DeletedAt `gorm:"index;"`
Name string `gorm:"not null;"`
Username string `gorm:"not null;unique;"`
Email string `gorm:"unique;"`
PasswordHash string `gorm:"type:varchar(255);"`
RequirePasswordRenew bool `gorm:"default:false;"`
// MFA
TOTPEnabled bool `gorm:"default:false;"`
TOTPSecret string
TOTPAuthURL string
// TODO rename to MFARecoveryCode
TOTPRecoveryCode string `gorm:"type:varchar(64);"`
// SSO id
SSOID string
// maybe has one
CompanyID *uuid.UUID `gorm:"type:uuid;index;"`
Company *Company
// has one
RoleID *uuid.UUID `gorm:"not null;type:uuid;index"`
Role *Role
// APIKey
APIKey string `gorm:"index"`
}
func (User) TableName() string {
return USER_TABLE
}

23
backend/database/utils.go Normal file
View File

@@ -0,0 +1,23 @@
package database
import (
"fmt"
"gorm.io/gorm"
)
type Migrater interface {
Migrate(db *gorm.DB) error
}
func UniqueIndexNameAndNullCompanyID(db *gorm.DB, tableName string) error {
// SQLITE / POSTGRES
// ensure name + null company id is unique
idx := fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS idx_%s_name_null_company_id ON %s (name) WHERE (company_id IS NULL)", tableName, tableName)
res := db.Exec(idx)
if res.Error != nil {
return fmt.Errorf("error creating index: %v on table %s", res.Error, tableName)
}
return nil
}

View File

@@ -0,0 +1,33 @@
package database
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
WEBHOOK_TABLE = "webhooks"
)
// Webhook is a gorm data model for webhooks
type Webhook struct {
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
CompanyID *uuid.UUID `gorm:"uniqueIndex:idx_webhooks_unique_name_and_company_id;type:uuid"`
Name string `gorm:"not null;uniqueIndex:idx_webhooks_unique_name_and_company_id;"`
URL string `gorm:"not null;"`
Secret string
}
func (e *Webhook) Migrate(db *gorm.DB) error {
// SQLITE
// ensure name + company id is unique
return UniqueIndexNameAndNullCompanyID(db, "webhooks")
}
func (Webhook) TableName() string {
return WEBHOOK_TABLE
}