mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-09 21:58:42 +02:00
Initial open source release
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// AllowDenyColumnsMap 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 AllowDenyColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.ALLOW_DENY_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.ALLOW_DENY_TABLE, "updated_at"),
|
||||
"hosting_website": repository.TableColumn(database.ALLOW_DENY_TABLE, "host_website"),
|
||||
"redirects": repository.TableColumn(database.ALLOW_DENY_TABLE, "redirect_url"),
|
||||
}
|
||||
|
||||
// AllowDeny is a controller
|
||||
type AllowDeny struct {
|
||||
Common
|
||||
AllowDenyService *service.AllowDeny
|
||||
}
|
||||
|
||||
// Create creates a new AllowDeny
|
||||
func (c *AllowDeny) Create(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.AllowDeny
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save
|
||||
id, err := c.AllowDenyService.Create(g, session, &req)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetAll gets AllowDenies
|
||||
func (c *AllowDeny) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByName()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get
|
||||
allowDenies, err := c.AllowDenyService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.AllowDenyOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
allowDenies,
|
||||
)
|
||||
}
|
||||
|
||||
// GetAllOverview gets AllowDenies
|
||||
func (c *AllowDeny) GetAllOverview(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByName()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
allowDenies, err := c.AllowDenyService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.AllowDenyOption{
|
||||
Fields: []string{
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"company_id",
|
||||
"name",
|
||||
"allowed",
|
||||
},
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
allowDenies,
|
||||
)
|
||||
}
|
||||
|
||||
// GetByID gets an AllowDeny by ID
|
||||
func (c *AllowDeny) GetByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get
|
||||
allowDeny, err := c.AllowDenyService.GetByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
allowDeny,
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateByID updates an AllowDeny
|
||||
func (c *AllowDeny) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.AllowDeny
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
|
||||
return
|
||||
}
|
||||
// update
|
||||
err := c.AllowDenyService.Update(g, session, id, &req)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteByID deletes an AllowDeny
|
||||
func (c *AllowDeny) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete
|
||||
err := c.AllowDenyService.DeleteByID(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// APISenderColumnsMap 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 APISenderColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.API_SENDER_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.API_SENDER_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.API_SENDER_TABLE, "name"),
|
||||
}
|
||||
|
||||
// APISender is a API sender controller
|
||||
type APISender struct {
|
||||
Common
|
||||
APISenderService *service.APISender
|
||||
}
|
||||
|
||||
// Create creates a new api sender
|
||||
func (a *APISender) Create(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.APISender
|
||||
if ok := a.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
id, err := a.APISenderService.Create(g, session, &req)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{"id": id.String()})
|
||||
}
|
||||
|
||||
// GetAll gets all api senders
|
||||
func (a *APISender) GetAll(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := a.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(APISenderColumnsMap)
|
||||
apiSenders, err := a.APISenderService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
repository.APISenderOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, apiSenders)
|
||||
}
|
||||
|
||||
// GetAllOverview gets all api senders with limited data
|
||||
func (a *APISender) GetAllOverview(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := a.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(APISenderColumnsMap)
|
||||
apiSenders, err := a.APISenderService.GetAllOverview(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
repository.APISenderOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, apiSenders)
|
||||
}
|
||||
|
||||
// GetByID gets a api sender by ID
|
||||
func (a *APISender) GetByID(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse reqeuest
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get api sender
|
||||
apiSender, err := a.APISenderService.GetByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&repository.APISenderOption{},
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, apiSender)
|
||||
}
|
||||
|
||||
// Update updates a api sender
|
||||
func (a *APISender) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.APISender
|
||||
if ok := a.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
err := a.APISenderService.UpdateByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeletebyID deletes a api sender by ID
|
||||
func (a *APISender) DeleteByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := a.APISenderService.DeleteByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// SendTest sends a api request test and outputs the api sender and response
|
||||
func (a *APISender) SendTest(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := a.APISenderService.SendTest(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// output the error
|
||||
if err != nil {
|
||||
a.Response.BadRequestMessage(g, err.Error())
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, data)
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// AssetOrderByMap 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 AssetsColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.ASSET_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.ASSET_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.ASSET_TABLE, "name"),
|
||||
"description": repository.TableColumn(database.ASSET_TABLE, "description"),
|
||||
"path": repository.TableColumn(database.ASSET_TABLE, "path"),
|
||||
}
|
||||
|
||||
// Asset is an static Asset controller
|
||||
type Asset struct {
|
||||
Common
|
||||
StaticAssetPath string
|
||||
DomainService *service.Domain
|
||||
OptionService *service.Option
|
||||
AssetService *service.Asset
|
||||
}
|
||||
|
||||
// GetContentByID get the content and mime type of an asset
|
||||
func (a *Asset) GetContentByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check permissions
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
_ = handleServerError(g, a.Response, err)
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
a.Response.Unauthorized(g)
|
||||
return
|
||||
}
|
||||
// get domain
|
||||
domain, err := vo.NewString255(g.Param("domain"))
|
||||
if err != nil {
|
||||
a.Logger.Errorw("invalid domain",
|
||||
"domain", domain,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Domain", err)
|
||||
return
|
||||
}
|
||||
// if the target is the global folder, use the global folder
|
||||
if domain.String() == data.ASSET_GLOBAL_FOLDER {
|
||||
// TODO this shold require special permissions or be prefixed with a special path
|
||||
// such as the company name or something that is prefixed
|
||||
_ = data.ASSET_GLOBAL_FOLDER
|
||||
}
|
||||
staticPath, err := securejoin.SecureJoin(a.StaticAssetPath, domain.String())
|
||||
if err != nil {
|
||||
a.Logger.Debugw("insecure path",
|
||||
"path", a.StaticAssetPath,
|
||||
"domain", domain.String(),
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
// get the file path
|
||||
pathDecoded, err := url.QueryUnescape(g.Param("path"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to decode path",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
|
||||
filePath, err := securejoin.SecureJoin(staticPath, pathDecoded)
|
||||
if err != nil {
|
||||
a.Logger.Debugw("insecure path",
|
||||
"path", pathDecoded,
|
||||
"error", err,
|
||||
)
|
||||
a.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
// check if the file exists
|
||||
a.Logger.Debugw("checking if asset exists",
|
||||
"path", filePath,
|
||||
)
|
||||
_, err = os.Stat(filePath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
a.Logger.Debugw("asset not found",
|
||||
"path", filePath,
|
||||
)
|
||||
a.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to get asset path info",
|
||||
"path", filePath,
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
// serve the file
|
||||
// #nosec
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to read asset",
|
||||
"path", filePath,
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
|
||||
fileExt := filepath.Ext(filePath)
|
||||
mimeType := ""
|
||||
switch fileExt {
|
||||
case ".html":
|
||||
mimeType = "text/html"
|
||||
case ".htm":
|
||||
mimeType = "text/html"
|
||||
case ".xhtml":
|
||||
mimeType = "application/xhtml+xml"
|
||||
default:
|
||||
mimeType = http.DetectContentType(content)
|
||||
}
|
||||
encodedContent := base64.StdEncoding.EncodeToString(content)
|
||||
|
||||
a.Response.OK(g, gin.H{
|
||||
"mimeType": mimeType,
|
||||
"file": encodedContent,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllForContext gets all static assets for a domain
|
||||
// and has a special case 'shared' to get all global assets
|
||||
func (a *Asset) GetAllForContext(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check permissions
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
_ = handleServerError(g, a.Response, err)
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
a.Response.Unauthorized(g)
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var domainID *uuid.UUID
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
domainParam := g.Param("domain")
|
||||
queryArgs, ok := a.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// set default sort by
|
||||
queryArgs.RemapOrderBy(AssetsColumnsMap)
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
a.Logger.Debugw("getting assets for domain",
|
||||
"domain", domainParam,
|
||||
"companyID", companyID,
|
||||
)
|
||||
// if there is no domain then it is a global asset request
|
||||
// else the domain name is the asset scope
|
||||
if len(domainParam) > 0 {
|
||||
domainName, err := vo.NewString255(domainParam)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("invalid domain",
|
||||
"domain", domainName,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Domain", err)
|
||||
return
|
||||
}
|
||||
// get the domains id and also check if the user has permission to retrieve it
|
||||
domain, err := a.DomainService.GetByName(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
domainName,
|
||||
&repository.DomainOption{},
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
did := domain.ID.MustGet()
|
||||
domainID = &did
|
||||
}
|
||||
// get assets
|
||||
a.Logger.Debugw("getting assets for domain by ID",
|
||||
"domainID", domainID,
|
||||
)
|
||||
assets, err := a.AssetService.GetAll(
|
||||
g,
|
||||
session,
|
||||
domainID,
|
||||
companyID,
|
||||
queryArgs,
|
||||
)
|
||||
// handle responses
|
||||
a.handleErrors(g, err)
|
||||
a.Response.OK(g, assets)
|
||||
}
|
||||
|
||||
// Create uploads an static asset
|
||||
func (a *Asset) Create(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// this is a form data request, so we must handle all fields manually as is it not parsed from the struct
|
||||
multipartData, err := g.MultipartForm()
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to get multipart form",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
if len(multipartData.File["files"]) == 0 {
|
||||
a.Logger.Debug("no files to upload")
|
||||
a.Response.BadRequestMessage(g, "No files selected")
|
||||
return
|
||||
}
|
||||
contextParam := g.PostForm("domain")
|
||||
// if no domain is set, use the global folder
|
||||
var domain *model.Domain
|
||||
// if a domain is supplied we look for its assets
|
||||
if len(contextParam) > 0 {
|
||||
// check that the domain exists
|
||||
name, err := vo.NewString255(contextParam)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("invalid domain name",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Domain", err)
|
||||
return
|
||||
}
|
||||
d, err := a.DomainService.GetByName(
|
||||
g,
|
||||
session,
|
||||
name,
|
||||
&repository.DomainOption{},
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
domain = d
|
||||
a.Logger.Debugw("uploading assets to domain",
|
||||
"domain", contextParam,
|
||||
)
|
||||
} else {
|
||||
a.Logger.Debug("uploading shared assets")
|
||||
}
|
||||
// map files to assets
|
||||
assets := []*model.Asset{}
|
||||
for _, file := range multipartData.File["files"] {
|
||||
// check max file size
|
||||
maxFile, err := a.OptionService.GetOption(g, session, data.OptionKeyMaxFileUploadSizeMB)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
ok, err := utils.CompareFileSizeFromString(file.Size, maxFile.Value.String())
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to compare file size",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
if !ok {
|
||||
a.Logger.Debugw("file too large",
|
||||
"filename", file.Filename,
|
||||
"size", file.Size,
|
||||
"maxSize", maxFile.Value.String(),
|
||||
)
|
||||
a.Response.ValidationFailed(
|
||||
g,
|
||||
"File",
|
||||
fmt.Errorf("file '%s' is too large", utils.ReadableFileName(file.Filename)),
|
||||
)
|
||||
return
|
||||
}
|
||||
// TODO multi user validate that the company id is the same as the session company id or that the session is a super admin
|
||||
// TODO can the creation of the ID be moved to the repo
|
||||
var domainID string
|
||||
if domain != nil {
|
||||
did := domain.ID.MustGet()
|
||||
domainID = did.String()
|
||||
}
|
||||
name, err := vo.NewOptionalString127(g.Request.PostFormValue("name"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse name",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Name", err)
|
||||
return
|
||||
}
|
||||
description, err := vo.NewOptionalString255(g.Request.PostFormValue("description"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse description",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Description", err)
|
||||
return
|
||||
}
|
||||
path, err := vo.NewRelativeFilePath(g.Request.PostFormValue("path"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse path",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "Path", err)
|
||||
return
|
||||
}
|
||||
companyID := nullable.NewNullNullable[uuid.UUID]()
|
||||
companyIDParam := g.PostForm("companyID")
|
||||
if len(companyIDParam) > 0 {
|
||||
cid, err := uuid.Parse(companyIDParam)
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse company id",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "CompanyID", err)
|
||||
}
|
||||
companyID.Set(cid)
|
||||
} else {
|
||||
companyID.SetNull()
|
||||
}
|
||||
|
||||
assetName := nullable.NewNullableWithValue(*name)
|
||||
assetDescription := nullable.NewNullableWithValue(*description)
|
||||
assetPath := nullable.NewNullableWithValue(*path)
|
||||
assetDomainID := nullable.NewNullNullable[uuid.UUID]()
|
||||
if len(domainID) > 0 {
|
||||
did, err := uuid.Parse(domainID)
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse domain id",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "DomainID", err)
|
||||
return
|
||||
}
|
||||
assetDomainID.Set(did)
|
||||
// if the asset belongs to a domain it must not be 'global' context
|
||||
if !companyID.IsSpecified() {
|
||||
a.Logger.Debugw(
|
||||
"cant add a shared asset to a company owned domain",
|
||||
"domainID", domainID,
|
||||
"domainOwnerCompanyID", companyID,
|
||||
)
|
||||
a.Response.ValidationFailed(
|
||||
g,
|
||||
"domainID",
|
||||
errors.New("cant add a shared asset to a company owned domain"),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
asset := model.Asset{
|
||||
Name: assetName,
|
||||
Description: assetDescription,
|
||||
Path: assetPath,
|
||||
File: *file,
|
||||
DomainID: assetDomainID,
|
||||
CompanyID: companyID,
|
||||
}
|
||||
if domain != nil {
|
||||
asset.DomainName = domain.Name
|
||||
}
|
||||
assets = append(assets, &asset)
|
||||
}
|
||||
// store the files on disk and in database
|
||||
ids, err := a.AssetService.Create(g, session, assets)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{
|
||||
"ids": ids,
|
||||
"files_uploaded": len(assets),
|
||||
})
|
||||
}
|
||||
|
||||
// GetByID gets an static asset by id
|
||||
func (a *Asset) GetByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the asset
|
||||
ctx := g.Request.Context()
|
||||
asset, err := a.AssetService.GetByID(ctx, session, id)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, asset)
|
||||
}
|
||||
|
||||
// UpdateByID updates an static asset by id
|
||||
func (a *Asset) UpdateByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.Asset
|
||||
if ok := a.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// update the asset
|
||||
ctx := g.Request.Context()
|
||||
err := a.AssetService.UpdateByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
req.Name,
|
||||
req.Description,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// RemoveByID removes an static asset
|
||||
// if the asset is a directory, it will be removed recursively
|
||||
func (a *Asset) RemoveByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// remove the asset
|
||||
ctx := g.Request.Context()
|
||||
err := a.AssetService.DeleteByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// AttachmentColumnsMap 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 AttachmentColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.ATTACHMENT_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.ATTACHMENT_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.ATTACHMENT_TABLE, "name"),
|
||||
"description": repository.TableColumn(database.ATTACHMENT_TABLE, "description"),
|
||||
"embedded content": repository.TableColumn(database.ATTACHMENT_TABLE, "embeddedContent"),
|
||||
"filename": repository.TableColumn(database.ATTACHMENT_TABLE, "filename"),
|
||||
}
|
||||
|
||||
// Attachment is an static Attachment controller
|
||||
type Attachment struct {
|
||||
Common
|
||||
StaticAttachmentPath string
|
||||
TemplateService *service.Template
|
||||
AttachmentService *service.Attachment
|
||||
OptionService *service.Option
|
||||
CompanyService *service.Company
|
||||
}
|
||||
|
||||
// GetContentByID returns the content and mime type of an attachment
|
||||
func (a *Attachment) GetContentByID(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the attachment
|
||||
ctx := g.Request.Context()
|
||||
attachment, err := a.AttachmentService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p := attachment.Path.MustGet().String()
|
||||
// serve the file
|
||||
// #nosec
|
||||
content, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to read file",
|
||||
"path", p,
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
|
||||
fileExt := filepath.Ext(p)
|
||||
mimeType := ""
|
||||
switch fileExt {
|
||||
case ".html":
|
||||
mimeType = "text/html"
|
||||
case ".htm":
|
||||
mimeType = "text/html"
|
||||
case ".xhtml":
|
||||
mimeType = "application/xhtml+xml"
|
||||
default:
|
||||
mimeType = http.DetectContentType(content)
|
||||
}
|
||||
// get by id is only used for admin viewing of an attachemnt, so all
|
||||
// embedded content must contain example data
|
||||
if attachment.EmbeddedContent.MustGet() {
|
||||
// build email
|
||||
domain := &model.Domain{
|
||||
Name: nullable.NewNullableWithValue(
|
||||
*vo.NewString255Must("example.test"),
|
||||
),
|
||||
}
|
||||
recipient := model.NewRecipientExample()
|
||||
campaignRecipient := model.CampaignRecipient{
|
||||
ID: nullable.NewNullableWithValue(
|
||||
uuid.New(),
|
||||
),
|
||||
Recipient: recipient,
|
||||
}
|
||||
email := model.NewEmailExample()
|
||||
// hacky
|
||||
email.Content = nullable.NewNullableWithValue(
|
||||
*vo.NewUnsafeOptionalString1MB(string(content)),
|
||||
)
|
||||
apiSender := model.NewAPISenderExample()
|
||||
b, err := a.TemplateService.CreateMailBody(
|
||||
"id",
|
||||
"/foo",
|
||||
domain,
|
||||
&campaignRecipient,
|
||||
email,
|
||||
apiSender,
|
||||
)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to appy template to attachment",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
content = []byte(b)
|
||||
}
|
||||
a.Response.OK(g, gin.H{
|
||||
"mimeType": mimeType,
|
||||
"file": base64.StdEncoding.EncodeToString(content),
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllForContext gets all attachments for a domain
|
||||
// and has a special case 'shared' to get all global attachments
|
||||
func (a *Attachment) GetAllForContext(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check permissions
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
a.Logger.Errorw("failed to check permissions",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
// TODO audit log
|
||||
_ = handleAuthorizationError(g, a.Response, errs.ErrAuthorizationFailed)
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// if there is no companyID then it is a global attachment request
|
||||
// else the company context name is the attachment scope
|
||||
if companyID != nil {
|
||||
// get the company id and to check if the user has permission to retrieve it
|
||||
_, err := a.CompanyService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
queryArgs, ok := a.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(AttachmentColumnsMap)
|
||||
// get attachments
|
||||
a.Logger.Debugw("getting attachments for company ID",
|
||||
"companyID", companyID,
|
||||
)
|
||||
attachments, err := a.AttachmentService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
queryArgs,
|
||||
)
|
||||
// handle responses
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, attachments)
|
||||
}
|
||||
|
||||
// Create uploads an attachment
|
||||
func (a *Attachment) Create(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
multipartData, err := g.MultipartForm()
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to get multipart form",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
if len(multipartData.File["files"]) == 0 {
|
||||
a.Logger.Debug("no files to upload")
|
||||
a.Response.BadRequestMessage(g, "No files selected")
|
||||
return
|
||||
}
|
||||
companyID := nullable.NewNullNullable[uuid.UUID]()
|
||||
companyIDParam := g.PostForm("companyID")
|
||||
if len(companyIDParam) > 0 {
|
||||
cid, err := uuid.Parse(companyIDParam)
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse company id",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "companyID", err)
|
||||
return
|
||||
}
|
||||
companyID.Set(cid)
|
||||
}
|
||||
nameParam, err := vo.NewOptionalString127(g.PostForm("name"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse name",
|
||||
"name", g.PostForm("name"),
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "name", err)
|
||||
return
|
||||
}
|
||||
name := nullable.NewNullableWithValue(*nameParam)
|
||||
descriptionParam, err := vo.NewOptionalString255(g.PostForm("description"))
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse description",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "description", err)
|
||||
return
|
||||
}
|
||||
description := nullable.NewNullableWithValue(*descriptionParam)
|
||||
embeddedContent := nullable.NewNullableWithValue(false)
|
||||
embeddedContentString := g.PostForm("embeddedContent")
|
||||
if strings.ToLower(embeddedContentString) == "true" {
|
||||
embeddedContent.Set(true)
|
||||
}
|
||||
attachments := []*model.Attachment{}
|
||||
for _, file := range multipartData.File["files"] {
|
||||
// TODO multi user validate that the company id is the same as the session company id or that the session is a super admin
|
||||
// check max file size
|
||||
maxFile, err := a.OptionService.GetOption(g, session, data.OptionKeyMaxFileUploadSizeMB)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
ok, err := utils.CompareFileSizeFromString(file.Size, maxFile.Value.String())
|
||||
if err != nil {
|
||||
a.Logger.Errorw("failed to compare file size",
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
if !ok {
|
||||
a.Logger.Debugw("file too large",
|
||||
"filename", file.Filename,
|
||||
"size", file.Size,
|
||||
"maxSize", maxFile.Value.String(),
|
||||
)
|
||||
a.Response.ValidationFailed(
|
||||
g,
|
||||
"File",
|
||||
fmt.Errorf("'%s' is too large", utils.ReadableFileName(file.Filename)),
|
||||
)
|
||||
return
|
||||
}
|
||||
fileNameParam, err := vo.NewFileName(file.Filename)
|
||||
if err != nil {
|
||||
a.Logger.Debugw("failed to parse filename",
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "filename", err)
|
||||
return
|
||||
}
|
||||
fileName := nullable.NewNullableWithValue(*fileNameParam)
|
||||
|
||||
attachment := model.Attachment{
|
||||
CompanyID: companyID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
EmbeddedContent: embeddedContent,
|
||||
File: file,
|
||||
FileName: fileName,
|
||||
}
|
||||
if err := attachment.Validate(); err != nil {
|
||||
a.Logger.Debugw("failed to validate attachment",
|
||||
"attachmentName", name,
|
||||
"error", err,
|
||||
)
|
||||
a.Response.ValidationFailed(g, "attachment", err)
|
||||
return
|
||||
}
|
||||
attachments = append(attachments, &attachment)
|
||||
}
|
||||
// store the files on disk and in database
|
||||
createdIDs, err := a.AttachmentService.Create(
|
||||
g,
|
||||
session,
|
||||
attachments,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{
|
||||
"ids": createdIDs,
|
||||
"files_uploaded": len(attachments),
|
||||
})
|
||||
}
|
||||
|
||||
// GetByID gets an static attachment by id
|
||||
func (a *Attachment) GetByID(g *gin.Context) {
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the attachment
|
||||
ctx := g.Request.Context()
|
||||
attachment, err := a.AttachmentService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, attachment)
|
||||
}
|
||||
|
||||
// UpdateByID updates an static attachment by id
|
||||
func (a *Attachment) UpdateByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.Attachment
|
||||
if ok := a.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update the attachment
|
||||
ctx := g.Request.Context()
|
||||
err := a.AttachmentService.UpdateByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// RemoveByID removes an static attachment
|
||||
// if the attachment is a directory, it will be removed recursively
|
||||
func (a *Attachment) RemoveByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := a.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := a.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// remove the attachment
|
||||
ctx := g.Request.Context()
|
||||
err := a.AttachmentService.DeleteByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := a.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
a.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,927 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/build"
|
||||
"github.com/phishingclub/phishingclub/cache"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/embedded"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
)
|
||||
|
||||
// allowedCampaignColumns 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 allowedCampaignColumns = map[string]string{
|
||||
"created_at": repository.TableColumn(database.CAMPAIGN_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.CAMPAIGN_TABLE, "updated_at"),
|
||||
"closed_at": repository.TableColumn(database.CAMPAIGN_TABLE, "closed_at"),
|
||||
"close_at": repository.TableColumn(database.CAMPAIGN_TABLE, "close_at"),
|
||||
"anonymized_at": repository.TableColumn(database.CAMPAIGN_TABLE, "anonymized_at"),
|
||||
"is_test": repository.TableColumn(database.CAMPAIGN_TABLE, "is_test"),
|
||||
"send_start_at": repository.TableColumn(database.CAMPAIGN_TABLE, "send_start_at"),
|
||||
"send_end_at": repository.TableColumn(database.CAMPAIGN_TABLE, "send_end_at"),
|
||||
"template": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "name"),
|
||||
"name": repository.TableColumn(database.CAMPAIGN_TABLE, "name"),
|
||||
}
|
||||
|
||||
// campaignEventColumns 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 campaignEventColumns = map[string]string{
|
||||
"created_at": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "updated_at"),
|
||||
"details": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "data"),
|
||||
"ip": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "ip_address"),
|
||||
"user-agent": repository.TableColumn(database.CAMPAIGN_EVENT_TABLE, "user_agent"),
|
||||
"email": repository.TableColumn(database.RECIPIENT_TABLE, "email"),
|
||||
"first_name": repository.TableColumn(database.RECIPIENT_TABLE, "first_name"),
|
||||
"last_name": repository.TableColumn(database.RECIPIENT_TABLE, "last_name"),
|
||||
"event": repository.TableColumn(database.EVENT_TABLE, "name"),
|
||||
}
|
||||
|
||||
// allowedCampaignRecipientColumns 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 allowedCampaignRecipientColumns = map[string]string{
|
||||
"created_at": "campaign_recipients.created_at",
|
||||
"updated_at": "campaign_recipients.updated_at",
|
||||
"send_at": "campaign_recipients.send_at",
|
||||
"sent_at": "campaign_recipients.sent_at",
|
||||
"cancelled_at": "campaign_recipients.cancelled_at",
|
||||
"status": "campaign_recipients.notable_event_id",
|
||||
"first_name": "recipients.first_name",
|
||||
"last_name": "recipients.last_name",
|
||||
"email": "recipients.email",
|
||||
}
|
||||
|
||||
// Campaign is a Campaign controller
|
||||
type Campaign struct {
|
||||
Common
|
||||
CampaignService *service.Campaign
|
||||
}
|
||||
|
||||
// CloseCampaignByID closes campaign
|
||||
func (c *Campaign) CloseCampaignByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// close campaigns
|
||||
err := c.CampaignService.CloseCampaignByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle responses
|
||||
if errors.Is(err, errs.ErrCampaignAlreadyClosed) {
|
||||
c.Response.ValidationFailed(g, "", err)
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// Create creates a new campaign
|
||||
func (c *Campaign) Create(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.Campaign
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// create and schedule the campaign
|
||||
id, err := c.CampaignService.Create(g.Request.Context(), session, &req)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{
|
||||
"id": id.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllEventTypes gets all event types
|
||||
func (c *Campaign) GetAllEventTypes(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check permissions
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
_ = handleServerError(g, c.Response, err)
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
c.Response.Unauthorized(g)
|
||||
return
|
||||
}
|
||||
// get all event names
|
||||
// we pick them out from the in memory cache
|
||||
ev := []gin.H{}
|
||||
for name, id := range cache.EventIDByName {
|
||||
ev = append(ev, gin.H{
|
||||
"id": id,
|
||||
"name": name,
|
||||
})
|
||||
}
|
||||
c.Response.OK(g, ev)
|
||||
}
|
||||
|
||||
// GetByID gets a campaign by its id
|
||||
func (c *Campaign) GetByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the campaign that needs to be updated
|
||||
campaign, err := c.CampaignService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&repository.CampaignOption{
|
||||
WithRecipientGroups: true,
|
||||
WithAllowDeny: true,
|
||||
WithDenyPage: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaign)
|
||||
}
|
||||
|
||||
// GetByName gets a campaign by name
|
||||
func (c *Campaign) GetByName(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
name := g.Param("name")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the campaign that needs to be updated
|
||||
campaign, err := c.CampaignService.GetByName(
|
||||
g,
|
||||
session,
|
||||
name,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
WithRecipientGroups: true,
|
||||
WithAllowDeny: true,
|
||||
WithDenyPage: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaign)
|
||||
}
|
||||
|
||||
// GetResultStats get campaign result stats
|
||||
func (c *Campaign) GetResultStats(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get
|
||||
stats, err := c.CampaignService.GetResultStats(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, stats)
|
||||
}
|
||||
|
||||
// GetCampaignStats get campaign stats
|
||||
// if no company id is provided it gets the global stats including all companies
|
||||
func (c *Campaign) GetStats(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get
|
||||
stats, err := c.CampaignService.GetStats(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, stats)
|
||||
}
|
||||
|
||||
// GetAll gets all campaigns with pagination
|
||||
func (c *Campaign) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
// get all campaigns
|
||||
campaigns, err := c.CampaignService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCampaignTemplate: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaigns)
|
||||
|
||||
}
|
||||
|
||||
// GetAll gets all campaigns within dates
|
||||
func (c *Campaign) GetAllWithinDates(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
// get start and end date for query
|
||||
startDate, err := time.Parse(time.RFC3339Nano, g.Query("start"))
|
||||
if err != nil {
|
||||
c.Response.ValidationFailed(g, "start", err)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(time.RFC3339Nano, g.Query("end"))
|
||||
if err != nil {
|
||||
c.Response.ValidationFailed(g, "end", err)
|
||||
return
|
||||
}
|
||||
// get all campaigns
|
||||
campaigns, err := c.CampaignService.GetAllWithinDates(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
startDate,
|
||||
endDate,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCampaignTemplate: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaigns)
|
||||
|
||||
}
|
||||
|
||||
// GetAllActive gets all active campaigns with pagination
|
||||
// if no company id is given it gets all globals including company
|
||||
func (c *Campaign) GetAllActive(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
if queryArgs.OrderBy == "" {
|
||||
queryArgs.OrderBy = "send_start_at"
|
||||
queryArgs.Desc = false
|
||||
}
|
||||
// get all campaigns
|
||||
campaigns, err := c.CampaignService.GetAllActive(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
WithCampaignTemplate: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaigns)
|
||||
}
|
||||
|
||||
// GetAllUpcoming gets all upcoming campaigns with pagination
|
||||
// if no company id is given it gets all globals including company
|
||||
func (c *Campaign) GetAllUpcoming(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
if queryArgs.OrderBy == "" {
|
||||
queryArgs.OrderBy = "send_start_at"
|
||||
queryArgs.Desc = false
|
||||
}
|
||||
// get all campaigns
|
||||
campaigns, err := c.CampaignService.GetAllUpcoming(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
WithCampaignTemplate: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaigns)
|
||||
}
|
||||
|
||||
// GetAllFinished gets all finished campaigns with pagination
|
||||
// if no company id is given it gets all globals including company
|
||||
func (c *Campaign) GetAllFinished(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
if queryArgs.OrderBy == "" {
|
||||
queryArgs.OrderBy = "send_start_at"
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
// get all campaigns
|
||||
campaigns, err := c.CampaignService.GetAllFinished(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.CampaignOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
WithCampaignTemplate: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaigns)
|
||||
}
|
||||
|
||||
// GetEventsByCampaignID gets events by campaign id
|
||||
func (c *Campaign) GetEventsByCampaignID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
// remap query args
|
||||
queryArgs.RemapOrderBy(campaignEventColumns)
|
||||
// set default sort order to desc
|
||||
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
||||
if sortOrder == "desc" {
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
var since *time.Time
|
||||
s, err := time.Parse(time.RFC3339Nano, g.Query("since"))
|
||||
if err == nil {
|
||||
since = &s
|
||||
}
|
||||
// get events by campaign id
|
||||
events, err := c.CampaignService.GetEventsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
queryArgs,
|
||||
since,
|
||||
nil,
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, events)
|
||||
}
|
||||
|
||||
// ExportEventsAsCSV exports a all campaign events as a CSV
|
||||
func (c *Campaign) ExportEventsAsCSV(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByCreatedAt()
|
||||
queryArgs.RemapOrderBy(campaignEventColumns)
|
||||
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
||||
if sortOrder == "desc" {
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
// get all rows
|
||||
queryArgs.Limit = 0
|
||||
queryArgs.Offset = 0
|
||||
// get events by campaign id
|
||||
events, err := c.CampaignService.GetEventsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
queryArgs,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
|
||||
headers := []string{
|
||||
"Created at",
|
||||
"Recipient name",
|
||||
"Recipient email",
|
||||
"Event name",
|
||||
"Event Details",
|
||||
"User-Agent",
|
||||
"IP",
|
||||
}
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, event := range events.Rows {
|
||||
row := []string{}
|
||||
// if the recipient has been deleted or anonymized
|
||||
if event.Recipient == nil {
|
||||
row = []string{
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
"anonymized",
|
||||
"anonymized",
|
||||
utils.CSVRemoveFormulaStart(cache.EventNameByID[event.EventID.String()]),
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
} else {
|
||||
row = []string{
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.FirstName.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.LastName.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.Email.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(cache.EventNameByID[event.EventID.String()]),
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.responseWithCSV(g, buffer, writer, "campaign_events.csv")
|
||||
}
|
||||
|
||||
// ExportSubmissionsAsCSV exports all campaign submissions as a CSV
|
||||
func (c *Campaign) ExportSubmissionsAsCSV(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByCreatedAt()
|
||||
queryArgs.RemapOrderBy(campaignEventColumns)
|
||||
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
||||
if sortOrder == "desc" {
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
// get all rows
|
||||
queryArgs.Limit = 0
|
||||
queryArgs.Offset = 0
|
||||
|
||||
// filter for submission events only
|
||||
submissionEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA]
|
||||
eventTypeFilter := []string{submissionEventID.String()}
|
||||
|
||||
// get submission events by campaign id
|
||||
events, err := c.CampaignService.GetEventsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
queryArgs,
|
||||
nil,
|
||||
eventTypeFilter,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
|
||||
headers := []string{
|
||||
"Submitted at",
|
||||
"Recipient first name",
|
||||
"Recipient last name",
|
||||
"Recipient email",
|
||||
"Submitted data",
|
||||
"User-Agent",
|
||||
"IP",
|
||||
}
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, event := range events.Rows {
|
||||
row := []string{}
|
||||
// if the recipient has been deleted or anonymized
|
||||
if event.Recipient == nil {
|
||||
row = []string{
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
"anonymized",
|
||||
"anonymized",
|
||||
"anonymized",
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
} else {
|
||||
row = []string{
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.FirstName.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.LastName.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(event.Recipient.Email.MustGet().String()),
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.responseWithCSV(g, buffer, writer, "campaign_submissions.csv")
|
||||
}
|
||||
|
||||
func (c *Campaign) GetCampaignEmail(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get email
|
||||
email, err := c.CampaignService.GetCampaignEmailBody(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, email)
|
||||
}
|
||||
|
||||
// GetCampaignURL gets a recipient landing page URL
|
||||
func (c *Campaign) GetCampaignURL(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
url, err := c.CampaignService.GetLandingPageURLByCampaignRecipientID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, url)
|
||||
}
|
||||
|
||||
// GetRecipientsByCampaignID gets recipients by campaign id
|
||||
func (c *Campaign) GetRecipientsByCampaignID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// endpoints is handled a bit differently and allows to
|
||||
// fetch an unlimited amount of rows if no offset and limit is set.
|
||||
// TODO this endpoint should be changed to a Result<T> so we fetch the rows as needed.
|
||||
offset := g.DefaultQuery("offset", "")
|
||||
limit := g.DefaultQuery("limit", "")
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// special case to retrieve ALL rows
|
||||
if offset == "" && limit == "" {
|
||||
queryArgs.Offset = 0
|
||||
queryArgs.Limit = 0
|
||||
}
|
||||
// remap query args
|
||||
queryArgs.DefaultSortBy("created_at")
|
||||
queryArgs.RemapOrderBy(allowedCampaignRecipientColumns)
|
||||
// get recipients by campaign id
|
||||
recipients, err := c.CampaignService.GetRecipientsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&repository.CampaignRecipientOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithRecipient: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, recipients)
|
||||
}
|
||||
|
||||
// TrackingPixel returns a tracking pixel
|
||||
func (c *Campaign) TrackingPixel(g *gin.Context) {
|
||||
// get the campaign recipient id from the query
|
||||
campaignRecipientID := g.Query("upn") // expect the campaign recipient id to be in here
|
||||
if campaignRecipientID == "" {
|
||||
c.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
campaignRecipientUUID, err := uuid.Parse(campaignRecipientID)
|
||||
if err != nil {
|
||||
c.Logger.Debugw(errs.MsgFailedToParseRequest,
|
||||
"error", err,
|
||||
)
|
||||
c.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
err = c.CampaignService.SaveTrackingPixelLoaded(
|
||||
g,
|
||||
&campaignRecipientUUID,
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to save tracking pixel loaded event",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
g.Header("Content-Type", "image/gif")
|
||||
if !build.Flags.Production {
|
||||
g.File("./embedded/tracking-pixel/sendgrid/open.gif")
|
||||
return
|
||||
}
|
||||
_, err = g.Writer.Write(embedded.TrackingPixel)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to write tracking pixel", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateByID updates a campaign by its id
|
||||
func (c *Campaign) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req model.Campaign
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update the campaign
|
||||
err := c.CampaignService.UpdateByID(g.Request.Context(), session, id, &req)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// handle responses
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// SetSentAtByCampaignRecipientID sets the sent at time for a campaign recipient
|
||||
func (c *Campaign) SetSentAtByCampaignRecipientID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// set sent at time
|
||||
err := c.CampaignService.SetSentAtByCampaignRecipientID(g.Request.Context(), session, id)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a campaign by its id
|
||||
func (c *Campaign) DeleteByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete
|
||||
err := c.CampaignService.DeleteByID(g, session, id)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// AnonymizeByID anonymizes a campaign by its id
|
||||
func (c *Campaign) AnonymizeByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// anonymize
|
||||
err := c.CampaignService.AnonymizeByID(g, session, id)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// GetCampaignStats gets campaign statistics by campaign ID
|
||||
func (c *Campaign) GetCampaignStats(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get stats
|
||||
stats, err := c.CampaignService.GetCampaignStats(g.Request.Context(), session, id)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, stats)
|
||||
}
|
||||
|
||||
// GetAllCampaignStats gets all campaign statistics with pagination
|
||||
func (c *Campaign) GetAllCampaignStats(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(allowedCampaignColumns)
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
|
||||
// get stats
|
||||
stats, err := c.CampaignService.GetAllCampaignStats(g.Request.Context(), session, queryArgs, companyID)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, stats)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// CampaignTemplateColumnsMap 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 CampaignTemplateColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "name"),
|
||||
"after_landing_page_redirect_url": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "after_landing_page_redirect_url"),
|
||||
"is_complete": repository.TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "is_usable"),
|
||||
"domain": repository.TableColumn(database.DOMAIN_TABLE, "name"),
|
||||
"before_landing_page": repository.TableColumn("before_landing_page", "name"),
|
||||
"landing_page": repository.TableColumn("landing_page", "name"),
|
||||
"after_landing_page": repository.TableColumn("after_landing_page", "name"),
|
||||
"smtp": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "name"),
|
||||
"api_sender": repository.TableColumn(database.API_SENDER_TABLE, "name"),
|
||||
"email": repository.TableColumn(database.EMAIL_TABLE, "name"),
|
||||
}
|
||||
|
||||
// CampaignTemplate is a campaign template controller
|
||||
type CampaignTemplate struct {
|
||||
Common
|
||||
CampaignTemplateService *service.CampaignTemplate
|
||||
}
|
||||
|
||||
// Create creates a campaign template
|
||||
func (c *CampaignTemplate) Create(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.CampaignTemplate
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save
|
||||
ctx := g.Request.Context()
|
||||
id, err := c.CampaignTemplateService.Create(ctx, session, &req)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetByID gets a campaign template by id
|
||||
func (c *CampaignTemplate) GetByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check if full data set should be loaded
|
||||
options := &repository.CampaignTemplateOption{}
|
||||
_, ok = g.GetQuery("full")
|
||||
if ok {
|
||||
options = &repository.CampaignTemplateOption{
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithAPISender: true,
|
||||
WithEmail: true,
|
||||
WithLandingPage: true,
|
||||
WithBeforeLandingPage: true,
|
||||
WithAfterLandingPage: true,
|
||||
WithIdentifier: true,
|
||||
}
|
||||
}
|
||||
// get
|
||||
ctx := g.Request.Context()
|
||||
campaignTemplate, err := c.CampaignTemplateService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
options,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, campaignTemplate)
|
||||
}
|
||||
|
||||
// GetAll gets all campaign templates
|
||||
func (c *CampaignTemplate) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
pagination, ok := c.handlePagination(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
usableOnlyQuery := g.Query("usableOnly")
|
||||
usableOnly := false
|
||||
if usableOnlyQuery == "true" {
|
||||
usableOnly = true
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(CampaignTemplateColumnsMap)
|
||||
columns := repository.SelectTable(database.CAMPAIGN_TEMPLATE_TABLE)
|
||||
templates, err := c.CampaignTemplateService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
pagination,
|
||||
&repository.CampaignTemplateOption{
|
||||
QueryArgs: queryArgs,
|
||||
Columns: columns,
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithAPISender: true,
|
||||
WithEmail: true,
|
||||
WithLandingPage: true,
|
||||
WithBeforeLandingPage: true,
|
||||
WithAfterLandingPage: true,
|
||||
UsableOnly: usableOnly,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, templates)
|
||||
}
|
||||
|
||||
// UpdateByID updates a campaign template by id
|
||||
func (c *CampaignTemplate) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.CampaignTemplate
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update
|
||||
err := c.CampaignTemplateService.UpdateByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a campaign template by id
|
||||
func (c *CampaignTemplate) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete
|
||||
err := c.CampaignTemplateService.DeleteByID(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/api"
|
||||
"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"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// DomainColumnsMap 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 CompanyColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.COMPANY_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.COMPANY_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.COMPANY_TABLE, "name"),
|
||||
}
|
||||
|
||||
// Company is a Company controller
|
||||
type Company struct {
|
||||
Common
|
||||
CompanyService *service.Company
|
||||
CampaignService *service.Campaign
|
||||
RecipientService *service.Recipient
|
||||
}
|
||||
|
||||
// GetByID gets a company by id
|
||||
func (c *Company) GetByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID, err := uuid.Parse(g.Param("id"))
|
||||
if err != nil {
|
||||
// ignore err as caused by bad user input
|
||||
_ = err
|
||||
c.Response.BadRequestMessage(g, api.InvalidCompanyID)
|
||||
return
|
||||
}
|
||||
// get company
|
||||
ctx := g.Request.Context()
|
||||
company, err := c.CompanyService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
&companyID,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, company)
|
||||
}
|
||||
|
||||
// ExportByCompanyID outputs a CSV with all events related to the recipient
|
||||
func (c *Company) ExportByCompanyID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the company exported
|
||||
company, err := c.CompanyService.GetByID(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
)
|
||||
// create ZIP file in memory
|
||||
zipBuffer := new(bytes.Buffer)
|
||||
zipWriter := zip.NewWriter(zipBuffer)
|
||||
zipFileName := fmt.Sprintf("company_export_%s.zip", company.Name.MustGet().String())
|
||||
|
||||
// add company data to zip
|
||||
{
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
headers := []string{
|
||||
"Created at",
|
||||
"Updated at",
|
||||
"Name",
|
||||
}
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
row := []string{
|
||||
utils.CSVFromDate(company.CreatedAt),
|
||||
utils.CSVFromDate(company.UpdatedAt),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(company.Name)),
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
writer.Flush()
|
||||
// add to zip
|
||||
f, err := zipWriter.Create("company.csv")
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(buffer.Bytes())
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// add recipients to zip
|
||||
{
|
||||
// get the recipients
|
||||
recipients, err := c.RecipientService.GetByCompanyID(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.RecipientOption{
|
||||
WithCompany: true,
|
||||
WithGroups: true,
|
||||
},
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// write a csv buffer with all recipient and their groups
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
headers := []string{
|
||||
"Created at",
|
||||
"Updated at",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Extra Identifier",
|
||||
"Name",
|
||||
"Position",
|
||||
"Department",
|
||||
"City",
|
||||
"Country",
|
||||
"Misc",
|
||||
}
|
||||
// find the recipient with the most groups and add that number of
|
||||
// extra headers for groups
|
||||
maxGroups := 0
|
||||
for _, recipient := range recipients.Rows {
|
||||
groups, _ := recipient.Groups.Get()
|
||||
if groupLen := len(groups); groupLen > maxGroups {
|
||||
maxGroups = groupLen
|
||||
}
|
||||
}
|
||||
for i := 1; i <= maxGroups; i++ {
|
||||
headers = append(headers, fmt.Sprintf("Group %d", i))
|
||||
}
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, recipient := range recipients.Rows {
|
||||
groups, _ := recipient.Groups.Get()
|
||||
row := []string{
|
||||
utils.CSVFromDate(recipient.CreatedAt),
|
||||
utils.CSVFromDate(recipient.UpdatedAt),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Email)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Phone)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.ExtraIdentifier)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.FirstName)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.LastName)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Position)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Department)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.City)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Country)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Misc)),
|
||||
}
|
||||
for _, group := range groups {
|
||||
row = append(row, group.Name.MustGet().String())
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
// add to zip
|
||||
f, err := zipWriter.Create("recipients.csv")
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(buffer.Bytes())
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// get all campaigns all recipient events
|
||||
{
|
||||
campaigns, err := c.CampaignService.GetByCompanyID(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.CampaignOption{},
|
||||
)
|
||||
for _, campaign := range campaigns.Rows {
|
||||
headers := []string{
|
||||
"Campaign",
|
||||
"Created at",
|
||||
"Recipient name",
|
||||
"Recipient email",
|
||||
"Event name",
|
||||
"Event Details",
|
||||
"User-Agent",
|
||||
"IP",
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
queryArgs := vo.QueryArgs{}
|
||||
queryArgs.OrderBy = repository.TableColumn(
|
||||
database.CAMPAIGN_EVENT_TABLE,
|
||||
"created_at",
|
||||
)
|
||||
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
||||
if sortOrder == "desc" {
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
// get all rows
|
||||
queryArgs.Limit = 0
|
||||
queryArgs.Offset = 0
|
||||
// get events by campaign id
|
||||
cid := campaign.ID.MustGet()
|
||||
events, err := c.CampaignService.GetEventsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&cid,
|
||||
&queryArgs,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, event := range events.Rows {
|
||||
firstName := "anonymized"
|
||||
lastName := "anonymized"
|
||||
recpEmail := "anonymized"
|
||||
if event.Recipient != nil {
|
||||
firstName = event.Recipient.FirstName.MustGet().String()
|
||||
lastName = event.Recipient.LastName.MustGet().String()
|
||||
recpEmail = event.Recipient.Email.MustGet().String()
|
||||
}
|
||||
row := []string{
|
||||
utils.CSVRemoveFormulaStart(campaign.Name.MustGet().String()),
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
utils.CSVRemoveFormulaStart(firstName),
|
||||
utils.CSVRemoveFormulaStart(lastName),
|
||||
utils.CSVRemoveFormulaStart(recpEmail),
|
||||
utils.CSVRemoveFormulaStart(cache.EventNameByID[event.EventID.String()]),
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// add a new subdirectory wit the event file in the zip
|
||||
writer.Flush()
|
||||
// add to zip
|
||||
filename := fmt.Sprintf("campaign_events/%s.csv", campaign.Name.MustGet().String())
|
||||
f, err := zipWriter.Create(filename)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(buffer.Bytes())
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// close zip
|
||||
err = zipWriter.Close()
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.responseWithZIP(g, zipBuffer, zipFileName)
|
||||
}
|
||||
|
||||
// ExportShared outputs a CSV with all shared recipients and events
|
||||
func (c *Company) ExportShared(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// create ZIP file in memory
|
||||
zipBuffer := new(bytes.Buffer)
|
||||
zipWriter := zip.NewWriter(zipBuffer)
|
||||
zipFileName := "shared_export_%s.zip"
|
||||
// add recipients to zip
|
||||
{
|
||||
// get the recipients
|
||||
recipients, err := c.RecipientService.GetByCompanyID(
|
||||
g,
|
||||
session,
|
||||
nil,
|
||||
&repository.RecipientOption{
|
||||
WithCompany: true,
|
||||
WithGroups: true,
|
||||
},
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// write a csv buffer with all recipient and their groups
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
headers := []string{
|
||||
"Created at",
|
||||
"Updated at",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Extra Identifier",
|
||||
"Name",
|
||||
"Position",
|
||||
"Department",
|
||||
"City",
|
||||
"Country",
|
||||
"Misc",
|
||||
}
|
||||
// find the recipient with the most groups and add that number of
|
||||
// extra headers for groups
|
||||
maxGroups := 0
|
||||
for _, recipient := range recipients.Rows {
|
||||
groups, _ := recipient.Groups.Get()
|
||||
if groupLen := len(groups); groupLen > maxGroups {
|
||||
maxGroups = groupLen
|
||||
}
|
||||
}
|
||||
for i := 1; i <= maxGroups; i++ {
|
||||
headers = append(headers, fmt.Sprintf("Group %d", i))
|
||||
}
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, recipient := range recipients.Rows {
|
||||
groups, _ := recipient.Groups.Get()
|
||||
row := []string{
|
||||
utils.CSVFromDate(recipient.CreatedAt),
|
||||
utils.CSVFromDate(recipient.UpdatedAt),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Email)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Phone)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.ExtraIdentifier)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.FirstName)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.LastName)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Position)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Department)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.City)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Country)),
|
||||
utils.CSVRemoveFormulaStart(utils.NullableToString(recipient.Misc)),
|
||||
}
|
||||
for _, group := range groups {
|
||||
row = append(row, group.Name.MustGet().String())
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
// add to zip
|
||||
f, err := zipWriter.Create("recipients.csv")
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(buffer.Bytes())
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// get all campaigns all recipient events
|
||||
{
|
||||
campaigns, err := c.CampaignService.GetByCompanyID(
|
||||
g,
|
||||
session,
|
||||
nil,
|
||||
&repository.CampaignOption{},
|
||||
)
|
||||
for _, campaign := range campaigns.Rows {
|
||||
headers := []string{
|
||||
"Campaign",
|
||||
"Created at",
|
||||
"Recipient name",
|
||||
"Recipient email",
|
||||
"Event name",
|
||||
"Event Details",
|
||||
"User-Agent",
|
||||
"IP",
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buffer)
|
||||
err = writer.Write(headers)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
queryArgs := vo.QueryArgs{}
|
||||
queryArgs.OrderBy = repository.TableColumn(
|
||||
database.CAMPAIGN_EVENT_TABLE,
|
||||
"created_at",
|
||||
)
|
||||
sortOrder := g.DefaultQuery("sortOrder", "desc")
|
||||
if sortOrder == "desc" {
|
||||
queryArgs.Desc = true
|
||||
}
|
||||
// get all rows
|
||||
queryArgs.Limit = 0
|
||||
queryArgs.Offset = 0
|
||||
// get events by campaign id
|
||||
cid := campaign.ID.MustGet()
|
||||
events, err := c.CampaignService.GetEventsByCampaignID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&cid,
|
||||
&queryArgs,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
for _, event := range events.Rows {
|
||||
firstName := "anonymized"
|
||||
lastName := "anonymized"
|
||||
recpEmail := "anonymized"
|
||||
if event.Recipient != nil {
|
||||
firstName = event.Recipient.FirstName.MustGet().String()
|
||||
lastName = event.Recipient.LastName.MustGet().String()
|
||||
recpEmail = event.Recipient.Email.MustGet().String()
|
||||
}
|
||||
row := []string{
|
||||
utils.CSVRemoveFormulaStart(campaign.Name.MustGet().String()),
|
||||
utils.CSVFromDate(event.CreatedAt),
|
||||
utils.CSVRemoveFormulaStart(firstName),
|
||||
utils.CSVRemoveFormulaStart(lastName),
|
||||
utils.CSVRemoveFormulaStart(recpEmail),
|
||||
utils.CSVRemoveFormulaStart(cache.EventNameByID[event.EventID.String()]),
|
||||
utils.CSVRemoveFormulaStart(event.Data.String()),
|
||||
utils.CSVRemoveFormulaStart(event.UserAgent.String()),
|
||||
utils.CSVRemoveFormulaStart(event.IP.String()),
|
||||
}
|
||||
err = writer.Write(row)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// add a new subdirectory wit the event file in the zip
|
||||
writer.Flush()
|
||||
// add to zip
|
||||
filename := fmt.Sprintf("campaign_events/%s.csv", campaign.Name.MustGet().String())
|
||||
f, err := zipWriter.Create(filename)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(buffer.Bytes())
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// close zip
|
||||
err := zipWriter.Close()
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.responseWithZIP(g, zipBuffer, zipFileName)
|
||||
}
|
||||
|
||||
// ChangeName changes a company name
|
||||
func (c *Company) ChangeName(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.Company
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// change company name
|
||||
err := c.CompanyService.UpdateByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, nil)
|
||||
}
|
||||
|
||||
// SoftDelete soft deletes a company
|
||||
func (c *Company) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// TODO company delete should FAIL if it has any relations to anything
|
||||
// delete company
|
||||
_, err := c.CompanyService.DeleteByID(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// Create creates a company
|
||||
func (c *Company) Create(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.Company
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save company
|
||||
ctx := g.Request.Context()
|
||||
company, err := c.CompanyService.Create(
|
||||
ctx,
|
||||
session,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{
|
||||
"id": company.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll gets all companies with pagination
|
||||
func (c *Company) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(CompanyColumnsMap)
|
||||
// get companies
|
||||
ctx := g.Request.Context()
|
||||
companies, err := c.CompanyService.GetAll(
|
||||
ctx,
|
||||
session,
|
||||
queryArgs,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, companies)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// DomainColumnsMap 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 DomainColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.DOMAIN_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.DOMAIN_TABLE, "updated_at"),
|
||||
"hosting_website": repository.TableColumn(database.DOMAIN_TABLE, "host_website"),
|
||||
"redirects": repository.TableColumn(database.DOMAIN_TABLE, "redirect_url"),
|
||||
}
|
||||
|
||||
// Domain
|
||||
type Domain struct {
|
||||
Common
|
||||
DomainService *service.Domain
|
||||
}
|
||||
|
||||
// Create creates a domain
|
||||
func (d *Domain) Create(g *gin.Context) {
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.Domain
|
||||
if ok := d.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save domain
|
||||
id, err := d.DomainService.Create(g, session, &req)
|
||||
// handle response
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetAll gets domains
|
||||
func (d *Domain) GetAll(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := d.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(DomainColumnsMap)
|
||||
// get domain
|
||||
domains, err := d.DomainService.GetAll(
|
||||
companyID,
|
||||
g.Request.Context(),
|
||||
session,
|
||||
queryArgs,
|
||||
true, // TODO there might not be any reason to retrieve the full relation here - optimize by removing it (false)
|
||||
)
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, domains)
|
||||
}
|
||||
|
||||
// GetAllOverview gets domains with limited data
|
||||
func (d *Domain) GetAllOverview(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := d.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(DomainColumnsMap)
|
||||
// get domains
|
||||
domains, err := d.DomainService.GetAllOverview(
|
||||
companyID,
|
||||
g.Request.Context(),
|
||||
session,
|
||||
queryArgs,
|
||||
)
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, domains)
|
||||
}
|
||||
|
||||
// GetByID gets a domain by id
|
||||
func (d *Domain) GetByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := d.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get domain
|
||||
ctx := g.Request.Context()
|
||||
domain, err := d.DomainService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
&repository.DomainOption{
|
||||
WithCompany: true,
|
||||
},
|
||||
)
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, domain)
|
||||
}
|
||||
|
||||
// GetByName gets a domain by name
|
||||
func (d *Domain) GetByName(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
name, err := vo.NewString255(g.Param("domain"))
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// get domain
|
||||
ctx := g.Request.Context()
|
||||
domain, err := d.DomainService.GetByName(
|
||||
ctx,
|
||||
session,
|
||||
name,
|
||||
&repository.DomainOption{},
|
||||
)
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, domain)
|
||||
}
|
||||
|
||||
// UpdateByID updates a domain by id
|
||||
func (d *Domain) UpdateByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := d.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.Domain
|
||||
if ok := d.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update domain
|
||||
err := d.DomainService.UpdateByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a domain by id
|
||||
func (d *Domain) DeleteByID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := d.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := d.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete domain
|
||||
err := d.DomainService.DeleteByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := d.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
d.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// EmailOrderByMap 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 EmailOrderByMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.EMAIL_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.EMAIL_TABLE, "created_at"),
|
||||
"name": repository.TableColumn(database.EMAIL_TABLE, "name"),
|
||||
"mail_from": repository.TableColumn(database.EMAIL_TABLE, "mail_from"),
|
||||
"from": repository.TableColumn(database.EMAIL_TABLE, "from"),
|
||||
"subject": repository.TableColumn(database.EMAIL_TABLE, "subject"),
|
||||
"tracking_pixel": repository.TableColumn(database.EMAIL_TABLE, "add_tracking_pixel"),
|
||||
}
|
||||
|
||||
// AddAttachmentsToEmailRequest is a request to add attachments to a message
|
||||
type AddAttachmentsToEmailRequest struct {
|
||||
Attachments []string `json:"ids"` // attachment IDs
|
||||
}
|
||||
|
||||
// RemoveAttachmentFromEmailRequest is a request to remove an attachment from a message
|
||||
type RemoveAttachmentFromEmailRequest struct {
|
||||
AttachmentID string `json:"attachmentID"`
|
||||
}
|
||||
|
||||
// SendTestEmailRequest is a request for sending a test of an e-mail
|
||||
type SendTestEmailRequest struct {
|
||||
SMTPID *uuid.UUID
|
||||
DomainID *uuid.UUID
|
||||
RecipientID *uuid.UUID
|
||||
}
|
||||
|
||||
// Email is a Email controller
|
||||
type Email struct {
|
||||
Common
|
||||
EmailService *service.Email
|
||||
TemplateService *service.Template
|
||||
EmailRepository *repository.Email
|
||||
}
|
||||
|
||||
// AddAttachments adds attachments to a email
|
||||
func (m *Email) AddAttachments(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var request AddAttachmentsToEmailRequest
|
||||
if ok := m.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(request.Attachments) == 0 {
|
||||
m.Response.BadRequestMessage(g, "No attachments provided")
|
||||
return
|
||||
}
|
||||
attachmentIDs := []*uuid.UUID{}
|
||||
for _, idParam := range request.Attachments {
|
||||
id, err := uuid.Parse(idParam)
|
||||
if err != nil {
|
||||
m.Logger.Debugw(errs.MsgFailedToParseUUID,
|
||||
"error", err,
|
||||
)
|
||||
m.Response.BadRequestMessage(g, "Invalid attachment ID")
|
||||
return
|
||||
}
|
||||
attachmentIDs = append(attachmentIDs, &id)
|
||||
}
|
||||
// add attachments to email
|
||||
err := m.EmailService.AddAttachments(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
attachmentIDs,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// RemoveAttachment removes an attachment from a email
|
||||
func (m *Email) RemoveAttachment(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req RemoveAttachmentFromEmailRequest
|
||||
if ok := m.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
attachmentID, err := uuid.Parse(req.AttachmentID)
|
||||
if err != nil {
|
||||
m.Logger.Debugw(errs.MsgFailedToParseUUID,
|
||||
"error", err,
|
||||
)
|
||||
m.Response.BadRequestMessage(g, "Invalid attachment ID")
|
||||
return
|
||||
}
|
||||
emailID, err := uuid.Parse(g.Param("id"))
|
||||
if err != nil {
|
||||
m.Logger.Debugw(errs.MsgFailedToParseUUID, "error", err)
|
||||
m.Response.BadRequestMessage(g, "Invalid message ID")
|
||||
return
|
||||
}
|
||||
// remove attachment from email
|
||||
err = m.EmailService.RemoveAttachment(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&emailID,
|
||||
&attachmentID,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// Create creates a email
|
||||
func (m *Email) Create(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.Email
|
||||
if ok := m.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save email
|
||||
id, err := m.EmailService.Create(
|
||||
g,
|
||||
session,
|
||||
&req,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
// SendTestEmail
|
||||
func (m *Email) SendTestEmail(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req SendTestEmailRequest
|
||||
if ok := m.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// send test email
|
||||
err := m.EmailService.SendTestEmail(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
req.SMTPID,
|
||||
req.DomainID,
|
||||
req.RecipientID,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// GetByID gets a email by ID
|
||||
func (m *Email) GetByID(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get email
|
||||
email, err := m.EmailService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, email)
|
||||
}
|
||||
|
||||
// GetContentByID gets a email content by ID
|
||||
func (m *Email) GetContentByID(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get
|
||||
email, err := m.EmailService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// build email
|
||||
domain := &model.Domain{
|
||||
Name: nullable.NewNullableWithValue(
|
||||
*vo.NewString255Must("example.test"),
|
||||
),
|
||||
}
|
||||
recipient := model.NewRecipientExample()
|
||||
campaignRecipient := model.CampaignRecipient{
|
||||
ID: nullable.NewNullableWithValue(
|
||||
uuid.New(),
|
||||
),
|
||||
Recipient: recipient,
|
||||
}
|
||||
apiSender := model.NewAPISenderExample()
|
||||
emailBody, err := m.TemplateService.CreateMailBody(
|
||||
"id",
|
||||
"/foo",
|
||||
domain,
|
||||
&campaignRecipient,
|
||||
email,
|
||||
apiSender,
|
||||
)
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, emailBody)
|
||||
}
|
||||
|
||||
// GetAll gets all emails using pagination
|
||||
func (m *Email) GetAll(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := m.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByName()
|
||||
queryArgs.RemapOrderBy(EmailOrderByMap)
|
||||
emails, err := m.EmailService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
queryArgs,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
|
||||
}
|
||||
m.Response.OK(g, emails)
|
||||
}
|
||||
|
||||
// GetOverviews gets all email overviews using pagination
|
||||
func (m *Email) GetOverviews(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
queryArgs, ok := m.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.RemapOrderBy(EmailOrderByMap)
|
||||
queryArgs.DefaultSortByName()
|
||||
emails, err := m.EmailService.GetOverviews(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
queryArgs,
|
||||
)
|
||||
// handle responses
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
|
||||
}
|
||||
m.Response.OK(g, emails)
|
||||
}
|
||||
|
||||
// UpdateByID updates a message by ID
|
||||
func (m *Email) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var email model.Email
|
||||
if ok := m.handleParseRequest(g, &email); !ok {
|
||||
return
|
||||
}
|
||||
// update message
|
||||
err := m.EmailService.UpdateByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&email,
|
||||
)
|
||||
// handle response
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a message by ID
|
||||
func (m *Email) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := m.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := m.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete message
|
||||
err := m.EmailService.DeleteByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := m.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
m.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Health is the Health controller
|
||||
type Health struct{}
|
||||
|
||||
// Health returns a 200 OK
|
||||
func (c *Health) Health(g *gin.Context) {
|
||||
g.Status(http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// IdentifierColumnsMap 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 IdentifierColumnsMap = map[string]string{
|
||||
"name": repository.TableColumn(database.IDENTIFIER_TABLE, "name"),
|
||||
}
|
||||
|
||||
type Identifier struct {
|
||||
Common
|
||||
IdentifierService *service.Identifier
|
||||
}
|
||||
|
||||
// GetAll gets all identifiers
|
||||
func (c *Identifier) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByName()
|
||||
// get
|
||||
identifiers, err := c.IdentifierService.GetAll(
|
||||
g,
|
||||
session,
|
||||
&repository.IdentifierOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
identifiers,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// Import handles import for templates like emails, landing pages and so on
|
||||
type Import struct {
|
||||
Common
|
||||
ImportService *service.Import
|
||||
}
|
||||
|
||||
// Import imports a .zip file
|
||||
func (im *Import) Import(g *gin.Context) {
|
||||
session, _, ok := im.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
f, err := g.FormFile("file")
|
||||
// handle responses
|
||||
if ok := im.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Read forCompany flag from form (treat "1" or "true" as true)
|
||||
forCompany := false
|
||||
if v := g.PostForm("forCompany"); v == "1" || v == "true" {
|
||||
forCompany = true
|
||||
}
|
||||
|
||||
// Read companyID from form data if provided
|
||||
var companyID *uuid.UUID
|
||||
if companyIDStr := g.PostForm("companyID"); companyIDStr != "" {
|
||||
if cid, err := uuid.Parse(companyIDStr); err == nil {
|
||||
companyID = &cid
|
||||
}
|
||||
}
|
||||
|
||||
summary, err := im.ImportService.Import(g, session, f, forCompany, companyID)
|
||||
if ok := im.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
im.Response.OK(g, summary)
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/cli"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/password"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"golang.org/x/net/context"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SetupAdminRequest is the request for the install action
|
||||
type SetupAdminRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
UserFullname string `json:"userFullname" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required"`
|
||||
}
|
||||
|
||||
// InitialSetup is a controller used by the CLI in the
|
||||
// initial setup process - it is not an API controller
|
||||
type InitialSetup struct {
|
||||
Common
|
||||
CLIOutputter cli.Outputter
|
||||
OptionRepository *repository.Option
|
||||
InstallService *service.InstallSetup
|
||||
OptionService *service.Option
|
||||
}
|
||||
|
||||
// IsInstalled checks if the application is installed
|
||||
// not as a
|
||||
func (is *InitialSetup) IsInstalled(ctx context.Context) (bool, error) {
|
||||
isInstalledOption, err := is.OptionRepository.GetByKey(
|
||||
ctx,
|
||||
data.OptionKeyIsInstalled,
|
||||
)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("could not get '%s' option: %w", data.OptionKeyIsInstalled, err)
|
||||
}
|
||||
return isInstalledOption.Value.String() == data.OptionValueIsInstalled, nil
|
||||
}
|
||||
|
||||
// HandleInitialSetup handles the initial setup of the application
|
||||
// this includes inserting the isInstalled option to not installed
|
||||
// and making or updating the sacrificial admin account
|
||||
func (is *InitialSetup) HandleInitialSetup(ctx context.Context) error {
|
||||
// setup option for is installed
|
||||
isInstalledOption, err := is.OptionRepository.GetByKey(
|
||||
ctx,
|
||||
data.OptionKeyIsInstalled,
|
||||
)
|
||||
// if the option does not exist, create it
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("%w: could not get '%s' option", err, data.OptionKeyIsInstalled)
|
||||
}
|
||||
key := vo.NewString64Must(data.OptionKeyIsInstalled)
|
||||
value := vo.NewOptionalString1MBMust(data.OptionValueIsNotInstalled)
|
||||
isInstalledOptionWithoutID := model.Option{
|
||||
Key: *key,
|
||||
Value: *value,
|
||||
}
|
||||
_, err = is.OptionRepository.Insert(
|
||||
ctx,
|
||||
&isInstalledOptionWithoutID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: could not insert entity for option '%s'", err, data.OptionKeyIsInstalled)
|
||||
}
|
||||
isInstalledOption, err = is.OptionRepository.GetByKey(
|
||||
ctx,
|
||||
isInstalledOptionWithoutID.Key.String(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: could not get created '%s' option", err, data.OptionKeyIsInstalled)
|
||||
}
|
||||
}
|
||||
// if no instance ID exists, add it
|
||||
instanceIDOption, err := is.OptionRepository.GetByKey(
|
||||
ctx,
|
||||
data.OptionKeyInstanceID,
|
||||
)
|
||||
// if the instance id option does not exist, create it
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("%w: could not get '%s' option", err, data.OptionKeyInstanceID)
|
||||
}
|
||||
key := vo.NewString64Must(data.OptionKeyInstanceID)
|
||||
instanceID := uuid.New()
|
||||
value := vo.NewOptionalString1MBMust(instanceID.String())
|
||||
instanceIDOption = &model.Option{
|
||||
Key: *key,
|
||||
Value: *value,
|
||||
}
|
||||
_, err = is.OptionRepository.Insert(
|
||||
ctx,
|
||||
instanceIDOption,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not insert instance ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// if installation is already complete, return error
|
||||
if isInstalledOption.Value.String() == data.OptionValueIsInstalled {
|
||||
return errs.ErrAlreadyInstalled
|
||||
}
|
||||
// setup accounts
|
||||
admin, password, err := is.InstallService.SetupAccounts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not setup initial admin account: %w", err)
|
||||
}
|
||||
is.CLIOutputter.PrintInitialAdminAccount(
|
||||
admin.Username.MustGet().String(),
|
||||
password.String(),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Install is the Install controller used by the API
|
||||
type Install struct {
|
||||
Common
|
||||
UserRepository *repository.User
|
||||
CompanyRepository *repository.Company
|
||||
OptionRepository *repository.Option
|
||||
DB *gorm.DB
|
||||
PasswordHasher password.Argon2Hasher
|
||||
}
|
||||
|
||||
// Install completes the installation by setting the initial administrators and options
|
||||
func (in *Install) Install(g *gin.Context) {
|
||||
tx := in.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
ok := in.install(g, tx)
|
||||
if !ok {
|
||||
if tx.Rollback().Error != nil {
|
||||
in.Logger.Errorw("failed to install - could not rollback transaction",
|
||||
"error", tx.Rollback().Error,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
result := tx.Commit()
|
||||
if result.Error != nil {
|
||||
in.Logger.Errorw("failed to install - could not commit transaction",
|
||||
"error", result.Error,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
// the admin user changed username and password
|
||||
// however as the install process is a special case, we wont
|
||||
// require re-authentication
|
||||
in.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// Install completes the installation by setting the initial administrators
|
||||
// username, password, email, name and company name
|
||||
func (in *Install) install(g *gin.Context, tx *gorm.DB) bool {
|
||||
// handle session
|
||||
_, user, ok := in.handleSession(g)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
role := user.Role
|
||||
if role == nil {
|
||||
in.Logger.Error("failed to install - session contain no role")
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
if !role.IsSuperAdministrator() {
|
||||
in.Logger.Info("failed to install - not super admin")
|
||||
// TODO add audit log
|
||||
in.Response.Forbidden(g)
|
||||
return false
|
||||
}
|
||||
// defer rollback or commit tx
|
||||
var request SetupAdminRequest
|
||||
if err := g.ShouldBindJSON(&request); err != nil {
|
||||
in.Logger.Debugw("failed to parse request",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.BadRequest(g)
|
||||
return false
|
||||
}
|
||||
ctx := g.Request.Context()
|
||||
// check if already installed
|
||||
isInstalled, err := in.OptionRepository.GetByKey(ctx, data.OptionKeyIsInstalled)
|
||||
if err != nil {
|
||||
in.Logger.Errorw("failed to install - could not get option",
|
||||
"optionKey", data.OptionKeyIsInstalled,
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
if isInstalled.Value.String() == data.OptionValueIsInstalled {
|
||||
in.Logger.Info("failed to install - already installed")
|
||||
in.Response.ServerErrorMessage(
|
||||
g,
|
||||
"Installation is already complete",
|
||||
)
|
||||
return false
|
||||
}
|
||||
// update the username
|
||||
newUsername, err := vo.NewUsername(request.Username)
|
||||
if err != nil {
|
||||
in.Logger.Infow("failed to install - invalid username",
|
||||
"username", request.Username,
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ValidationFailed(g, "Username", err)
|
||||
return false
|
||||
}
|
||||
if newUsername.String() == user.Username.MustGet().String() {
|
||||
in.Logger.Infow("failed to install - new username is the same as the current",
|
||||
"username", newUsername.String(),
|
||||
"error", err,
|
||||
)
|
||||
in.Response.BadRequestMessage(
|
||||
g,
|
||||
"Username may not be the same as the current",
|
||||
)
|
||||
return false
|
||||
}
|
||||
userID := user.ID.MustGet()
|
||||
err = in.UserRepository.UpdateUsernameByIDWithTransaction(
|
||||
ctx,
|
||||
tx,
|
||||
&userID,
|
||||
newUsername,
|
||||
)
|
||||
if err != nil {
|
||||
in.Logger.Infow("failed to install - could not update username",
|
||||
"username", newUsername.String(),
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
// update the password
|
||||
newPassword, err := vo.NewReasonableLengthPassword(request.NewPassword)
|
||||
if err != nil {
|
||||
in.Logger.Infow("failed to install - invalid password",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.BadRequestMessage(g, "invalid password")
|
||||
return false
|
||||
}
|
||||
hash, err := in.PasswordHasher.Hash(newPassword.String())
|
||||
if err != nil {
|
||||
in.Logger.Errorw("failed to install - could not hash password",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
err = in.UserRepository.UpdatePasswordHashByIDWithTransaction(
|
||||
ctx,
|
||||
tx,
|
||||
&userID,
|
||||
hash,
|
||||
)
|
||||
if err != nil {
|
||||
in.Logger.Errorw("failed to install - could not update password",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
// update the name
|
||||
newName, err := vo.NewUserFullname(request.UserFullname)
|
||||
if err != nil {
|
||||
in.Logger.Infow("failed to install - invalid name",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ValidationFailed(g, "Name", err)
|
||||
return false
|
||||
}
|
||||
err = in.UserRepository.UpdateFullNameByIDWithTransaction(
|
||||
ctx,
|
||||
tx,
|
||||
&userID,
|
||||
newName,
|
||||
)
|
||||
if err != nil {
|
||||
in.Logger.Infow("failed to install - could not update name",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerError(g)
|
||||
return false
|
||||
}
|
||||
// update installed option to installed
|
||||
option := model.Option{
|
||||
Key: *vo.NewString64Must(data.OptionKeyIsInstalled),
|
||||
Value: *vo.NewOptionalString1MBMust(data.OptionValueIsInstalled),
|
||||
}
|
||||
err = in.OptionRepository.UpdateByKeyWithTransaction(
|
||||
ctx,
|
||||
tx,
|
||||
&option,
|
||||
)
|
||||
if err != nil {
|
||||
in.Logger.Errorw("failed to install - could not create install option",
|
||||
"error", err,
|
||||
)
|
||||
in.Response.ServerErrorMessage(g, "failed to create install option")
|
||||
return false
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type SetLevelRequest struct {
|
||||
Level string `json:"level"`
|
||||
DBLevel string `json:"dbLevel"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Common
|
||||
OptionService *service.Option
|
||||
Database *gorm.DB
|
||||
LoggerAtom *zap.AtomicLevel
|
||||
}
|
||||
|
||||
// Panic is a test utility
|
||||
func (c *Log) Panic(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if session == nil {
|
||||
if ok := c.handleErrors(g, errors.New("no session")); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Deeper()
|
||||
}
|
||||
|
||||
func (c *Log) Deeper() {
|
||||
panic("panic test")
|
||||
}
|
||||
|
||||
// Slow is a test utility
|
||||
func (c *Log) Slow(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if session == nil {
|
||||
if ok := c.handleErrors(g, errors.New("no session")); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Logger.Debugf("Slow request testing start")
|
||||
time.Sleep(10 * time.Second)
|
||||
c.Logger.Debugf("Slow request testing stop")
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// GetLevel gets the log level
|
||||
func (c *Log) GetLevel(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get the log levels
|
||||
logLevelOption, err := c.OptionService.GetOption(
|
||||
g,
|
||||
session,
|
||||
data.OptionKeyLogLevel,
|
||||
)
|
||||
// handle errors
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
dbLogLevelOption, err := c.OptionService.GetOption(g, session, data.OptionKeyDBLogLevel)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{
|
||||
"level": logLevelOption.Value,
|
||||
"dbLevel": dbLogLevelOption.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// SetLevel sets the log level
|
||||
func (c *Log) SetLevel(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var request SetLevelRequest
|
||||
if ok := c.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
if request.Level == "" && request.DBLevel == "" {
|
||||
c.Response.BadRequestMessage(g, "level or dbLevel is required")
|
||||
return
|
||||
}
|
||||
if request.DBLevel != "" {
|
||||
switch request.DBLevel {
|
||||
case "silent":
|
||||
c.Database.Logger = c.Database.Logger.LogMode(logger.Silent)
|
||||
case "info":
|
||||
c.Database.Logger = c.Database.Logger.LogMode(logger.Info)
|
||||
case "warn":
|
||||
c.Database.Logger = c.Database.Logger.LogMode(logger.Warn)
|
||||
case "error":
|
||||
c.Database.Logger = c.Database.Logger.LogMode(logger.Error)
|
||||
default:
|
||||
c.Logger.Debugw("invalid db log level",
|
||||
"level", request.DBLevel,
|
||||
)
|
||||
c.Response.BadRequestMessage(g, "unknown DB log level")
|
||||
return
|
||||
}
|
||||
// set db log level in database
|
||||
dbLevel := vo.NewOptionalString1MBMust(request.DBLevel)
|
||||
dbLogLevelOption := model.Option{
|
||||
Key: *vo.NewString64Must(data.OptionKeyDBLogLevel),
|
||||
Value: *dbLevel,
|
||||
}
|
||||
err := c.persist(
|
||||
g,
|
||||
session,
|
||||
&dbLogLevelOption,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if request.Level != "" {
|
||||
switch request.Level {
|
||||
case "debug":
|
||||
c.LoggerAtom.SetLevel(zap.DebugLevel)
|
||||
case "info":
|
||||
c.LoggerAtom.SetLevel(zap.InfoLevel)
|
||||
case "warn":
|
||||
c.LoggerAtom.SetLevel(zap.WarnLevel)
|
||||
case "error":
|
||||
c.LoggerAtom.SetLevel(zap.ErrorLevel)
|
||||
default:
|
||||
c.Logger.Debugw("invalid log level",
|
||||
"level", request.Level,
|
||||
)
|
||||
c.Response.BadRequestMessage(g, "Unknown log level")
|
||||
return
|
||||
}
|
||||
|
||||
// set log level in in memory logger struct
|
||||
logLevel := model.Option{
|
||||
Key: *vo.NewString64Must(data.OptionKeyLogLevel),
|
||||
Value: *vo.NewOptionalString1MBMust(request.Level),
|
||||
}
|
||||
err := c.persist(
|
||||
g,
|
||||
session,
|
||||
&logLevel,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Response.OK(g, nil)
|
||||
}
|
||||
|
||||
// TestLog tests the log
|
||||
// Sends a log message for each log level debug, info, warn, error
|
||||
func (c *Log) TestLog(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check permissions
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
handleServerError(g, c.Response, err)
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
// TODO audit log
|
||||
c.Response.Unauthorized(g)
|
||||
return
|
||||
}
|
||||
c.Logger.Debug("Log: DEBUG Test")
|
||||
c.Logger.Info("Log: INFO Test")
|
||||
c.Logger.Warn("Log: WARN Test")
|
||||
c.Logger.Error("Log: ERROR Test")
|
||||
c.Response.OK(g, nil)
|
||||
}
|
||||
|
||||
// persit saves the log level
|
||||
// TODO this has become empty and superflous
|
||||
func (c *Log) persist(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
logLevel *model.Option,
|
||||
) error {
|
||||
return c.OptionService.SetOptionByKey(
|
||||
ctx,
|
||||
session,
|
||||
logLevel,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// Option is a Option controller
|
||||
type Option struct {
|
||||
Common
|
||||
OptionService *service.Option
|
||||
}
|
||||
|
||||
// Get a update option
|
||||
func (c *Option) Get(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
key := g.Param("key")
|
||||
if key == "" {
|
||||
c.Response.BadRequestMessage(g, "option is required")
|
||||
return
|
||||
}
|
||||
ctx := g.Request.Context()
|
||||
option, err := c.OptionService.GetOption(
|
||||
ctx,
|
||||
session,
|
||||
key,
|
||||
)
|
||||
if ok := handleServerError(g, c.Response, err); !ok {
|
||||
return
|
||||
}
|
||||
if key == data.OptionKeyAdminSSOLogin {
|
||||
option, err = c.OptionService.MaskSSOSecret(option)
|
||||
if ok := handleServerError(g, c.Response, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Response.OK(g, option)
|
||||
}
|
||||
|
||||
// Update sets a option
|
||||
func (c *Option) Update(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.Option
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
err := c.OptionService.SetOptionByKey(g, session, &req)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// PageColumnsMap 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 PageColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.PAGE_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.PAGE_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.PAGE_TABLE, "name"),
|
||||
}
|
||||
|
||||
// Page is a Page controller
|
||||
type Page struct {
|
||||
Common
|
||||
PageService *service.Page
|
||||
TemplateService *service.Template
|
||||
}
|
||||
|
||||
// Create creates a page
|
||||
func (p *Page) Create(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.Page
|
||||
if ok := p.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save page
|
||||
id, err := p.PageService.Create(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetContentByID serves a page by id
|
||||
func (p *Page) GetContentByID(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := p.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get page
|
||||
page, err := p.PageService.GetByID(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&repository.PageOption{},
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
content, err := page.Content.Get()
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// build response
|
||||
phishingPage, err := p.TemplateService.ApplyPageMock(content.String())
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, phishingPage.String())
|
||||
}
|
||||
|
||||
// GetAll gets pages using pagination
|
||||
func (p *Page) GetAll(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := p.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get pages
|
||||
pages, err := p.PageService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.PageOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, pages)
|
||||
}
|
||||
|
||||
// GetOverview gets pages overview using pagination
|
||||
func (p *Page) GetOverview(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := p.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get pages
|
||||
pages, err := p.PageService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyID,
|
||||
&repository.PageOption{
|
||||
Fields: []string{"id", "created_at", "updated_at", "name", "company_id"},
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, pages)
|
||||
}
|
||||
|
||||
// GetByID gets a page by id
|
||||
func (p *Page) GetByID(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := p.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get page
|
||||
page, err := p.PageService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
// do I really need to preload this?
|
||||
&repository.PageOption{
|
||||
WithCompany: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, page)
|
||||
}
|
||||
|
||||
// UpdateByID updates a page by id
|
||||
func (p *Page) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := p.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.Page
|
||||
if ok := p.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update page
|
||||
err := p.PageService.UpdateByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a page by id
|
||||
func (p *Page) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := p.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := p.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete page
|
||||
err := p.PageService.DeleteByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := p.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
p.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"image/png"
|
||||
"net/http"
|
||||
|
||||
"github.com/boombuler/barcode"
|
||||
"github.com/boombuler/barcode/qr"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// QRCodeRequest is the request to generate a QR code from a TOTP URL
|
||||
type QRCodeRequest struct {
|
||||
URL string `json:"url"`
|
||||
DotSize int `json:"dotSize"`
|
||||
}
|
||||
|
||||
// QRGenerator is the QR controller
|
||||
type QRGenerator struct {
|
||||
Common
|
||||
}
|
||||
|
||||
// QRGenerator creates a HTML QR code
|
||||
// It is returned in an JSON response
|
||||
func (q *QRGenerator) ToHTML(g *gin.Context) {
|
||||
_, _, ok := q.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req QRCodeRequest
|
||||
if ok := q.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// generate QR code
|
||||
qrCodeBuf, err := service.GenerateQRCode(req.URL, req.DotSize)
|
||||
if err != nil {
|
||||
q.Logger.Debugw("failed to genereate QR code",
|
||||
"error", err,
|
||||
)
|
||||
q.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
q.Response.OK(g, qrCodeBuf)
|
||||
}
|
||||
|
||||
// ToTOTPURL generates a QR code from a TOTP URL
|
||||
func (q *QRGenerator) ToTOTPURL(g *gin.Context) {
|
||||
_, _, ok := q.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req QRCodeRequest
|
||||
if ok := q.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// generate QR code
|
||||
qrCode, err := qr.Encode(
|
||||
req.URL,
|
||||
qr.M,
|
||||
qr.Auto,
|
||||
)
|
||||
if err != nil {
|
||||
q.Logger.Debugw("failed to generate QR code",
|
||||
"error", err,
|
||||
)
|
||||
q.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
qrCode, err = barcode.Scale(qrCode, 200, 200)
|
||||
if err != nil {
|
||||
q.Logger.Debugw("failed to scale QR code",
|
||||
"error", err,
|
||||
)
|
||||
q.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
// output QR code as png
|
||||
g.Writer.Header().Set("Content-Type", "image/png")
|
||||
err = png.Encode(g.Writer, qrCode)
|
||||
if err == nil {
|
||||
q.Logger.Debugw("failed to encode QR code",
|
||||
"error", err,
|
||||
)
|
||||
q.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
// respond
|
||||
g.Status(http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
_, 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, &gin.H{})
|
||||
}
|
||||
|
||||
// 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{})
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// RecipientGroupColumnsMap 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 RecipientGroupColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.RECIPIENT_GROUP_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.RECIPIENT_GROUP_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.RECIPIENT_GROUP_TABLE, "name"),
|
||||
}
|
||||
|
||||
// AddRecipientRequest is a request to add recipients to a recipient group
|
||||
type AddRecipientRequest struct {
|
||||
RecipientIDs []string `json:"recipientIDs"`
|
||||
}
|
||||
|
||||
// RemoveRecipientRequest is a request to remove recipients from a recipient group
|
||||
type RemoveRecipientRequest struct {
|
||||
RecipientIDs []string `json:"recipientIDs"`
|
||||
}
|
||||
|
||||
// RecipientGroup is a recipient group controller
|
||||
type RecipientGroup struct {
|
||||
Common
|
||||
RecipientGroupService *service.RecipientGroup
|
||||
}
|
||||
|
||||
// Create creates a new recipient group
|
||||
func (r *RecipientGroup) Create(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.RecipientGroup
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save recipient group
|
||||
recipientGroupID, err := r.RecipientGroupService.Create(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(
|
||||
g,
|
||||
&gin.H{
|
||||
"id": recipientGroupID.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetAll returns all recipient groups using pagination
|
||||
func (r *RecipientGroup) GetAll(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := r.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByName()
|
||||
queryArgs.RemapOrderBy(RecipientGroupColumnsMap)
|
||||
companyContextID := companyIDFromRequestQuery(g)
|
||||
|
||||
// get recipient groups
|
||||
recipientGroups, err := r.RecipientGroupService.GetAll(
|
||||
g,
|
||||
session,
|
||||
companyContextID,
|
||||
&repository.RecipientGroupOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
WithRecipientCount: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, recipientGroups)
|
||||
}
|
||||
|
||||
// GetByID gets a recipient group by id
|
||||
func (r *RecipientGroup) GetByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse id
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
recipientGroup, err := r.RecipientGroupService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&repository.RecipientGroupOption{
|
||||
WithCompany: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, recipientGroup)
|
||||
}
|
||||
|
||||
// GetRecipientsByGroupID gets recipients by recipient group id
|
||||
func (r *RecipientGroup) GetRecipientsByGroupID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse id
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := r.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortBy("email")
|
||||
// remap query args
|
||||
queryArgs.RemapOrderBy(recipientColumnByMap)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get recipients
|
||||
ctx := g.Request.Context()
|
||||
recipients, err := r.RecipientGroupService.GetRecipientsByGroupID(
|
||||
ctx,
|
||||
session,
|
||||
id,
|
||||
&repository.RecipientOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
r.Response.OK(g, recipients)
|
||||
}
|
||||
|
||||
// UpdateByID updates a recipient group by id
|
||||
// updates only the name and company relations
|
||||
func (r *RecipientGroup) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse id
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.RecipientGroup
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// check if recipient group exists already exists
|
||||
err := r.RecipientGroupService.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 to a recipient group
|
||||
func (r *RecipientGroup) Import(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
groupID, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
err := r.RecipientGroupService.Import(
|
||||
g,
|
||||
session,
|
||||
req.Recipients,
|
||||
req.IgnoreOverwriteEmptyFields.MustGet(),
|
||||
groupID,
|
||||
req.CompanyID,
|
||||
)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, &gin.H{})
|
||||
}
|
||||
|
||||
// AddRecipients adds recipients to a recipient group
|
||||
func (r *RecipientGroup) AddRecipients(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse group ID
|
||||
groupID, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req AddRecipientRequest
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// parse recipient ids
|
||||
recipientIDs := []*uuid.UUID{}
|
||||
for _, id := range req.RecipientIDs {
|
||||
rid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
r.Logger.Debugw("failed to add recipients to recipient group",
|
||||
"error", fmt.Errorf("failed to parse recipient id: %w", err),
|
||||
)
|
||||
r.Response.BadRequestMessage(g, "invalid recipient id")
|
||||
return
|
||||
}
|
||||
recipientIDs = append(recipientIDs, &rid)
|
||||
}
|
||||
// add recipients
|
||||
err := r.RecipientGroupService.AddRecipients(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
groupID,
|
||||
recipientIDs,
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, &gin.H{})
|
||||
}
|
||||
|
||||
// RemoveRecipients removes a recipient from a recipient group
|
||||
func (r *RecipientGroup) RemoveRecipients(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse id
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req RemoveRecipientRequest
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// parse recipient ids
|
||||
recipientIDs := []*uuid.UUID{}
|
||||
for _, id := range req.RecipientIDs {
|
||||
rid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
r.Logger.Debugw("failed to remove recipients from recipient group",
|
||||
"error", fmt.Errorf("failed to parse recipient id: %w", err),
|
||||
)
|
||||
r.Response.BadRequestMessage(g, "invalid recipient id")
|
||||
return
|
||||
}
|
||||
recipientIDs = append(recipientIDs, &rid)
|
||||
}
|
||||
// remove recipients
|
||||
err := r.RecipientGroupService.RemoveRecipients(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
recipientIDs,
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, &gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a recipient group by id
|
||||
// deleting a group also deletes all recipients in that group
|
||||
func (r *RecipientGroup) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse id
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete recipient group
|
||||
err := r.RecipientGroupService.DeleteByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(
|
||||
g,
|
||||
&gin.H{},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/api"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
)
|
||||
|
||||
// SMTPConfigurationColumnsMap 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 SMTPConfigurationColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "name"),
|
||||
"host": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "host"),
|
||||
"port": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "port"),
|
||||
"username": repository.TableColumn(database.SMTP_CONFIGURATION_TABLE, "username"),
|
||||
}
|
||||
|
||||
// SMTPConfiguration is a controller
|
||||
type SMTPConfiguration struct {
|
||||
Common
|
||||
SMTPConfigurationService *service.SMTPConfiguration
|
||||
}
|
||||
|
||||
// Create creates a new SMTPConfiguration
|
||||
func (c *SMTPConfiguration) Create(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.SMTPConfiguration
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save SMTP configuration
|
||||
id, err := c.SMTPConfigurationService.Create(g, session, &req)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetAll gets SMTP configurations
|
||||
func (c *SMTPConfiguration) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(SMTPConfigurationColumnsMap)
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get
|
||||
smtpConfigs, err := c.SMTPConfigurationService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.SMTPConfigurationOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithCompany: true,
|
||||
WithHeaders: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, smtpConfigs)
|
||||
}
|
||||
|
||||
// GetByID gets a SMTP configuration by an ID
|
||||
func (c *SMTPConfiguration) GetByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get SMTP configuration
|
||||
smtpConfig, err := c.SMTPConfigurationService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&repository.SMTPConfigurationOption{
|
||||
WithCompany: true,
|
||||
WithHeaders: true,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, smtpConfig)
|
||||
}
|
||||
|
||||
type SMTPConfigurationTestEmailRequest struct {
|
||||
Email vo.Email `json:"email" binding:"required,email"`
|
||||
MailFrom vo.Email `json:"mailFrom" binding:"required,mailFrom"`
|
||||
}
|
||||
|
||||
// TestEmail tests the connection to a SMTP configuration
|
||||
func (c *SMTPConfiguration) TestEmail(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req SMTPConfigurationTestEmailRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// test dial
|
||||
err := c.SMTPConfigurationService.SendTestEmail(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&req.Email,
|
||||
&req.MailFrom,
|
||||
)
|
||||
// handle any error as a validation error
|
||||
if err != nil {
|
||||
err = errs.NewValidationError(err)
|
||||
}
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// UpdateByID updates a SMTP configuration - but not the headers
|
||||
func (c *SMTPConfiguration) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.SMTPConfiguration
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
err := c.SMTPConfigurationService.UpdateByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// AddHeader adds a header to a SMTP configuration
|
||||
func (c *SMTPConfiguration) AddHeader(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.SMTPHeader
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save header
|
||||
smtpID, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
createdID, err := c.SMTPConfigurationService.AddHeader(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
smtpID,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{
|
||||
"id": createdID.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveHeader removes a header from a SMTP configuration
|
||||
func (c *SMTPConfiguration) RemoveHeader(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
headerID, err := uuid.Parse(g.Param("headerID"))
|
||||
if err != nil {
|
||||
c.Logger.Debugw("invalid header id",
|
||||
"headerID", g.Param("headerID"),
|
||||
"error", err,
|
||||
)
|
||||
c.Response.BadRequestMessage(g, api.InvalidSMTPConfigurationID)
|
||||
return
|
||||
}
|
||||
// remove header
|
||||
err = c.SMTPConfigurationService.RemoveHeader(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&headerID,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a SMTP configuration
|
||||
func (c *SMTPConfiguration) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete
|
||||
err := c.SMTPConfigurationService.DeleteByID(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// SSO the single sign on controller
|
||||
type SSO struct {
|
||||
Common
|
||||
*service.SSO
|
||||
}
|
||||
|
||||
// Upsert upserts a SSO configuration
|
||||
func (s *SSO) Upsert(g *gin.Context) {
|
||||
session, _, ok := s.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var request model.SSOOption
|
||||
if ok := s.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
// handle upsert
|
||||
err := s.SSO.Upsert(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&request,
|
||||
)
|
||||
// handle responses
|
||||
if ok := s.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
s.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
func (s *SSO) IsEnabled(g *gin.Context) {
|
||||
// if no sso client is setup, then it is not enabled
|
||||
if s.SSO.MSALClient == nil {
|
||||
s.Response.OK(g, false)
|
||||
return
|
||||
}
|
||||
s.Response.OK(g, true)
|
||||
}
|
||||
|
||||
func (s *SSO) EntreIDLogin(g *gin.Context) {
|
||||
authURL, err := s.SSO.EntreIDLogin(g)
|
||||
if errors.Is(err, errs.ErrSSODisabled) {
|
||||
s.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
if ok := s.handleErrors(g, err); !ok {
|
||||
s.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
g.Redirect(http.StatusTemporaryRedirect, authURL)
|
||||
}
|
||||
|
||||
func (s *SSO) EntreIDCallBack(g *gin.Context) {
|
||||
code := g.Query("code")
|
||||
session, err := s.SSO.HandlEntraIDCallback(g, code)
|
||||
if err != nil {
|
||||
g.Redirect(http.StatusTemporaryRedirect, "/login?ssoAuthError=1")
|
||||
return
|
||||
}
|
||||
if ok := s.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// Set the session in the cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: data.SessionCookieKey,
|
||||
Value: session.ID.String(),
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Expires: *session.MaxAgeAt,
|
||||
}
|
||||
http.SetCookie(g.Writer, cookie)
|
||||
g.Redirect(http.StatusTemporaryRedirect, "/dashboard")
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Common
|
||||
UpdateService *service.Update
|
||||
OptionService *service.Option
|
||||
}
|
||||
|
||||
// CheckForUpdateCached checks if there is a new update from cache
|
||||
func (u *Update) CheckForUpdateCached(g *gin.Context) {
|
||||
session, _, ok := u.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
updateAvailable, usingSystemd, err := u.UpdateService.CheckForUpdateCached(g, session)
|
||||
if ok := u.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
u.Response.OK(g, gin.H{
|
||||
"updateAvailable": updateAvailable,
|
||||
"updateInApp": usingSystemd,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckForUpdate checks if there is a new update
|
||||
func (u *Update) CheckForUpdate(g *gin.Context) {
|
||||
session, _, ok := u.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
updateAvailable, usingSystemd, err := u.UpdateService.CheckForUpdate(g, session)
|
||||
if ok := u.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
u.Response.OK(g, gin.H{
|
||||
"updateAvailable": updateAvailable,
|
||||
"updateInApp": usingSystemd,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUpdateDetails gets details about the newest software update
|
||||
func (u *Update) GetUpdateDetails(g *gin.Context) {
|
||||
session, _, ok := u.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opt, err := u.OptionService.GetOption(g, session, data.OptionKeyUsingSystemd)
|
||||
if ok := u.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
details, err := u.UpdateService.GetUpdateDetails(g, session)
|
||||
if err != nil && !errors.Is(err, errs.ErrNoUpdateAvailable) {
|
||||
if ok := u.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, errs.ErrNoUpdateAvailable) {
|
||||
u.Response.OK(g, gin.H{
|
||||
"updateAvailable": false,
|
||||
"updateInApp": opt.Value.String() == data.OptionValueUsingSystemdYes,
|
||||
"downloadURL": "",
|
||||
"latestVersion": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
u.Response.OK(g, gin.H{
|
||||
"updateAvailable": true,
|
||||
"updateInApp": opt.Value.String() == data.OptionValueUsingSystemdYes,
|
||||
"downloadURL": details.DownloadURL,
|
||||
"latestVersion": details.LatestVersion,
|
||||
})
|
||||
}
|
||||
|
||||
// RunUpdate performs an update
|
||||
func (u *Update) RunUpdate(g *gin.Context) {
|
||||
session, _, ok := u.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := u.UpdateService.RunUpdate(g, session)
|
||||
if ok := u.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
u.Response.OK(g, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,922 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var SessionColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.SESSION_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.SESSION_TABLE, "updated_at"),
|
||||
"ip_address": repository.TableColumn(database.SESSION_TABLE, "ip_address"),
|
||||
}
|
||||
|
||||
var UserColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.USER_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.USER_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.USER_TABLE, "name"),
|
||||
"username": repository.TableColumn(database.USER_TABLE, "username"),
|
||||
"email": repository.TableColumn(database.USER_TABLE, "email"),
|
||||
}
|
||||
|
||||
// UserLoginRequest is a request for login with username and password
|
||||
type UserLoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TOTP string `json:"totp"`
|
||||
MFARecoveryCode string `json:"recoveryCode"`
|
||||
}
|
||||
|
||||
// UserSetupTOTPRequest is a request for setting up TOTP
|
||||
type UserSetupTOTPRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// UserSetupDisableTOTPRequest is a request for disabling TOTP
|
||||
type UserDisableTOTPRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// UserVerifyTOTPRequest is a request for verifying TOTP
|
||||
type UserVerifyTOTPRequest struct {
|
||||
TOTP string `json:"token"`
|
||||
}
|
||||
|
||||
// UserLoginWithMFARecoveryCodeRequest is a request for login with MFA recovery code
|
||||
type UserLoginWithMFARecoveryCodeRequest struct {
|
||||
RecoveryCode string `json:"recoveryCode"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// User is the change email controller
|
||||
type User struct {
|
||||
Common
|
||||
UserService *service.User
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
func (c *User) Create(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.UserUpsertRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// create user
|
||||
newUserID, err := c.UserService.Create(
|
||||
g,
|
||||
session,
|
||||
&req,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": newUserID.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetMaskedAPIKey gets logged-in users masked API key
|
||||
func (c *User) GetMaskedAPIKey(g *gin.Context) {
|
||||
session, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.handleErrors(g, errors.New("no user in session"))
|
||||
}
|
||||
// get
|
||||
cid := user.ID.MustGet()
|
||||
apiKey, err := c.UserService.GetMaskedAPIKey(
|
||||
g,
|
||||
session,
|
||||
&cid,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"apiKey": apiKey,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpsertAPIKey create/update API key
|
||||
func (c *User) UpsertAPIKey(g *gin.Context) {
|
||||
session, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.handleErrors(g, errors.New("no user in session"))
|
||||
}
|
||||
// create user
|
||||
uid := user.ID.MustGet()
|
||||
apiKey, err := c.UserService.UpsertAPIKey(
|
||||
g,
|
||||
session,
|
||||
&uid,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"apiKey": apiKey,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveAPIKey removes a api key
|
||||
func (c *User) RemoveAPIKey(g *gin.Context) {
|
||||
session, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.handleErrors(g, errors.New("no user in session"))
|
||||
}
|
||||
// create user
|
||||
uid := user.ID.MustGet()
|
||||
err := c.UserService.RemoveAPIKey(
|
||||
g,
|
||||
session,
|
||||
&uid,
|
||||
)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateByID updates a user by ID
|
||||
func (c *User) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.User
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// update user
|
||||
err := c.UserService.Update(
|
||||
g,
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// Delete deletes a user
|
||||
func (c *User) Delete(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete user
|
||||
err := c.UserService.Delete(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// GetAll gets all users using pagination
|
||||
func (c *User) GetAll(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(UserColumnsMap)
|
||||
// get user
|
||||
users, err := c.UserService.GetAll(g, session, &repository.UserOption{
|
||||
QueryArgs: queryArgs,
|
||||
WithRole: true,
|
||||
WithCompany: true,
|
||||
})
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, users)
|
||||
}
|
||||
|
||||
// GetByID gets a user by ID
|
||||
func (c *User) GetByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get user
|
||||
user, err := c.UserService.GetByID(g, session, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, user)
|
||||
}
|
||||
|
||||
// ChangeEmailOnLoggedInUser changes email on logged in user
|
||||
// this is an administrator action
|
||||
func (c *User) ChangeEmailOnLoggedInUser(g *gin.Context) {
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse and validate request
|
||||
var request model.UserChangeEmailRequest
|
||||
if ok := c.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
// change email
|
||||
userID := sessionUser.ID.MustGet()
|
||||
changedEmail, err := c.UserService.ChangeEmailAsAdministrator(
|
||||
g,
|
||||
session,
|
||||
&userID,
|
||||
&request.Email,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{"email": changedEmail.String()},
|
||||
)
|
||||
}
|
||||
|
||||
// ChangeFullnameOnLoggedInUser is the handler for change fullname
|
||||
func (c *User) ChangeFullnameOnLoggedInUser(g *gin.Context) {
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.UserChangeFullnameRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// change fullname
|
||||
userID := sessionUser.ID.MustGet()
|
||||
_, err := c.UserService.ChangeFullname(
|
||||
g,
|
||||
session,
|
||||
&userID,
|
||||
&req.NewFullname,
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// ChangePasswordOnLoggedInUser changes the password on the logged in user
|
||||
func (c *User) ChangePasswordOnLoggedInUser(g *gin.Context) {
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.UserChangePasswordRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// change password
|
||||
err := c.UserService.ChangePassword(
|
||||
g,
|
||||
session,
|
||||
&req.CurrentPassword,
|
||||
&req.NewPassword,
|
||||
)
|
||||
// handle response
|
||||
if errors.Is(err, errs.ErrUserWrongPasword) {
|
||||
c.Response.BadRequestMessage(g, "Invalid current password")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// invalidate all currently running sessions
|
||||
userID := sessionUser.ID.MustGet()
|
||||
err = c.SessionService.ExpireAllByUserID(g, session, &userID)
|
||||
// partial error, the password is changed but the sessions are not invalidated
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
"password changed - all sessions have been invalidated",
|
||||
)
|
||||
}
|
||||
|
||||
// ChangeUsernameOnLoggedInUser changes the username
|
||||
func (c *User) ChangeUsernameOnLoggedInUser(g *gin.Context) {
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req model.UserChangeUsernameOnLoggedInRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
userID := sessionUser.ID.MustGet()
|
||||
// change username
|
||||
err := c.UserService.ChangeUsername(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&userID,
|
||||
&req.NewUsername,
|
||||
)
|
||||
// handle error
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// ExpireSessionByID expires a session by ID
|
||||
// a administrator can expire any session
|
||||
// a user can expire their own sessions
|
||||
func (c *User) ExpireSessionByID(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
if !isAuthorized {
|
||||
c.Response.Forbidden(g)
|
||||
return
|
||||
}
|
||||
err = c.SessionService.Expire(g, id)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
"session expired",
|
||||
)
|
||||
}
|
||||
|
||||
// GetSessionsByUserID gets all sessions by user ID
|
||||
func (c *User) GetSessionsOnLoggedInUser(g *gin.Context) {
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := c.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
queryArgs.RemapOrderBy(SessionColumnsMap)
|
||||
userID := sessionUser.ID.MustGet()
|
||||
sessions, err := c.SessionService.GetSessionsByUserID(
|
||||
g,
|
||||
session,
|
||||
&userID,
|
||||
&repository.SessionOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
data := []map[string]interface{}{}
|
||||
for _, sess := range sessions.Rows {
|
||||
idStr := sess.ID.String()
|
||||
data = append(data, map[string]interface{}{
|
||||
"id": idStr,
|
||||
"current": idStr == session.ID.String(),
|
||||
"ip": sess.IP,
|
||||
"createdAt": sess.CreatedAt,
|
||||
"updatedAt": sess.UpdatedAt,
|
||||
})
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{"sessions": data},
|
||||
)
|
||||
}
|
||||
|
||||
// Login logs in a user
|
||||
func (c *User) Login(g *gin.Context) {
|
||||
// parse req
|
||||
var req UserLoginRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
user, err := c.UserService.AuthenticateUsernameWithPassword(
|
||||
g,
|
||||
req.Username,
|
||||
req.Password,
|
||||
g.ClientIP(),
|
||||
)
|
||||
if errors.Is(err, errs.ErrUserWrongPasword) {
|
||||
c.Response.BadRequestMessage(g, "Invalid password")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.Response.BadRequestMessage(g, "Invalid credentials")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// if the user has MFA enabled then we check the MFA flow
|
||||
// if the user has MFA enabled, we must check if there is a
|
||||
// valid MFA or a valid recovery code
|
||||
userID := user.ID.MustGet()
|
||||
MFATokenSupplied := len(req.TOTP) > 0
|
||||
MFARecoveryCodeSupplied := len(req.MFARecoveryCode) > 0
|
||||
mfaEnabled, err := c.UserService.IsTOTPEnabledByUserID(
|
||||
g,
|
||||
&userID,
|
||||
)
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid TOTP")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
if mfaEnabled {
|
||||
// if tokens or recovery codes are supplied
|
||||
// return mfa is required
|
||||
if !MFATokenSupplied && !MFARecoveryCodeSupplied {
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"mfa": true,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
// if the client has given both a TOTP and a recovery code
|
||||
// we return a bad request
|
||||
if MFATokenSupplied && MFARecoveryCodeSupplied {
|
||||
c.Response.BadRequestMessage(g, "Cannot supply both MFA token and MFA recovery code")
|
||||
return
|
||||
}
|
||||
// verify the TOTP MFA token
|
||||
userID := user.ID.MustGet()
|
||||
if MFATokenSupplied && !MFARecoveryCodeSupplied {
|
||||
// if MFA is enabled, verify the TOTP
|
||||
totpToken, err := vo.NewString64(req.TOTP)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create TOTP",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "TOTP", err)
|
||||
return
|
||||
}
|
||||
err = c.UserService.CheckTOTP(
|
||||
g,
|
||||
&userID,
|
||||
totpToken,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid TOTP")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the user has MFA enabled and the client has supplied a recovery code
|
||||
// we verify the recovery code
|
||||
if !MFATokenSupplied && MFARecoveryCodeSupplied {
|
||||
recoveryCode, err := vo.NewString64(req.MFARecoveryCode)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create recovery code",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "RecoveryCode", err)
|
||||
return
|
||||
}
|
||||
verifiedMFA, err := c.UserService.CheckMFARecoveryCode(
|
||||
g,
|
||||
&userID,
|
||||
recoveryCode,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUserWrongRecoveryCode) {
|
||||
c.Response.BadRequestMessage(g, "Invalid recovery code")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !verifiedMFA {
|
||||
c.Response.BadRequestMessage(g, "Invalid recovery code")
|
||||
return
|
||||
}
|
||||
// as the recovery code is valid, we can now disable MFA
|
||||
err = c.UserService.DisableTOTP(g, &userID)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// create a new session
|
||||
session, err := c.SessionService.Create(
|
||||
g,
|
||||
user,
|
||||
g.ClientIP(),
|
||||
)
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
// Set the session in the cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: data.SessionCookieKey,
|
||||
Value: session.ID.String(),
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Expires: *session.MaxAgeAt,
|
||||
}
|
||||
http.SetCookie(g.Writer, cookie)
|
||||
c.Response.OK(g, session)
|
||||
}
|
||||
|
||||
// expireCookieAndStatusOK expires the cookie and returns a 200 OK
|
||||
func (c *User) expireCookieAndStatusOK(g *gin.Context) {
|
||||
g.SetCookie(
|
||||
data.SessionCookieKey,
|
||||
"",
|
||||
-1,
|
||||
"/",
|
||||
"",
|
||||
false,
|
||||
true,
|
||||
)
|
||||
c.logoutOK(g)
|
||||
}
|
||||
|
||||
// logoutOK returns a 200 OK
|
||||
func (c *User) logoutOK(g *gin.Context) {
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{"message": "logged out"},
|
||||
)
|
||||
}
|
||||
|
||||
// Logout logs out the user
|
||||
// only invalidates the session if the session cookie is
|
||||
// in the request, this should reduce the risk of CSRF logout
|
||||
func (c *User) Logout(g *gin.Context) {
|
||||
sessionCookie, err := g.Cookie(data.SessionCookieKey)
|
||||
if err != nil {
|
||||
c.logoutOK(g)
|
||||
return
|
||||
}
|
||||
sessionID, err := uuid.Parse(sessionCookie)
|
||||
if err != nil {
|
||||
c.logoutOK(g)
|
||||
return
|
||||
}
|
||||
ctx := g.Request.Context()
|
||||
err = c.SessionService.Expire(ctx, &sessionID)
|
||||
if err != nil {
|
||||
c.expireCookieAndStatusOK(g)
|
||||
return
|
||||
}
|
||||
c.expireCookieAndStatusOK(g)
|
||||
}
|
||||
|
||||
// SessionPing pings the session
|
||||
func (c *User) SessionPing(g *gin.Context) {
|
||||
// handle session
|
||||
session, sessionUser, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Logger.Debugw("pinged session for user",
|
||||
"userID", sessionUser.ID.MustGet().String(),
|
||||
)
|
||||
sessionRole := sessionUser.Role
|
||||
if sessionRole == nil {
|
||||
c.Logger.Error("failed to load role from session user")
|
||||
c.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
sessionCompany := sessionUser.Company
|
||||
companyName := ""
|
||||
if sessionCompany != nil {
|
||||
companyName = sessionCompany.Name.MustGet().String()
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"userID": sessionUser.ID,
|
||||
"username": sessionUser.Username.MustGet().String(),
|
||||
"name": sessionUser.Name.MustGet().String(),
|
||||
"role": sessionRole.Name,
|
||||
"company": companyName,
|
||||
"ip": session.IP,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// InvalidateAllSessionByUserID is the nuclear session button for a user
|
||||
func (c *User) InvalidateAllSessionByUserID(g *gin.Context) {
|
||||
session, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var userID *uuid.UUID
|
||||
// parse req
|
||||
var req model.InvalidateAllSessionRequest
|
||||
err := g.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
if user == nil || !user.ID.IsSpecified() {
|
||||
c.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
uid := user.ID.MustGet()
|
||||
userID = &uid
|
||||
} else {
|
||||
if req.UserID == nil {
|
||||
c.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
userID = req.UserID
|
||||
}
|
||||
// invalidate
|
||||
err = c.SessionService.ExpireAllByUserID(g, session, userID)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// SetupTOTP generates a new TOTP MFA secrets
|
||||
func (c *User) SetupTOTP(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var request UserSetupTOTPRequest
|
||||
if ok := c.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
passwd, err := vo.NewReasonableLengthPassword(request.Password)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create password",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "Password", err)
|
||||
return
|
||||
}
|
||||
// get and save TOTP for user
|
||||
totpValues, err := c.UserService.SetupTOTP(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
passwd,
|
||||
)
|
||||
// handle response
|
||||
if errors.Is(err, errs.ErrAuthenticationFailed) {
|
||||
c.Response.BadRequestMessage(g, "Incorrect password")
|
||||
return
|
||||
}
|
||||
if ok := handleServerError(g, c.Response, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"base32": totpValues.Secret,
|
||||
"url": totpValues.URL,
|
||||
"recoveryCode": totpValues.RecoveryCode,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// SetupVerifyTOTP verifies a TOTP
|
||||
func (c *User) SetupVerifyTOTP(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req UserVerifyTOTPRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
totp, err := vo.NewString64(req.TOTP)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create TOTP",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "TOTP", err)
|
||||
return
|
||||
}
|
||||
// verify TOTP
|
||||
err = c.UserService.SetupCheckTOTP(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
totp,
|
||||
)
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
"TOTP verified",
|
||||
)
|
||||
}
|
||||
|
||||
// IsTOTPEnabled checks if TOTP is enabled
|
||||
func (c *User) IsTOTPEnabled(g *gin.Context) {
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// check if TOTP is enabled
|
||||
isEnabled, err := c.UserService.IsTOTPEnabled(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
)
|
||||
// handle response
|
||||
if ok := handleServerError(g, c.Response, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
gin.H{"enabled": isEnabled},
|
||||
)
|
||||
}
|
||||
|
||||
// DisableTOTP disables TOTP
|
||||
func (c *User) DisableTOTP(g *gin.Context) {
|
||||
_, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var request UserDisableTOTPRequest
|
||||
if ok := c.handleParseRequest(g, &request); !ok {
|
||||
return
|
||||
}
|
||||
token, err := vo.NewString64(request.Token)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create token",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "Token", err)
|
||||
return
|
||||
}
|
||||
// check TOTP
|
||||
userID := user.ID.MustGet()
|
||||
err = c.UserService.CheckTOTP(
|
||||
g.Request.Context(),
|
||||
&userID,
|
||||
token,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid token")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// disable TOTP
|
||||
err = c.UserService.DisableTOTP(
|
||||
g.Request.Context(),
|
||||
&userID,
|
||||
)
|
||||
// handle response
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid token")
|
||||
return
|
||||
}
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
"TOTP disabled",
|
||||
)
|
||||
}
|
||||
|
||||
// VerifyTOTP verifies a TOTP
|
||||
func (c *User) VerifyTOTP(g *gin.Context) {
|
||||
_, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse req
|
||||
var req UserVerifyTOTPRequest
|
||||
if ok := c.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
totp, err := vo.NewString64(req.TOTP)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to create TOTP",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "TOTP", err)
|
||||
return
|
||||
}
|
||||
// verify TOTP
|
||||
userID := user.ID.MustGet()
|
||||
err = c.UserService.CheckTOTP(
|
||||
g.Request.Context(),
|
||||
&userID,
|
||||
totp,
|
||||
)
|
||||
if errors.Is(err, errs.ErrUserWrongTOTP) {
|
||||
c.Response.BadRequestMessage(g, "Invalid token")
|
||||
return
|
||||
}
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(
|
||||
g,
|
||||
"TOTP verified",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/api"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Common is a common controller base struct it holds common operations on the
|
||||
// common dependencies
|
||||
type Common struct {
|
||||
Response api.JSONResponseHandler
|
||||
Logger *zap.SugaredLogger
|
||||
SessionService *service.Session
|
||||
}
|
||||
|
||||
// handleSession handles the session and returns the session and user
|
||||
// if the session is not valid, a 401 response is sent
|
||||
func (c *Common) handleSession(
|
||||
g *gin.Context,
|
||||
) (*model.Session, *model.User, bool) {
|
||||
s, ok := g.Get("session")
|
||||
if !ok {
|
||||
c.Logger.Debug("session not found in context")
|
||||
c.Response.Unauthorized(g)
|
||||
return nil, nil, false
|
||||
}
|
||||
session, ok := s.(*model.Session)
|
||||
if !ok {
|
||||
c.Logger.Error("session in context is not of type model.Session")
|
||||
c.Response.Unauthorized(g)
|
||||
return nil, nil, false
|
||||
}
|
||||
user := session.User
|
||||
if user == nil {
|
||||
c.Logger.Error("user not found in session")
|
||||
c.Response.Unauthorized(g)
|
||||
return nil, nil, false
|
||||
}
|
||||
return session, user, true
|
||||
}
|
||||
|
||||
// HandleParseRequest parses the request and returns true if successful
|
||||
// if the request is not parsable, a 400 response is sent
|
||||
func (c *Common) handleParseRequest(
|
||||
g *gin.Context,
|
||||
req any,
|
||||
) bool {
|
||||
body, err := io.ReadAll(g.Request.Body)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to read request body",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.BadRequest(g)
|
||||
return false
|
||||
}
|
||||
if err := utils.Unmarshal(body, &req); err != nil {
|
||||
c.Logger.Debugw("failed to parse request",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.BadRequestMessage(g, err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleParseIDParam parses the id parameter from the request
|
||||
// and returns it if successful
|
||||
// if the id is not parsable, a 400 response is sent
|
||||
func (c *Common) handleParseIDParam(
|
||||
g *gin.Context,
|
||||
) (*uuid.UUID, bool) {
|
||||
id, err := uuid.Parse(g.Param("id"))
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to parse id",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.BadRequestMessage(g, errs.MsgFailedToParseUUID)
|
||||
return nil, false
|
||||
}
|
||||
return &id, true
|
||||
}
|
||||
|
||||
// handlePagination parses the pagination from the request and returns it
|
||||
// if the pagination is not valid, a 400 response is sent
|
||||
func (c *Common) handlePagination(
|
||||
g *gin.Context,
|
||||
) (*vo.Pagination, bool) {
|
||||
pagination, err := vo.NewPaginationFromRequest(g)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("invalid offset or limit",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "pagination", err)
|
||||
return nil, false
|
||||
}
|
||||
return pagination, true
|
||||
}
|
||||
|
||||
// handleQueryArgs parses the query from the request and returns it
|
||||
func (c *Common) handleQueryArgs(g *gin.Context) (*vo.QueryArgs, bool) {
|
||||
q, err := vo.QueryFromRequest(g)
|
||||
if err != nil {
|
||||
c.Logger.Debugw("failed to parse query",
|
||||
"error", err,
|
||||
)
|
||||
c.Response.ValidationFailed(g, "query args", err)
|
||||
return nil, false
|
||||
}
|
||||
return q, true
|
||||
}
|
||||
|
||||
// handleErrors is a helper function to handle common handleErrors
|
||||
// it most often checks for more than what is needed, but is
|
||||
// useful to avoid missing any error handling and saving time
|
||||
// it returns true if no errors are found
|
||||
// it returns false if an error is found and a response is sent
|
||||
func (c *Common) handleErrors(
|
||||
g *gin.Context,
|
||||
err error,
|
||||
) bool {
|
||||
if err != nil {
|
||||
if ok := handleAuthorizationError(g, c.Response, err); !ok {
|
||||
c.Logger.Debugw("authorization error",
|
||||
"auth_error", err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
if ok := handleValidationError(g, c.Response, err); !ok {
|
||||
c.Logger.Debugw("validation error",
|
||||
"validation_error", err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
if ok := handleCustomError(g, c.Response, err); !ok {
|
||||
c.Logger.Debugw("custom error",
|
||||
"custom_error", err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
if ok := handleDBRowNotFound(g, c.Response, err); !ok {
|
||||
c.Logger.Debugw("DB row not found error",
|
||||
"error", err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
c.Logger.Errorw("API unknown error type", "error", err)
|
||||
_ = handleServerError(g, c.Response, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// responseWithCSV
|
||||
func (c *Common) responseWithCSV(
|
||||
g *gin.Context,
|
||||
buffer *bytes.Buffer,
|
||||
writer *csv.Writer,
|
||||
name string,
|
||||
) {
|
||||
writer.Flush()
|
||||
if err := writer.Error(); err != nil {
|
||||
c.handleErrors(g, err)
|
||||
return
|
||||
}
|
||||
// Set CSV response headers
|
||||
setSecureContentDisposition(g, name)
|
||||
g.Header("Content-Type", "text/csv")
|
||||
g.Header("Content-Length", fmt.Sprint(buffer.Len()))
|
||||
|
||||
// Write the CSV buffer to the response
|
||||
_, err := g.Writer.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
c.handleErrors(g, err)
|
||||
}
|
||||
}
|
||||
|
||||
// responseWithZIP
|
||||
func (c *Common) responseWithZIP(
|
||||
g *gin.Context,
|
||||
buffer *bytes.Buffer,
|
||||
name string,
|
||||
) {
|
||||
g.Header("Content-Type", "application/zip")
|
||||
setSecureContentDisposition(g, name)
|
||||
g.Header("Content-Transfer-Encoding", "binary")
|
||||
g.Header("Expires", "0")
|
||||
g.Header("Cache-Control", "must-revalidate")
|
||||
g.Header("Pragma", "public")
|
||||
g.Header("Content-Length", fmt.Sprintf("%d", buffer.Len()))
|
||||
|
||||
_, err := g.Writer.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
c.handleErrors(g, err)
|
||||
}
|
||||
}
|
||||
|
||||
// companyIDFromRequestQuery returns the companyID as a UUID from the query
|
||||
// or nil if not found
|
||||
func companyIDFromRequestQuery(g *gin.Context) *uuid.UUID {
|
||||
companyID := g.Query("companyID")
|
||||
if companyID != "" {
|
||||
cid, err := uuid.Parse(companyID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &cid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSessionInGinContext sets the session in the gin context
|
||||
func SetSessionInGinContext(c *gin.Context, s *model.Session) {
|
||||
c.Set("session", s)
|
||||
}
|
||||
|
||||
// handleDBRowNotFound checks if the error is a not found error
|
||||
// if it is, a 404 response is sent
|
||||
// if it is not, true is returned
|
||||
func handleDBRowNotFound(
|
||||
g *gin.Context,
|
||||
responseHandler api.JSONResponseHandler,
|
||||
err error,
|
||||
) bool {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// error is logged in service
|
||||
_ = err
|
||||
responseHandler.NotFound(g)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleAuthorizationError checks if the error is an authorization error
|
||||
// if it is, a 403 response is sent
|
||||
// if it is not, true is returned
|
||||
func handleAuthorizationError(
|
||||
g *gin.Context,
|
||||
responseHandler api.JSONResponseHandler,
|
||||
err error,
|
||||
) bool {
|
||||
if errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
// error is logged in service
|
||||
_ = err
|
||||
responseHandler.Forbidden(g)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleValidationError checks if the error is a validation error
|
||||
// if it is, a 400 response is sent
|
||||
// if it is not, true is returned
|
||||
func handleValidationError(
|
||||
g *gin.Context,
|
||||
responseHandler api.JSONResponseHandler,
|
||||
err error,
|
||||
) bool {
|
||||
if errors.As(err, &errs.ValidationError{}) {
|
||||
// error is logged in service
|
||||
_ = err
|
||||
responseHandler.BadRequestMessage(g, err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleCustomError checks if the error is a custom error
|
||||
// if it is a 400 response is sent
|
||||
// if it is not, true is returned
|
||||
func handleCustomError(
|
||||
g *gin.Context,
|
||||
responseHandler api.JSONResponseHandler,
|
||||
err error,
|
||||
) bool {
|
||||
if errors.As(err, &errs.CustomError{}) {
|
||||
// error is logged in service
|
||||
_ = err
|
||||
responseHandler.BadRequestMessage(g, err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleServerError checks if the error is a server error
|
||||
// if it is, a 500 response is sent
|
||||
// if it is not, true is returned
|
||||
func handleServerError(
|
||||
g *gin.Context,
|
||||
responseHandler api.JSONResponseHandler,
|
||||
err error,
|
||||
) bool {
|
||||
if err != nil {
|
||||
// error is logged in service
|
||||
_ = err
|
||||
responseHandler.ServerError(g)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setSecureContentDisposition(c *gin.Context, filename string) {
|
||||
// Strip any directory components
|
||||
filename = filepath.Base(filename)
|
||||
|
||||
// Remove any potentially problematic characters
|
||||
filename = strings.Map(func(r rune) rune {
|
||||
// Keep only alphanumeric, space, dash, underscore and dot
|
||||
if (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
(r == ' ' || r == '-' || r == '_' || r == '.') {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, filename)
|
||||
|
||||
// Ensure we still have a valid filename
|
||||
if filename == "" || filename == "." || filename == ".." {
|
||||
filename = time.Now().UTC().Format("20060102150405")
|
||||
}
|
||||
|
||||
// Properly encode the filename for Content-Disposition
|
||||
encodedFilename := mime.QEncoding.Encode("utf-8", filename)
|
||||
|
||||
c.Header("Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="%s";`,
|
||||
encodedFilename,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Common) requiresFlag(g *gin.Context, featureFlag string) {
|
||||
// handle session
|
||||
_, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Response.ServerErrorMessage(g, "requires "+featureFlag+" edition")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// Version is a controller
|
||||
type Version struct {
|
||||
Common
|
||||
versionService *service.Version
|
||||
}
|
||||
|
||||
// Get application version
|
||||
func (c *Version) Get(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
version, err := c.versionService.Get(g.Request.Context(), session)
|
||||
if ok := handleServerError(g, c.Response, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, version)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// WebhookColumnsMap 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 WebhookColumnsMap = map[string]string{
|
||||
"created_at": repository.TableColumn(database.WEBHOOK_TABLE, "created_at"),
|
||||
"updated_at": repository.TableColumn(database.WEBHOOK_TABLE, "updated_at"),
|
||||
"name": repository.TableColumn(database.WEBHOOK_TABLE, "name"),
|
||||
}
|
||||
|
||||
// Webhook is a controller
|
||||
type Webhook struct {
|
||||
Common
|
||||
WebhookService *service.Webhook
|
||||
}
|
||||
|
||||
// Create creates a new webhook
|
||||
func (w *Webhook) Create(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
var req model.Webhook
|
||||
if ok := w.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save webhook
|
||||
id, err := w.WebhookService.Create(g.Request.Context(), session, &req)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(
|
||||
g,
|
||||
gin.H{
|
||||
"id": id.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetAll gets the webhooks
|
||||
func (w *Webhook) GetAll(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
queryArgs, ok := w.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
// get
|
||||
webhooks, err := w.WebhookService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.WebhookOption{
|
||||
QueryArgs: queryArgs,
|
||||
},
|
||||
)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(
|
||||
g,
|
||||
webhooks,
|
||||
)
|
||||
}
|
||||
|
||||
// GetByID gets a webhook by id
|
||||
func (w *Webhook) GetByID(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := w.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// get
|
||||
webhook, err := w.WebhookService.GetByID(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(g, webhook)
|
||||
}
|
||||
|
||||
// Update updates a webhook
|
||||
func (w *Webhook) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := w.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req model.Webhook
|
||||
if ok := w.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
// save
|
||||
err := w.WebhookService.Update(g.Request.Context(), session, id, &req)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(g, nil)
|
||||
}
|
||||
|
||||
// DeleteByID deletes a webhook by id
|
||||
func (w *Webhook) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := w.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// delete
|
||||
err := w.WebhookService.DeleteByID(g, session, id)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(g, nil)
|
||||
}
|
||||
|
||||
// SendTest sends a test webhook
|
||||
func (w *Webhook) SendTest(g *gin.Context) {
|
||||
session, _, ok := w.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := w.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// send
|
||||
data, err := w.WebhookService.SendTest(g.Request.Context(), session, id)
|
||||
// handle response
|
||||
if ok := w.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
w.Response.OK(g, data)
|
||||
}
|
||||
Reference in New Issue
Block a user