mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-02-12 16:12:44 +00:00
525 lines
12 KiB
Go
525 lines
12 KiB
Go
package controller
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/csv"
|
|
"fmt"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/oapi-codegen/nullable"
|
|
"github.com/phishingclub/phishingclub/cache"
|
|
"github.com/phishingclub/phishingclub/database"
|
|
"github.com/phishingclub/phishingclub/model"
|
|
"github.com/phishingclub/phishingclub/repository"
|
|
"github.com/phishingclub/phishingclub/service"
|
|
"github.com/phishingclub/phishingclub/utils"
|
|
)
|
|
|
|
// recipientColumnByMap is a map between the frontend and the backend
|
|
// so the frontend has user friendly names instead of direct references
|
|
// to the database schema
|
|
// this is tied to a slice in the repository package
|
|
var recipientColumnByMap = map[string]string{
|
|
"created_at": repository.TableColumn(database.RECIPIENT_TABLE, "created_at"),
|
|
"updated_at": repository.TableColumn(database.RECIPIENT_TABLE, "updated_at"),
|
|
"email": repository.TableColumn(database.RECIPIENT_TABLE, "email"),
|
|
"phone": repository.TableColumn(database.RECIPIENT_TABLE, "phone"),
|
|
"extra identifier": repository.TableColumn(database.RECIPIENT_TABLE, "extra_identifier"),
|
|
"first_name": repository.TableColumn(database.RECIPIENT_TABLE, "first_name"),
|
|
"last_name": repository.TableColumn(database.RECIPIENT_TABLE, "last_name"),
|
|
"position": repository.TableColumn(database.RECIPIENT_TABLE, "position"),
|
|
"department": repository.TableColumn(database.RECIPIENT_TABLE, "department"),
|
|
"city": repository.TableColumn(database.RECIPIENT_TABLE, "city"),
|
|
"country": repository.TableColumn(database.RECIPIENT_TABLE, "country"),
|
|
"misc": repository.TableColumn(database.RECIPIENT_TABLE, "misc"),
|
|
"repeat_offender": "is_repeat_offender", // Special case - don't use TableColumn
|
|
}
|
|
|
|
var recipientCampaignEventColumnMap = utils.MergeStringMaps(
|
|
campaignEventColumns,
|
|
map[string]string{
|
|
"event": repository.TableColumnName(database.EVENT_TABLE),
|
|
"created": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "created_at"),
|
|
"campaign": repository.TableColumn(database.CAMPAIGN_TABLE, "name"),
|
|
},
|
|
)
|
|
|
|
// Recipient is a Recipient controller
|
|
type Recipient struct {
|
|
Common
|
|
RecipientService *service.Recipient
|
|
}
|
|
|
|
// Create inserts a new recipient
|
|
func (r *Recipient) Create(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
var req model.Recipient
|
|
if ok := r.handleParseRequest(g, &req); !ok {
|
|
return
|
|
}
|
|
// save recipient
|
|
id, err := r.RecipientService.Create(
|
|
g.Request.Context(),
|
|
session,
|
|
&req,
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(
|
|
g,
|
|
gin.H{
|
|
"id": id.String(),
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetCampaignEvents gets all campaign events by recipient id and campaign id
|
|
// gets all events if campaign id is nil
|
|
func (r *Recipient) GetCampaignEvents(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
recipientID, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// optional param
|
|
var campaignID *uuid.UUID
|
|
cid, err := uuid.Parse(g.Query("campaignID"))
|
|
if err == nil {
|
|
campaignID = &cid
|
|
}
|
|
queryArgs, ok := r.handleQueryArgs(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
queryArgs.DefaultSortByCreatedAt()
|
|
// remap query args
|
|
queryArgs.RemapOrderBy(recipientCampaignEventColumnMap)
|
|
// get events
|
|
events, err := r.RecipientService.GetAllCampaignEvents(
|
|
g.Request.Context(),
|
|
session,
|
|
recipientID,
|
|
campaignID,
|
|
queryArgs,
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, events)
|
|
}
|
|
|
|
// Export outputs a zip with recipient, groups and all events related to the recipient
|
|
func (r *Recipient) Export(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
recipientID, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// get the recipient
|
|
recp, err := r.RecipientService.GetByID(
|
|
g,
|
|
session,
|
|
recipientID,
|
|
&repository.RecipientOption{
|
|
WithCompany: true,
|
|
WithGroups: true,
|
|
},
|
|
)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
recipientBuffer := &bytes.Buffer{}
|
|
recipientWriter := csv.NewWriter(recipientBuffer)
|
|
recpHeaders := []string{
|
|
"Created at",
|
|
"Updated at",
|
|
"Email",
|
|
"Phone",
|
|
"Extra Identifier",
|
|
"Name",
|
|
"Position",
|
|
"Department",
|
|
"City",
|
|
"Country",
|
|
"Misc",
|
|
}
|
|
groups, _ := recp.Groups.Get()
|
|
for i := range groups {
|
|
recpHeaders = append(recpHeaders, fmt.Sprintf("Group %d", i+1))
|
|
}
|
|
err = recipientWriter.Write(recpHeaders)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
row := []string{
|
|
utils.CSVFromDate(recp.CreatedAt),
|
|
utils.CSVFromDate(recp.UpdatedAt),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Email)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Phone)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.ExtraIdentifier)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.FirstName)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.LastName)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Position)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Department)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.City)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Country)),
|
|
utils.CSVRemoveFormulaStart(utils.NullableToString(recp.Misc)),
|
|
}
|
|
for _, group := range groups {
|
|
row = append(row, group.Name.MustGet().String())
|
|
}
|
|
err = recipientWriter.Write(row)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
recipientWriter.Flush()
|
|
|
|
queryArgs, ok := r.handleQueryArgs(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
queryArgs.DefaultSortByCreatedAt()
|
|
// remap query args
|
|
queryArgs.RemapOrderBy(recipientCampaignEventColumnMap)
|
|
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
|
if sortOrder == "desc" {
|
|
queryArgs.Desc = true
|
|
}
|
|
|
|
// get all rows
|
|
queryArgs.Limit = 0
|
|
queryArgs.Offset = 0
|
|
// get events
|
|
events, err := r.RecipientService.GetAllCampaignEvents(
|
|
g.Request.Context(),
|
|
session,
|
|
recipientID,
|
|
nil,
|
|
queryArgs,
|
|
)
|
|
// handle response
|
|
eventsBuffer := &bytes.Buffer{}
|
|
eventsWriter := csv.NewWriter(eventsBuffer)
|
|
|
|
headers := []string{
|
|
"Created at",
|
|
"Campaign",
|
|
"IP",
|
|
"User-Agent",
|
|
"Event Details",
|
|
"Event",
|
|
}
|
|
err = eventsWriter.Write(headers)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
for _, event := range events.Rows {
|
|
row := []string{}
|
|
row = []string{
|
|
utils.CSVFromDate(event.CreatedAt),
|
|
utils.CSVRemoveFormulaStart(event.CampaignName),
|
|
utils.CSVRemoveFormulaStart(event.IP.String()),
|
|
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
|
utils.CSVRemoveFormulaStart(event.Data.String()),
|
|
utils.CSVRemoveFormulaStart(cache.EventNameByID[event.EventID.String()]),
|
|
}
|
|
err = eventsWriter.Write(row)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
}
|
|
eventsWriter.Flush()
|
|
|
|
// create ZIP file in memory
|
|
zipBuffer := new(bytes.Buffer)
|
|
zipWriter := zip.NewWriter(zipBuffer)
|
|
zipFileName := fmt.Sprintf("recipient_export_%s.zip", recp.Email.MustGet().String())
|
|
|
|
// add events to zip
|
|
{
|
|
f, err := zipWriter.Create("recipient.csv")
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
_, err = f.Write(recipientBuffer.Bytes())
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
}
|
|
// add events to zip
|
|
{
|
|
f, err := zipWriter.Create("events.csv")
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
_, err = f.Write(eventsBuffer.Bytes())
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
}
|
|
// close zip
|
|
err = zipWriter.Close()
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
|
|
r.responseWithZIP(g, zipBuffer, zipFileName)
|
|
}
|
|
|
|
// GetRepeatOffenderCount gets the repeat offender count
|
|
func (r *Recipient) GetRepeatOffenderCount(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// parse request
|
|
companyID := companyIDFromRequestQuery(g)
|
|
|
|
// get count
|
|
count, err := r.RecipientService.GetRepeatOffenderCount(
|
|
g.Request.Context(),
|
|
session,
|
|
companyID,
|
|
)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
|
|
r.Response.OK(g, count)
|
|
}
|
|
|
|
// GetOrphaned gets all recipients that are not in any group
|
|
func (r *Recipient) GetOrphaned(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
companyID := companyIDFromRequestQuery(g)
|
|
queryArgs, ok := r.handleQueryArgs(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
queryArgs.DefaultSortBy("first_name")
|
|
// remap query args
|
|
queryArgs.RemapOrderBy(recipientColumnByMap)
|
|
// get orphaned recipients
|
|
recipients, err := r.RecipientService.GetOrphaned(
|
|
g.Request.Context(),
|
|
companyID,
|
|
session,
|
|
&repository.RecipientOption{
|
|
QueryArgs: queryArgs,
|
|
},
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, recipients)
|
|
}
|
|
|
|
// DeleteAllOrphaned deletes all recipients that are not in any group
|
|
func (r *Recipient) DeleteAllOrphaned(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
companyID := companyIDFromRequestQuery(g)
|
|
// delete orphaned recipients
|
|
count, err := r.RecipientService.DeleteAllOrphaned(
|
|
g.Request.Context(),
|
|
companyID,
|
|
session,
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, gin.H{
|
|
"count": count,
|
|
})
|
|
}
|
|
|
|
// GetAll gets all recipients
|
|
func (r *Recipient) GetAll(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
companyID := companyIDFromRequestQuery(g)
|
|
queryArgs, ok := r.handleQueryArgs(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
queryArgs.DefaultSortBy("first_name")
|
|
// remap query args
|
|
queryArgs.RemapOrderBy(recipientColumnByMap)
|
|
// get recipients
|
|
recipients, err := r.RecipientService.GetAll(
|
|
g.Request.Context(),
|
|
companyID,
|
|
session,
|
|
&repository.RecipientOption{
|
|
QueryArgs: queryArgs,
|
|
},
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, recipients)
|
|
}
|
|
|
|
// GetByID gets a recipient by id
|
|
func (r *Recipient) GetByID(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse id
|
|
id, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// get recipient
|
|
recipient, err := r.RecipientService.GetByID(
|
|
g.Request.Context(),
|
|
session,
|
|
id,
|
|
&repository.RecipientOption{
|
|
WithCompany: true,
|
|
WithGroups: true,
|
|
},
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, recipient)
|
|
}
|
|
|
|
// GetStatsByID gets a recipient campaign stats by id
|
|
func (r *Recipient) GetStatsByID(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse id
|
|
id, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// get recipient stats
|
|
stats, err := r.RecipientService.GetStatsByID(
|
|
g.Request.Context(),
|
|
session,
|
|
id,
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, stats)
|
|
}
|
|
|
|
// UpdateByID updates a recipient by id
|
|
func (r *Recipient) UpdateByID(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
id, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req model.Recipient
|
|
if ok := r.handleParseRequest(g, &req); !ok {
|
|
return
|
|
}
|
|
err := r.RecipientService.UpdateByID(
|
|
g.Request.Context(),
|
|
session,
|
|
id,
|
|
&req,
|
|
)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, gin.H{})
|
|
}
|
|
|
|
// Import imports recipients
|
|
func (r *Recipient) Import(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse request
|
|
var req struct {
|
|
Recipients []*model.Recipient `json:"recipients"`
|
|
CompanyID *uuid.UUID `json:"companyID"`
|
|
IgnoreOverwriteEmptyFields nullable.Nullable[bool] `json:"ignoreOverwriteEmptyFields"`
|
|
}
|
|
if ok := r.handleParseRequest(g, &req); !ok {
|
|
return
|
|
}
|
|
// IgnoreOverwriteEmptyFields default value is true
|
|
if !req.IgnoreOverwriteEmptyFields.IsSpecified() || req.IgnoreOverwriteEmptyFields.IsNull() {
|
|
req.IgnoreOverwriteEmptyFields = nullable.NewNullableWithValue(true)
|
|
}
|
|
result, err := r.RecipientService.Import(
|
|
g,
|
|
session,
|
|
req.Recipients,
|
|
req.IgnoreOverwriteEmptyFields.MustGet(),
|
|
req.CompanyID,
|
|
)
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, result)
|
|
}
|
|
|
|
// DeleteByID deletes a recipient by id
|
|
func (r *Recipient) DeleteByID(g *gin.Context) {
|
|
session, _, ok := r.handleSession(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// parse id
|
|
id, ok := r.handleParseIDParam(g)
|
|
if !ok {
|
|
return
|
|
}
|
|
// delete recipient
|
|
err := r.RecipientService.DeleteByID(g, session, id)
|
|
// handle response
|
|
if ok := r.handleErrors(g, err); !ok {
|
|
return
|
|
}
|
|
r.Response.OK(g, gin.H{})
|
|
}
|