Fixing SSRF by requiring an API key for all import endpoints. Fixes #1026

pull/986/head^2
Jordan Wright 2018-03-26 21:04:22 -05:00
parent 9ba3f04d1e
commit 2131c17c33
No known key found for this signature in database
GPG Key ID: 138D5AD2331B3C11
9 changed files with 134 additions and 114 deletions

View File

@ -102,6 +102,13 @@ func (s *ControllersSuite) SetupTest() {
c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)
}
func (s *ControllersSuite) TestRequireAPIKey() {
resp, err := http.Post(fmt.Sprintf("%s/api/import/site", as.URL), "application/json", nil)
s.Nil(err)
defer resp.Body.Close()
s.Equal(resp.StatusCode, http.StatusBadRequest)
}
func (s *ControllersSuite) TestSiteImportBaseHref() {
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -59,9 +59,9 @@ func CreateAdminRouter() http.Handler {
api.HandleFunc("/smtp/", Use(API_SMTP, mid.RequireAPIKey))
api.HandleFunc("/smtp/{id:[0-9]+}", Use(API_SMTP_Id, mid.RequireAPIKey))
api.HandleFunc("/util/send_test_email", Use(API_Send_Test_Email, mid.RequireAPIKey))
api.HandleFunc("/import/group", API_Import_Group)
api.HandleFunc("/import/email", API_Import_Email)
api.HandleFunc("/import/site", API_Import_Site)
api.HandleFunc("/import/group", Use(API_Import_Group, mid.RequireAPIKey))
api.HandleFunc("/import/email", Use(API_Import_Email, mid.RequireAPIKey))
api.HandleFunc("/import/site", Use(API_Import_Site, mid.RequireAPIKey))
// Setup static file serving
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))

View File

@ -1 +1 @@
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}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 $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},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()});
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}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 $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},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",e,!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()});

File diff suppressed because one or more lines are too long

View File

@ -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=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),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).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),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],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"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit("+s.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+s.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).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()})});
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),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group?api_key="+user.api_key,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),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],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"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-target='#modal' onclick='edit("+s.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+s.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).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()})});

View File

@ -194,8 +194,8 @@ var api = {
}
},
// import handles all of the "import" functions in the api
import_email: function (raw) {
return query("/import/email", "POST", {}, false)
import_email: function (req) {
return query("/import/email", "POST", req, false)
},
// clone_site handles importing a site by url
clone_site: function (req) {

View File

@ -240,16 +240,10 @@ function importEmail() {
if (!raw) {
modalError("No Content Specified!")
} else {
$.ajax({
method: "POST",
url: "/api/import/email",
data: JSON.stringify({
api.import_email({
content: raw,
convert_links: convert_links
}),
dataType: "json",
contentType: "application/json"
})
})
.success(function (data) {
$("#text_editor").val(data.text)
$("#html_editor").val(data.html)
@ -337,7 +331,8 @@ $(document).ready(function () {
if (
this.$element[0] !== e.target && !this.$element.has(e.target).length
// CKEditor compatibility fix start.
&& !$(e.target).closest('.cke_dialog, .cke').length
&&
!$(e.target).closest('.cke_dialog, .cke').length
// CKEditor compatibility fix end.
) {
this.$element.trigger('focus');

View File

@ -3,7 +3,7 @@ var groups = []
// Save attempts to POST or PUT to /groups/
function save(id) {
var targets = []
$.each($("#targetsTable").DataTable().rows().data(), function(i, target) {
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
targets.push({
first_name: unescapeHtml(target[0]),
last_name: unescapeHtml(target[1]),
@ -12,35 +12,35 @@ function save(id) {
})
})
var group = {
name: $("#name").val(),
targets: targets
}
// Submit the 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) {
.success(function (data) {
successFlash("Group updated successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function(data) {
.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) {
.success(function (data) {
successFlash("Group added successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function(data) {
.error(function (data) {
modalError(data.responseJSON.message)
})
}
@ -60,16 +60,16 @@ function edit(id) {
targets: "no-sort"
}]
})
$("#modalSubmit").unbind('click').click(function() {
$("#modalSubmit").unbind('click').click(function () {
save(id)
})
if (id == -1) {
var group = {}
} else {
api.groupId.get(id)
.success(function(group) {
.success(function (group) {
$("#name").val(group.name)
$.each(group.targets, function(i, record) {
$.each(group.targets, function (i, record) {
targets.DataTable()
.row.add([
escapeHtml(record.first_name),
@ -81,14 +81,15 @@ function edit(id) {
});
})
.error(function() {
.error(function () {
errorFlash("Error fetching group")
})
}
// Handle file uploads
$("#csvupload").fileupload({
url: "/api/import/group?api_key=" + user.api_key,
dataType: "json",
add: function(e, data) {
add: function (e, data) {
$("#modal\\.flashes").empty()
var acceptFileTypes = /(csv|txt)$/i;
var filename = data.originalFiles[0]['name']
@ -98,8 +99,8 @@ function edit(id) {
}
data.submit();
},
done: function(e, data) {
$.each(data.result, function(i, record) {
done: function (e, data) {
$.each(data.result, function (i, record) {
addTarget(
record.first_name,
record.last_name,
@ -112,14 +113,16 @@ function edit(id) {
}
function deleteGroup(id) {
var group = groups.find(function(x){return x.id === 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) {
.success(function (data) {
successFlash(data.message)
load()
})
@ -162,7 +165,7 @@ function load() {
$("#emptyMessage").hide()
$("#loading").show()
api.groups.summary()
.success(function(response) {
.success(function (response) {
$("#loading").hide()
if (response.total > 0) {
groups = response.groups
@ -176,7 +179,7 @@ function load() {
}]
});
groupTable.clear();
$.each(groups, function(i, group) {
$.each(groups, function (i, group) {
groupTable.row.add([
escapeHtml(group.name),
escapeHtml(group.num_targets),
@ -193,16 +196,16 @@ function load() {
$("#emptyMessage").show()
}
})
.error(function() {
.error(function () {
errorFlash("Error fetching groups")
})
}
$(document).ready(function() {
$(document).ready(function () {
load()
// Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function() {
// Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function () {
addTarget(
$("#firstName").val(),
$("#lastName").val(),
@ -216,13 +219,13 @@ $(document).ready(function() {
return false;
});
// Handle Deletion
$("#targetsTable").on("click", "span>i.fa-trash-o", function() {
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
targets.DataTable()
.row($(this).parents('tr'))
.remove()
.draw();
});
$("#modal").on("hide.bs.modal", function() {
$("#modal").on("hide.bs.modal", function () {
dismiss();
});
});

View File

@ -3,24 +3,35 @@
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
<li>
<a href="/">Dashboard</a>
</li>
<li><a href="/campaigns">Campaigns</a>
<li>
<a href="/campaigns">Campaigns</a>
</li>
<li class="active"><a href="/users">Users &amp; Groups</a>
<li class="active">
<a href="/users">Users &amp; Groups</a>
</li>
<li><a href="/templates">Email Templates</a>
<li>
<a href="/templates">Email Templates</a>
</li>
<li><a href="/landing_pages">Landing Pages</a>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li><a href="/sending_profiles">Sending Profiles</a>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li><a href="/settings">Settings</a>
<li>
<a href="/settings">Settings</a>
</li>
<li><hr></li>
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
<li>
<hr>
</li>
<li><a href="/api/">API Documentation</a>
<li>
<a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
</li>
<li>
<a href="/api/">API Documentation</a>
</li>
</ul>
</div>
@ -34,7 +45,8 @@
</div>
<div id="flashes" class="row"></div>
<div class="row">
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-target="#modal"><i class="fa fa-plus"></i> New Group</button>
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-target="#modal">
<i class="fa fa-plus"></i> New Group</button>
</div>
&nbsp;
<div id="loading">
@ -62,63 +74,66 @@
</div>
<!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
</div>
<div class="modal-body">
<div class="row" id="modal.flashes"></div>
<label class="control-label" for="name">Name:</label>
<div class="form-group">
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name" autofocus/>
</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" data-url="/api/import/group" multiple>
</span>
</div>
<div class="row">
<form id="targetForm">
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="First Name" id="firstName">
</div>
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
</div>
<div class="col-sm-3">
<input type="email" class="form-control" placeholder="Email" id="email" required>
</div>
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Position" id="position">
</div>
<div class="col-sm-1">
<button type="submit" class="btn btn-danger btn-lg"><i class="fa fa-plus"></i> Add</button>
</div>
</form>
</div>
<br />
<table id="targetsTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Position</th>
<th class="no-sort"></th>
<tbody>
</tbody>
</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 class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
</div>
<div class="modal-body">
<div class="row" id="modal.flashes"></div>
<label class="control-label" for="name">Name:</label>
<div class="form-group">
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name" autofocus/>
</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>
</span>
</div>
<div class="row">
<form id="targetForm">
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="First Name" id="firstName">
</div>
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
</div>
<div class="col-sm-3">
<input type="email" class="form-control" placeholder="Email" id="email" required>
</div>
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Position" id="position">
</div>
<div class="col-sm-1">
<button type="submit" class="btn btn-danger btn-lg">
<i class="fa fa-plus"></i> Add</button>
</div>
</form>
</div>
<br />
<table id="targetsTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Position</th>
<th class="no-sort"></th>
<tbody>
</tbody>
</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>
</div>
{{end}}
{{define "scripts"}}
{{end}} {{define "scripts"}}
<script src="/js/dist/app/users.min.js"></script>
{{end}}