Files
phishingclub/backend/controller/recipient.go
Ronni Skansing 170f92aa72 added status modal after import recipients
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-12-04 11:23:52 +01:00

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{})
}