Initial open source release

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

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# Description
The `model` folder represents the core data structures. It includes:
- Factories for creating and initializing entities
- Type definitions for all domain objects
- Validation logic to ensure data integrity
- Custom marshalling/unmarshalling for JSON handling
- Request/Response structures for API endpoints
- Helper methods for data transformation
- Business rules and constraints
- Data transfer objects (DTOs) for external communication
The members mainly use `nullable.Nullable` to convey if a field is set so we know
if it should be updated etc.
The structures in this folder serve as the contract between the API layer and the database layer, handling all necessary data transformations and validations before persistence or transmission.
Files affixed with `View` are read-only models meant only for outgoing API data,
this could be joined data from a database.
+119
View File
@@ -0,0 +1,119 @@
package model
import (
"fmt"
"net"
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// AllowDeny is a model for allow deny listing
type AllowDeny struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String127] `json:"name"`
Cidrs nullable.Nullable[vo.IPNetSlice] `json:"cidrs"`
Allowed nullable.Nullable[bool] `json:"allowed"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
}
// Validate checks if the allow deny list has a valid state
func (r *AllowDeny) Validate() error {
if err := validate.NullableFieldRequired("name", r.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("cidrs", r.Cidrs); err != nil {
return err
}
if v := r.Cidrs.MustGet(); len(v) == 0 {
return errs.NewValidationError(
errors.New("cidrs must include atleast one CIDR"),
)
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (r *AllowDeny) ToDBMap() map[string]any {
m := map[string]any{}
if r.Name.IsSpecified() {
m["name"] = nil
if name, err := r.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if r.Cidrs.IsSpecified() {
m["cidrs"] = nil
if cidrs, err := r.Cidrs.Get(); err == nil {
cidrsStr := ""
cidrsLen := len(cidrs)
for i, cidr := range cidrs {
if i == cidrsLen {
cidrsStr += fmt.Sprintf("%s", cidr.String())
} else {
cidrsStr += fmt.Sprintf("%s\n", cidr.String())
}
}
m["cidrs"] = cidrsStr
}
}
if r.Allowed.IsSpecified() {
m["allowed"] = nil
if allowed, err := r.Allowed.Get(); err == nil {
m["allowed"] = allowed
}
}
if r.CompanyID.IsSpecified() {
if r.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = r.CompanyID.MustGet()
}
}
return m
}
func (r *AllowDeny) IsIPAllowed(ip string) (bool, error) {
isTypeAllowList := r.Allowed.MustGet()
cidrs, err := r.Cidrs.Get()
if err != nil {
return false, errs.Wrap(err)
}
netIP := net.ParseIP(ip)
if netIP == nil {
return false, fmt.Errorf("invalid ip address: %s", ip)
}
for _, cidr := range cidrs {
isInRange := cidr.Contains(netIP)
// if allow list and ip is within range
if isTypeAllowList && isInRange {
return true, nil
}
// if deny list and ip is within range
if !isTypeAllowList && isInRange {
return false, nil
}
}
// If this is an allow list and we didn't find the IP, it's not allowed
if isTypeAllowList {
return false, nil
}
// If this is a deny list and we didn't find the IP, it is allowed
return true, nil
}
+286
View File
@@ -0,0 +1,286 @@
package model
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// APISender is a API sender
type APISender struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Name nullable.Nullable[vo.String64] `json:"name"`
APIKey nullable.Nullable[vo.OptionalString255] `json:"apiKey"`
CustomField1 nullable.Nullable[vo.OptionalString255] `json:"customField1"`
CustomField2 nullable.Nullable[vo.OptionalString255] `json:"customField2"`
CustomField3 nullable.Nullable[vo.OptionalString255] `json:"customField3"`
CustomField4 nullable.Nullable[vo.OptionalString255] `json:"customField4"`
RequestMethod nullable.Nullable[vo.HTTPMethod] `json:"requestMethod"`
RequestURL nullable.Nullable[vo.String255] `json:"requestURL"`
RequestHeaders nullable.Nullable[APISenderHeaders] `json:"requestHeaders"`
RequestBody nullable.Nullable[vo.OptionalString1MB] `json:"requestBody"`
ExpectedResponseStatusCode nullable.Nullable[int] `json:"expectedResponseStatusCode"`
ExpectedResponseHeaders nullable.Nullable[APISenderHeaders] `json:"expectedResponseHeaders"`
ExpectedResponseBody nullable.Nullable[vo.OptionalString1MB] `json:"expectedResponseBody"`
}
// Validate checks if the API sender has a valid state
func (a *APISender) Validate() error {
if err := validate.NullableFieldRequired("name", a.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("requestMethod", a.RequestMethod); err != nil {
return err
}
if err := validate.NullableFieldRequired("requestURL", a.RequestURL); err != nil {
return err
}
// one of the following is required
if (!a.ExpectedResponseStatusCode.IsSpecified() || a.ExpectedResponseStatusCode.IsNull()) &&
!isSpecifiedStringWithContent(a.ExpectedResponseBody) &&
!isSpecifiedStringWithContent(a.ExpectedResponseHeaders) {
return validate.WrapErrorWithField(errors.New("expectedResponseStatusCode, expectedResponseBody or expectedResponseHeaders must be supplied"), "Missing field")
}
return nil
}
func isSpecifiedStringWithContent[T fmt.Stringer](s nullable.Nullable[T]) bool {
return s.IsSpecified() && !s.IsNull() && s.MustGet().String() != ""
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (a *APISender) ToDBMap() map[string]interface{} {
m := map[string]interface{}{}
if a.Name.IsSpecified() {
m["name"] = nil
if name, err := a.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if a.CompanyID.IsSpecified() {
if a.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = a.CompanyID.MustGet()
}
}
if a.APIKey.IsSpecified() {
m["api_key"] = nil
if apiKey, err := a.APIKey.Get(); err == nil {
m["api_key"] = apiKey.String()
}
}
if a.CustomField1.IsSpecified() {
m["custom_field1"] = nil
if customField1, err := a.CustomField1.Get(); err == nil {
m["custom_field1"] = customField1.String()
}
}
if a.CustomField2.IsSpecified() {
m["custom_field2"] = nil
if customField2, err := a.CustomField2.Get(); err == nil {
m["custom_field2"] = customField2.String()
}
}
if a.CustomField3.IsSpecified() {
m["custom_field3"] = nil
if customField3, err := a.CustomField3.Get(); err == nil {
m["custom_field3"] = customField3.String()
}
}
if a.CustomField4.IsSpecified() {
m["custom_field4"] = nil
if customField4, err := a.CustomField4.Get(); err == nil {
m["custom_field4"] = customField4.String()
}
}
if a.RequestMethod.IsSpecified() {
m["request_method"] = nil
if requestMethod, err := a.RequestMethod.Get(); err == nil {
m["request_method"] = requestMethod.String()
}
}
if a.RequestURL.IsSpecified() {
m["request_url"] = nil
if requestURL, err := a.RequestURL.Get(); err == nil {
m["request_url"] = requestURL.String()
}
}
if a.RequestHeaders.IsSpecified() {
m["request_headers"] = nil
if requestHeaders, err := a.RequestHeaders.Get(); err == nil {
m["request_headers"] = requestHeaders.String()
}
}
if a.RequestBody.IsSpecified() {
m["request_body"] = nil
if requestBody, err := a.RequestBody.Get(); err == nil {
m["request_body"] = requestBody.String()
}
}
if a.ExpectedResponseStatusCode.IsSpecified() {
m["expected_response_status_code"] = nil
if expectedResponseStatusCode, err := a.ExpectedResponseStatusCode.Get(); err == nil {
m["expected_response_status_code"] = expectedResponseStatusCode
}
}
if a.ExpectedResponseHeaders.IsSpecified() {
m["expected_response_headers"] = nil
if expectedResponseHeaders, err := a.ExpectedResponseHeaders.Get(); err == nil {
m["expected_response_headers"] = expectedResponseHeaders.String()
}
}
if a.ExpectedResponseBody.IsSpecified() {
m["expected_response_body"] = nil
if expectedResponseBody, err := a.ExpectedResponseBody.Get(); err == nil {
m["expected_response_body"] = expectedResponseBody.String()
}
}
return m
}
// TODO should the rest of the code be moved to value object
type HTTPHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
// APISenderHeaders is a header for a API sender
type APISenderHeaders struct {
Headers []*HTTPHeader
}
// NewAPISenderHeader creates a new APISenderHeader
// it takes a newline separated string with headers of the format .+: .+ (key: value)
func NewAPISenderHeader(headers string) (*APISenderHeaders, error) {
headers = strings.TrimSpace(headers)
if headers == "" {
return &APISenderHeaders{}, nil
}
// split the headers
lines := strings.Split(headers, "\n")
// if there is a single header
if len(lines) == 0 {
return &APISenderHeaders{
Headers: []*HTTPHeader{},
}, nil
}
headerLines := []*HTTPHeader{}
for _, line := range lines {
// split the key value
parts := strings.Split(line, ":")
// there should be atleast 2 parts, key and value
if len(parts) < 2 {
return nil, validate.WrapErrorWithField(
fmt.Errorf("invalid header: %s", line),
"header",
)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(strings.Join(parts[1:], ":"))
headerLines = append(headerLines, &HTTPHeader{
Key: key,
Value: value,
})
}
return &APISenderHeaders{
Headers: headerLines,
}, nil
}
// MarshalJSON implements the json.Marshaler interface
func (s APISenderHeaders) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// UnmarshalJSON unmarshals the json into a string
func (s *APISenderHeaders) UnmarshalJSON(data []byte) error {
var header string
if err := json.Unmarshal(data, &header); err != nil {
return err
}
ss, err := NewAPISenderHeader(header)
if err != nil {
return err
}
s.Headers = ss.Headers
return nil
}
// String returns the string representation of the APISenderHeader
func (a APISenderHeaders) String() string {
headers := ""
for _, header := range a.Headers {
headers += header.Key + ": " + header.Value + "\n"
}
return headers
}
func NewAPISenderExample() *APISender {
apiSenderRequestHeaders, err := NewAPISenderHeader("foo: bar")
if err != nil {
panic("APISender example data MUST be valid")
}
return &APISender{
Name: nullable.NewNullableWithValue(
*vo.NewString64Must("Example"),
),
APIKey: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must(
"rj90jf09jr09j2r",
),
),
CustomField1: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must(
"custom1",
),
),
CustomField2: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must(
"custom2",
),
),
CustomField3: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must(
"custom3",
),
),
CustomField4: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must(
"custom4",
),
),
RequestMethod: nullable.NewNullableWithValue(
*vo.NewHTTPMethodMust("POST"),
),
RequestURL: nullable.NewNullableWithValue(
*vo.NewString255Must("https://example.com"),
),
RequestHeaders: nullable.NewNullableWithValue(
*apiSenderRequestHeaders,
),
RequestBody: nullable.NewNullableWithValue(
*vo.NewOptionalString1MBMust("<p>Hello World</p>"),
),
ExpectedResponseStatusCode: nullable.NewNullableWithValue(200),
ExpectedResponseHeaders: nullable.NewNullableWithValue(
*apiSenderRequestHeaders,
),
ExpectedResponseBody: nullable.NewNullableWithValue(
*vo.NewOptionalString1MBMust("<p>World</p>"),
),
}
}
+79
View File
@@ -0,0 +1,79 @@
package model
import (
"mime/multipart"
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Asset is a file Asset entity
type Asset struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
DomainName nullable.Nullable[vo.String255] `json:"domainName"`
DomainID nullable.Nullable[uuid.UUID] `json:"domainID"`
Name nullable.Nullable[vo.OptionalString127] `json:"name"`
Description nullable.Nullable[vo.OptionalString255] `json:"description"`
Path nullable.Nullable[vo.RelativeFilePath] `json:"path"`
File multipart.FileHeader `json:"-"`
}
// Validate checks if the Asset has a valid state
func (a *Asset) Validate() error {
if err := validate.NullableFieldRequired("name", a.Name); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (a *Asset) ToDBMap() map[string]any {
m := map[string]any{}
if a.CompanyID.IsSpecified() {
if a.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = a.CompanyID.MustGet()
}
}
if a.DomainName.IsSpecified() {
m["domain_name"] = nil
if domainName, err := a.DomainName.Get(); err == nil {
m["domain_name"] = domainName.String()
}
}
// TODO is a global asset attached to a domain? if not then this should be possible to set to null like company ID
if a.DomainID.IsSpecified() {
m["domain_id"] = nil
if domainID, err := a.DomainID.Get(); err == nil {
m["domain_id"] = domainID.String()
}
}
if a.Name.IsSpecified() {
m["name"] = nil
if name, err := a.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if a.Description.IsSpecified() {
m["description"] = nil
if description, err := a.Description.Get(); err == nil {
m["description"] = description.String()
}
}
if a.Path.IsSpecified() {
m["path"] = nil
if path, err := a.Path.Get(); err == nil {
m["path"] = path.String()
}
}
return m
}
+76
View File
@@ -0,0 +1,76 @@
package model
import (
"mime/multipart"
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
type Attachment struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Name nullable.Nullable[vo.OptionalString127] `json:"name"`
Description nullable.Nullable[vo.OptionalString255] `json:"description"`
FileName nullable.Nullable[vo.FileName] `json:"fileName"`
EmbeddedContent nullable.Nullable[bool] `json:"embeddedContent"`
// path is the calculated path to the file
Path nullable.Nullable[vo.RelativeFilePath] `json:"path"`
// used in the API to upload files
File *multipart.FileHeader `json:"-"`
}
func (a *Attachment) Validate() error {
if err := validate.NullableFieldRequired("name", a.Name); err != nil {
return err
}
return nil
}
func (a *Attachment) ToDBMap() map[string]any {
m := map[string]any{}
if a.CompanyID.IsSpecified() {
if a.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = a.CompanyID.MustGet()
}
}
if a.Name.IsSpecified() {
m["name"] = nil
if name, err := a.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if a.Description.IsSpecified() {
m["description"] = nil
if description, err := a.Description.Get(); err == nil {
m["description"] = description.String()
}
}
if a.FileName.IsSpecified() {
m["filename"] = nil
if fileName, err := a.FileName.Get(); err == nil {
m["filename"] = fileName.String()
}
// if name is not set, use file name
if m["name"] == nil {
m["name"] = m["filename"]
}
}
if a.Path.IsSpecified() {
m["path"] = nil
if path, err := a.Path.Get(); err == nil {
m["path"] = path.String()
}
}
if v, err := a.EmbeddedContent.Get(); err == nil {
m["embedded_content"] = v
}
return m
}
+390
View File
@@ -0,0 +1,390 @@
package model
import (
"fmt"
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/utils"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Campaign is a phishing Campaign entity
type Campaign struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CloseAt nullable.Nullable[time.Time] `json:"closeAt"`
ClosedAt nullable.Nullable[time.Time] `json:"closedAt"`
AnonymizeAt nullable.Nullable[time.Time] `json:"anonymizeAt"`
AnonymizedAt nullable.Nullable[time.Time] `json:"anonymizedAt"`
SortField nullable.Nullable[vo.CampaignSortField] `json:"sortField"`
SortOrder nullable.Nullable[vo.CampaignSendingOrder] `json:"sortOrder"`
SendStartAt nullable.Nullable[time.Time] `json:"sendStartAt"`
SendEndAt nullable.Nullable[time.Time] `json:"sendEndAt"`
ConstraintWeekDays nullable.Nullable[vo.CampaignWeekDays] `json:"constraintWeekDays"`
ConstraintStartTime nullable.Nullable[vo.CampaignTimeConstraint] `json:"constraintStartTime"`
ConstraintEndTime nullable.Nullable[vo.CampaignTimeConstraint] `json:"constraintEndTime"`
Name nullable.Nullable[vo.String64] `json:"name"`
SaveSubmittedData nullable.Nullable[bool] `json:"saveSubmittedData"`
IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"`
IsTest nullable.Nullable[bool] `json:"isTest"`
TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"`
Template *CampaignTemplate `json:"template"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
RecipientGroups []*RecipientGroup `json:"recipientGroups"`
RecipientGroupIDs nullable.Nullable[[]*uuid.UUID] `json:"recipientGroupIDs,omitempty"`
AllowDeny []*AllowDeny `json:"allowDeny"`
AllowDenyIDs nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"`
DenyPageID nullable.Nullable[uuid.UUID] `json:"denyPageID,omitempty"`
DenyPage *Page `json:"denyPage"`
WebhookID nullable.Nullable[uuid.UUID] `json:"webhookID"`
// must not be set by a user
NotableEventID nullable.Nullable[uuid.UUID] `json:"notableEventID"`
NotableEventName string `json:"notableEventName"`
}
// Validate checks if the campaign has a valid state
func (c *Campaign) Validate() error {
if err := validate.NullableFieldRequired("name", c.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("sortField", c.SortField); err != nil {
return err
}
if err := validate.NullableFieldRequired("sortOrder", c.SortOrder); err != nil {
return err
}
// if a start or end is set, then end must be equal or after the start
if c.SendStartAt.IsSpecified() && !c.SendStartAt.IsNull() || (c.SendEndAt.IsSpecified() && !c.SendEndAt.IsNull()) {
if err := validate.NullableFieldRequired("sendStartAt", c.SendStartAt); err != nil {
return err
}
if err := validate.NullableFieldRequired("sendEndAt", c.SendEndAt); err != nil {
return err
}
if c.SendEndAt.MustGet().Before(c.SendStartAt.MustGet()) {
return validate.WrapErrorWithField(errors.New("send end time must be after start time"), "sendEndAt")
}
}
if err := validate.NullableFieldRequired("templateID", c.TemplateID); err != nil {
return err
}
if err := validate.NullableFieldRequired("RecipientGroupIDs", c.RecipientGroupIDs); err != nil {
return err
}
if len(c.RecipientGroupIDs.MustGet()) == 0 {
return validate.WrapErrorWithField(errors.New("must have at least one recipient group"), "RecipientGroupIDs")
}
if err := c.ValidateDenyPage(); err != nil {
return err
}
// if ConstraintWeekDays or ConstraintStartTime or ConstraintEndTime is set, then this is a 'scheduled type'
// this requires that all fields are set and that end time is equal or after start time
if (c.ConstraintWeekDays.IsSpecified() && !c.ConstraintWeekDays.IsNull()) ||
(c.ConstraintStartTime.IsSpecified() && !c.ConstraintStartTime.IsNull()) ||
(c.ConstraintEndTime.IsSpecified() && !c.ConstraintEndTime.IsNull()) {
// check required fields are set
if err := c.ValidateSendTimesSet(); err != nil {
return err
}
if err := validate.NullableFieldRequired("ConstraintWeekDays", c.ConstraintWeekDays); err != nil {
return err
}
if c.ConstraintWeekDays.MustGet().Count() == 0 {
return validate.WrapErrorWithField(errors.New("must have at least one day selected"), "ConstraintWeekDays")
}
if err := validate.NullableFieldRequired("ConstraintStartTime", c.ConstraintStartTime); err != nil {
return err
}
if err := validate.NullableFieldRequired("ConstraintEndTime", c.ConstraintEndTime); err != nil {
return err
}
// check that times and days are valid
constraintStartTime := c.ConstraintStartTime.MustGet()
constraintEndTime := c.ConstraintEndTime.MustGet()
if constraintStartTime.IsAfter(constraintEndTime) {
return validate.WrapErrorWithField(errors.New("constraint end time must be after start time"), "ConstraintEndTime")
}
if constraintStartTime.IsEqual(constraintEndTime) {
return validate.WrapErrorWithField(errors.New("constraint end time must be after start time"), "ConstraintEndTime")
}
startAt := c.SendStartAt.MustGet()
endAt := c.SendEndAt.MustGet()
// check that selected weekdays are within the start and end date
isWithin := c.ConstraintWeekDays.MustGet().IsWithin(&startAt, &endAt)
if !isWithin {
return validate.WrapErrorWithField(
fmt.Errorf(
"constraint week days must be within the start (%s) and end date (%s)",
startAt.Format("2006-01-02"),
endAt.Format("2006-01-02"),
),
"ConstraintWeekDays",
)
}
}
// ensure closeAt and anonymize is correctly set after other dates if set
if c.CloseAt.IsSpecified() && !c.CloseAt.IsNull() {
closeAt := c.CloseAt.MustGet()
if v, err := c.SendEndAt.Get(); err == nil {
if closeAt.Before(v) {
return validate.WrapErrorWithField(errors.New("close at must be after end date"), "CloseAt")
}
}
}
if c.AnonymizeAt.IsSpecified() && !c.AnonymizeAt.IsNull() {
anonymizeAt := c.AnonymizeAt.MustGet()
if v, err := c.CloseAt.Get(); err == nil {
if anonymizeAt.Before(v) {
return validate.WrapErrorWithField(errors.New("anonymize at must be after close date"), "AnonymizeAt")
}
}
if v, err := c.SendEndAt.Get(); err != nil {
if anonymizeAt.Before(v) {
return validate.WrapErrorWithField(errors.New("anonymize at must be after end date"), "AnonymizeAt")
}
}
if v, err := c.SendStartAt.Get(); err == nil {
if anonymizeAt.Before(v) {
return validate.WrapErrorWithField(errors.New("anonymize at must be after start date"), "AnonymizeAt")
}
}
}
// must not be set from api consumers
if c.NotableEventID.IsSpecified() && !c.NotableEventID.IsNull() {
c.NotableEventID.SetNull()
}
return nil
}
// ValidateSendTimesSet checks that the send start and end times are set
func (c *Campaign) ValidateSendTimesSet() error {
if err := validate.NullableFieldRequired("sendStartAt", c.SendStartAt); err != nil {
return err
}
if err := validate.NullableFieldRequired("sendEndAt", c.SendEndAt); err != nil {
return err
}
return nil
}
// ValidateScheduledType checks times related to a scheduled type campaign
func (c *Campaign) ValidateScheduledTimes() error {
if err := c.ValidateSendTimesSet(); err != nil {
return err
}
if err := validate.NullableFieldRequired("ConstraintWeekDays", c.ConstraintWeekDays); err != nil {
return err
}
if err := validate.NullableFieldRequired("ConstraintStartTime", c.ConstraintStartTime); err != nil {
return err
}
if err := validate.NullableFieldRequired("ConstraintEndTime", c.ConstraintEndTime); err != nil {
return err
}
return nil
}
// ValidateNoSendTimesSet checks that the send start and end times are not set
func (c *Campaign) ValidateNoSendTimesSet() error {
if c.SendStartAt.IsSpecified() && !c.SendStartAt.IsNull() {
return validate.WrapErrorWithField(errors.New("send start time must not be set"), "sendStartAt")
}
if c.SendEndAt.IsSpecified() && !c.SendEndAt.IsNull() {
return validate.WrapErrorWithField(errors.New("send end time must not be set"), "sendEndAt")
}
return nil
}
// ValidateDenyPage checks that a deny page is set
func (c *Campaign) ValidateDenyPage() error {
if c.DenyPageID.IsSpecified() && !c.DenyPageID.IsNull() {
if c.AllowDenyIDs.IsSpecified() && (c.AllowDenyIDs.IsNull() || len(c.AllowDenyIDs.MustGet()) == 0) {
return validate.WrapErrorWithField(errors.New("requires a allow deny IDs to be set"), "denyPage")
}
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (c *Campaign) ToDBMap() map[string]any {
m := map[string]any{}
if c.Name.IsSpecified() {
m["name"] = nil
if v, err := c.Name.Get(); err == nil {
m["name"] = v.String()
}
}
if c.SortField.IsSpecified() {
m["sort_field"] = nil
if v, err := c.SortField.Get(); err == nil {
m["sort_field"] = v.String()
}
}
if c.SortOrder.IsSpecified() {
m["sort_order"] = nil
if v, err := c.SortOrder.Get(); err == nil {
m["sort_order"] = v.String()
}
}
if c.SendStartAt.IsSpecified() {
m["send_start_at"] = nil
if v, err := c.SendStartAt.Get(); err == nil {
m["send_start_at"] = utils.RFC3339UTC(v)
}
}
if c.ConstraintWeekDays.IsSpecified() {
m["constraint_week_days"] = nil
if v, err := c.ConstraintWeekDays.Get(); err == nil {
m["constraint_week_days"] = v.Int()
}
}
if c.ConstraintStartTime.IsSpecified() {
m["constraint_start_time"] = nil
if v, err := c.ConstraintStartTime.Get(); err == nil {
m["constraint_start_time"] = v.String()
}
}
if c.ConstraintEndTime.IsSpecified() {
m["constraint_end_time"] = nil
if v, err := c.ConstraintEndTime.Get(); err == nil {
m["constraint_end_time"] = v.String()
}
}
if c.SendEndAt.IsSpecified() {
m["send_end_at"] = nil
if v, err := c.SendEndAt.Get(); err == nil {
m["send_end_at"] = utils.RFC3339UTC(v)
}
}
if c.CloseAt.IsSpecified() {
m["close_at"] = nil
if v, err := c.CloseAt.Get(); err == nil {
m["close_at"] = utils.RFC3339UTC(v)
}
}
if c.ClosedAt.IsSpecified() {
m["closed_at"] = nil
if v, err := c.ClosedAt.Get(); err == nil {
m["closed_at"] = utils.RFC3339UTC(v)
}
}
if c.AnonymizeAt.IsSpecified() {
m["anonymize_at"] = nil
if v, err := c.AnonymizeAt.Get(); err == nil {
m["anonymize_at"] = utils.RFC3339UTC(v)
}
}
if c.SaveSubmittedData.IsSpecified() {
m["save_submitted_data"] = false
if v, err := c.SaveSubmittedData.Get(); err == nil {
m["save_submitted_data"] = v
}
}
if c.IsTest.IsSpecified() {
m["is_test"] = false
if v, err := c.IsTest.Get(); err == nil {
m["is_test"] = v
}
}
if c.IsAnonymous.IsSpecified() {
m["is_anonymous"] = false
if v, err := c.IsAnonymous.Get(); err == nil {
m["is_anonymous"] = v
}
}
if c.TemplateID.IsSpecified() {
m["campaign_template_id"] = nil
if v, err := c.TemplateID.Get(); err == nil {
m["campaign_template_id"] = v.String()
}
}
if c.CompanyID.IsSpecified() {
if c.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = c.CompanyID.MustGet()
}
}
allowDenyIsSet := c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0
if allowDenyIsSet {
if v, err := c.DenyPageID.Get(); err == nil {
m["deny_page_id"] = v.String()
} else {
m["deny_page_id"] = nil
}
} else {
m["deny_page_id"] = nil
}
if c.WebhookID.IsSpecified() {
m["webhook_id"] = nil
if v, err := c.WebhookID.Get(); err == nil {
m["webhook_id"] = v.String()
}
}
if v, err := c.NotableEventID.Get(); err == nil {
m["notable_event_id"] = v.String()
}
return m
}
// Close sets the close at timestamp to now
// dont confuse with method Closed
func (c *Campaign) Close() error {
if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() {
return errs.ErrCampaignAlreadyClosed
}
if c.CloseAt.IsSpecified() && !c.CloseAt.IsNull() {
return errs.ErrCampaignAlreadySetToClose
}
c.CloseAt.Set(time.Now().UTC())
return nil
}
// Closed sets the closed at timestamp to now
// dont confuse with method Close
func (c *Campaign) Closed() error {
if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() {
return errs.ErrCampaignAlreadyClosed
}
c.ClosedAt.Set(time.Now().UTC())
return nil
}
// Anonymize sets the anonymized at timestamp
func (c *Campaign) Anonymize() error {
if c.AnonymizedAt.IsSpecified() && !c.AnonymizedAt.IsNull() {
return errs.ErrCampaignAlreadyAnonymized
}
c.AnonymizedAt.Set(time.Now().UTC())
return nil
}
// IsActive returns true if the campaign is active
func (c *Campaign) IsActive() bool {
now := time.Now()
if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() && c.ClosedAt.MustGet().Before(now) {
return false
}
return true
}
// IsSelfManaged returns true if the campaign is self managed
func (c *Campaign) IsSelfManaged() bool {
return c.SendStartAt.IsSpecified() && c.SendStartAt.IsNull() && c.SendEndAt.IsSpecified() && c.SendEndAt.IsNull()
}
+22
View File
@@ -0,0 +1,22 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/phishingclub/phishingclub/vo"
)
type CampaignEvent struct {
ID *uuid.UUID `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
CampaignID *uuid.UUID `json:"campaignID"`
IP *vo.OptionalString64 `json:"ip"`
UserAgent *vo.OptionalString255 `json:"userAgent"`
Data *vo.OptionalString1MB `json:"data"`
AnonymizedID *uuid.UUID `json:"anonymizedID"`
// if null the recipient has been anonymized
RecipientID *uuid.UUID `json:"recipientID"`
Recipient *Recipient `json:"recipient,omitempty"`
EventID *uuid.UUID `json:"eventID"`
}
+109
View File
@@ -0,0 +1,109 @@
package model
import (
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/utils"
"github.com/phishingclub/phishingclub/validate"
)
// CampaignRecipient is a campaign recipient
// this model must not be consumed from a endpoint
type CampaignRecipient struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CancelledAt nullable.Nullable[time.Time] `json:"cancelledAt"`
SendAt nullable.Nullable[time.Time] `json:"sendAt"`
LastAttemptAt nullable.Nullable[time.Time] `json:"lastAttemptAt"`
SentAt nullable.Nullable[time.Time] `json:"sentAt"`
SelfManaged nullable.Nullable[bool] `json:"selfManaged"`
AnonymizedID nullable.Nullable[uuid.UUID] `json:"anonymizedID"`
CampaignID nullable.Nullable[uuid.UUID] `json:"campaignID"`
Campaign *Campaign `json:"campaign"`
// null recipientID means that the data has been anonymized
RecipientID nullable.Nullable[uuid.UUID] `json:"recipientID"`
Recipient *Recipient `json:"recipient"`
NotableEventID nullable.Nullable[uuid.UUID] `json:"notableEventID"`
NotableEventName string `json:"notableEventName"`
}
// Validate validates the campaign recipient
func (c *CampaignRecipient) Validate() error {
if err := validate.NullableFieldRequired("campaignID", c.CampaignID); err != nil {
return err
}
anonymizedAtErr := validate.NullableFieldRequired("anonymizedID", c.AnonymizedID)
recipientIDErr := validate.NullableFieldRequired("recipientID", c.RecipientID)
if anonymizedAtErr == nil && recipientIDErr == nil {
return recipientIDErr
}
if anonymizedAtErr != nil && recipientIDErr != nil {
return validate.WrapErrorWithField(
errors.New("AnonymizedID can not be set with recipientID"),
"recipientID",
)
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (c *CampaignRecipient) ToDBMap() map[string]any {
m := map[string]any{}
if c.CancelledAt.IsSpecified() {
m["cancelled_at"] = nil
if v, err := c.CancelledAt.Get(); err == nil {
m["cancelled_at"] = utils.RFC3339UTC(v)
}
}
if c.SendAt.IsSpecified() {
m["send_at"] = nil
if v, err := c.SendAt.Get(); err == nil {
m["send_at"] = utils.RFC3339UTC(v)
}
}
if c.LastAttemptAt.IsSpecified() {
m["last_attempt_at"] = nil
if v, err := c.LastAttemptAt.Get(); err == nil {
m["last_attempt_at"] = utils.RFC3339UTC(v)
}
}
if c.SentAt.IsSpecified() {
m["sent_at"] = nil
if v, err := c.SentAt.Get(); err == nil {
m["sent_at"] = utils.RFC3339UTC(v)
}
}
if c.SelfManaged.IsSpecified() {
m["self_managed"] = nil
if v, err := c.SelfManaged.Get(); err == nil {
m["self_managed"] = v
}
}
if c.CampaignID.IsSpecified() {
m["campaign_id"] = nil
if v, err := c.CampaignID.Get(); err == nil {
m["campaign_id"] = v
}
}
if c.RecipientID.IsSpecified() {
m["recipient_id"] = nil
if v, err := c.RecipientID.Get(); err == nil {
m["recipient_id"] = v
}
}
if c.NotableEventID.IsSpecified() {
m["notable_event_id"] = nil
if v, err := c.NotableEventID.Get(); err == nil {
m["notable_event_id"] = v
}
}
return m
}
+9
View File
@@ -0,0 +1,9 @@
package model
type CampaignResultView struct {
Recipients int64 `json:"recipients"`
EmailsSent int64 `json:"emailsSent"`
TrackingPixelLoaded int64 `json:"trackingPixelLoaded"`
WebsiteLoaded int64 `json:"clickedLink"`
SubmittedData int64 `json:"submittedData"`
}
+187
View File
@@ -0,0 +1,187 @@
package model
import (
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// CampaignTemplate is a campaign template
type CampaignTemplate struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String64] `json:"name"`
DomainID nullable.Nullable[uuid.UUID] `json:"domainID"`
Domain *Domain `json:"domain"`
BeforeLandingPageID nullable.Nullable[uuid.UUID] `json:"beforeLandingPageID"`
BeforeLandingePage *Page `json:"beforeLandingPage"`
LandingPageID nullable.Nullable[uuid.UUID] `json:"landingPageID"`
LandingPage *Page `json:"landingPage"`
AfterLandingPageID nullable.Nullable[uuid.UUID] `json:"afterLandingPageID"`
AfterLandingPage *Page `json:"afterLandingPage"`
AfterLandingPageRedirectURL nullable.Nullable[vo.OptionalString255] `json:"afterLandingPageRedirectURL"`
URLIdentifierID nullable.Nullable[*uuid.UUID] `json:"urlIdentifierID"`
URLIdentifier *Identifier `json:"urlIdentifier"`
StateIdentifierID nullable.Nullable[*uuid.UUID] `json:"stateIdentifierID"`
StateIdentifier *Identifier `json:"stateIdentifier"`
URLPath nullable.Nullable[vo.URLPath] `json:"urlPath"`
EmailID nullable.Nullable[uuid.UUID] `json:"emailID"`
Email *Email `json:"email"`
SMTPConfigurationID nullable.Nullable[uuid.UUID] `json:"smtpConfigurationID"`
SMTPConfiguration *SMTPConfiguration `json:"smtpConfiguration"`
APISenderID nullable.Nullable[uuid.UUID] `json:"apiSenderID"`
APISender *APISender `json:"apiSender"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
IsUsable nullable.Nullable[bool] `json:"isUsable"`
}
// Validate checks if the campaign template has a valid state
func (c *CampaignTemplate) Validate() error {
if err := validate.NullableFieldRequired("name", c.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("urlIdentifierID", c.URLIdentifierID); err != nil {
return err
}
if err := validate.NullableFieldRequired("stateIdentifierID", c.StateIdentifierID); err != nil {
return err
}
if a, err := c.URLIdentifierID.Get(); err == nil {
if b, err := c.StateIdentifierID.Get(); err == nil {
if a.String() == b.String() {
return errs.NewValidationError(
errors.New("URL and state identifier can not be the same"),
)
}
}
}
if err := validate.NullableFieldRequired("urlPath", c.URLPath); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (c *CampaignTemplate) ToDBMap() map[string]any {
m := map[string]any{}
if c.Name.IsSpecified() {
m["name"] = nil
if name, err := c.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if c.DomainID.IsSpecified() {
if c.DomainID.IsNull() {
m["domain_id"] = nil
} else {
m["domain_id"] = c.DomainID.MustGet()
}
}
if c.BeforeLandingPageID.IsSpecified() {
if c.BeforeLandingPageID.IsNull() {
m["before_landing_page_id"] = nil
} else {
m["before_landing_page_id"] = c.BeforeLandingPageID.MustGet()
}
}
if c.LandingPageID.IsSpecified() {
if c.LandingPageID.IsNull() {
m["landing_page_id"] = nil
} else {
m["landing_page_id"] = c.LandingPageID.MustGet()
}
}
if c.AfterLandingPageID.IsSpecified() {
if c.AfterLandingPageID.IsNull() {
m["after_landing_page_id"] = nil
} else {
m["after_landing_page_id"] = c.AfterLandingPageID.MustGet()
}
}
if c.AfterLandingPageRedirectURL.IsSpecified() {
if c.AfterLandingPageRedirectURL.IsNull() {
m["after_landing_page_redirect_url"] = nil
} else {
m["after_landing_page_redirect_url"] = c.AfterLandingPageRedirectURL.MustGet().String()
}
}
if c.EmailID.IsSpecified() {
if c.EmailID.IsNull() {
m["email_id"] = nil
} else {
m["email_id"] = c.EmailID.MustGet()
}
}
if c.SMTPConfigurationID.IsSpecified() {
if c.SMTPConfigurationID.IsNull() {
m["smtp_configuration_id"] = nil
} else {
m["smtp_configuration_id"] = c.SMTPConfigurationID.MustGet()
}
}
if c.APISenderID.IsSpecified() {
if c.APISenderID.IsNull() {
m["api_sender_id"] = nil
} else {
m["api_sender_id"] = c.APISenderID.MustGet()
}
}
if c.CompanyID.IsSpecified() {
if c.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = c.CompanyID.MustGet()
}
}
if v, err := c.URLIdentifierID.Get(); err == nil {
m["url_identifier_id"] = v
}
if v, err := c.StateIdentifierID.Get(); err == nil {
m["state_identifier_id"] = v
}
if v, err := c.URLPath.Get(); err == nil {
m["url_path"] = v.String()
}
_, errDomain := c.DomainID.Get()
_, errSMTP := c.SMTPConfigurationID.Get()
_, errAPISender := c.APISenderID.Get()
_, errEmail := c.EmailID.Get()
_, errLandingPage := c.LandingPageID.Get()
m["is_usable"] = errDomain == nil &&
errEmail == nil &&
errLandingPage == nil &&
(errSMTP == nil || errAPISender == nil)
return m
}
+7
View File
@@ -0,0 +1,7 @@
package model
type CampaignsStatView struct {
Active int64 `json:"active"`
Upcoming int64 `json:"upcoming"`
Finished int64 `json:"finished"`
}
+40
View File
@@ -0,0 +1,40 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Company is a company
type Company struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String64] `json:"name"`
}
// Validate checks if the Company configuration with a valid state
func (c *Company) Validate() error {
if err := validate.NullableFieldRequired("name", c.Name); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (c *Company) ToDBMap() map[string]any {
m := map[string]any{}
if c.Name.IsSpecified() {
m["name"] = nil
if name, err := c.Name.Get(); err == nil {
m["name"] = name.String()
}
}
return m
}
+166
View File
@@ -0,0 +1,166 @@
package model
import (
"time"
"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
type Domain struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String255] `json:"name"`
HostWebsite nullable.Nullable[bool] `json:"hostWebsite"`
ManagedTLS nullable.Nullable[bool] `json:"managedTLS"`
OwnManagedTLS nullable.Nullable[bool] `json:"ownManagedTLS"`
// private key
OwnManagedTLSKey nullable.Nullable[string] `json:"ownManagedTLSKey"`
// cert
OwnManagedTLSPem nullable.Nullable[string] `json:"ownManagedTLSPem"`
PageContent nullable.Nullable[vo.OptionalString1MB] `json:"pageContent"`
PageNotFoundContent nullable.Nullable[vo.OptionalString1MB] `json:"pageNotFoundContent"`
RedirectURL nullable.Nullable[vo.OptionalString1024] `json:"redirectURL"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
}
// Validate checks if the Domain configuration with a valid state
func (d *Domain) Validate() error {
if err := validate.NullableFieldRequired("name", d.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("hostWebsite", d.HostWebsite); err != nil {
return err
}
if err := validate.NullableFieldRequired("managedTLS", d.ManagedTLS); err != nil {
return err
}
if err := validate.NullableFieldRequired("pageContent", d.PageContent); err != nil {
return err
}
if err := validate.NullableFieldRequired("pageNotFoundContent", d.PageNotFoundContent); err != nil {
return err
}
if err := validate.NullableFieldRequired("redirectURL", d.RedirectURL); err != nil {
return err
}
//
//
ownManagedTLS, err := d.OwnManagedTLS.Get()
ownManagedTLSSet := err == nil && ownManagedTLS
// cant both have managed and own managed tls
if managedTLS, err := d.ManagedTLS.Get(); err == nil && managedTLS && ownManagedTLSSet {
return errs.NewValidationError(errors.New(
"Domain TLS can not both be managed and own managed",
))
}
if ownManagedTLS {
// handle own managed ManagedTLS
ownManagedTLSKey, err := d.OwnManagedTLSKey.Get()
ownManagedTLSPem, err := d.OwnManagedTLSPem.Get()
ownManagedTLSKeyIsSet := err == nil && len(ownManagedTLSKey) > 0
ownManagedTLSPemIsSet := err == nil && len(ownManagedTLSPem) > 0
// both must be set, not one of
if (ownManagedTLSKeyIsSet && !ownManagedTLSPemIsSet) ||
(!ownManagedTLSKeyIsSet && ownManagedTLSPemIsSet) {
return errs.NewValidationError(errors.New(
"Own managed TLS requires a private key (.key) and a certificate (.pem)",
))
}
}
/*
// TODO hostWebsite vs redirectURL are mutually exclusive
hostWebsite := d.HostWebsite.MustGet()
redirectURL := d.RedirectURL.MustGet()
redirectURLLen := len(redirectURL.String())
if hostWebsite && redirectURLLen > 0 {
return validate.WrapErrorWithField(
errors.New("both can not be set"),
"Host website and redirect url",
)
} */
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (d *Domain) ToDBMap() map[string]any {
m := map[string]any{}
if d.Name.IsSpecified() {
m["name"] = nil
if name, err := d.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if d.HostWebsite.IsSpecified() {
m["host_website"] = nil
if hostWebsite, err := d.HostWebsite.Get(); err == nil {
m["host_website"] = hostWebsite
}
m["redirect_url"] = ""
}
if d.RedirectURL.IsSpecified() {
m["redirect_url"] = nil
if redirectURL, err := d.RedirectURL.Get(); err == nil {
m["redirect_url"] = redirectURL.String()
}
}
if d.PageContent.IsSpecified() {
m["page_content"] = nil
if staticPage, err := d.PageContent.Get(); err == nil {
m["page_content"] = staticPage.String()
}
}
if d.PageNotFoundContent.IsSpecified() {
m["page_not_found_content"] = nil
if staticNotFound, err := d.PageNotFoundContent.Get(); err == nil {
m["page_not_found_content"] = staticNotFound.String()
}
}
if d.CompanyID.IsSpecified() {
if d.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = d.CompanyID.MustGet()
}
}
if d.ManagedTLS.IsSpecified() {
m["managed_tls_certs"] = false
if d.ManagedTLS.IsNull() {
m["managed_tls_certs"] = nil
} else {
m["managed_tls_certs"] = d.ManagedTLS.MustGet()
}
}
if d.OwnManagedTLS.IsSpecified() {
m["own_managed_tls"] = false
if d.OwnManagedTLS.IsNull() {
m["own_managed_tls"] = nil
} else {
m["own_managed_tls"] = d.OwnManagedTLS.MustGet()
}
}
return m
}
// DomainOverview is a subset of the domain as used as read-only
type DomainOverview struct {
ID uuid.UUID `json:"id,omitempty"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name string `json:"name"`
HostWebsite bool `json:"hostWebsite"`
ManagedTLS bool `json:"managedTLS"`
OwnManagedTLS bool `json:"ownManagedTLS"`
RedirectURL string `json:"redirectURL"`
CompanyID *uuid.UUID `json:"companyID"`
}
+134
View File
@@ -0,0 +1,134 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Email is a e-mail
type Email struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String64] `json:"name"`
MailEnvelopeFrom nullable.Nullable[vo.MailEnvelopeFrom] `json:"mailEnvelopeFrom"` // Bounce / Return-Path
MailHeaderFrom nullable.Nullable[vo.Email] `json:"mailHeaderFrom"`
MailHeaderSubject nullable.Nullable[vo.OptionalString255] `json:"mailHeaderSubject"`
Content nullable.Nullable[vo.OptionalString1MB] `json:"content"`
AddTrackingPixel nullable.Nullable[bool] `json:"addTrackingPixel"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Attachments []*Attachment `json:"attachments"`
Company *Company `json:"company"`
}
// Validate checks if the mail has a valid state
func (m *Email) Validate() error {
if err := validate.NullableFieldRequired("name", m.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("mailEnvelopeFrom", m.MailEnvelopeFrom); err != nil {
return err
}
if err := validate.NullableFieldRequired("mailHeaderSubject", m.MailHeaderSubject); err != nil {
return err
}
if err := validate.NullableFieldRequired("addTrackingPixel", m.MailHeaderFrom); err != nil {
return err
}
if err := validate.NullableFieldRequired("Content", m.Content); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (s *Email) ToDBMap() map[string]any {
m := map[string]any{}
if s.Name.IsSpecified() {
m["name"] = nil
if name, err := s.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if s.MailEnvelopeFrom.IsSpecified() {
m["mail_from"] = nil
if envelopeFrom, err := s.MailEnvelopeFrom.Get(); err == nil {
m["mail_from"] = envelopeFrom.String()
}
}
if s.MailHeaderFrom.IsSpecified() {
m["from"] = nil
if headerFrom, err := s.MailHeaderFrom.Get(); err == nil {
m["from"] = headerFrom.String()
}
}
if s.MailHeaderSubject.IsSpecified() {
m["subject"] = nil
if headerSubject, err := s.MailHeaderSubject.Get(); err == nil {
m["subject"] = headerSubject.String()
}
}
if s.Content.IsSpecified() {
m["content"] = nil
if content, err := s.Content.Get(); err == nil {
m["content"] = content.String()
}
}
if s.AddTrackingPixel.IsSpecified() {
m["add_tracking_pixel"] = nil
if addTrackingPixel, err := s.AddTrackingPixel.Get(); err == nil {
m["add_tracking_pixel"] = addTrackingPixel
}
}
if s.CompanyID.IsSpecified() {
if s.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = s.CompanyID.MustGet()
}
}
return m
}
func NewEmailExample() *Email {
return &Email{
Name: nullable.NewNullableWithValue(
*vo.NewString64Must("ExampleEmail"),
),
MailEnvelopeFrom: nullable.NewNullableWithValue(
*vo.NewMailEnvelopeFromMust("sender@example.test"),
),
MailHeaderFrom: nullable.NewNullableWithValue(
*vo.NewEmailMust("Mallory <m@example.test>"),
),
MailHeaderSubject: nullable.NewNullableWithValue(
*vo.NewOptionalString255Must("SubjectLine"),
),
Content: nullable.NewNullableWithValue(
*vo.NewOptionalString1MBMust("Content"),
),
AddTrackingPixel: nullable.NewNullableWithValue(true),
}
}
// EmailOverview is a e-mail model without content and attachments
type EmailOverview struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String64] `json:"name"`
MailEnvelopeFrom nullable.Nullable[vo.MailEnvelopeFrom] `json:"mailEnvelopeFrom"` // Bounce / Return-Path
MailHeaderFrom nullable.Nullable[vo.Email] `json:"mailHeaderFrom"`
MailHeaderSubject nullable.Nullable[vo.OptionalString255] `json:"mailHeaderSubject"`
AddTrackingPixel nullable.Nullable[bool] `json:"addTrackingPixel"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
}
+27
View File
@@ -0,0 +1,27 @@
package model
import (
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
)
type Identifier struct {
ID nullable.Nullable[*uuid.UUID] `json:"id"`
Name nullable.Nullable[string] `json:"name"`
}
func (i *Identifier) Validate() error {
if err := validate.NullableFieldRequired("name", i.Name); err != nil {
return err
}
return nil
}
func (i *Identifier) ToDBMap() map[string]any {
m := make(map[string]any)
if v, err := i.Name.Get(); err == nil {
m["name"] = v
}
return m
}
+14
View File
@@ -0,0 +1,14 @@
package model
import (
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/vo"
)
// Option is an Option
type Option struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
Key vo.String64 `json:"key"`
Value vo.OptionalString1MB `json:"value"`
}
+60
View File
@@ -0,0 +1,60 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Page is a Page
type Page struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Name nullable.Nullable[vo.String64] `json:"name"`
Content nullable.Nullable[vo.OptionalString1MB] `json:"content"`
Company *Company `json:"-"`
}
// Validate checks if the page has a valid state
func (p *Page) Validate() error {
if err := validate.NullableFieldRequired("name", p.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("content", p.Content); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (p *Page) ToDBMap() map[string]any {
m := map[string]any{}
if p.Name.IsSpecified() {
m["name"] = nil
if name, err := p.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if p.Content.IsSpecified() {
m["content"] = nil
if content, err := p.Content.Get(); err == nil {
m["content"] = content.String()
}
}
if p.CompanyID.IsSpecified() {
if p.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = p.CompanyID.MustGet()
}
}
return m
}
+221
View File
@@ -0,0 +1,221 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Recipient is a Recipient
type Recipient struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Email nullable.Nullable[vo.Email] `json:"email"`
Phone nullable.Nullable[vo.OptionalString127] `json:"phone"`
ExtraIdentifier nullable.Nullable[vo.OptionalString127] `json:"extraIdentifier"`
FirstName nullable.Nullable[vo.OptionalString127] `json:"firstName"`
LastName nullable.Nullable[vo.OptionalString127] `json:"lastName"`
Position nullable.Nullable[vo.OptionalString127] `json:"position"`
Department nullable.Nullable[vo.OptionalString127] `json:"department"`
City nullable.Nullable[vo.OptionalString127] `json:"city"`
Country nullable.Nullable[vo.OptionalString127] `json:"country"`
Misc nullable.Nullable[vo.OptionalString127] `json:"misc"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"-"`
Groups nullable.Nullable[[]*RecipientGroup] `json:"groups"`
}
// Validate checks if the recipient has a valid state
func (r *Recipient) Validate() error {
if err := validate.NullableFieldRequired("email", r.Email); err != nil {
return err
}
return nil
}
// NullifyEmptyOptionals sets empty values to a nullable null, so they are not overwritten
func (r *Recipient) NullifyEmptyOptionals() {
if r.Phone.IsSpecified() && !r.Phone.IsNull() && r.Phone.MustGet().String() == "" {
r.Phone.SetNull()
}
if r.ExtraIdentifier.IsSpecified() && !r.ExtraIdentifier.IsNull() && r.ExtraIdentifier.MustGet().String() == "" {
r.ExtraIdentifier.SetNull()
}
if r.FirstName.IsSpecified() && !r.FirstName.IsNull() && r.FirstName.MustGet().String() == "" {
r.FirstName.SetNull()
}
if r.LastName.IsSpecified() && !r.LastName.IsNull() && r.LastName.MustGet().String() == "" {
r.LastName.SetNull()
}
if r.Position.IsSpecified() && !r.Position.IsNull() && r.Position.MustGet().String() == "" {
r.Position.SetNull()
}
if r.Department.IsSpecified() && !r.Department.IsNull() && r.Department.MustGet().String() == "" {
r.Department.SetNull()
}
if r.City.IsSpecified() && !r.City.IsNull() && r.City.MustGet().String() == "" {
r.City.SetNull()
}
if r.Country.IsSpecified() && !r.Country.IsNull() && r.Country.MustGet().String() == "" {
r.Country.SetNull()
}
if r.Misc.IsSpecified() && !r.Misc.IsNull() && r.Misc.MustGet().String() == "" {
r.Misc.SetNull()
}
}
// EmptyStringNulledOptionals sets nulled optional values to a empty string or zero value.
func (r *Recipient) EmptyStringNulledOptionals() {
if r.Phone.IsSpecified() && r.Phone.IsNull() {
r.Phone.Set(*vo.NewOptionalString127Must(""))
}
if r.ExtraIdentifier.IsSpecified() && r.ExtraIdentifier.IsNull() {
r.ExtraIdentifier.Set(*vo.NewOptionalString127Must(""))
}
if r.FirstName.IsSpecified() && r.FirstName.IsNull() {
r.FirstName.Set(*vo.NewOptionalString127Must(""))
}
if r.LastName.IsSpecified() && r.LastName.IsNull() {
r.LastName.Set(*vo.NewOptionalString127Must(""))
}
if r.Position.IsSpecified() && r.Position.IsNull() {
r.Position.Set(*vo.NewOptionalString127Must(""))
}
if r.Department.IsSpecified() && r.Department.IsNull() {
r.Department.Set(*vo.NewOptionalString127Must(""))
}
if r.City.IsSpecified() && r.City.IsNull() {
r.City.Set(*vo.NewOptionalString127Must(""))
}
if r.Country.IsSpecified() && r.Country.IsNull() {
r.Country.Set(*vo.NewOptionalString127Must(""))
}
if r.Misc.IsSpecified() && r.Misc.IsNull() {
r.Misc.Set(*vo.NewOptionalString127Must(""))
}
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (r *Recipient) ToDBMap() map[string]any {
m := map[string]any{}
if r.Email.IsSpecified() {
m["email"] = nil
if email, err := r.Email.Get(); err == nil {
if email.String() == "" {
m["email"] = nil // due to the unique constraint
} else {
m["email"] = email.String()
}
}
}
if r.Phone.IsSpecified() {
m["phone"] = nil
if phone, err := r.Phone.Get(); err == nil {
if phone.String() == "" {
m["phone"] = nil // due to the unique constraint
} else {
m["phone"] = phone.String()
}
}
}
if r.ExtraIdentifier.IsSpecified() {
m["extra_identifier"] = nil
if extraIdentifier, err := r.ExtraIdentifier.Get(); err == nil {
if extraIdentifier.String() == "" {
m["extra_identifier"] = nil // due to the unique constraint
} else {
m["extra_identifier"] = extraIdentifier.String()
}
}
}
if r.FirstName.IsSpecified() {
m["first_name"] = nil
if firstName, err := r.FirstName.Get(); err == nil {
m["first_name"] = firstName.String()
}
}
if r.LastName.IsSpecified() {
m["last_name"] = nil
if lastName, err := r.LastName.Get(); err == nil {
m["last_name"] = lastName.String()
}
}
if r.Position.IsSpecified() {
m["position"] = nil
if position, err := r.Position.Get(); err == nil {
m["position"] = position.String()
}
}
if r.Department.IsSpecified() {
m["department"] = nil
if department, err := r.Department.Get(); err == nil {
m["department"] = department.String()
}
}
if r.City.IsSpecified() {
m["city"] = nil
if city, err := r.City.Get(); err == nil {
m["city"] = city.String()
}
}
if r.Country.IsSpecified() {
m["country"] = nil
if country, err := r.Country.Get(); err == nil {
m["country"] = country.String()
}
}
if r.Misc.IsSpecified() {
m["misc"] = nil
if misc, err := r.Misc.Get(); err == nil {
m["misc"] = misc.String()
}
}
if r.CompanyID.IsSpecified() {
if r.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = r.CompanyID.MustGet()
}
}
return m
}
func NewRecipientExample() *Recipient {
return &Recipient{
Email: nullable.NewNullableWithValue(
*vo.NewEmailMust("Rick <rick@company.test>"),
),
Phone: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("1234567890"),
),
ExtraIdentifier: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("ExtraIdentifier"),
),
FirstName: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("Rick"),
),
LastName: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("Xanders"),
),
Position: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("CEO"),
),
Department: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("IT"),
),
City: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("Fredericia"),
),
Country: nullable.NewNullableWithValue(
*vo.NewOptionalString127Must("Denmark"),
),
}
}
@@ -0,0 +1,9 @@
package model
type RecipientCampaignEvent struct {
CampaignEvent
// event name
Name string `json:"name"`
CampaignName string `json:"campaignName"`
}
@@ -0,0 +1,10 @@
package model
type RecipientCampaignStatsView struct {
CampaignsParticiated int64 `json:"campaignsParticiated"`
CampaignsTrackingPixelLoaded int64 `json:"campaignsTrackingPixelLoaded"`
CampaignsPhishingPageLoaded int64 `json:"campaignsPhishingPageLoaded"`
CampaignsDataSubmitted int64 `json:"campaignsDataSubmitted"`
RepeatLinkClicks int64 `json:"repeatLinkClicks"`
RepeatSubmissions int64 `json:"repeatSubmissions"`
}
+55
View File
@@ -0,0 +1,55 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
const RECIPIENT_COUNT_NOT_LOADED = int64(-1)
const RECIPIENT_GROUP_COUNT_NOT_LOADED = int64(-1)
// RecipientGroup is an entity for recipient group
type RecipientGroup struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String127] `json:"name"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Recipients []*Recipient `json:"-"`
IsRecipientsLoaded bool `json:"-"`
RecipientCount nullable.Nullable[int64] `json:"recipientCount"`
IsRecipientCountLoaded bool `json:"-"`
Company *Company `json:"-"`
}
// Validate checks if the recipient group has a valid state
func (rg *RecipientGroup) Validate() error {
if err := validate.NullableFieldRequired("name", rg.Name); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (rg *RecipientGroup) ToDBMap() map[string]any {
m := map[string]any{}
if rg.Name.IsSpecified() {
name := rg.Name.MustGet()
m["name"] = name.String()
}
if rg.CompanyID.IsSpecified() {
if rg.CompanyID.IsNull() {
m["company_id"] = nil
} else {
m["company_id"] = rg.CompanyID.MustGet()
}
}
return m
}
+9
View File
@@ -0,0 +1,9 @@
package model
import "github.com/google/uuid"
type RecipientGroupRecipient struct {
ID *uuid.UUID
RecipientID *uuid.UUID
RecipientGroupID *uuid.UUID
}
+15
View File
@@ -0,0 +1,15 @@
package model
// RecipientView extends Recipient with additional presentation fields
type RecipientView struct {
*Recipient // Embed the base Recipient model
IsRepeatOffender bool `json:"isRepeatOffender"`
}
// NewRecipientView creates a RecipientView from a Recipient
func NewRecipientView(r *Recipient) *RecipientView {
return &RecipientView{
Recipient: r,
IsRepeatOffender: false,
}
}
+21
View File
@@ -0,0 +1,21 @@
package model
type Result[T any] struct {
Rows []*T `json:"rows"`
HasNextPage bool `json:"hasNextPage"`
}
func NewResult[T any](rows []*T) *Result[T] {
return &Result[T]{
Rows: rows,
HasNextPage: false,
}
}
func NewEmptyResult[T any]() *Result[T] {
t := []*T{}
return &Result[T]{
Rows: t,
HasNextPage: false,
}
}
+69
View File
@@ -0,0 +1,69 @@
package model
import (
"fmt"
"github.com/google/uuid"
"github.com/phishingclub/phishingclub/data"
)
// Role is user role and defines the permissions of the user
type Role struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
// Validate checks if the role has a valid state
func (r *Role) Validate() error {
if err := validateRoleName(r.Name); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (r *Role) ToDBMap() map[string]any {
m := map[string]any{}
if r.Name != "" {
m["name"] = r.Name
}
return m
}
func (r *Role) IsAuthorized(permission string) bool {
perms := r.Permissions()
for _, perm := range perms {
if perm == permission {
return true
}
}
return false
}
// Permissions gets the permissions of the role
func (r *Role) Permissions() []string {
perms, ok := data.RolePermissions[r.Name]
if !ok {
return []string{}
}
return perms
}
// IsSuperAdministrator checks if the role is a super administrator
func (r *Role) IsSuperAdministrator() bool {
return r.Name == data.RoleSuperAdministrator
}
func validateRoleName(name string) error {
// ensure only valid role names are used
switch name {
case data.RoleSystem:
case data.RoleSuperAdministrator:
case data.RoleCompanyUser:
default:
return fmt.Errorf("invalid role name: %s", name)
}
return nil
}
+81
View File
@@ -0,0 +1,81 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/errs"
)
// todo move this to a global place like data or a config of sorts
const (
SessionIdleTimeout = 24 * time.Hour
SessionMaxAgeAt = 24 * 3 * time.Hour
)
// used in runtime for API requests
// api session is only for the single request, more like a api session contex
type APISession struct {
IP string
UserID *uuid.UUID
}
// Session reprensents a user session
// no Validate or ToDBMap as it is never created from user input
type Session struct {
ID *uuid.UUID `json:"id,omitempty"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
ExpiresAt *time.Time `json:"expiresAt"`
MaxAgeAt *time.Time `json:"maxAgeAt"`
IP string `json:"ip"`
User *User `json:"user"`
IsUserLoaded bool `json:"-"`
IsAPITokenRequest bool
}
// NewSystemSession creates a new system session
func NewSystemSession() (*Session, error) {
id, err := uuid.Parse(data.SystemSessionID)
if err != nil {
return nil, errs.Wrap(err)
}
now := time.Now()
longTimeFromNow := now.Add(time.Duration(420 * time.Now().Year())).UTC()
expiresAt := &longTimeFromNow
maxAgeAt := &longTimeFromNow
user, err := NewSystemUser()
if err != nil {
return nil, errs.Wrap(err)
}
return &Session{
ID: &id,
ExpiresAt: expiresAt,
MaxAgeAt: maxAgeAt,
IP: "127.0.0.1",
User: user,
IsUserLoaded: true,
}, nil
}
// Renew updates the session
func (s *Session) Renew(lease time.Duration) {
now := time.Now().UTC()
expiresAt := now.Add(lease)
s.ExpiresAt = &expiresAt
}
// IsExpired returns true if the session is expired
func (s *Session) IsExpired() bool {
// is total lifetime over max lifetime?
if time.Now().After(*s.MaxAgeAt) {
return true
}
// is idle timeout over?
if time.Now().After(*s.ExpiresAt) {
return true
}
return false
}
+141
View File
@@ -0,0 +1,141 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// SMTPConfiguration is a configuration for sending mails
type SMTPConfiguration struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.String127] `json:"name"`
Host nullable.Nullable[vo.String255] `json:"host"`
Port nullable.Nullable[vo.Port] `json:"port"`
Username nullable.Nullable[vo.OptionalString255] `json:"username"`
Password nullable.Nullable[vo.OptionalString255] `json:"password"`
IgnoreCertErrors nullable.Nullable[bool] `json:"ignoreCertErrors"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
Headers []*SMTPHeader `json:"headers"`
}
// Validate checks if the SMTP configuration has a valid state
func (s *SMTPConfiguration) Validate() error {
if err := validate.NullableFieldRequired("name", s.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("host", s.Host); err != nil {
return err
}
if err := validate.NullableFieldRequired("port", s.Port); err != nil {
return err
}
if err := validate.NullableFieldRequired("username", s.Username); err != nil {
return err
}
if err := validate.NullableFieldRequired("password", s.Password); err != nil {
return err
}
if err := validate.NullableFieldRequired("ignoreCertErrors", s.IgnoreCertErrors); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (s *SMTPConfiguration) ToDBMap() map[string]any {
m := map[string]any{}
if s.Name.IsSpecified() {
m["name"] = nil
if name, err := s.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if s.Host.IsSpecified() {
m["host"] = nil
if host, err := s.Host.Get(); err == nil {
m["host"] = host.String()
}
}
if s.Port.IsSpecified() {
m["port"] = nil
if port, err := s.Port.Get(); err == nil {
m["port"] = port.Uint16()
}
}
if s.Username.IsSpecified() {
m["username"] = nil
if username, err := s.Username.Get(); err == nil {
m["username"] = username.String()
}
}
if s.Password.IsSpecified() {
m["password"] = nil
if password, err := s.Password.Get(); err == nil {
m["password"] = password.String()
}
}
if s.IgnoreCertErrors.IsSpecified() {
m["ignore_cert_errors"] = nil
if ignoreCertErrors, err := s.IgnoreCertErrors.Get(); err == nil {
m["ignore_cert_errors"] = ignoreCertErrors
}
}
if v, err := s.CompanyID.Get(); err == nil {
m["company_id"] = v.String()
}
return m
}
// SMTPHeader is a header for a specific SMTP configuration
type SMTPHeader struct {
ID uuid.UUID `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
SmtpID nullable.Nullable[uuid.UUID] `json:"smtpID"`
Key nullable.Nullable[vo.String127] `json:"key"`
Value nullable.Nullable[vo.String255] `json:"value"`
}
func (s *SMTPHeader) Validate() error {
if err := validate.NullableFieldRequired("smtpID", s.SmtpID); err != nil {
return err
}
if err := validate.NullableFieldRequired("key", s.Key); err != nil {
return err
}
if err := validate.NullableFieldRequired("value", s.Value); err != nil {
return err
}
return nil
}
func (s *SMTPHeader) ToDBMap() map[string]interface{} {
m := map[string]interface{}{}
if s.SmtpID.IsSpecified() {
if smtpID, err := s.SmtpID.Get(); err == nil {
m["smtp_configuration_id"] = smtpID.String()
}
}
if s.Key.IsSpecified() {
m["key"] = nil
if key, err := s.Key.Get(); err == nil {
m["key"] = key.String()
}
}
if s.Value.IsSpecified() {
m["value"] = nil
if value, err := s.Value.Get(); err == nil {
m["value"] = value.String()
}
}
return m
}
+80
View File
@@ -0,0 +1,80 @@
package model
import (
"encoding/json"
"fmt"
"github.com/go-errors/errors"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
type SSOOption struct {
Enabled bool `json:"enabled"`
ClientID vo.OptionalString64 `json:"clientID"`
TenantID vo.OptionalString64 `json:"tenantID"`
ClientSecret vo.OptionalString1024 `json:"clientSecret"`
RedirectURL vo.OptionalString1024 `json:"redirectURL"`
}
func NewSSOOptionDefault() *SSOOption {
return &SSOOption{
Enabled: false,
ClientID: *vo.NewEmptyOptionalString64(),
TenantID: *vo.NewEmptyOptionalString64(),
ClientSecret: *vo.NewEmptyOptionalString1024(),
RedirectURL: *vo.NewEmptyOptionalString1024(),
}
}
func NewSSOOptionFromJSON(jsonData []byte) (*SSOOption, error) {
option := &SSOOption{}
err := json.Unmarshal(jsonData, option)
if err != nil {
return nil, validate.WrapErrorWithField(
errs.NewValidationError(
errors.New("invalid format"),
),
"Option",
)
}
return option, nil
}
func NewSSOOptionFromOption(option *Option) (*SSOOption, error) {
if option == nil {
return nil, fmt.Errorf("option cannot be nil")
}
ssooption, err := NewSSOOptionFromJSON([]byte(option.Value.String()))
if err != nil {
return nil, validate.WrapErrorWithField(
errs.NewValidationError(
errors.New("invalid format"),
),
"SSOOption",
)
}
return ssooption, nil
}
func (l *SSOOption) ToJSON() ([]byte, error) {
return json.Marshal(l)
}
func (l *SSOOption) ToOption() (*Option, error) {
json, err := l.ToJSON()
if err != nil {
return nil, errs.Wrap(err)
}
str, err := vo.NewOptionalString1MB(string(json))
if err != nil {
return nil, errs.Wrap(err)
}
return &Option{
Key: *vo.NewString64Must(data.OptionKeyAdminSSOLogin),
Value: *str,
}, nil
}
+153
View File
@@ -0,0 +1,153 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
const SYSTEM_USER_ID = "3eb19071-fbbb-4736-9991-02ba532a7849"
// User is a user of the system, including the company and role
type User struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
Name nullable.Nullable[vo.UserFullname] `json:"name"`
Username nullable.Nullable[vo.Username] `json:"username"`
Email nullable.Nullable[vo.Email] `json:"email"`
RequirePasswordRenew nullable.Nullable[bool] `json:"requirePasswordRenew"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Company *Company `json:"company"`
RoleID nullable.Nullable[uuid.UUID] `json:"roleID"`
Role *Role `json:"role"`
SSOID nullable.Nullable[string] `json:"ssoID"`
// apiKey is only get/set externally from this and never output except when created
}
// Validate checks if the user has a valid state
func (u *User) Validate() error {
if err := validate.NullableFieldRequired("name", u.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("username", u.Username); err != nil {
return err
}
if err := validate.NullableFieldRequired("email", u.Email); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (u *User) ToDBMap() map[string]any {
m := map[string]any{}
if u.Name.IsSpecified() {
m["name"] = nil
if name, err := u.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if u.Username.IsSpecified() {
m["username"] = nil
if username, err := u.Username.Get(); err == nil {
m["username"] = username.String()
}
}
if u.Email.IsSpecified() {
m["email"] = nil
if email, err := u.Email.Get(); err == nil {
m["email"] = email.String()
}
}
if u.RequirePasswordRenew.IsSpecified() {
m["require_password_renew"] = nil
if requirePasswordRenew, err := u.RequirePasswordRenew.Get(); err == nil {
m["require_password_renew"] = requirePasswordRenew
}
}
if u.CompanyID.IsSpecified() {
m["company_id"] = nil
if companyID, err := u.CompanyID.Get(); err == nil {
m["company_id"] = companyID
}
}
if u.RoleID.IsSpecified() {
m["role_id"] = nil
if roleID, err := u.RoleID.Get(); err == nil {
m["role_id"] = roleID
}
}
if u.SSOID.IsSpecified() {
m["sso_id"] = nil
if ssoID, err := u.SSOID.Get(); err == nil {
m["sso_id"] = ssoID
}
}
return m
}
// UserUpsertRequest is a request for creating a new user
type UserUpsertRequest struct {
Username vo.Username `json:"username"`
Password vo.ReasonableLengthPassword `json:"password"`
Email vo.Email `json:"email"`
Fullname vo.UserFullname `json:"fullname"`
}
// UserChangeEmailRequest is a request for changing the email of a user
type UserChangeEmailRequest struct {
Email vo.Email `json:"email"`
}
// UserChangeFullnameRequest is the change fullname request
type UserChangeFullnameRequest struct {
NewFullname vo.UserFullname `json:"fullname"`
}
type InvalidateAllSessionRequest struct {
UserID *uuid.UUID `json:"userID"`
}
// UserChangePasswordRequest is a request for changing password
type UserChangePasswordRequest struct {
CurrentPassword vo.ReasonableLengthPassword `json:"currentPassword" binding:"required"`
NewPassword vo.ReasonableLengthPassword `json:"newPassword" binding:"required"`
}
// UserChangeUsernameOnLoggedInRequest is the change username request
type UserChangeUsernameOnLoggedInRequest struct {
NewUsername vo.Username `json:"username"`
}
// NewUser creates a new user which is used for internal system actions and
// cant not be used to login or by a human.
func NewSystemUser() (*User, error) {
role := &Role{
Name: data.RoleSystem,
}
id := uuid.MustParse(SYSTEM_USER_ID)
return &User{
ID: nullable.NewNullableWithValue(id),
Name: nullable.NewNullableWithValue(*vo.NewUserFullnameMust("system")),
Username: nullable.NewNullableWithValue(*vo.NewUsernameMust("system")),
Email: nullable.NewNullableWithValue(
*vo.NewEmailMust("system@example.com"),
),
RequirePasswordRenew: nullable.NewNullableWithValue(false),
Company: nil,
Role: role,
}, nil
}
type APIUser struct {
APIKeyHash [32]byte
ID *uuid.UUID
}
+61
View File
@@ -0,0 +1,61 @@
package model
import (
"time"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
)
// Webhook is a gorm data model for webhooks
type Webhook struct {
ID nullable.Nullable[uuid.UUID] `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
Name nullable.Nullable[vo.String127] `json:"name"`
URL nullable.Nullable[vo.String1024] `json:"url"`
Secret nullable.Nullable[vo.OptionalString1024] `json:"secret"`
}
// Validate runs the validations for this struct
func (w *Webhook) Validate() error {
if err := validate.NullableFieldRequired("name", w.Name); err != nil {
return err
}
if err := validate.NullableFieldRequired("secret", w.URL); err != nil {
return err
}
return nil
}
// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (w *Webhook) ToDBMap() map[string]any {
m := map[string]any{}
if w.Name.IsSpecified() {
m["name"] = nil
if name, err := w.Name.Get(); err == nil {
m["name"] = name.String()
}
}
if w.URL.IsSpecified() {
m["url"] = nil
if url, err := w.URL.Get(); err == nil {
m["url"] = url.String()
}
}
if w.Secret.IsSpecified() {
m["secret"] = nil
if secret, err := w.Secret.Get(); err == nil {
m["secret"] = secret.String()
}
}
if v, err := w.CompanyID.Get(); err == nil {
m["company_id"] = v.String()
}
return m
}