mirror of https://github.com/gophish/gophish
Initial suport for pagination
This commit adds support for pagination for users (Users and Groups menu item).pagination-support
parent
8b8e88b077
commit
90d46eb463
|
@ -4,11 +4,13 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
@ -24,16 +26,35 @@ func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
//POST: Create a new group and return it as JSON
|
||||
// POST: Create a new group and return it as JSON
|
||||
case r.Method == "POST":
|
||||
g := models.Group{}
|
||||
// Put the request into a group
|
||||
err := json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
|
||||
// Check if content is CSV
|
||||
// NB We can only upload one file at a time for new group creation, as three seperate POSTs are sent in quick succesion. We can't
|
||||
// identify which group the 1+n files should be assigned to. Need to give this more thought, perhaps we can upload multiple files in one POST,
|
||||
// or assign a temporary tracking token to link them together.
|
||||
var csvmode = false
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
csvmode = true
|
||||
targets, groupname, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
g.Name = groupname
|
||||
g.Targets = targets
|
||||
} else {
|
||||
// else decode as JSON
|
||||
err := json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||
return
|
||||
|
@ -45,6 +66,12 @@ func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
||||
// will still return the full list.
|
||||
if csvmode == true {
|
||||
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
@ -68,14 +95,51 @@ func (as *Server) GroupsSummary(w http.ResponseWriter, r *http.Request) {
|
|||
func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64))
|
||||
|
||||
// Paramters passed by DataTables for pagination are handled below
|
||||
v := r.URL.Query()
|
||||
search := v.Get("search[value]")
|
||||
|
||||
sortcolumn := v.Get("order[0][column]")
|
||||
sortdir := v.Get("order[0][dir]")
|
||||
sortby := v.Get("columns[" + sortcolumn + "][data]")
|
||||
order := sortby + " " + sortdir // e.g "first_name asc"
|
||||
|
||||
start, err := strconv.ParseInt(v.Get("start"), 0, 64)
|
||||
if err != nil {
|
||||
start = -1 // Default. gorm will ignore with this value.
|
||||
}
|
||||
length, err := strconv.ParseInt(v.Get("length"), 0, 64)
|
||||
if err != nil {
|
||||
length = -1 // Default. gorm will ignore with this value.
|
||||
}
|
||||
draw, err := strconv.ParseInt(v.Get("draw"), 0, 64)
|
||||
if err != nil {
|
||||
draw = -1 // If the draw value is missing we can assume this is not a DataTable request and return regular API result
|
||||
}
|
||||
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), start, length, search, order)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
|
||||
// If draw paratmer is -1 return regular API response, otherwise return pagination response
|
||||
if draw == -1 {
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
} else {
|
||||
// Handle pagination for DataTable
|
||||
gs, _ := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64)) // We need to get the total number of records of the group
|
||||
dT := models.DataTable{Draw: draw, RecordsTotal: gs.NumTargets, RecordsFiltered: gs.NumTargets}
|
||||
dT.Data = make([]interface{}, len(g.Targets)) // Pseudocode of 'dT.Data = g.Targets'. https://golang.org/doc/faq#convert_slice_of_interface
|
||||
for i, v := range g.Targets {
|
||||
dT.Data[i] = v
|
||||
}
|
||||
JSONResponse(w, dT, http.StatusOK)
|
||||
}
|
||||
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteGroup(&g)
|
||||
if err != nil {
|
||||
|
@ -86,11 +150,29 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
|||
case r.Method == "PUT":
|
||||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding group: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
//Check if content is CSV
|
||||
var csvmode = false
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
csvmode = true
|
||||
targets, groupname, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// We need to fetch all the existing targets for this group, so as to not overwrite them below
|
||||
et, _ := models.GetTargets(id, -1, -1, "", "")
|
||||
g.Targets = append(targets, et...)
|
||||
g.Name = groupname
|
||||
g.Id = id // ID isn't supplied in the CSV file upload. Perhaps we could use the filename paramter for this? I'm not sure if this is necessary though.
|
||||
} else { // else JSON
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding group: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
|
@ -103,6 +185,12 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
||||
// will still return the full list.
|
||||
if csvmode == true {
|
||||
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
@ -121,3 +209,81 @@ func (as *Server) GroupSummary(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// GroupTarget handles interactions with individual targets
|
||||
func (as *Server) GroupTarget(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
gid, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
||||
|
||||
// Ensure the group belongs to the user
|
||||
_, err := models.GetGroup(gid, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
t := models.Target{}
|
||||
err = json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Unable to decode target"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "PUT":
|
||||
// Add an individual target to a group
|
||||
err = models.AddTargetToGroup(t, gid)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Unable to add target to group"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Added target to group"}, http.StatusCreated)
|
||||
case r.Method == "DELETE":
|
||||
|
||||
err := models.DeleteTarget(&t, gid, ctx.Get(r, "user_id").(int64)) // We pass the group id to update modified date, and userid to ensure user owner the target
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Deleted target"}, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// GroupRename handles renaming of a group (without supplying all the targets, as in Group() PUT above)
|
||||
func (as *Server) GroupRename(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.Method == "PUT":
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding group: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.UpdateGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Group renamed"}, http.StatusCreated)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ type emailResponse struct {
|
|||
|
||||
// ImportGroup imports a CSV of group members
|
||||
func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||
ts, err := util.ParseCSV(r)
|
||||
ts, _, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -71,6 +71,9 @@ func (as *Server) registerRoutes() {
|
|||
router.HandleFunc("/groups/summary", as.GroupsSummary)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}", as.Group)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}/summary", as.GroupSummary)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}/rename", as.GroupRename)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}/target/add", as.GroupTarget)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}/target/delete", as.GroupTarget)
|
||||
router.HandleFunc("/templates/", as.Templates)
|
||||
router.HandleFunc("/templates/{id:[0-9]+}", as.Template)
|
||||
router.HandleFunc("/pages/", as.Pages)
|
||||
|
|
116
models/group.go
116
models/group.go
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
|
@ -46,7 +47,7 @@ type GroupTarget struct {
|
|||
// Target contains the fields needed for individual targets specified by the user
|
||||
// Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups
|
||||
type Target struct {
|
||||
Id int64 `json:"-"`
|
||||
Id int64 `json:"id"`
|
||||
BaseRecipient
|
||||
}
|
||||
|
||||
|
@ -59,6 +60,15 @@ type BaseRecipient struct {
|
|||
Position string `json:"position"`
|
||||
}
|
||||
|
||||
// DataTable is used to return a JSON object suitable for consumption by DataTables
|
||||
// when using pagination
|
||||
type DataTable struct {
|
||||
Draw int64 `json:"draw"`
|
||||
RecordsTotal int64 `json:"recordsTotal"`
|
||||
RecordsFiltered int64 `json:"recordsFiltered"`
|
||||
Data []interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// FormatAddress returns the email address to use in the "To" header of the email
|
||||
func (r *BaseRecipient) FormatAddress() string {
|
||||
addr := r.Email
|
||||
|
@ -114,7 +124,7 @@ func GetGroups(uid int64) ([]Group, error) {
|
|||
return gs, err
|
||||
}
|
||||
for i := range gs {
|
||||
gs[i].Targets, err = GetTargets(gs[i].Id)
|
||||
gs[i].Targets, err = GetTargets(gs[i].Id, -1, -1, "", "")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
@ -144,14 +154,15 @@ func GetGroupSummaries(uid int64) (GroupSummaries, error) {
|
|||
}
|
||||
|
||||
// GetGroup returns the group, if it exists, specified by the given id and user_id.
|
||||
func GetGroup(id int64, uid int64) (Group, error) {
|
||||
// Filter on number of results and starting point with 'start' and 'length' for pagination.
|
||||
func GetGroup(id int64, uid int64, start int64, length int64, search string, order string) (Group, error) {
|
||||
g := Group{}
|
||||
err := db.Where("user_id=? and id=?", uid, id).Find(&g).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return g, err
|
||||
}
|
||||
g.Targets, err = GetTargets(g.Id)
|
||||
g.Targets, err = GetTargets(g.Id, start, length, search, order)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
@ -183,7 +194,7 @@ func GetGroupByName(n string, uid int64) (Group, error) {
|
|||
log.Error(err)
|
||||
return g, err
|
||||
}
|
||||
g.Targets, err = GetTargets(g.Id)
|
||||
g.Targets, err = GetTargets(g.Id, -1, -1, "", "")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
@ -226,7 +237,7 @@ func PutGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
// Fetch group's existing targets from database.
|
||||
ts, err := GetTargets(g.Id)
|
||||
ts, err := GetTargets(g.Id, -1, -1, "", "")
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"group_id": g.Id,
|
||||
|
@ -312,6 +323,63 @@ func DeleteGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// DeleteTarget deletes a single target from a group given by target ID
|
||||
func DeleteTarget(t *Target, gid int64, uid int64) error {
|
||||
|
||||
targetOwner, err := GetTargetOwner(t.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetOwner != uid {
|
||||
return errors.New("No such target id (wrong owner)")
|
||||
}
|
||||
|
||||
err = db.Delete(t).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.Where("target_id=?", t.Id).Delete(&GroupTarget{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Update group modification date
|
||||
err = db.Debug().Model(&Group{}).Where("id=?", gid).Update("ModifiedDate", time.Now().UTC()).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateGroup updates a given group (without updating the targets)
|
||||
// Note: I thought about putting this in the Group() function, but we'd have to skip the validation and have a boolean
|
||||
// indicating we just want to rename the group.
|
||||
func UpdateGroup(g *Group) error {
|
||||
if g.Name == "" {
|
||||
return ErrGroupNameNotSpecified
|
||||
}
|
||||
err := db.Save(g).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTargetToGroup adds a single given target to a group by group ID
|
||||
func AddTargetToGroup(nt Target, gid int64) error {
|
||||
// Check if target already exists in group
|
||||
tmpt, err := GetTargetByEmail(gid, nt.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If target exists in group, update it.
|
||||
if len(tmpt) > 0 {
|
||||
nt.Id = tmpt[0].Id
|
||||
err = UpdateTarget(db, nt)
|
||||
} else {
|
||||
err = insertTargetIntoGroup(db, nt, gid)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.Model(&Group{}).Where("id=?", gid).Update("ModifiedDate", time.Now().UTC()).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func insertTargetIntoGroup(tx *gorm.DB, t Target, gid int64) error {
|
||||
if _, err := mail.ParseAddress(t.Email); err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
|
@ -357,8 +425,40 @@ func UpdateTarget(tx *gorm.DB, target Target) error {
|
|||
}
|
||||
|
||||
// GetTargets performs a many-to-many select to get all the Targets for a Group
|
||||
func GetTargets(gid int64) ([]Target, error) {
|
||||
// Start, length, and search can be supplied, or -1, -1, "" to ignore
|
||||
func GetTargets(gid int64, start int64, length int64, search string, order string) ([]Target, error) {
|
||||
|
||||
ts := []Target{}
|
||||
err := db.Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Scan(&ts).Error
|
||||
var err error
|
||||
|
||||
order = strings.TrimSpace(order)
|
||||
search = strings.TrimSpace(search)
|
||||
if order == "" {
|
||||
order = "targets.first_name asc"
|
||||
} else {
|
||||
order = "targets." + order
|
||||
}
|
||||
|
||||
// TODO: Rather than having two queries create a partial query and include the search options. Haven't been able to figure out how yet.
|
||||
if search != "" {
|
||||
search = "%" + search + "%"
|
||||
err = db.Order(order).Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Where("targets.first_name LIKE ? OR targets.last_name LIKE ? OR targets.email LIKE ? or targets.position LIKE ?", search, search, search, search, search).Offset(start).Limit(length).Scan(&ts).Error
|
||||
} else {
|
||||
err = db.Order(order).Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Offset(start).Limit(length).Scan(&ts).Error
|
||||
}
|
||||
return ts, err
|
||||
}
|
||||
|
||||
// GetTargetByEmail gets a single target from a group by email address and group id
|
||||
func GetTargetByEmail(gid int64, email string) ([]Target, error) {
|
||||
ts := []Target{}
|
||||
err := db.Table("targets").Select("targets.id, targets.email, targets.first_name, targets.last_name, targets.position").Joins("left join group_targets gt ON targets.id = gt.target_id").Where("gt.group_id=?", gid).Where("targets.email=?", email).First(&ts).Error
|
||||
return ts, err
|
||||
}
|
||||
|
||||
// GetTargetOwner returns the user id owner of a given target id
|
||||
func GetTargetOwner(tid int64) (int64, error) {
|
||||
g := Group{}
|
||||
err := db.Table("groups").Select("groups.user_id").Joins("left join group_targets on group_targets.group_id = groups.id").Where("group_targets.target_id = ?", tid).Scan(&g).Error
|
||||
return g.UserId, err
|
||||
}
|
||||
|
|
|
@ -137,6 +137,26 @@ var api = {
|
|||
// delete() - Deletes a group at DELETE /groups/:id
|
||||
delete: function (id) {
|
||||
return query("/groups/" + id, "DELETE", {}, false)
|
||||
},
|
||||
// summary() - Queries the API for GET /groups/:id/summary
|
||||
summary: function (id) {
|
||||
return query("/groups/" + id + "/summary", "GET", {}, true)
|
||||
},
|
||||
// rename() - Renames a group
|
||||
rename: function(id, group) {
|
||||
return query("/groups/" + id + "/rename", "PUT", group, false)
|
||||
},
|
||||
// add() - Adds a single target to a group
|
||||
addtarget: function(id, target) {
|
||||
return query("/groups/" + id + "/target/add", "PUT", target, false)
|
||||
},
|
||||
// delete() - Deletes a single targert by id at DELETE /groups/:id
|
||||
deletetarget: function (id, target) {
|
||||
return query("/groups/" + id + "/target/delete", "DELETE", target, false)
|
||||
},
|
||||
// post() - Posts a CSV group to POST /groups/:id/csv
|
||||
post: function (id, data) {
|
||||
return query("/groups/" + id + "/csv", "POST", data, false)
|
||||
}
|
||||
},
|
||||
// templates contains the endpoints for /templates
|
||||
|
|
|
@ -1,99 +1,82 @@
|
|||
var groups = []
|
||||
|
||||
// Save attempts to POST or PUT to /groups/
|
||||
function save(id) {
|
||||
var targets = []
|
||||
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
|
||||
targets.push({
|
||||
first_name: unescapeHtml(target[0]),
|
||||
last_name: unescapeHtml(target[1]),
|
||||
email: unescapeHtml(target[2]),
|
||||
position: unescapeHtml(target[3])
|
||||
})
|
||||
})
|
||||
var group = {
|
||||
name: $("#name").val(),
|
||||
targets: targets
|
||||
}
|
||||
// Submit the group
|
||||
if (id != -1) {
|
||||
// If we're just editing an existing group,
|
||||
// we need to PUT /groups/:id
|
||||
group.id = id
|
||||
api.groupId.put(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new group, POST it
|
||||
// to /groups
|
||||
api.groups.post(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group added successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
||||
$("#name").val("")
|
||||
$("#modal\\.flashes").empty()
|
||||
}
|
||||
|
||||
function edit(id) {
|
||||
targets = $("#targetsTable").dataTable({
|
||||
function instantiateDataTable(id) {
|
||||
$('#targetsTable').dataTable( {
|
||||
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||
select: true,
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
})
|
||||
$("#modalSubmit").unbind('click').click(function () {
|
||||
save(id)
|
||||
})
|
||||
if (id == -1) {
|
||||
var group = {}
|
||||
}],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": {
|
||||
url: "/api/groups/" + id,
|
||||
'beforeSend': function (request) {
|
||||
request.setRequestHeader("Authorization", "Bearer " + user.api_key);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'first_name', render: escapeHtml},
|
||||
{ data: 'last_name', render: escapeHtml },
|
||||
{ data: 'email', render: escapeHtml },
|
||||
{ data: 'position', render: escapeHtml },
|
||||
{ data: null,
|
||||
render: function ( data, type, row ) {
|
||||
return '<span style="cursor:pointer;"><i class="fa fa-trash-o" id="' + data.id + '"></i></span>';
|
||||
}
|
||||
}]
|
||||
} );
|
||||
}
|
||||
|
||||
function edit(id) {
|
||||
|
||||
$("#groupid").val(id)
|
||||
|
||||
if (id == -1 ){ // New group
|
||||
$("#targetsTable_wrapper").hide()
|
||||
$("#targetsTable").hide()
|
||||
} else {
|
||||
api.groupId.get(id)
|
||||
api.groupId.summary(id)
|
||||
.success(function (group) {
|
||||
$("#name").val(group.name)
|
||||
targetRows = []
|
||||
$.each(group.targets, function (i, record) {
|
||||
targetRows.push([
|
||||
escapeHtml(record.first_name),
|
||||
escapeHtml(record.last_name),
|
||||
escapeHtml(record.email),
|
||||
escapeHtml(record.position),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
])
|
||||
});
|
||||
targets.DataTable().rows.add(targetRows).draw()
|
||||
$("#name").val(escapeHtml(group.name))
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching group")
|
||||
.error(function (data) {
|
||||
modalError("Error fetching group name")
|
||||
})
|
||||
instantiateDataTable(id)
|
||||
}
|
||||
|
||||
// Handle file uploads
|
||||
csvurl = "/api/groups/" // New group
|
||||
method = "POST"
|
||||
if (id != -1) {
|
||||
csvurl = "/api/groups/" + id // Update existing group
|
||||
method = "PUT"
|
||||
}
|
||||
$("#csvupload").fileupload({
|
||||
url: "/api/import/group",
|
||||
url: csvurl,
|
||||
method: method,
|
||||
dataType: "json",
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
||||
},
|
||||
add: function (e, data) {
|
||||
$("#modal\\.flashes").empty()
|
||||
name = $("#name").val()
|
||||
data.paramName = escapeHtml(name) // Send group name
|
||||
|
||||
if (name == "") {
|
||||
modalError("No group name supplied")
|
||||
$("#name").focus();
|
||||
return false;
|
||||
}
|
||||
var acceptFileTypes = /(csv|txt)$/i;
|
||||
var filename = data.originalFiles[0]['name']
|
||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||
|
@ -103,18 +86,33 @@ function edit(id) {
|
|||
data.submit();
|
||||
},
|
||||
done: function (e, data) {
|
||||
$.each(data.result, function (i, record) {
|
||||
addTarget(
|
||||
record.first_name,
|
||||
record.last_name,
|
||||
record.email,
|
||||
record.position);
|
||||
});
|
||||
targets.DataTable().draw();
|
||||
if (!('id' in data.result)) {
|
||||
modalError("Failed to upload CSV file")
|
||||
} else {
|
||||
$("#targetsTable_wrapper").show()
|
||||
$("#targetsTable").show()
|
||||
edit(data.result.id)
|
||||
load()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function saveGroupName() {
|
||||
id = parseInt($("#groupid").val())
|
||||
if (id == -1) { return }
|
||||
name = $("#name").val() // Check for length etc + handle escapes
|
||||
data = {"id": id, "name":name}
|
||||
|
||||
api.groupId.rename(id, data)
|
||||
.success(function (msg) {
|
||||
load()
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
|
||||
var downloadCSVTemplate = function () {
|
||||
var csvScope = [{
|
||||
'First Name': 'Example',
|
||||
|
@ -140,7 +138,6 @@ var downloadCSVTemplate = function () {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var deleteGroup = function (id) {
|
||||
var group = groups.find(function (x) {
|
||||
return x.id === id
|
||||
|
@ -184,33 +181,46 @@ var deleteGroup = function (id) {
|
|||
}
|
||||
|
||||
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
||||
// Create new data row.
|
||||
var email = escapeHtml(emailInput).toLowerCase();
|
||||
var newRow = [
|
||||
escapeHtml(firstNameInput),
|
||||
escapeHtml(lastNameInput),
|
||||
email,
|
||||
escapeHtml(positionInput),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
];
|
||||
|
||||
// Check table to see if email already exists.
|
||||
var targetsTable = targets.DataTable();
|
||||
var existingRowIndex = targetsTable
|
||||
.column(2, {
|
||||
order: "index"
|
||||
}) // Email column has index of 2
|
||||
.data()
|
||||
.indexOf(email);
|
||||
// Update or add new row as necessary.
|
||||
if (existingRowIndex >= 0) {
|
||||
targetsTable
|
||||
.row(existingRowIndex, {
|
||||
order: "index"
|
||||
$("#modal\\.flashes").empty()
|
||||
groupId = $("#groupid").val()
|
||||
target = {
|
||||
"email": emailInput.toLowerCase(),
|
||||
"first_name": firstNameInput,
|
||||
"last_name": lastNameInput,
|
||||
"position": positionInput
|
||||
}
|
||||
|
||||
if (groupId == -1){ // Create new group with target
|
||||
groupname = $("#name").val()
|
||||
groupname = escapeHtml(groupname)
|
||||
group = {"name": groupname, targets: [target]}
|
||||
|
||||
api.groups.post(group)
|
||||
.success(function (data) {
|
||||
//ajax fertch and show table
|
||||
if (!('id' in data)) {
|
||||
modalError("Failed to add target")
|
||||
} else {
|
||||
instantiateDataTable(data.id)
|
||||
$('#targetsTable').DataTable().draw('page');
|
||||
$('#targetsTable_wrapper').show()
|
||||
$('#targetsTable').show()
|
||||
$("#groupid").val(data.id)
|
||||
}
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
|
||||
} else { // Add single target to existing group
|
||||
api.groupId.addtarget(groupId, target)
|
||||
.success(function (data){
|
||||
load()
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
.data(newRow);
|
||||
} else {
|
||||
targetsTable.row.add(newRow);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,7 +249,7 @@ function load() {
|
|||
escapeHtml(group.name),
|
||||
escapeHtml(group.num_targets),
|
||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
|
||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id +")'>\
|
||||
<i class='fa fa-pencil'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
||||
|
@ -262,6 +272,14 @@ $(document).ready(function () {
|
|||
// Setup the event listeners
|
||||
// Handle manual additions
|
||||
$("#targetForm").submit(function () {
|
||||
|
||||
// Validate group name is present
|
||||
if ($("#name").val() == "") {
|
||||
modalError("No group name supplied")
|
||||
$("#name").focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate the form data
|
||||
var targetForm = document.getElementById("targetForm")
|
||||
if (!targetForm.checkValidity()) {
|
||||
|
@ -273,19 +291,35 @@ $(document).ready(function () {
|
|||
$("#lastName").val(),
|
||||
$("#email").val(),
|
||||
$("#position").val());
|
||||
targets.DataTable().draw();
|
||||
|
||||
$('#targetsTable').DataTable().draw();
|
||||
|
||||
// Reset user input.
|
||||
$("#targetForm>div>input").val('');
|
||||
$("#firstName").focus();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Handle Deletion
|
||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
||||
targets.DataTable()
|
||||
// We allow emtpy groups with this new pagination model. TODO: Do we need to revisit this?
|
||||
targetId=parseInt(this.id)
|
||||
groupId=$("#groupid").val()
|
||||
target = {"id" : targetId}
|
||||
|
||||
api.groupId.deletetarget(groupId, target)
|
||||
.success(function (msg) {
|
||||
load()
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError("Failed to delete user. Please try again later.")
|
||||
})
|
||||
|
||||
$('#targetsTable').DataTable()
|
||||
.row($(this).parents('tr'))
|
||||
.remove()
|
||||
.draw();
|
||||
.draw('page');
|
||||
|
||||
});
|
||||
$("#modal").on("hide.bs.modal", function () {
|
||||
dismiss();
|
||||
|
|
|
@ -49,14 +49,20 @@
|
|||
<div class="row" id="modal.flashes"></div>
|
||||
<label class="control-label" for="name">Name:</label>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
||||
autofocus />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" onclick="saveGroupName()" type="button">Save</button>
|
||||
</span>
|
||||
</div>
|
||||
<input id="groupid" type="hidden"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
|
||||
title="Supports CSV files" id="fileUpload">
|
||||
<i class="fa fa-plus"></i> Bulk Import Users
|
||||
<input type="file" id="csvupload" multiple>
|
||||
<input type="file" id="csvupload" name=""> <!-- Removed 'multiple' keyword due to new CSV upload functionality. Fails with new group creation. See Groups function in api/group.go -->
|
||||
</span>
|
||||
<span id="csv-template" class="text-muted small">
|
||||
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
||||
|
@ -83,6 +89,7 @@
|
|||
</div>
|
||||
<br />
|
||||
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
|
@ -90,17 +97,20 @@
|
|||
<th>Email</th>
|
||||
<th>Position</th>
|
||||
<th class="no-sort"></th>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script src="/js/dist/app/groups.min.js"></script>
|
||||
{{end}}
|
|
@ -44,11 +44,12 @@ func ParseMail(r *http.Request) (email.Email, error) {
|
|||
}
|
||||
|
||||
// ParseCSV contains the logic to parse the user provided csv file containing Target entries
|
||||
func ParseCSV(r *http.Request) ([]models.Target, error) {
|
||||
func ParseCSV(r *http.Request) ([]models.Target, string, error) {
|
||||
mr, err := r.MultipartReader()
|
||||
ts := []models.Target{}
|
||||
name := ""
|
||||
if err != nil {
|
||||
return ts, err
|
||||
return ts, name, err
|
||||
}
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
|
@ -60,6 +61,7 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
|
|||
continue
|
||||
}
|
||||
defer part.Close()
|
||||
name = part.FormName()
|
||||
reader := csv.NewReader(part)
|
||||
reader.TrimLeadingSpace = true
|
||||
record, err := reader.Read()
|
||||
|
@ -121,7 +123,7 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
|
|||
ts = append(ts, t)
|
||||
}
|
||||
}
|
||||
return ts, nil
|
||||
return ts, name, nil
|
||||
}
|
||||
|
||||
// CheckAndCreateSSL is a helper to setup self-signed certificates for the administrative interface.
|
||||
|
|
Loading…
Reference in New Issue