From 7453fd3b48b8d15048c016f7ac2dd78a028c1512 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Sat, 14 Jan 2017 17:26:04 -0600 Subject: [PATCH] Added summary routes for groups. Routes: /api/groups/summary /api/groups/:id/summary The UI is now using these routes for the "Users & Groups" page. --- controllers/api.go | 29 ++++++++++++ controllers/route.go | 2 + models/group.go | 54 +++++++++++++++++++++ static/js/dist/app/gophish.min.js | 2 +- static/js/dist/app/users.min.js | 2 +- static/js/src/app/gophish.js | 4 ++ static/js/src/app/users.js | 79 ++++++++++++++++--------------- templates/users.html | 2 +- 8 files changed, 133 insertions(+), 41 deletions(-) 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'), - "
\ -
" ]).draw() diff --git a/templates/users.html b/templates/users.html index a558eff8..42021401 100644 --- a/templates/users.html +++ b/templates/users.html @@ -50,7 +50,7 @@ Name - Members + # of Members Modified Date