mirror of
https://github.com/gophish/gophish
synced 2024-11-12 23:37:11 +00:00
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.
This commit is contained in:
parent
faadf0c850
commit
84096b8724
32 changed files with 3595 additions and 532 deletions
3
.babelrc
Normal file
3
.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
83
auth/auth.go
83
auth/auth.go
|
@ -1,49 +1,24 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"crypto/rand"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
"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.
|
||||
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
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("Password cannot be blank")
|
||||
|
||||
// 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")
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// Login attempts to login the user given a request.
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
// if it's valid, changes the password for the authenticated user.
|
||||
func ChangePassword(r *http.Request) error {
|
||||
|
|
|
@ -19,6 +19,7 @@ type APISuite struct {
|
|||
apiKey string
|
||||
config *config.Config
|
||||
apiServer *Server
|
||||
admin models.User
|
||||
}
|
||||
|
||||
func (s *APISuite) SetupSuite() {
|
||||
|
@ -37,6 +38,7 @@ func (s *APISuite) SetupSuite() {
|
|||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
s.admin = u
|
||||
// Move our cwd up to the project root for help with resolving
|
||||
// static assets
|
||||
err = os.Chdir("../")
|
||||
|
@ -49,6 +51,15 @@ func (s *APISuite) TearDownTest() {
|
|||
for _, campaign := range campaigns {
|
||||
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() {
|
||||
|
|
|
@ -3,9 +3,9 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = auth.GenerateSecureKey()
|
||||
u.ApiKey = util.GenerateSecureKey()
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/worker"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
@ -64,6 +65,8 @@ func (as *Server) registerRoutes() {
|
|||
router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
|
||||
router.HandleFunc("/smtp/", as.SendingProfiles)
|
||||
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("/import/group", as.ImportGroup)
|
||||
router.HandleFunc("/import/email", as.ImportEmail)
|
||||
|
|
218
controllers/api/user.go
Normal file
218
controllers/api/user.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
188
controllers/api/user_test.go
Normal file
188
controllers/api/user_test.go
Normal file
|
@ -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() {
|
||||
router := mux.NewRouter()
|
||||
// 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("/logout", Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", Use(as.Templates, mid.RequireLogin))
|
||||
router.HandleFunc("/users", Use(as.Users, mid.RequireLogin))
|
||||
router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin))
|
||||
router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin))
|
||||
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||
router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin))
|
||||
router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin))
|
||||
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
|
||||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
// Create the API routes
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
router.PathPrefix("/api/").Handler(api)
|
||||
|
@ -114,11 +114,11 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
||||
|
||||
// Setup CSRF Protection
|
||||
csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()),
|
||||
csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
|
||||
csrf.FieldName("csrf_token"),
|
||||
csrf.Secure(as.config.UseTLS))
|
||||
adminHandler := csrfHandler(router)
|
||||
adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||
adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||
|
||||
// Setup GZIP compression
|
||||
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
||||
|
@ -129,15 +129,6 @@ func (as *AdminServer) registerRoutes() {
|
|||
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 {
|
||||
Title string
|
||||
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
|
||||
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
|
@ -224,11 +179,11 @@ func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) {
|
|||
getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Users handles the default path and template execution
|
||||
func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) {
|
||||
// Groups handles the default path and template execution
|
||||
func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
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
|
||||
|
@ -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,
|
||||
// a session is created
|
||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -32,10 +32,10 @@ import (
|
|||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/controllers"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
|
@ -94,7 +94,7 @@ func main() {
|
|||
}
|
||||
adminConfig := conf.AdminConf
|
||||
adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
|
||||
auth.Store.Options.Secure = adminConfig.UseTLS
|
||||
middleware.Store.Options.Secure = adminConfig.UseTLS
|
||||
|
||||
phishConfig := conf.PhishConf
|
||||
phishServer := controllers.NewPhishingServer(phishConfig)
|
||||
|
|
18
gulpfile.js
18
gulpfile.js
|
@ -9,11 +9,12 @@ var gulp = require('gulp'),
|
|||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
cleanCSS = require('gulp-clean-css'),
|
||||
babel = require('gulp-babel'),
|
||||
|
||||
js_directory = 'static/js/src/',
|
||||
css_directory = 'static/css/',
|
||||
vendor_directory = js_directory + 'vendor/',
|
||||
app_directory = js_directory + 'app/**/*.js',
|
||||
app_directory = js_directory + 'app/',
|
||||
dest_js_directory = 'static/js/dist/',
|
||||
dest_css_directory = 'static/css/dist/';
|
||||
|
||||
|
@ -48,8 +49,19 @@ vendorjs = function () {
|
|||
}
|
||||
|
||||
scripts = function () {
|
||||
// Gophish app files
|
||||
return gulp.src(app_directory)
|
||||
// Gophish app files - non-ES6
|
||||
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({
|
||||
suffix: '.min'
|
||||
}))
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"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.
|
||||
// This includes setting the User and Session keys and values as necessary for use in later functions.
|
||||
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 session
|
||||
session, _ := auth.Store.Get(r, "gophish")
|
||||
session, _ := Store.Get(r, "gophish")
|
||||
// Put the session in the context so that we can
|
||||
// reuse the values in different handlers
|
||||
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) {
|
||||
if u := ctx.Get(r, "user"); u != nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
} else {
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
23
middleware/session.go
Normal file
23
middleware/session.go
Normal file
|
@ -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
|
||||
// which maps to a set of permissions.
|
||||
type Role struct {
|
||||
ID int64 `json:"id"`
|
||||
ID int64 `json:"-"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
|
119
models/user.go
119
models/user.go
|
@ -1,11 +1,22 @@
|
|||
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.
|
||||
type User struct {
|
||||
Id int64 `json:"id"`
|
||||
Username string `json:"username" sql:"not null;unique"`
|
||||
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"`
|
||||
RoleID int64 `json:"-"`
|
||||
}
|
||||
|
@ -18,6 +29,13 @@ func GetUser(id int64) (User, error) {
|
|||
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
|
||||
// error is thrown.
|
||||
func GetUserByAPIKey(key string) (User, error) {
|
||||
|
@ -39,3 +57,102 @@ func PutUser(u *User) error {
|
|||
err := db.Save(u).Error
|
||||
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(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",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-css": "^4.2.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-clean-css": "^4.0.0",
|
||||
"gulp-cli": "^2.2.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
|
@ -22,6 +26,8 @@
|
|||
"gulp-uglify": "^3.0.2",
|
||||
"gulp-wrap": "^0.15.0",
|
||||
"jshint": "^2.10.2",
|
||||
"jshint-stylish": "^2.2.1"
|
||||
"jshint-stylish": "^2.2.1",
|
||||
"webpack": "^4.32.2",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
2
static/css/dist/gophish.css
vendored
2
static/css/dist/gophish.css
vendored
File diff suppressed because one or more lines are too long
4
static/css/main.css
vendored
4
static/css/main.css
vendored
|
@ -702,6 +702,10 @@ table.dataTable {
|
|||
background-color: #37485a;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#resultsMapContainer {
|
||||
display: none;
|
||||
}
|
||||
|
|
2
static/js/dist/app/gophish.min.js
vendored
2
static/js/dist/app/gophish.min.js
vendored
|
@ -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()});
|
1
static/js/dist/app/groups.min.js
vendored
Normal file
1
static/js/dist/app/groups.min.js
vendored
Normal file
|
@ -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)});
|
2
static/js/dist/app/users.min.js
vendored
2
static/js/dist/app/users.min.js
vendored
|
@ -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) {
|
||||
return $("<div/>").text(text).html()
|
||||
}
|
||||
window.escapeHtml = escapeHtml
|
||||
|
||||
function unescapeHtml(html) {
|
||||
return $("<div/>").html(html).text()
|
||||
|
@ -196,6 +197,32 @@ var api = {
|
|||
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_email: function (req) {
|
||||
return query("/import/email", "POST", req, false)
|
||||
|
@ -212,6 +239,7 @@ var api = {
|
|||
return query("/reset", "POST", {}, true)
|
||||
}
|
||||
}
|
||||
window.api = api
|
||||
|
||||
// Register our moment.js datatables listeners
|
||||
$(document).ready(function () {
|
||||
|
|
284
static/js/src/app/groups.js
Normal file
284
static/js/src/app/groups.js
Normal file
|
@ -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/
|
||||
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
|
||||
// Save attempts to POST or PUT to /users/
|
||||
const save = (id) => {
|
||||
// Validate that the passwords match
|
||||
if ($("#password").val() !== $("#confirm_password").val()) {
|
||||
modalError("Passwords must match.")
|
||||
return
|
||||
}
|
||||
// Submit the group
|
||||
let user = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
role: $("#role").val()
|
||||
}
|
||||
// Submit the user
|
||||
if (id != -1) {
|
||||
// If we're just editing an existing group,
|
||||
// we need to PUT /groups/:id
|
||||
group.id = id
|
||||
api.groupId.put(group)
|
||||
// If we're just editing an existing user,
|
||||
// we need to PUT /user/:id
|
||||
user.id = id
|
||||
api.userId.put(user)
|
||||
.success(function (data) {
|
||||
successFlash("Group updated successfully!")
|
||||
successFlash(`User ${user.username} updated successfully!`)
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
|
@ -31,11 +28,11 @@ function save(id) {
|
|||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new group, POST it
|
||||
// to /groups
|
||||
api.groups.post(group)
|
||||
// Else, if this is a new user, POST it
|
||||
// to /user
|
||||
api.users.post(user)
|
||||
.success(function (data) {
|
||||
successFlash("Group added successfully!")
|
||||
successFlash(`User ${user.username} registered successfully!`)
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
|
@ -46,133 +43,65 @@ function save(id) {
|
|||
}
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
||||
$("#name").val("")
|
||||
const dismiss = () => {
|
||||
$("#username").val("")
|
||||
$("#password").val("")
|
||||
$("#confirm_password").val("")
|
||||
$("#role").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 () {
|
||||
const edit = (id) => {
|
||||
$("#modalSubmit").unbind('click').click(() => {
|
||||
save(id)
|
||||
})
|
||||
$("#role").select2()
|
||||
if (id == -1) {
|
||||
var group = {}
|
||||
$("#role").val("user")
|
||||
$("#role").trigger("change")
|
||||
} 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()
|
||||
});
|
||||
|
||||
api.userId.get(id)
|
||||
.success(function (user) {
|
||||
$("#username").val(user.username)
|
||||
$("#role").val(user.role.slug)
|
||||
$("#role").trigger("change")
|
||||
})
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var deleteGroup = function (id) {
|
||||
var group = groups.find(function (x) {
|
||||
return x.id === id
|
||||
})
|
||||
if (!group) {
|
||||
const deleteUser = (id) => {
|
||||
var user = users.find(x => x.id == id)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
swal({
|
||||
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",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete " + escapeHtml(group.name),
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
preConfirm: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
api.groupId.delete(id)
|
||||
.success(function (msg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
api.userId.delete(id)
|
||||
.success((msg) => {
|
||||
resolve()
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
reject(data.responseJSON.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
'Group Deleted!',
|
||||
'This group has been deleted!',
|
||||
'User Deleted!',
|
||||
`The user account for ${user.username} and all associated objects have been deleted!`,
|
||||
'success'
|
||||
);
|
||||
$('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.
|
||||
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()
|
||||
const load = () => {
|
||||
$("#userTable").hide()
|
||||
$("#loading").show()
|
||||
api.groups.summary()
|
||||
.success(function (response) {
|
||||
api.users.get()
|
||||
.success((us) => {
|
||||
users = us
|
||||
$("#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 + ")'>\
|
||||
$("#userTable").show()
|
||||
let userTable = $("#userTable").DataTable({
|
||||
destroy: true,
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
});
|
||||
userTable.clear();
|
||||
$.each(users, (i, user) => {
|
||||
userTable.row.add([
|
||||
escapeHtml(user.username),
|
||||
escapeHtml(user.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='" + user.id + "'>\
|
||||
<i class='fa fa-pencil'></i>\
|
||||
</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>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
})
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
]).draw()
|
||||
})
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching groups")
|
||||
.error(() => {
|
||||
errorFlash("Error fetching users")
|
||||
})
|
||||
}
|
||||
|
||||
$(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)
|
||||
// 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'))
|
||||
})
|
||||
});
|
106
templates/groups.html
Normal file
106
templates/groups.html
Normal file
|
@ -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>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/users">Users & Groups</a>
|
||||
<a href="/groups">Users & Groups</a>
|
||||
</li>
|
||||
<li> <a href="/templates">Email Templates</a>
|
||||
</li>
|
||||
|
@ -21,8 +21,13 @@
|
|||
<a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings">Settings <span class="badge pull-right">Admin</span></a>
|
||||
<a href="/settings">Account Settings</span></a>
|
||||
</li>
|
||||
{{if .ModifySystem}}
|
||||
<li>
|
||||
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<hr>
|
||||
</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">
|
||||
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
||||
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>
|
||||
<!-- Tab Panes -->
|
||||
<div class="tab-content">
|
||||
|
@ -22,19 +23,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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}}
|
||||
<div class="row">
|
||||
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}" class="form-control"
|
||||
readonly />
|
||||
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}"
|
||||
class="form-control" readonly />
|
||||
</div>
|
||||
<form id="apiResetForm">
|
||||
<button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
|
||||
|
@ -46,26 +40,30 @@
|
|||
<div class="row">
|
||||
<label for="username" class="col-sm-2 control-label form-label">Username:</label>
|
||||
<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>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
|
||||
<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>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
||||
<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>
|
||||
<br />
|
||||
<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">
|
||||
<input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
|
||||
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="row">
|
||||
<h1 class="page-header">
|
||||
Users & Groups
|
||||
{{.Title}}
|
||||
</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>
|
||||
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||
data-user-id="-1" data-target="#modal">
|
||||
<i class="fa fa-plus"></i> New User</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;">
|
||||
<table id="userTable" class="table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th># of Members</th>
|
||||
<th>Modified Date</th>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th class="col-md-2 no-sort"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -43,56 +37,30 @@
|
|||
<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>
|
||||
<h4 class="modal-title" id="groupModalLabel">New User</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" id="modal_body">
|
||||
<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">
|
||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
||||
autofocus />
|
||||
<input type="text" class="form-control" placeholder="Username" id="username" autofocus />
|
||||
</div>
|
||||
<label class="control-label" for="password">Password:</label>
|
||||
<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>
|
||||
<input type="password" class="form-control" placeholder="Password" id="password" required />
|
||||
</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>
|
||||
<label class="control-label" for="confirm_password">Confirm Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
|
||||
required />
|
||||
</div>
|
||||
<label class="control-label" for="role">Role:</label>
|
||||
<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>
|
||||
<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>
|
||||
|
|
19
util/util.go
19
util/util.go
|
@ -21,6 +21,7 @@ import (
|
|||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jordan-wright/email"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -190,3 +191,21 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
log.Info("TLS Certificate Generation complete")
|
||||
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
|
||||
}
|
||||
|
|
21
webpack.config.js
Normal file
21
webpack.config.js
Normal file
|
@ -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 a new issue