mirror of https://github.com/gophish/gophish
Implement User Management API (#1473)
This implements the first pass for a user management API allowing users with the `ModifySystem` permission to create, modify, and delete users. In addition to this, any user is able to use the API to view or modify their own account information.1257-lets-encrypt
parent
faadf0c850
commit
84096b8724
83
auth/auth.go
83
auth/auth.go
|
@ -1,49 +1,24 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"crypto/rand"
|
|
||||||
|
|
||||||
ctx "github.com/gophish/gophish/context"
|
ctx "github.com/gophish/gophish/context"
|
||||||
log "github.com/gophish/gophish/logger"
|
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
//init registers the necessary models to be saved in the session later
|
|
||||||
func init() {
|
|
||||||
gob.Register(&models.User{})
|
|
||||||
gob.Register(&models.Flash{})
|
|
||||||
Store.Options.HttpOnly = true
|
|
||||||
// This sets the maxAge to 5 days for all cookies
|
|
||||||
Store.MaxAge(86400 * 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store contains the session information for the request
|
|
||||||
var Store = sessions.NewCookieStore(
|
|
||||||
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
|
|
||||||
[]byte(securecookie.GenerateRandomKey(32)))
|
|
||||||
|
|
||||||
// ErrInvalidPassword is thrown when a user provides an incorrect password.
|
// ErrInvalidPassword is thrown when a user provides an incorrect password.
|
||||||
var ErrInvalidPassword = errors.New("Invalid Password")
|
var ErrInvalidPassword = errors.New("Invalid Password")
|
||||||
|
|
||||||
|
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
|
||||||
|
// or change password functions
|
||||||
|
var ErrPasswordMismatch = errors.New("Password cannot be blank")
|
||||||
|
|
||||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||||
// or change password functions
|
// or change password functions
|
||||||
var ErrEmptyPassword = errors.New("Password cannot be blank")
|
var ErrEmptyPassword = errors.New("No password provided")
|
||||||
|
|
||||||
// ErrPasswordMismatch is thrown when a user provides passwords that do not match
|
|
||||||
var ErrPasswordMismatch = errors.New("Passwords must match")
|
|
||||||
|
|
||||||
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
|
||||||
var ErrUsernameTaken = errors.New("Username already taken")
|
|
||||||
|
|
||||||
// Login attempts to login the user given a request.
|
// Login attempts to login the user given a request.
|
||||||
func Login(r *http.Request) (bool, models.User, error) {
|
func Login(r *http.Request) (bool, models.User, error) {
|
||||||
|
@ -61,54 +36,6 @@ func Login(r *http.Request) (bool, models.User, error) {
|
||||||
return true, u, nil
|
return true, u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register attempts to register the user given a request.
|
|
||||||
func Register(r *http.Request) (bool, error) {
|
|
||||||
username := r.FormValue("username")
|
|
||||||
newPassword := r.FormValue("password")
|
|
||||||
confirmPassword := r.FormValue("confirm_password")
|
|
||||||
u, err := models.GetUserByUsername(username)
|
|
||||||
// If the given username already exists, throw an error and return false
|
|
||||||
if err == nil {
|
|
||||||
return false, ErrUsernameTaken
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have an error which is not simply indicating that no user was found, report it
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
log.Warn(err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u = models.User{}
|
|
||||||
// If we've made it here, we should have a valid username given
|
|
||||||
// Check that the passsword isn't blank
|
|
||||||
if newPassword == "" {
|
|
||||||
return false, ErrEmptyPassword
|
|
||||||
}
|
|
||||||
// Make sure passwords match
|
|
||||||
if newPassword != confirmPassword {
|
|
||||||
return false, ErrPasswordMismatch
|
|
||||||
}
|
|
||||||
// Let's create the password hash
|
|
||||||
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
u.Username = username
|
|
||||||
u.Hash = string(h)
|
|
||||||
u.ApiKey = GenerateSecureKey()
|
|
||||||
err = models.PutUser(&u)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateSecureKey creates a secure key to use
|
|
||||||
// as an API key
|
|
||||||
func GenerateSecureKey() string {
|
|
||||||
// Inspired from gorilla/securecookie
|
|
||||||
k := make([]byte, 32)
|
|
||||||
io.ReadFull(rand.Reader, k)
|
|
||||||
return fmt.Sprintf("%x", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangePassword verifies the current password provided in the request and,
|
// ChangePassword verifies the current password provided in the request and,
|
||||||
// if it's valid, changes the password for the authenticated user.
|
// if it's valid, changes the password for the authenticated user.
|
||||||
func ChangePassword(r *http.Request) error {
|
func ChangePassword(r *http.Request) error {
|
||||||
|
|
|
@ -19,6 +19,7 @@ type APISuite struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
config *config.Config
|
config *config.Config
|
||||||
apiServer *Server
|
apiServer *Server
|
||||||
|
admin models.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APISuite) SetupSuite() {
|
func (s *APISuite) SetupSuite() {
|
||||||
|
@ -37,6 +38,7 @@ func (s *APISuite) SetupSuite() {
|
||||||
u, err := models.GetUser(1)
|
u, err := models.GetUser(1)
|
||||||
s.Nil(err)
|
s.Nil(err)
|
||||||
s.apiKey = u.ApiKey
|
s.apiKey = u.ApiKey
|
||||||
|
s.admin = u
|
||||||
// Move our cwd up to the project root for help with resolving
|
// Move our cwd up to the project root for help with resolving
|
||||||
// static assets
|
// static assets
|
||||||
err = os.Chdir("../")
|
err = os.Chdir("../")
|
||||||
|
@ -49,6 +51,15 @@ func (s *APISuite) TearDownTest() {
|
||||||
for _, campaign := range campaigns {
|
for _, campaign := range campaigns {
|
||||||
models.DeleteCampaign(campaign.Id)
|
models.DeleteCampaign(campaign.Id)
|
||||||
}
|
}
|
||||||
|
// Cleanup all users except the original admin
|
||||||
|
users, _ := models.GetUsers()
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Id == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := models.DeleteUser(user.Id)
|
||||||
|
s.Nil(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APISuite) SetupTest() {
|
func (s *APISuite) SetupTest() {
|
||||||
|
|
|
@ -3,9 +3,9 @@ package api
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gophish/gophish/auth"
|
|
||||||
ctx "github.com/gophish/gophish/context"
|
ctx "github.com/gophish/gophish/context"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gophish/gophish/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset (/api/reset) resets the currently authenticated user's API key
|
// Reset (/api/reset) resets the currently authenticated user's API key
|
||||||
|
@ -13,7 +13,7 @@ func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
|
||||||
switch {
|
switch {
|
||||||
case r.Method == "POST":
|
case r.Method == "POST":
|
||||||
u := ctx.Get(r, "user").(models.User)
|
u := ctx.Get(r, "user").(models.User)
|
||||||
u.ApiKey = auth.GenerateSecureKey()
|
u.ApiKey = util.GenerateSecureKey()
|
||||||
err := models.PutUser(&u)
|
err := models.PutUser(&u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
mid "github.com/gophish/gophish/middleware"
|
mid "github.com/gophish/gophish/middleware"
|
||||||
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/gophish/gophish/worker"
|
"github.com/gophish/gophish/worker"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -64,6 +65,8 @@ func (as *Server) registerRoutes() {
|
||||||
router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
|
router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
|
||||||
router.HandleFunc("/smtp/", as.SendingProfiles)
|
router.HandleFunc("/smtp/", as.SendingProfiles)
|
||||||
router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile)
|
router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile)
|
||||||
|
router.HandleFunc("/users/", mid.Use(as.Users, mid.RequirePermission(models.PermissionModifySystem)))
|
||||||
|
router.HandleFunc("/users/{id:[0-9]+}", mid.Use(as.User))
|
||||||
router.HandleFunc("/util/send_test_email", as.SendTestEmail)
|
router.HandleFunc("/util/send_test_email", as.SendTestEmail)
|
||||||
router.HandleFunc("/import/group", as.ImportGroup)
|
router.HandleFunc("/import/group", as.ImportGroup)
|
||||||
router.HandleFunc("/import/email", as.ImportEmail)
|
router.HandleFunc("/import/email", as.ImportEmail)
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||||
|
// or change password functions
|
||||||
|
var ErrEmptyPassword = errors.New("No password provided")
|
||||||
|
|
||||||
|
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
||||||
|
var ErrUsernameTaken = errors.New("Username already taken")
|
||||||
|
|
||||||
|
// ErrEmptyUsername is thrown when a user attempts to register a username that is taken.
|
||||||
|
var ErrEmptyUsername = errors.New("No username provided")
|
||||||
|
|
||||||
|
// ErrEmptyRole is throws when no role is provided when creating or modifying a user.
|
||||||
|
var ErrEmptyRole = errors.New("No role specified")
|
||||||
|
|
||||||
|
// ErrInsufficientPermission is thrown when a user attempts to change an
|
||||||
|
// attribute (such as the role) for which they don't have permission.
|
||||||
|
var ErrInsufficientPermission = errors.New("Permission denied")
|
||||||
|
|
||||||
|
// userRequest is the payload which represents the creation of a new user.
|
||||||
|
type userRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ur *userRequest) Validate(existingUser *models.User) error {
|
||||||
|
switch {
|
||||||
|
case ur.Username == "":
|
||||||
|
return ErrEmptyUsername
|
||||||
|
case ur.Role == "":
|
||||||
|
return ErrEmptyRole
|
||||||
|
}
|
||||||
|
// Verify that the username isn't already taken. We consider two cases:
|
||||||
|
// * We're creating a new user, in which case any match is a conflict
|
||||||
|
// * We're modifying a user, in which case any match with a different ID is
|
||||||
|
// a conflict.
|
||||||
|
possibleConflict, err := models.GetUserByUsername(ur.Username)
|
||||||
|
if err == nil {
|
||||||
|
if existingUser == nil {
|
||||||
|
return ErrUsernameTaken
|
||||||
|
}
|
||||||
|
if possibleConflict.Id != existingUser.Id {
|
||||||
|
return ErrUsernameTaken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we have an error which is not simply indicating that no user was found, report it
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users contains functions to retrieve a list of existing users or create a
|
||||||
|
// new user. Users with the ModifySystem permissions can view and create users.
|
||||||
|
func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
us, err := models.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, us, http.StatusOK)
|
||||||
|
return
|
||||||
|
case r.Method == "POST":
|
||||||
|
ur := &userRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(ur)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ur.Validate(nil)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ur.Password == "" {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := util.NewHash(ur.Password)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role, err := models.GetRoleBySlug(ur.Role)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := models.User{
|
||||||
|
Username: ur.Username,
|
||||||
|
Hash: hash,
|
||||||
|
ApiKey: util.GenerateSecureKey(),
|
||||||
|
Role: role,
|
||||||
|
RoleID: role.ID,
|
||||||
|
}
|
||||||
|
err = models.PutUser(&user)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, user, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User contains functions to retrieve or delete a single user. Users with
|
||||||
|
// the ModifySystem permission can view and modify any user. Otherwise, users
|
||||||
|
// may only view or delete their own account.
|
||||||
|
func (as *Server) User(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
|
// If the user doesn't have ModifySystem permissions, we need to verify
|
||||||
|
// that they're only taking action on their account.
|
||||||
|
currentUser := ctx.Get(r, "user").(models.User)
|
||||||
|
hasSystem, err := currentUser.HasPermission(models.PermissionModifySystem)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !hasSystem && currentUser.Id != id {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: http.StatusText(http.StatusForbidden)}, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existingUser, err := models.GetUser(id)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "User not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
JSONResponse(w, existingUser, http.StatusOK)
|
||||||
|
case r.Method == "DELETE":
|
||||||
|
err = models.DeleteUser(id)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("Deleted user account for %s", existingUser.Username)
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "User deleted Successfully!"}, http.StatusOK)
|
||||||
|
case r.Method == "PUT":
|
||||||
|
ur := &userRequest{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(ur)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error decoding user request: %v", err)
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ur.Validate(&existingUser)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("invalid user request received: %v", err)
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existingUser.Username = ur.Username
|
||||||
|
// Only users with the ModifySystem permission are able to update a
|
||||||
|
// user's role. This prevents a privilege escalation letting users
|
||||||
|
// upgrade their own account.
|
||||||
|
if !hasSystem && ur.Role != existingUser.Role.Slug {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: ErrInsufficientPermission.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role, err := models.GetRoleBySlug(ur.Role)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If our user is trying to change the role of an admin, we need to
|
||||||
|
// ensure that it isn't the last user account with the Admin role.
|
||||||
|
if existingUser.Role.Slug == models.RoleAdmin && existingUser.Role.ID != role.ID {
|
||||||
|
err = models.EnsureEnoughAdmins()
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingUser.Role = role
|
||||||
|
existingUser.RoleID = role.ID
|
||||||
|
// We don't force the password to be provided, since it may be an admin
|
||||||
|
// managing the user's account, and making a simple change like
|
||||||
|
// updating the username or role. However, if it _is_ provided, we'll
|
||||||
|
// update the stored hash.
|
||||||
|
//
|
||||||
|
// Note that we don't force the current password to be provided. The
|
||||||
|
// assumption here is that the API key is a proper bearer token proving
|
||||||
|
// authenticated access to the account.
|
||||||
|
if ur.Password != "" {
|
||||||
|
hash, err := util.NewHash(ur.Password)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existingUser.Hash = hash
|
||||||
|
}
|
||||||
|
err = models.PutUser(&existingUser)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, existingUser, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
ctx "github.com/gophish/gophish/context"
|
||||||
|
"github.com/gophish/gophish/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
|
||||||
|
role, err := models.GetRoleBySlug(slug)
|
||||||
|
s.Nil(err)
|
||||||
|
unauthorizedUser := &models.User{
|
||||||
|
Username: "foo",
|
||||||
|
Hash: "bar",
|
||||||
|
ApiKey: "12345",
|
||||||
|
Role: role,
|
||||||
|
RoleID: role.ID,
|
||||||
|
}
|
||||||
|
err = models.PutUser(unauthorizedUser)
|
||||||
|
s.Nil(err)
|
||||||
|
return unauthorizedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APISuite) TestGetUsers() {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/api/users", nil)
|
||||||
|
r = ctx.Set(r, "user", s.admin)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.Users(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusOK)
|
||||||
|
|
||||||
|
got := []models.User{}
|
||||||
|
err := json.NewDecoder(w.Body).Decode(&got)
|
||||||
|
s.Nil(err)
|
||||||
|
|
||||||
|
// We only expect one user
|
||||||
|
s.Equal(1, len(got))
|
||||||
|
// And it should be the admin user
|
||||||
|
s.Equal(s.admin.Id, got[0].Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APISuite) TestCreateUser() {
|
||||||
|
payload := &userRequest{
|
||||||
|
Username: "foo",
|
||||||
|
Password: "bar",
|
||||||
|
Role: models.RoleUser,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
s.Nil(err)
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(body))
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
r = ctx.Set(r, "user", s.admin)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.Users(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusOK)
|
||||||
|
|
||||||
|
got := &models.User{}
|
||||||
|
err = json.NewDecoder(w.Body).Decode(got)
|
||||||
|
s.Nil(err)
|
||||||
|
s.Equal(got.Username, payload.Username)
|
||||||
|
s.Equal(got.Role.Slug, payload.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModifyUser tests that a user with the appropriate access is able to
|
||||||
|
// modify their username and password.
|
||||||
|
func (s *APISuite) TestModifyUser() {
|
||||||
|
unpriviledgedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||||
|
newPassword := "new-password"
|
||||||
|
newUsername := "new-username"
|
||||||
|
payload := userRequest{
|
||||||
|
Username: newUsername,
|
||||||
|
Password: newPassword,
|
||||||
|
Role: unpriviledgedUser.Role.Slug,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
s.Nil(err)
|
||||||
|
url := fmt.Sprintf("/api/users/%d", unpriviledgedUser.Id)
|
||||||
|
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unpriviledgedUser.ApiKey))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.ServeHTTP(w, r)
|
||||||
|
response := &models.User{}
|
||||||
|
err = json.NewDecoder(w.Body).Decode(response)
|
||||||
|
s.Nil(err)
|
||||||
|
s.Equal(w.Code, http.StatusOK)
|
||||||
|
s.Equal(response.Username, newUsername)
|
||||||
|
got, err := models.GetUser(unpriviledgedUser.Id)
|
||||||
|
s.Nil(err)
|
||||||
|
s.Equal(response.Username, got.Username)
|
||||||
|
s.Equal(newUsername, got.Username)
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(got.Hash), []byte(newPassword))
|
||||||
|
s.Nil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnauthorizedListUsers ensures that users without the ModifySystem
|
||||||
|
// permission are unable to list the users registered in Gophish.
|
||||||
|
func (s *APISuite) TestUnauthorizedListUsers() {
|
||||||
|
// First, let's create a standard user which doesn't
|
||||||
|
// have ModifySystem permissions.
|
||||||
|
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||||
|
// We'll try to make a request to the various users API endpoints to
|
||||||
|
// ensure that they fail. Previously, we could hit the handlers directly
|
||||||
|
// but we need to go through the router for this test to ensure the
|
||||||
|
// middleware gets applied.
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/api/users/", nil)
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.ServeHTTP(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnauthorizedModifyUsers verifies that users without ModifySystem
|
||||||
|
// permission (a "standard" user) can only get or modify their own information.
|
||||||
|
func (s *APISuite) TestUnauthorizedGetUser() {
|
||||||
|
// First, we'll make sure that a user with the "user" role is unable to
|
||||||
|
// get the information of another user (in this case, the main admin).
|
||||||
|
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||||
|
url := fmt.Sprintf("/api/users/%d", s.admin.Id)
|
||||||
|
r := httptest.NewRequest(http.MethodGet, url, nil)
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.ServeHTTP(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnauthorizedModifyRole ensures that users without the ModifySystem
|
||||||
|
// privilege are unable to modify their own role, preventing a potential
|
||||||
|
// privilege escalation issue.
|
||||||
|
func (s *APISuite) TestUnauthorizedSetRole() {
|
||||||
|
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||||
|
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||||
|
payload := &userRequest{
|
||||||
|
Username: unauthorizedUser.Username,
|
||||||
|
Role: models.RoleAdmin,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
s.Nil(err)
|
||||||
|
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.ServeHTTP(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusBadRequest)
|
||||||
|
response := &models.Response{}
|
||||||
|
err = json.NewDecoder(w.Body).Decode(response)
|
||||||
|
s.Nil(err)
|
||||||
|
s.Equal(response.Message, ErrInsufficientPermission.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModifyWithExistingUsername verifies that it's not possible to modify
|
||||||
|
// an user's username to one which already exists.
|
||||||
|
func (s *APISuite) TestModifyWithExistingUsername() {
|
||||||
|
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||||
|
payload := &userRequest{
|
||||||
|
Username: s.admin.Username,
|
||||||
|
Role: unauthorizedUser.Role.Slug,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
s.Nil(err)
|
||||||
|
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||||
|
r := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(body))
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.apiServer.ServeHTTP(w, r)
|
||||||
|
s.Equal(w.Code, http.StatusBadRequest)
|
||||||
|
expected := &models.Response{
|
||||||
|
Message: ErrUsernameTaken.Error(),
|
||||||
|
Success: false,
|
||||||
|
}
|
||||||
|
got := &models.Response{}
|
||||||
|
err = json.NewDecoder(w.Body).Decode(got)
|
||||||
|
s.Nil(err)
|
||||||
|
s.Equal(got.Message, expected.Message)
|
||||||
|
}
|
|
@ -95,17 +95,17 @@ func (as *AdminServer) Shutdown() error {
|
||||||
func (as *AdminServer) registerRoutes() {
|
func (as *AdminServer) registerRoutes() {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
// Base Front-end routes
|
// Base Front-end routes
|
||||||
router.HandleFunc("/", Use(as.Base, mid.RequireLogin))
|
router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
|
||||||
router.HandleFunc("/login", as.Login)
|
router.HandleFunc("/login", as.Login)
|
||||||
router.HandleFunc("/logout", Use(as.Logout, mid.RequireLogin))
|
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
|
||||||
router.HandleFunc("/campaigns", Use(as.Campaigns, mid.RequireLogin))
|
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
|
||||||
router.HandleFunc("/campaigns/{id:[0-9]+}", Use(as.CampaignID, mid.RequireLogin))
|
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
|
||||||
router.HandleFunc("/templates", Use(as.Templates, mid.RequireLogin))
|
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||||
router.HandleFunc("/users", Use(as.Users, mid.RequireLogin))
|
router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin))
|
||||||
router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin))
|
router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin))
|
||||||
router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin))
|
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
|
||||||
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
|
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||||
router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem)))
|
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||||
// Create the API routes
|
// Create the API routes
|
||||||
api := api.NewServer(api.WithWorker(as.worker))
|
api := api.NewServer(api.WithWorker(as.worker))
|
||||||
router.PathPrefix("/api/").Handler(api)
|
router.PathPrefix("/api/").Handler(api)
|
||||||
|
@ -114,11 +114,11 @@ func (as *AdminServer) registerRoutes() {
|
||||||
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
||||||
|
|
||||||
// Setup CSRF Protection
|
// Setup CSRF Protection
|
||||||
csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()),
|
csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
|
||||||
csrf.FieldName("csrf_token"),
|
csrf.FieldName("csrf_token"),
|
||||||
csrf.Secure(as.config.UseTLS))
|
csrf.Secure(as.config.UseTLS))
|
||||||
adminHandler := csrfHandler(router)
|
adminHandler := csrfHandler(router)
|
||||||
adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||||
|
|
||||||
// Setup GZIP compression
|
// Setup GZIP compression
|
||||||
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
||||||
|
@ -129,15 +129,6 @@ func (as *AdminServer) registerRoutes() {
|
||||||
as.server.Handler = adminHandler
|
as.server.Handler = adminHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use allows us to stack middleware to process the request
|
|
||||||
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
|
|
||||||
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
|
|
||||||
for _, m := range mid {
|
|
||||||
handler = m(handler)
|
|
||||||
}
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateParams struct {
|
type templateParams struct {
|
||||||
Title string
|
Title string
|
||||||
Flashes []interface{}
|
Flashes []interface{}
|
||||||
|
@ -160,42 +151,6 @@ func newTemplateParams(r *http.Request) templateParams {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register creates a new user
|
|
||||||
func (as *AdminServer) Register(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// If it is a post request, attempt to register the account
|
|
||||||
// Now that we are all registered, we can log the user in
|
|
||||||
params := templateParams{Title: "Register", Token: csrf.Token(r)}
|
|
||||||
session := ctx.Get(r, "session").(*sessions.Session)
|
|
||||||
switch {
|
|
||||||
case r.Method == "GET":
|
|
||||||
params.Flashes = session.Flashes()
|
|
||||||
session.Save(r, w)
|
|
||||||
templates := template.New("template")
|
|
||||||
_, err := templates.ParseFiles("templates/register.html", "templates/flashes.html")
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
|
||||||
case r.Method == "POST":
|
|
||||||
//Attempt to register
|
|
||||||
succ, err := auth.Register(r)
|
|
||||||
//If we've registered, redirect to the login page
|
|
||||||
if succ {
|
|
||||||
Flash(w, r, "success", "Registration successful!")
|
|
||||||
session.Save(r, w)
|
|
||||||
http.Redirect(w, r, "/login", 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check the error
|
|
||||||
m := err.Error()
|
|
||||||
log.Error(err)
|
|
||||||
Flash(w, r, "danger", m)
|
|
||||||
session.Save(r, w)
|
|
||||||
http.Redirect(w, r, "/register", 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base handles the default path and template execution
|
// Base handles the default path and template execution
|
||||||
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
|
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
|
||||||
params := newTemplateParams(r)
|
params := newTemplateParams(r)
|
||||||
|
@ -224,11 +179,11 @@ func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) {
|
||||||
getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
|
getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users handles the default path and template execution
|
// Groups handles the default path and template execution
|
||||||
func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) {
|
func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) {
|
||||||
params := newTemplateParams(r)
|
params := newTemplateParams(r)
|
||||||
params.Title = "Users & Groups"
|
params.Title = "Users & Groups"
|
||||||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
getTemplate(w, "groups").ExecuteTemplate(w, "base", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LandingPages handles the default path and template execution
|
// LandingPages handles the default path and template execution
|
||||||
|
@ -271,6 +226,14 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserManagement is an admin-only handler that allows for the registration
|
||||||
|
// and management of user accounts within Gophish.
|
||||||
|
func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := newTemplateParams(r)
|
||||||
|
params.Title = "User Management"
|
||||||
|
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||||
|
}
|
||||||
|
|
||||||
// Login handles the authentication flow for a user. If credentials are valid,
|
// Login handles the authentication flow for a user. If credentials are valid,
|
||||||
// a session is created
|
// a session is created
|
||||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -32,10 +32,10 @@ import (
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
|
||||||
"github.com/gophish/gophish/auth"
|
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
"github.com/gophish/gophish/controllers"
|
"github.com/gophish/gophish/controllers"
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
|
"github.com/gophish/gophish/middleware"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ func main() {
|
||||||
}
|
}
|
||||||
adminConfig := conf.AdminConf
|
adminConfig := conf.AdminConf
|
||||||
adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
|
adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
|
||||||
auth.Store.Options.Secure = adminConfig.UseTLS
|
middleware.Store.Options.Secure = adminConfig.UseTLS
|
||||||
|
|
||||||
phishConfig := conf.PhishConf
|
phishConfig := conf.PhishConf
|
||||||
phishServer := controllers.NewPhishingServer(phishConfig)
|
phishServer := controllers.NewPhishingServer(phishConfig)
|
||||||
|
|
18
gulpfile.js
18
gulpfile.js
|
@ -9,11 +9,12 @@ var gulp = require('gulp'),
|
||||||
concat = require('gulp-concat'),
|
concat = require('gulp-concat'),
|
||||||
uglify = require('gulp-uglify'),
|
uglify = require('gulp-uglify'),
|
||||||
cleanCSS = require('gulp-clean-css'),
|
cleanCSS = require('gulp-clean-css'),
|
||||||
|
babel = require('gulp-babel'),
|
||||||
|
|
||||||
js_directory = 'static/js/src/',
|
js_directory = 'static/js/src/',
|
||||||
css_directory = 'static/css/',
|
css_directory = 'static/css/',
|
||||||
vendor_directory = js_directory + 'vendor/',
|
vendor_directory = js_directory + 'vendor/',
|
||||||
app_directory = js_directory + 'app/**/*.js',
|
app_directory = js_directory + 'app/',
|
||||||
dest_js_directory = 'static/js/dist/',
|
dest_js_directory = 'static/js/dist/',
|
||||||
dest_css_directory = 'static/css/dist/';
|
dest_css_directory = 'static/css/dist/';
|
||||||
|
|
||||||
|
@ -48,8 +49,19 @@ vendorjs = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
scripts = function () {
|
scripts = function () {
|
||||||
// Gophish app files
|
// Gophish app files - non-ES6
|
||||||
return gulp.src(app_directory)
|
return gulp.src([
|
||||||
|
app_directory + 'autocomplete.js',
|
||||||
|
app_directory + 'campaign_results.js',
|
||||||
|
app_directory + 'campaigns.js',
|
||||||
|
app_directory + 'dashboard.js',
|
||||||
|
app_directory + 'groups.js',
|
||||||
|
app_directory + 'landing_pages.js',
|
||||||
|
app_directory + 'sending_profiles.js',
|
||||||
|
app_directory + 'settings.js',
|
||||||
|
app_directory + 'templates.js',
|
||||||
|
app_directory + 'gophish.js',
|
||||||
|
])
|
||||||
.pipe(rename({
|
.pipe(rename({
|
||||||
suffix: '.min'
|
suffix: '.min'
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gophish/gophish/auth"
|
|
||||||
ctx "github.com/gophish/gophish/context"
|
ctx "github.com/gophish/gophish/context"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
|
@ -31,6 +30,15 @@ func CSRFExceptions(handler http.Handler) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use allows us to stack middleware to process the request
|
||||||
|
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
|
||||||
|
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
|
||||||
|
for _, m := range mid {
|
||||||
|
handler = m(handler)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
// GetContext wraps each request in a function which fills in the context for a given request.
|
// GetContext wraps each request in a function which fills in the context for a given request.
|
||||||
// This includes setting the User and Session keys and values as necessary for use in later functions.
|
// This includes setting the User and Session keys and values as necessary for use in later functions.
|
||||||
func GetContext(handler http.Handler) http.HandlerFunc {
|
func GetContext(handler http.Handler) http.HandlerFunc {
|
||||||
|
@ -43,7 +51,7 @@ func GetContext(handler http.Handler) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
// Set the context appropriately here.
|
// Set the context appropriately here.
|
||||||
// Set the session
|
// Set the session
|
||||||
session, _ := auth.Store.Get(r, "gophish")
|
session, _ := Store.Get(r, "gophish")
|
||||||
// Put the session in the context so that we can
|
// Put the session in the context so that we can
|
||||||
// reuse the values in different handlers
|
// reuse the values in different handlers
|
||||||
r = ctx.Set(r, "session", session)
|
r = ctx.Set(r, "session", session)
|
||||||
|
@ -107,11 +115,12 @@ func RequireLogin(handler http.Handler) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if u := ctx.Get(r, "user"); u != nil {
|
if u := ctx.Get(r, "user"); u != nil {
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
q.Set("next", r.URL.Path)
|
q.Set("next", r.URL.Path)
|
||||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
|
||||||
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// init registers the necessary models to be saved in the session later
|
||||||
|
func init() {
|
||||||
|
gob.Register(&models.User{})
|
||||||
|
gob.Register(&models.Flash{})
|
||||||
|
Store.Options.HttpOnly = true
|
||||||
|
// This sets the maxAge to 5 days for all cookies
|
||||||
|
Store.MaxAge(86400 * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store contains the session information for the request
|
||||||
|
var Store = sessions.NewCookieStore(
|
||||||
|
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
|
||||||
|
[]byte(securecookie.GenerateRandomKey(32)))
|
|
@ -48,7 +48,7 @@ const (
|
||||||
// Role represents a user role within Gophish. Each user has a single role
|
// Role represents a user role within Gophish. Each user has a single role
|
||||||
// which maps to a set of permissions.
|
// which maps to a set of permissions.
|
||||||
type Role struct {
|
type Role struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"-"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
119
models/user.go
119
models/user.go
|
@ -1,11 +1,22 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
log "github.com/gophish/gophish/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrModifyingOnlyAdmin occurs when there is an attempt to modify the only
|
||||||
|
// user account with the Admin role in such a way that there will be no user
|
||||||
|
// accounts left in Gophish with that role.
|
||||||
|
var ErrModifyingOnlyAdmin = errors.New("Cannot remove the only administrator")
|
||||||
|
|
||||||
// User represents the user model for gophish.
|
// User represents the user model for gophish.
|
||||||
type User struct {
|
type User struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Username string `json:"username" sql:"not null;unique"`
|
Username string `json:"username" sql:"not null;unique"`
|
||||||
Hash string `json:"-"`
|
Hash string `json:"-"`
|
||||||
ApiKey string `json:"api_key" sql:"not null;unique"`
|
ApiKey string `json:"-" sql:"not null;unique"`
|
||||||
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
|
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
|
||||||
RoleID int64 `json:"-"`
|
RoleID int64 `json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -18,6 +29,13 @@ func GetUser(id int64) (User, error) {
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers returns the users registered in Gophish
|
||||||
|
func GetUsers() ([]User, error) {
|
||||||
|
us := []User{}
|
||||||
|
err := db.Preload("Role").Find(&us).Error
|
||||||
|
return us, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an
|
// GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an
|
||||||
// error is thrown.
|
// error is thrown.
|
||||||
func GetUserByAPIKey(key string) (User, error) {
|
func GetUserByAPIKey(key string) (User, error) {
|
||||||
|
@ -39,3 +57,102 @@ func PutUser(u *User) error {
|
||||||
err := db.Save(u).Error
|
err := db.Save(u).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureEnoughAdmins ensures that there is more than one user account in
|
||||||
|
// Gophish with the Admin role. This function is meant to be called before
|
||||||
|
// modifying a user account with the Admin role in a non-revokable way.
|
||||||
|
func EnsureEnoughAdmins() error {
|
||||||
|
role, err := GetRoleBySlug(RoleAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var adminCount int
|
||||||
|
err = db.Model(&User{}).Where("role_id=?", role.ID).Count(&adminCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adminCount == 1 {
|
||||||
|
return ErrModifyingOnlyAdmin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes the given user. To ensure that there is always at least
|
||||||
|
// one user account with the Admin role, this function will refuse to delete
|
||||||
|
// the last Admin.
|
||||||
|
func DeleteUser(id int64) error {
|
||||||
|
existing, err := GetUser(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If the user is an admin, we need to verify that it's not the last one.
|
||||||
|
if existing.Role.Slug == RoleAdmin {
|
||||||
|
err = EnsureEnoughAdmins()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
campaigns, err := GetCampaigns(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Delete the campaigns
|
||||||
|
log.Infof("Deleting campaigns for user ID %d", id)
|
||||||
|
for _, campaign := range campaigns {
|
||||||
|
err = DeleteCampaign(campaign.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("Deleting pages for user ID %d", id)
|
||||||
|
// Delete the landing pages
|
||||||
|
pages, err := GetPages(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, page := range pages {
|
||||||
|
err = DeletePage(page.Id, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete the templates
|
||||||
|
log.Infof("Deleting templates for user ID %d", id)
|
||||||
|
templates, err := GetTemplates(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, template := range templates {
|
||||||
|
err = DeleteTemplate(template.Id, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete the groups
|
||||||
|
log.Infof("Deleting groups for user ID %d", id)
|
||||||
|
groups, err := GetGroups(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, group := range groups {
|
||||||
|
err = DeleteGroup(&group)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete the sending profiles
|
||||||
|
log.Infof("Deleting sending profiles for user ID %d", id)
|
||||||
|
profiles, err := GetSMTPs(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, profile := range profiles {
|
||||||
|
err = DeleteSMTP(profile.Id, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finally, delete the user
|
||||||
|
err = db.Where("id=?", id).Delete(&User{}).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -59,3 +59,44 @@ func (s *ModelsSuite) TestGeneratedAPIKey(c *check.C) {
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012")
|
c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) verifyRoleCount(c *check.C, roleID, expected int64) {
|
||||||
|
var adminCount int64
|
||||||
|
err := db.Model(&User{}).Where("role_id=?", roleID).Count(&adminCount).Error
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
c.Assert(adminCount, check.Equals, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestDeleteLastAdmin(c *check.C) {
|
||||||
|
// Create a new admin user
|
||||||
|
role, err := GetRoleBySlug(RoleAdmin)
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
newAdmin := User{
|
||||||
|
Username: "new-admin",
|
||||||
|
Hash: "123456",
|
||||||
|
ApiKey: "123456",
|
||||||
|
Role: role,
|
||||||
|
RoleID: role.ID,
|
||||||
|
}
|
||||||
|
err = PutUser(&newAdmin)
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
// Ensure that there are two admins
|
||||||
|
s.verifyRoleCount(c, role.ID, 2)
|
||||||
|
|
||||||
|
// Delete the newly created admin - this should work since we have more
|
||||||
|
// than one current admin.
|
||||||
|
err = DeleteUser(newAdmin.Id)
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
// Verify that we now have one admin
|
||||||
|
s.verifyRoleCount(c, role.ID, 1)
|
||||||
|
|
||||||
|
// Try to delete the last admin - this should fail since we always want at
|
||||||
|
// least one admin active in Gophish.
|
||||||
|
err = DeleteUser(1)
|
||||||
|
c.Assert(err, check.Equals, ErrModifyingOnlyAdmin)
|
||||||
|
|
||||||
|
// Verify that the admin wasn't deleted
|
||||||
|
s.verifyRoleCount(c, role.ID, 1)
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,12 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://getgophish.com",
|
"homepage": "https://getgophish.com",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.4.5",
|
||||||
|
"@babel/preset-env": "^7.4.5",
|
||||||
|
"babel-loader": "^8.0.6",
|
||||||
"clean-css": "^4.2.1",
|
"clean-css": "^4.2.1",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
|
"gulp-babel": "^8.0.0",
|
||||||
"gulp-clean-css": "^4.0.0",
|
"gulp-clean-css": "^4.0.0",
|
||||||
"gulp-cli": "^2.2.0",
|
"gulp-cli": "^2.2.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
|
@ -22,6 +26,8 @@
|
||||||
"gulp-uglify": "^3.0.2",
|
"gulp-uglify": "^3.0.2",
|
||||||
"gulp-wrap": "^0.15.0",
|
"gulp-wrap": "^0.15.0",
|
||||||
"jshint": "^2.10.2",
|
"jshint": "^2.10.2",
|
||||||
"jshint-stylish": "^2.2.1"
|
"jshint-stylish": "^2.2.1",
|
||||||
|
"webpack": "^4.32.2",
|
||||||
|
"webpack-cli": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -702,6 +702,10 @@ table.dataTable {
|
||||||
background-color: #37485a;
|
background-color: #37485a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-badge {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
#resultsMapContainer {
|
#resultsMapContainer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)}})}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)},reset:function(){return query("/reset","POST",{},!0)}};$(document).ready(function(){var t=location.pathname;$(".nav-sidebar li").each(function(){var e=$(this);e.find("a").attr("href")===t&&e.addClass("active")}),$.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,r,n){return $.ajax({url:"/api"+e,async:n,method:t,data:JSON.stringify(r),dataType:"json",contentType:"application/json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)}})}function escapeHtml(e){return $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}window.escapeHtml=escapeHtml;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)}},users:{get:function(){return query("/users/","GET",{},!0)},post:function(e){return query("/users/","POST",e,!0)}},userId:{get:function(e){return query("/users/"+e,"GET",{},!0)},put:function(e){return query("/users/"+e.id,"PUT",e,!0)},delete:function(e){return query("/users/"+e,"DELETE",{},!0)}},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)},reset:function(){return query("/reset","POST",{},!0)}};window.api=api,$(document).ready(function(){var t=location.pathname;$(".nav-sidebar li").each(function(){var e=$(this);e.find("a").attr("href")===t&&e.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});
|
|
@ -0,0 +1 @@
|
||||||
|
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.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",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download",e),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(s){var e=groups.find(function(e){return e.id===s});e&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(s).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching 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()}),$("#csv-template").click(downloadCSVTemplate)});
|
|
@ -1 +1 @@
|
||||||
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.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",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download",e),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(s){var e=groups.find(function(e){return e.id===s});e&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(s).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching 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()}),$("#csv-template").click(downloadCSVTemplate)});
|
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){var r=[],n=function(){$("#username").val(""),$("#password").val(""),$("#confirm_password").val(""),$("#role").val(""),$("#modal\\.flashes").empty()},o=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){if($("#password").val()===$("#confirm_password").val()){var t={username:$("#username").val(),password:$("#password").val(),role:$("#role").val()};-1!=e?(t.id=e,api.userId.put(t).success(function(e){successFlash("User ".concat(t.username," updated successfully!")),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.users.post(t).success(function(e){successFlash("User ".concat(t.username," registered successfully!")),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}else modalError("Passwords must match.")}(e)}),$("#role").select2(),-1==e?($("#role").val("user"),$("#role").trigger("change")):api.userId.get(e).success(function(e){$("#username").val(e.username),$("#role").val(e.role.slug),$("#role").trigger("change")}).error(function(){errorFlash("Error fetching user")})},s=function(){$("#userTable").hide(),$("#loading").show(),api.users.get().success(function(e){r=e,$("#loading").hide(),$("#userTable").show();var t=$("#userTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(r,function(e,r){t.row.add([escapeHtml(r.username),escapeHtml(r.role.name),"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='"+r.id+"'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger delete_button' data-user-id='"+r.id+"'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}).error(function(){errorFlash("Error fetching users")})};$(document).ready(function(){s(),$("#modal").on("hide.bs.modal",function(){n()}),$.fn.select2.defaults.set("width","100%"),$.fn.select2.defaults.set("dropdownParent",$("#role-select")),$.fn.select2.defaults.set("theme","bootstrap"),$.fn.select2.defaults.set("sorter",function(e){return e.sort(function(e,t){return e.text.toLowerCase()>t.text.toLowerCase()?1:e.text.toLowerCase()<t.text.toLowerCase()?-1:0})}),$("#new_button").on("click",function(){o(-1)}),$("#userTable").on("click",".edit_button",function(e){o($(this).attr("data-user-id"))}),$("#userTable").on("click",".delete_button",function(e){var t,n;t=$(this).attr("data-user-id"),(n=r.find(function(e){return e.id==t}))&&swal({title:"Are you sure?",text:"This will delete the account for ".concat(n.username," as well as all of the objects they have created.\n\nThis can't be undone!"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,r){api.userId.delete(t).success(function(t){e()}).error(function(e){r(e.responseJSON.message)})})}}).then(function(){swal("User Deleted!","The user account for ".concat(n.username," and all associated objects have been deleted!"),"success"),$('button:contains("OK")').on("click",function(){location.reload()})})})})}]);
|
|
@ -32,6 +32,7 @@ function query(endpoint, method, data, async) {
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
return $("<div/>").text(text).html()
|
return $("<div/>").text(text).html()
|
||||||
}
|
}
|
||||||
|
window.escapeHtml = escapeHtml
|
||||||
|
|
||||||
function unescapeHtml(html) {
|
function unescapeHtml(html) {
|
||||||
return $("<div/>").html(html).text()
|
return $("<div/>").html(html).text()
|
||||||
|
@ -196,6 +197,32 @@ var api = {
|
||||||
return query("/smtp/" + id, "DELETE", {}, false)
|
return query("/smtp/" + id, "DELETE", {}, false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// users contains the endpoints for /users
|
||||||
|
users: {
|
||||||
|
// get() - Queries the API for GET /users
|
||||||
|
get: function () {
|
||||||
|
return query("/users/", "GET", {}, true)
|
||||||
|
},
|
||||||
|
// post() - Posts a user to POST /users
|
||||||
|
post: function (user) {
|
||||||
|
return query("/users/", "POST", user, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// userId contains the endpoints for /users/:id
|
||||||
|
userId: {
|
||||||
|
// get() - Queries the API for GET /users/:id
|
||||||
|
get: function (id) {
|
||||||
|
return query("/users/" + id, "GET", {}, true)
|
||||||
|
},
|
||||||
|
// put() - Puts a user to PUT /users/:id
|
||||||
|
put: function (user) {
|
||||||
|
return query("/users/" + user.id, "PUT", user, true)
|
||||||
|
},
|
||||||
|
// delete() - Deletes a user at DELETE /users/:id
|
||||||
|
delete: function (id) {
|
||||||
|
return query("/users/" + id, "DELETE", {}, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
// import handles all of the "import" functions in the api
|
// import handles all of the "import" functions in the api
|
||||||
import_email: function (req) {
|
import_email: function (req) {
|
||||||
return query("/import/email", "POST", req, false)
|
return query("/import/email", "POST", req, false)
|
||||||
|
@ -212,6 +239,7 @@ var api = {
|
||||||
return query("/reset", "POST", {}, true)
|
return query("/reset", "POST", {}, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
window.api = api
|
||||||
|
|
||||||
// Register our moment.js datatables listeners
|
// Register our moment.js datatables listeners
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
|
@ -0,0 +1,284 @@
|
||||||
|
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({
|
||||||
|
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
$("#modalSubmit").unbind('click').click(function () {
|
||||||
|
save(id)
|
||||||
|
})
|
||||||
|
if (id == -1) {
|
||||||
|
var group = {}
|
||||||
|
} else {
|
||||||
|
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),
|
||||||
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
|
]).draw()
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
errorFlash("Error fetching group")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle file uploads
|
||||||
|
$("#csvupload").fileupload({
|
||||||
|
url: "/api/import/group",
|
||||||
|
dataType: "json",
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
||||||
|
},
|
||||||
|
add: function (e, data) {
|
||||||
|
$("#modal\\.flashes").empty()
|
||||||
|
var acceptFileTypes = /(csv|txt)$/i;
|
||||||
|
var filename = data.originalFiles[0]['name']
|
||||||
|
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||||
|
modalError("Unsupported file extension (use .csv or .txt)")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadCSVTemplate = function () {
|
||||||
|
var csvScope = [{
|
||||||
|
'First Name': 'Example',
|
||||||
|
'Last Name': 'User',
|
||||||
|
'Email': 'foobar@example.com',
|
||||||
|
'Position': 'Systems Administrator'
|
||||||
|
}]
|
||||||
|
var filename = 'group_template.csv'
|
||||||
|
var csvString = Papa.unparse(csvScope, {})
|
||||||
|
var csvData = new Blob([csvString], {
|
||||||
|
type: 'text/csv;charset=utf-8;'
|
||||||
|
});
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(csvData, filename);
|
||||||
|
} else {
|
||||||
|
var csvURL = window.URL.createObjectURL(csvData);
|
||||||
|
var dlLink = document.createElement('a');
|
||||||
|
dlLink.href = csvURL;
|
||||||
|
dlLink.setAttribute('download', filename)
|
||||||
|
document.body.appendChild(dlLink)
|
||||||
|
dlLink.click();
|
||||||
|
document.body.removeChild(dlLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var deleteGroup = function (id) {
|
||||||
|
var group = groups.find(function (x) {
|
||||||
|
return x.id === id
|
||||||
|
})
|
||||||
|
if (!group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
swal({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: "This will delete the group. This can't be undone!",
|
||||||
|
type: "warning",
|
||||||
|
animation: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Delete " + escapeHtml(group.name),
|
||||||
|
confirmButtonColor: "#428bca",
|
||||||
|
reverseButtons: true,
|
||||||
|
allowOutsideClick: false,
|
||||||
|
preConfirm: function () {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
api.groupId.delete(id)
|
||||||
|
.success(function (msg) {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.error(function (data) {
|
||||||
|
reject(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).then(function () {
|
||||||
|
swal(
|
||||||
|
'Group Deleted!',
|
||||||
|
'This group has been deleted!',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
$('button:contains("OK")').on('click', function () {
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
||||||
|
// Create new data row.
|
||||||
|
var email = escapeHtml(emailInput).toLowerCase();
|
||||||
|
var newRow = [
|
||||||
|
escapeHtml(firstNameInput),
|
||||||
|
escapeHtml(lastNameInput),
|
||||||
|
email,
|
||||||
|
escapeHtml(positionInput),
|
||||||
|
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check table to see if email already exists.
|
||||||
|
var targetsTable = targets.DataTable();
|
||||||
|
var existingRowIndex = targetsTable
|
||||||
|
.column(2, {
|
||||||
|
order: "index"
|
||||||
|
}) // Email column has index of 2
|
||||||
|
.data()
|
||||||
|
.indexOf(email);
|
||||||
|
// Update or add new row as necessary.
|
||||||
|
if (existingRowIndex >= 0) {
|
||||||
|
targetsTable
|
||||||
|
.row(existingRowIndex, {
|
||||||
|
order: "index"
|
||||||
|
})
|
||||||
|
.data(newRow);
|
||||||
|
} else {
|
||||||
|
targetsTable.row.add(newRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
$("#groupTable").hide()
|
||||||
|
$("#emptyMessage").hide()
|
||||||
|
$("#loading").show()
|
||||||
|
api.groups.summary()
|
||||||
|
.success(function (response) {
|
||||||
|
$("#loading").hide()
|
||||||
|
if (response.total > 0) {
|
||||||
|
groups = response.groups
|
||||||
|
$("#emptyMessage").hide()
|
||||||
|
$("#groupTable").show()
|
||||||
|
var groupTable = $("#groupTable").DataTable({
|
||||||
|
destroy: true,
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
groupTable.clear();
|
||||||
|
$.each(groups, function (i, group) {
|
||||||
|
groupTable.row.add([
|
||||||
|
escapeHtml(group.name),
|
||||||
|
escapeHtml(group.num_targets),
|
||||||
|
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
|
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
|
||||||
|
<i class='fa fa-pencil'></i>\
|
||||||
|
</button>\
|
||||||
|
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
||||||
|
<i class='fa fa-trash-o'></i>\
|
||||||
|
</button></div>"
|
||||||
|
]).draw()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
$("#emptyMessage").show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
errorFlash("Error fetching groups")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
load()
|
||||||
|
// Setup the event listeners
|
||||||
|
// Handle manual additions
|
||||||
|
$("#targetForm").submit(function () {
|
||||||
|
addTarget(
|
||||||
|
$("#firstName").val(),
|
||||||
|
$("#lastName").val(),
|
||||||
|
$("#email").val(),
|
||||||
|
$("#position").val());
|
||||||
|
targets.DataTable().draw();
|
||||||
|
|
||||||
|
// Reset user input.
|
||||||
|
$("#targetForm>div>input").val('');
|
||||||
|
$("#firstName").focus();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
// Handle Deletion
|
||||||
|
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
||||||
|
targets.DataTable()
|
||||||
|
.row($(this).parents('tr'))
|
||||||
|
.remove()
|
||||||
|
.draw();
|
||||||
|
});
|
||||||
|
$("#modal").on("hide.bs.modal", function () {
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
$("#csv-template").click(downloadCSVTemplate)
|
||||||
|
});
|
|
@ -1,28 +1,25 @@
|
||||||
var groups = []
|
let users = []
|
||||||
|
|
||||||
// Save attempts to POST or PUT to /groups/
|
// Save attempts to POST or PUT to /users/
|
||||||
function save(id) {
|
const save = (id) => {
|
||||||
var targets = []
|
// Validate that the passwords match
|
||||||
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
|
if ($("#password").val() !== $("#confirm_password").val()) {
|
||||||
targets.push({
|
modalError("Passwords must match.")
|
||||||
first_name: unescapeHtml(target[0]),
|
return
|
||||||
last_name: unescapeHtml(target[1]),
|
|
||||||
email: unescapeHtml(target[2]),
|
|
||||||
position: unescapeHtml(target[3])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
var group = {
|
|
||||||
name: $("#name").val(),
|
|
||||||
targets: targets
|
|
||||||
}
|
}
|
||||||
// Submit the group
|
let user = {
|
||||||
|
username: $("#username").val(),
|
||||||
|
password: $("#password").val(),
|
||||||
|
role: $("#role").val()
|
||||||
|
}
|
||||||
|
// Submit the user
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
// If we're just editing an existing group,
|
// If we're just editing an existing user,
|
||||||
// we need to PUT /groups/:id
|
// we need to PUT /user/:id
|
||||||
group.id = id
|
user.id = id
|
||||||
api.groupId.put(group)
|
api.userId.put(user)
|
||||||
.success(function (data) {
|
.success(function (data) {
|
||||||
successFlash("Group updated successfully!")
|
successFlash(`User ${user.username} updated successfully!`)
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
$("#modal").modal('hide')
|
$("#modal").modal('hide')
|
||||||
|
@ -31,11 +28,11 @@ function save(id) {
|
||||||
modalError(data.responseJSON.message)
|
modalError(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Else, if this is a new group, POST it
|
// Else, if this is a new user, POST it
|
||||||
// to /groups
|
// to /user
|
||||||
api.groups.post(group)
|
api.users.post(user)
|
||||||
.success(function (data) {
|
.success(function (data) {
|
||||||
successFlash("Group added successfully!")
|
successFlash(`User ${user.username} registered successfully!`)
|
||||||
load()
|
load()
|
||||||
dismiss()
|
dismiss()
|
||||||
$("#modal").modal('hide')
|
$("#modal").modal('hide')
|
||||||
|
@ -46,133 +43,65 @@ function save(id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismiss() {
|
const dismiss = () => {
|
||||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
$("#username").val("")
|
||||||
$("#name").val("")
|
$("#password").val("")
|
||||||
|
$("#confirm_password").val("")
|
||||||
|
$("#role").val("")
|
||||||
$("#modal\\.flashes").empty()
|
$("#modal\\.flashes").empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(id) {
|
const edit = (id) => {
|
||||||
targets = $("#targetsTable").dataTable({
|
$("#modalSubmit").unbind('click').click(() => {
|
||||||
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
|
||||||
columnDefs: [{
|
|
||||||
orderable: false,
|
|
||||||
targets: "no-sort"
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
$("#modalSubmit").unbind('click').click(function () {
|
|
||||||
save(id)
|
save(id)
|
||||||
})
|
})
|
||||||
|
$("#role").select2()
|
||||||
if (id == -1) {
|
if (id == -1) {
|
||||||
var group = {}
|
$("#role").val("user")
|
||||||
|
$("#role").trigger("change")
|
||||||
} else {
|
} else {
|
||||||
api.groupId.get(id)
|
api.userId.get(id)
|
||||||
.success(function (group) {
|
.success(function (user) {
|
||||||
$("#name").val(group.name)
|
$("#username").val(user.username)
|
||||||
$.each(group.targets, function (i, record) {
|
$("#role").val(user.role.slug)
|
||||||
targets.DataTable()
|
$("#role").trigger("change")
|
||||||
.row.add([
|
|
||||||
escapeHtml(record.first_name),
|
|
||||||
escapeHtml(record.last_name),
|
|
||||||
escapeHtml(record.email),
|
|
||||||
escapeHtml(record.position),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
]).draw()
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.error(function () {
|
.error(function () {
|
||||||
errorFlash("Error fetching group")
|
errorFlash("Error fetching user")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Handle file uploads
|
|
||||||
$("#csvupload").fileupload({
|
|
||||||
url: "/api/import/group",
|
|
||||||
dataType: "json",
|
|
||||||
beforeSend: function (xhr) {
|
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
|
||||||
},
|
|
||||||
add: function (e, data) {
|
|
||||||
$("#modal\\.flashes").empty()
|
|
||||||
var acceptFileTypes = /(csv|txt)$/i;
|
|
||||||
var filename = data.originalFiles[0]['name']
|
|
||||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
|
||||||
modalError("Unsupported file extension (use .csv or .txt)")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloadCSVTemplate = function () {
|
|
||||||
var csvScope = [{
|
|
||||||
'First Name': 'Example',
|
|
||||||
'Last Name': 'User',
|
|
||||||
'Email': 'foobar@example.com',
|
|
||||||
'Position': 'Systems Administrator'
|
|
||||||
}]
|
|
||||||
var filename = 'group_template.csv'
|
|
||||||
var csvString = Papa.unparse(csvScope, {})
|
|
||||||
var csvData = new Blob([csvString], {
|
|
||||||
type: 'text/csv;charset=utf-8;'
|
|
||||||
});
|
|
||||||
if (navigator.msSaveBlob) {
|
|
||||||
navigator.msSaveBlob(csvData, filename);
|
|
||||||
} else {
|
|
||||||
var csvURL = window.URL.createObjectURL(csvData);
|
|
||||||
var dlLink = document.createElement('a');
|
|
||||||
dlLink.href = csvURL;
|
|
||||||
dlLink.setAttribute('download', filename)
|
|
||||||
document.body.appendChild(dlLink)
|
|
||||||
dlLink.click();
|
|
||||||
document.body.removeChild(dlLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteUser = (id) => {
|
||||||
var deleteGroup = function (id) {
|
var user = users.find(x => x.id == id)
|
||||||
var group = groups.find(function (x) {
|
if (!user) {
|
||||||
return x.id === id
|
|
||||||
})
|
|
||||||
if (!group) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
swal({
|
swal({
|
||||||
title: "Are you sure?",
|
title: "Are you sure?",
|
||||||
text: "This will delete the group. This can't be undone!",
|
text: `This will delete the account for ${user.username} as well as all of the objects they have created.\n\nThis can't be undone!`,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
animation: false,
|
animation: false,
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: "Delete " + escapeHtml(group.name),
|
confirmButtonText: "Delete",
|
||||||
confirmButtonColor: "#428bca",
|
confirmButtonColor: "#428bca",
|
||||||
reverseButtons: true,
|
reverseButtons: true,
|
||||||
allowOutsideClick: false,
|
allowOutsideClick: false,
|
||||||
preConfirm: function () {
|
preConfirm: function () {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise((resolve, reject) => {
|
||||||
api.groupId.delete(id)
|
api.userId.delete(id)
|
||||||
.success(function (msg) {
|
.success((msg) => {
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.error(function (data) {
|
.error((data) => {
|
||||||
reject(data.responseJSON.message)
|
reject(data.responseJSON.message)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
swal(
|
swal(
|
||||||
'Group Deleted!',
|
'User Deleted!',
|
||||||
'This group has been deleted!',
|
`The user account for ${user.username} and all associated objects have been deleted!`,
|
||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
$('button:contains("OK")').on('click', function () {
|
$('button:contains("OK")').on('click', function () {
|
||||||
|
@ -181,104 +110,69 @@ var deleteGroup = function (id) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
|
||||||
// Create new data row.
|
|
||||||
var email = escapeHtml(emailInput).toLowerCase();
|
|
||||||
var newRow = [
|
|
||||||
escapeHtml(firstNameInput),
|
|
||||||
escapeHtml(lastNameInput),
|
|
||||||
email,
|
|
||||||
escapeHtml(positionInput),
|
|
||||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check table to see if email already exists.
|
const load = () => {
|
||||||
var targetsTable = targets.DataTable();
|
$("#userTable").hide()
|
||||||
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"
|
|
||||||
})
|
|
||||||
.data(newRow);
|
|
||||||
} else {
|
|
||||||
targetsTable.row.add(newRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
$("#groupTable").hide()
|
|
||||||
$("#emptyMessage").hide()
|
|
||||||
$("#loading").show()
|
$("#loading").show()
|
||||||
api.groups.summary()
|
api.users.get()
|
||||||
.success(function (response) {
|
.success((us) => {
|
||||||
|
users = us
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
if (response.total > 0) {
|
$("#userTable").show()
|
||||||
groups = response.groups
|
let userTable = $("#userTable").DataTable({
|
||||||
$("#emptyMessage").hide()
|
|
||||||
$("#groupTable").show()
|
|
||||||
var groupTable = $("#groupTable").DataTable({
|
|
||||||
destroy: true,
|
destroy: true,
|
||||||
columnDefs: [{
|
columnDefs: [{
|
||||||
orderable: false,
|
orderable: false,
|
||||||
targets: "no-sort"
|
targets: "no-sort"
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
groupTable.clear();
|
userTable.clear();
|
||||||
$.each(groups, function (i, group) {
|
$.each(users, (i, user) => {
|
||||||
groupTable.row.add([
|
userTable.row.add([
|
||||||
escapeHtml(group.name),
|
escapeHtml(user.username),
|
||||||
escapeHtml(group.num_targets),
|
escapeHtml(user.role.name),
|
||||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
|
||||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
|
|
||||||
<i class='fa fa-pencil'></i>\
|
<i class='fa fa-pencil'></i>\
|
||||||
</button>\
|
</button>\
|
||||||
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
<button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
|
||||||
<i class='fa fa-trash-o'></i>\
|
<i class='fa fa-trash-o'></i>\
|
||||||
</button></div>"
|
</button></div>"
|
||||||
]).draw()
|
]).draw()
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
$("#emptyMessage").show()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.error(function () {
|
.error(() => {
|
||||||
errorFlash("Error fetching groups")
|
errorFlash("Error fetching users")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
load()
|
load()
|
||||||
// Setup the event listeners
|
// Setup the event listeners
|
||||||
// Handle manual additions
|
|
||||||
$("#targetForm").submit(function () {
|
|
||||||
addTarget(
|
|
||||||
$("#firstName").val(),
|
|
||||||
$("#lastName").val(),
|
|
||||||
$("#email").val(),
|
|
||||||
$("#position").val());
|
|
||||||
targets.DataTable().draw();
|
|
||||||
|
|
||||||
// Reset user input.
|
|
||||||
$("#targetForm>div>input").val('');
|
|
||||||
$("#firstName").focus();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
// Handle Deletion
|
|
||||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
|
||||||
targets.DataTable()
|
|
||||||
.row($(this).parents('tr'))
|
|
||||||
.remove()
|
|
||||||
.draw();
|
|
||||||
});
|
|
||||||
$("#modal").on("hide.bs.modal", function () {
|
$("#modal").on("hide.bs.modal", function () {
|
||||||
dismiss();
|
dismiss();
|
||||||
});
|
});
|
||||||
$("#csv-template").click(downloadCSVTemplate)
|
// Select2 Defaults
|
||||||
|
$.fn.select2.defaults.set("width", "100%");
|
||||||
|
$.fn.select2.defaults.set("dropdownParent", $("#role-select"));
|
||||||
|
$.fn.select2.defaults.set("theme", "bootstrap");
|
||||||
|
$.fn.select2.defaults.set("sorter", function (data) {
|
||||||
|
return data.sort(function (a, b) {
|
||||||
|
if (a.text.toLowerCase() > b.text.toLowerCase()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.text.toLowerCase() < b.text.toLowerCase()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
$("#new_button").on("click", function () {
|
||||||
|
edit(-1)
|
||||||
|
})
|
||||||
|
$("#userTable").on('click', '.edit_button', function (e) {
|
||||||
|
edit($(this).attr('data-user-id'))
|
||||||
|
})
|
||||||
|
$("#userTable").on('click', '.delete_button', function (e) {
|
||||||
|
deleteUser($(this).attr('data-user-id'))
|
||||||
|
})
|
||||||
});
|
});
|
|
@ -0,0 +1,106 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="page-header">
|
||||||
|
Users & Groups
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div id="flashes" class="row"></div>
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
|
||||||
|
data-target="#modal">
|
||||||
|
<i class="fa fa-plus"></i> New Group</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||||
|
</div>
|
||||||
|
<div id="emptyMessage" class="row" style="display:none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No groups created yet. Let's create one!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<table id="groupTable" class="table" style="display:none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th># of Members</th>
|
||||||
|
<th>Modified Date</th>
|
||||||
|
<th class="col-md-2 no-sort"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</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">×</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>
|
||||||
|
<span id="csv-template" class="text-muted small">
|
||||||
|
<i class="fa fa-file-excel-o"></i> Download CSV Template</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>
|
||||||
|
{{end}} {{define "scripts"}}
|
||||||
|
<script src="/js/dist/app/groups.min.js"></script>
|
||||||
|
{{end}}
|
|
@ -10,7 +10,7 @@
|
||||||
<a href="/campaigns">Campaigns</a>
|
<a href="/campaigns">Campaigns</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/users">Users & Groups</a>
|
<a href="/groups">Users & Groups</a>
|
||||||
</li>
|
</li>
|
||||||
<li> <a href="/templates">Email Templates</a>
|
<li> <a href="/templates">Email Templates</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -21,8 +21,13 @@
|
||||||
<a href="/sending_profiles">Sending Profiles</a>
|
<a href="/sending_profiles">Sending Profiles</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/settings">Settings <span class="badge pull-right">Admin</span></a>
|
<a href="/settings">Account Settings</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
{{if .ModifySystem}}
|
||||||
|
<li>
|
||||||
|
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
<li>
|
<li>
|
||||||
<hr>
|
<hr>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
{{ define "base" }}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
|
|
||||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
|
||||||
<link rel="shortcut icon" href="/favicon.png">
|
|
||||||
|
|
||||||
<title>Gophish - {{ .Title }}</title>
|
|
||||||
|
|
||||||
<link href="/css/dist/gophish.css" rel='stylesheet' type='text/css'>
|
|
||||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="navbar-header">
|
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
|
||||||
<span class="sr-only">Toggle navigation</span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
|
||||||
<img class="navbar-logo" src="/images/logo_inv_small.png" />
|
|
||||||
<a class="navbar-brand" href="/"> gophish</a>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-collapse collapse">
|
|
||||||
<ul class="nav navbar-nav navbar-right">
|
|
||||||
<li>
|
|
||||||
<a id="login-button" href="/login">
|
|
||||||
<button type="button" class="btn btn-primary">Login</button>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<form class="form-signin" action="/register" method="POST">
|
|
||||||
<img id="logo" src="/images/logo_purple.png" />
|
|
||||||
<h2 class="form-signin-heading">Please register below</h2>
|
|
||||||
{{template "flashes" .Flashes}}
|
|
||||||
<input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus />
|
|
||||||
<input type="password" name="password" class="form-control middle-input" placeholder="Password"
|
|
||||||
autocomplete="off" required />
|
|
||||||
<input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password"
|
|
||||||
autocomplete="off" required />
|
|
||||||
<input type="hidden" name="csrf_token" value="{{.Token}}" />
|
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<!-- Placed at the end of the document so the pages load faster -->
|
|
||||||
<script src="/js/dist/vendor.min.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
|
@ -8,7 +8,8 @@
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
||||||
data-toggle="tab">Account Settings</a></li>
|
data-toggle="tab">Account Settings</a></li>
|
||||||
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI Settings</a></li>
|
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
|
||||||
|
Settings</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- Tab Panes -->
|
<!-- Tab Panes -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
@ -22,19 +23,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row">
|
|
||||||
<label class="col-sm-2 control-label form-label">Register a New User</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<a href="/register" class="btn btn-primary"><i class="fa fa-plus"></i> Add User</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
|
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}" class="form-control"
|
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}"
|
||||||
readonly />
|
class="form-control" readonly />
|
||||||
</div>
|
</div>
|
||||||
<form id="apiResetForm">
|
<form id="apiResetForm">
|
||||||
<button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
|
<button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
|
||||||
|
@ -46,26 +40,30 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="username" class="col-sm-2 control-label form-label">Username:</label>
|
<label for="username" class="col-sm-2 control-label form-label">Username:</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="text" id="username" name="username" value="{{.User.Username}}" class="form-control" />
|
<input type="text" id="username" name="username" value="{{.User.Username}}"
|
||||||
|
class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
|
<label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="password" id="current_password" name="current_password" autocomplete="off" class="form-control" />
|
<input type="password" id="current_password" name="current_password" autocomplete="off"
|
||||||
|
class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="password" id="new_password" name="new_password" autocomplete="off" class="form-control" />
|
<input type="password" id="new_password" name="new_password" autocomplete="off"
|
||||||
|
class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New Password:</label>
|
<label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New
|
||||||
|
Password:</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
|
<input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
|
||||||
class="form-control" />
|
class="form-control" />
|
||||||
|
|
|
@ -2,31 +2,25 @@
|
||||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h1 class="page-header">
|
<h1 class="page-header">
|
||||||
Users & Groups
|
{{.Title}}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="flashes" class="row"></div>
|
<div id="flashes" class="row"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
|
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||||
data-target="#modal">
|
data-user-id="-1" data-target="#modal">
|
||||||
<i class="fa fa-plus"></i> New Group</button>
|
<i class="fa fa-plus"></i> New User</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||||
</div>
|
</div>
|
||||||
<div id="emptyMessage" class="row" style="display:none;">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
No groups created yet. Let's create one!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<table id="groupTable" class="table" style="display:none;">
|
<table id="userTable" class="table" style="display:none;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Username</th>
|
||||||
<th># of Members</th>
|
<th>Role</th>
|
||||||
<th>Modified Date</th>
|
|
||||||
<th class="col-md-2 no-sort"></th>
|
<th class="col-md-2 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -43,56 +37,30 @@
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
|
<h4 class="modal-title" id="groupModalLabel">New User</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" id="modal_body">
|
||||||
<div class="row" id="modal.flashes"></div>
|
<div class="row" id="modal.flashes"></div>
|
||||||
<label class="control-label" for="name">Name:</label>
|
<label class="control-label" for="username">Username:</label>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
<input type="text" class="form-control" placeholder="Username" id="username" autofocus />
|
||||||
autofocus />
|
|
||||||
</div>
|
</div>
|
||||||
|
<label class="control-label" for="password">Password:</label>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right" title="Supports CSV files"
|
<input type="password" class="form-control" placeholder="Password" id="password" required />
|
||||||
id="fileUpload">
|
|
||||||
<i class="fa fa-plus"></i> Bulk Import Users
|
|
||||||
<input type="file" id="csvupload" multiple>
|
|
||||||
</span>
|
|
||||||
<span id="csv-template" class="text-muted small">
|
|
||||||
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<label class="control-label" for="confirm_password">Confirm Password:</label>
|
||||||
<form id="targetForm">
|
<div class="form-group">
|
||||||
<div class="col-sm-2">
|
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
|
||||||
<input type="text" class="form-control" placeholder="First Name" id="firstName">
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-2">
|
<label class="control-label" for="role">Role:</label>
|
||||||
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
|
<div class="form-group" id="role-select">
|
||||||
|
<select class="form-control" placeholder="" id="role" />
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="user">User</option>
|
||||||
|
</select>
|
||||||
</div>
|
</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>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
|
19
util/util.go
19
util/util.go
|
@ -21,6 +21,7 @@ import (
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/jordan-wright/email"
|
"github.com/jordan-wright/email"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -190,3 +191,21 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
||||||
log.Info("TLS Certificate Generation complete")
|
log.Info("TLS Certificate Generation complete")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateSecureKey creates a secure key to use as an API key
|
||||||
|
func GenerateSecureKey() string {
|
||||||
|
// Inspired from gorilla/securecookie
|
||||||
|
k := make([]byte, 32)
|
||||||
|
io.ReadFull(rand.Reader, k)
|
||||||
|
return fmt.Sprintf("%x", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHash hashes the provided password and returns the bcrypt hash (using the
|
||||||
|
// default 10 rounds) as a string.
|
||||||
|
func NewHash(pass string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
|
||||||
|
entry: {
|
||||||
|
users: './users',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'static', 'js', 'dist', 'app'),
|
||||||
|
filename: '[name].min.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue