diff --git a/controllers/api.go b/controllers/api.go
index f961981c..a9ec4f03 100644
--- a/controllers/api.go
+++ b/controllers/api.go
@@ -214,6 +214,20 @@ func API_Groups(w http.ResponseWriter, r *http.Request) {
}
}
+// API_Groups_Summary returns a summary of the groups owned by the current user.
+func API_Groups_Summary(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case r.Method == "GET":
+ gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
+ if err != nil {
+ Logger.Println(err)
+ JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+ JSONResponse(w, gs, http.StatusOK)
+ }
+}
+
// API_Groups_Id returns details about the requested group.
// If the group is not valid, API_Groups_Id returns null.
func API_Groups_Id(w http.ResponseWriter, r *http.Request) {
@@ -253,6 +267,21 @@ func API_Groups_Id(w http.ResponseWriter, r *http.Request) {
}
}
+// API_Groups_Id_Summary returns a summary of the groups owned by the current user.
+func API_Groups_Id_Summary(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case r.Method == "GET":
+ vars := mux.Vars(r)
+ id, _ := strconv.ParseInt(vars["id"], 0, 64)
+ g, err := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64))
+ if err != nil {
+ JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
+ return
+ }
+ JSONResponse(w, g, http.StatusOK)
+ }
+}
+
// API_Templates handles the functionality for the /api/templates endpoint
func API_Templates(w http.ResponseWriter, r *http.Request) {
switch {
diff --git a/controllers/route.go b/controllers/route.go
index aa21bcf2..91e56d8e 100644
--- a/controllers/route.go
+++ b/controllers/route.go
@@ -54,7 +54,9 @@ func CreateAdminRouter() http.Handler {
api.HandleFunc("/campaigns/{id:[0-9]+}/summary", Use(API_Campaign_Id_Summary, mid.RequireAPIKey))
api.HandleFunc("/campaigns/{id:[0-9]+}/complete", Use(API_Campaigns_Id_Complete, mid.RequireAPIKey))
api.HandleFunc("/groups/", Use(API_Groups, mid.RequireAPIKey))
+ api.HandleFunc("/groups/summary", Use(API_Groups_Summary, mid.RequireAPIKey))
api.HandleFunc("/groups/{id:[0-9]+}", Use(API_Groups_Id, mid.RequireAPIKey))
+ api.HandleFunc("/groups/{id:[0-9]+}/summary", Use(API_Groups_Id_Summary, mid.RequireAPIKey))
api.HandleFunc("/templates/", Use(API_Templates, mid.RequireAPIKey))
api.HandleFunc("/templates/{id:[0-9]+}", Use(API_Templates_Id, mid.RequireAPIKey))
api.HandleFunc("/pages/", Use(API_Pages, mid.RequireAPIKey))
diff --git a/models/group.go b/models/group.go
index 4dbda4ff..6f53698a 100644
--- a/models/group.go
+++ b/models/group.go
@@ -18,6 +18,22 @@ type Group struct {
Targets []Target `json:"targets" sql:"-"`
}
+// GroupSummaries is a struct representing the overview of Groups.
+type GroupSummaries struct {
+ Total int64 `json:"total"`
+ Groups []GroupSummary `json:"groups"`
+}
+
+// GroupSummary represents a summary of the Group model. The only
+// difference is that, instead of listing the Targets (which could be expensive
+// for large groups), it lists the target count.
+type GroupSummary struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ ModifiedDate time.Time `json:"modified_date"`
+ NumTargets int64 `json:"num_targets"`
+}
+
// GroupTarget is used for a many-to-many relationship between 1..* Groups and 1..* Targets
type GroupTarget struct {
GroupId int64 `json:"-"`
@@ -71,6 +87,27 @@ func GetGroups(uid int64) ([]Group, error) {
return gs, nil
}
+// GetGroupSummaries returns the summaries for the groups
+// created by the given uid.
+func GetGroupSummaries(uid int64) (GroupSummaries, error) {
+ gs := GroupSummaries{}
+ query := db.Table("groups").Where("user_id=?", uid)
+ err := query.Select("id, name, modified_date").Scan(&gs.Groups).Error
+ if err != nil {
+ Logger.Println(err)
+ return gs, err
+ }
+ for i := range gs.Groups {
+ query = db.Table("group_targets").Where("group_id=?", gs.Groups[i].Id)
+ err = query.Count(&gs.Groups[i].NumTargets).Error
+ if err != nil {
+ return gs, err
+ }
+ }
+ gs.Total = int64(len(gs.Groups))
+ return gs, nil
+}
+
// GetGroup returns the group, if it exists, specified by the given id and user_id.
func GetGroup(id int64, uid int64) (Group, error) {
g := Group{}
@@ -86,6 +123,23 @@ func GetGroup(id int64, uid int64) (Group, error) {
return g, nil
}
+// GetGroupSummary returns the summary for the requested group
+func GetGroupSummary(id int64, uid int64) (GroupSummary, error) {
+ g := GroupSummary{}
+ query := db.Table("groups").Where("user_id=? and id=?", uid, id)
+ err := query.Select("id, name, modified_date").Scan(&g).Error
+ if err != nil {
+ Logger.Println(err)
+ return g, err
+ }
+ query = db.Table("group_targets").Where("group_id=?", id)
+ err = query.Count(&g.NumTargets).Error
+ if err != nil {
+ return g, err
+ }
+ return g, nil
+}
+
// GetGroupByName returns the group, if it exists, specified by the given name and user_id.
func GetGroupByName(n string, uid int64) (Group, error) {
g := Group{}
diff --git a/static/js/dist/app/gophish.min.js b/static/js/dist/app/gophish.min.js
index d99a0856..2114b44b 100644
--- a/static/js/dist/app/gophish.min.js
+++ b/static/js/dist/app/gophish.min.js
@@ -1 +1 @@
-function errorFlash(e){$("#flashes").empty(),$("#flashes").append('
'+e+"
")}function successFlash(e){$("#flashes").empty(),$("#flashes").append(' '+e+"
")}function modalError(e){$("#modal\\.flashes").empty().append(' '+e+"
")}function query(e,t,n,r){return $.ajax({url:"/api"+e+"?api_key="+user.api_key,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json"})}function escapeHtml(e){return $("").text(e).html()}function unescapeHtml(e){return $("").html(e).text()}var api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",{},!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)}};$(document).ready(function(){$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});
\ No newline at end of file
+function errorFlash(e){$("#flashes").empty(),$("#flashes").append(' '+e+"
")}function successFlash(e){$("#flashes").empty(),$("#flashes").append(' '+e+"
")}function modalError(e){$("#modal\\.flashes").empty().append(' '+e+"
")}function query(e,t,n,r){return $.ajax({url:"/api"+e+"?api_key="+user.api_key,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json"})}function escapeHtml(e){return $("").text(e).html()}function unescapeHtml(e){return $("").html(e).text()}var api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",{},!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)}};$(document).ready(function(){$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});
\ No newline at end of file
diff --git a/static/js/dist/app/users.min.js b/static/js/dist/app/users.min.js
index fe7a77ac..32590a10 100644
--- a/static/js/dist/app/users.min.js
+++ b/static/js/dist/app/users.min.js
@@ -1 +1 @@
-function save(a){var e=[];$.each($("#targetsTable").DataTable().rows().data(),function(a,s){e.push({first_name:unescapeHtml(s[0]),last_name:unescapeHtml(s[1]),email:unescapeHtml(s[2]),position:unescapeHtml(s[3])})});var s={name:$("#name").val(),targets:e};a!=-1?(s.id=groups[a].id,api.groupId.put(s).success(function(a){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(a){modalError(a.responseJSON.message)})):api.groups.post(s).success(function(a){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(a){modalError(a.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(a){targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(a)}),a==-1?group={}:(group=groups[a],$("#name").val(group.name),$.each(group.targets,function(a,e){targets.DataTable().row.add([escapeHtml(e.first_name),escapeHtml(e.last_name),escapeHtml(e.email),escapeHtml(e.position),'']).draw()})),$("#csvupload").fileupload({dataType:"json",add:function(a,e){$("#modal\\.flashes").empty();var s=/(csv|txt)$/i,t=e.originalFiles[0].name;return t&&!s.test(t.split(".").pop())?(modalError("Unsupported file extension (use .csv or .txt)"),!1):void e.submit()},done:function(a,e){$.each(e.result,function(a,e){addTarget(e.first_name,e.last_name,e.email,e.position)}),targets.DataTable().draw()}})}function deleteGroup(a){confirm("Delete "+groups[a].name+"?")&&api.groupId.delete(groups[a].id).success(function(a){successFlash(a.message),load()})}function addTarget(a,e,s,t){var o=escapeHtml(s).toLowerCase(),r=[escapeHtml(a),escapeHtml(e),o,escapeHtml(t),''],n=targets.DataTable(),l=n.column(2,{order:"index"}).data().indexOf(o);l>=0?n.row(l,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.get().success(function(a){$("#loading").hide(),a.length>0?(groups=a,$("#emptyMessage").hide(),$("#groupTable").show(),groupTable=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),groupTable.clear(),$.each(groups,function(a,e){var s="";$.each(e.targets,function(a,e){if(s+=e.email+", ",s.length>50)return s=s.slice(0,-3)+"...",!1}),groupTable.row.add([escapeHtml(e.name),escapeHtml(s),moment(e.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"
"]).draw()})):$("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}var groups=[];$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()})});
\ No newline at end of file
+function save(a){var e=[];$.each($("#targetsTable").DataTable().rows().data(),function(a,s){e.push({first_name:unescapeHtml(s[0]),last_name:unescapeHtml(s[1]),email:unescapeHtml(s[2]),position:unescapeHtml(s[3])})});var s={name:$("#name").val(),targets:e};a!=-1?(s.id=a,api.groupId.put(s).success(function(a){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(a){modalError(a.responseJSON.message)})):api.groups.post(s).success(function(a){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(a){modalError(a.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(a){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(a)}),a==-1);else api.groupId.get(a).success(function(a){$("#name").val(a.name),$.each(a.targets,function(a,e){targets.DataTable().row.add([escapeHtml(e.first_name),escapeHtml(e.last_name),escapeHtml(e.email),escapeHtml(e.position),'']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({dataType:"json",add:function(a,e){$("#modal\\.flashes").empty();var s=/(csv|txt)$/i,t=e.originalFiles[0].name;return t&&!s.test(t.split(".").pop())?(modalError("Unsupported file extension (use .csv or .txt)"),!1):void e.submit()},done:function(a,e){$.each(e.result,function(a,e){addTarget(e.first_name,e.last_name,e.email,e.position)}),targets.DataTable().draw()}})}function deleteGroup(a){var e=groups.find(function(e){return e.id===a});return e?void(confirm("Delete "+e.name+"?")&&api.groupId.delete(a).success(function(a){successFlash(a.message),load()})):void console.log("wat")}function addTarget(a,e,s,t){var o=escapeHtml(s).toLowerCase(),r=[escapeHtml(a),escapeHtml(e),o,escapeHtml(t),''],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);i>=0?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(a){if($("#loading").hide(),a.total>0){groups=a.groups,$("#emptyMessage").hide(),$("#groupTable").show();var e=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});e.clear(),$.each(groups,function(a,s){e.row.add([escapeHtml(s.name),escapeHtml(s.num_targets),moment(s.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"
"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}var groups=[];$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()})});
\ No newline at end of file
diff --git a/static/js/src/app/gophish.js b/static/js/src/app/gophish.js
index 0169cf78..c588c664 100644
--- a/static/js/src/app/gophish.js
+++ b/static/js/src/app/gophish.js
@@ -85,6 +85,10 @@ var api = {
// post() - Posts a group to POST /groups
post: function(group) {
return query("/groups/", "POST", group, false)
+ },
+ // summary() - Queries the API for GET /groups/summary
+ summary: function() {
+ return query("/groups/summary", "GET", {}, true)
}
},
// groupId contains the endpoints for /groups/:id
diff --git a/static/js/src/app/users.js b/static/js/src/app/users.js
index ed9e3d58..a1defa2d 100644
--- a/static/js/src/app/users.js
+++ b/static/js/src/app/users.js
@@ -1,7 +1,7 @@
var groups = []
// Save attempts to POST or PUT to /groups/
-function save(idx) {
+function save(id) {
var targets = []
$.each($("#targetsTable").DataTable().rows().data(), function(i, target) {
targets.push({
@@ -16,10 +16,10 @@ function save(idx) {
targets: targets
}
// Submit the group
- if (idx != -1) {
+ if (id != -1) {
// If we're just editing an existing group,
// we need to PUT /groups/:id
- group.id = groups[idx].id
+ group.id = id
api.groupId.put(group)
.success(function(data) {
successFlash("Group updated successfully!")
@@ -52,7 +52,7 @@ function dismiss() {
$("#modal\\.flashes").empty()
}
-function edit(idx) {
+function edit(id) {
targets = $("#targetsTable").dataTable({
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
columnDefs: [{
@@ -61,23 +61,29 @@ function edit(idx) {
}]
})
$("#modalSubmit").unbind('click').click(function() {
- save(idx)
+ save(id)
})
- if (idx == -1) {
- group = {}
+ if (id == -1) {
+ var group = {}
} else {
- group = groups[idx]
- $("#name").val(group.name)
- $.each(group.targets, function(i, record) {
- targets.DataTable()
- .row.add([
- escapeHtml(record.first_name),
- escapeHtml(record.last_name),
- escapeHtml(record.email),
- escapeHtml(record.position),
- ''
- ]).draw()
- });
+ api.groupId.get(id)
+ .success(function(group) {
+ $("#name").val(group.name)
+ $.each(group.targets, function(i, record) {
+ targets.DataTable()
+ .row.add([
+ escapeHtml(record.first_name),
+ escapeHtml(record.last_name),
+ escapeHtml(record.email),
+ escapeHtml(record.position),
+ ''
+ ]).draw()
+ });
+
+ })
+ .error(function() {
+ errorFlash("Error fetching group")
+ })
}
// Handle file uploads
$("#csvupload").fileupload({
@@ -105,9 +111,14 @@ function edit(idx) {
})
}
-function deleteGroup(idx) {
- if (confirm("Delete " + groups[idx].name + "?")) {
- api.groupId.delete(groups[idx].id)
+function deleteGroup(id) {
+ var group = groups.find(function(x){return x.id === id})
+ if (!group) {
+ console.log('wat');
+ return
+ }
+ if (confirm("Delete " + group.name + "?")) {
+ api.groupId.delete(id)
.success(function(data) {
successFlash(data.message)
load()
@@ -150,14 +161,14 @@ function load() {
$("#groupTable").hide()
$("#emptyMessage").hide()
$("#loading").show()
- api.groups.get()
- .success(function(gs) {
+ api.groups.summary()
+ .success(function(response) {
$("#loading").hide()
- if (gs.length > 0) {
- groups = gs
+ if (response.total > 0) {
+ groups = response.groups
$("#emptyMessage").hide()
$("#groupTable").show()
- groupTable = $("#groupTable").DataTable({
+ var groupTable = $("#groupTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
@@ -166,22 +177,14 @@ function load() {
});
groupTable.clear();
$.each(groups, function(i, group) {
- var targets = ""
- $.each(group.targets, function(i, target) {
- targets += target.email + ", "
- if (targets.length > 50) {
- targets = targets.slice(0, -3) + "..."
- return false;
- }
- })
groupTable.row.add([
escapeHtml(group.name),
- escapeHtml(targets),
+ escapeHtml(group.num_targets),
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
- "