diff --git a/controllers/api/group.go b/controllers/api/group.go index 700fa3e6..56c58244 100644 --- a/controllers/api/group.go +++ b/controllers/api/group.go @@ -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) + + } +} diff --git a/controllers/api/import.go b/controllers/api/import.go index efaf0178..0900d227 100644 --- a/controllers/api/import.go +++ b/controllers/api/import.go @@ -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 diff --git a/controllers/api/server.go b/controllers/api/server.go index 4e21cf4b..2738413f 100644 --- a/controllers/api/server.go +++ b/controllers/api/server.go @@ -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) diff --git a/models/group.go b/models/group.go index 5a4c59fd..3bdc4d93 100644 --- a/models/group.go +++ b/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 +} diff --git a/static/js/src/app/gophish.js b/static/js/src/app/gophish.js index cb17d1da..ce33af2f 100644 --- a/static/js/src/app/gophish.js +++ b/static/js/src/app/gophish.js @@ -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 diff --git a/static/js/src/app/groups.js b/static/js/src/app/groups.js index 1bf701d8..b78c3f19 100644 --- a/static/js/src/app/groups.js +++ b/static/js/src/app/groups.js @@ -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 ''; + } + }] + } ); +} + +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), - '' - ]) - }); - 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), - '' - ]; - // 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'), - "
{{end}} {{define "scripts"}} + {{end}} \ No newline at end of file diff --git a/util/util.go b/util/util.go index 06651c66..4f264230 100644 --- a/util/util.go +++ b/util/util.go @@ -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.