Projects STRLCPY gophish Commits 84096b87
🤬
  • 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.
  • Loading...
  • Jordan Wright committed with GitHub 5 years ago
    84096b87
    1 parent faadf0c8
  • ■ ■ ■ ■ ■ ■
    .babelrc
     1 +{
     2 + "presets": ["@babel/preset-env"]
     3 +}
  • ■ ■ ■ ■ ■
    auth/auth.go
    1 1  package auth
    2 2   
    3 3  import (
    4  - "encoding/gob"
    5 4   "errors"
    6  - "fmt"
    7  - "io"
    8 5   "net/http"
    9 6   
    10  - "crypto/rand"
    11  - 
    12 7   ctx "github.com/gophish/gophish/context"
    13  - log "github.com/gophish/gophish/logger"
    14 8   "github.com/gophish/gophish/models"
    15  - "github.com/gorilla/securecookie"
    16  - "github.com/gorilla/sessions"
    17  - "github.com/jinzhu/gorm"
    18 9   "golang.org/x/crypto/bcrypt"
    19 10  )
    20 11   
    21  -//init registers the necessary models to be saved in the session later
    22  -func init() {
    23  - gob.Register(&models.User{})
    24  - gob.Register(&models.Flash{})
    25  - Store.Options.HttpOnly = true
    26  - // This sets the maxAge to 5 days for all cookies
    27  - Store.MaxAge(86400 * 5)
    28  -}
    29  - 
    30  -// Store contains the session information for the request
    31  -var Store = sessions.NewCookieStore(
    32  - []byte(securecookie.GenerateRandomKey(64)), //Signing key
    33  - []byte(securecookie.GenerateRandomKey(32)))
    34  - 
    35 12  // ErrInvalidPassword is thrown when a user provides an incorrect password.
    36 13  var ErrInvalidPassword = errors.New("Invalid Password")
     14 + 
     15 +// ErrPasswordMismatch is thrown when a user provides a blank password to the register
     16 +// or change password functions
     17 +var ErrPasswordMismatch = errors.New("Password cannot be blank")
    37 18   
    38 19  // ErrEmptyPassword is thrown when a user provides a blank password to the register
    39 20  // or change password functions
    40  -var ErrEmptyPassword = errors.New("Password cannot be blank")
    41  - 
    42  -// ErrPasswordMismatch is thrown when a user provides passwords that do not match
    43  -var ErrPasswordMismatch = errors.New("Passwords must match")
    44  - 
    45  -// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
    46  -var ErrUsernameTaken = errors.New("Username already taken")
     21 +var ErrEmptyPassword = errors.New("No password provided")
    47 22   
    48 23  // Login attempts to login the user given a request.
    49 24  func Login(r *http.Request) (bool, models.User, error) {
    skipped 9 lines
    59 34   return false, models.User{}, ErrInvalidPassword
    60 35   }
    61 36   return true, u, nil
    62  -}
    63  - 
    64  -// Register attempts to register the user given a request.
    65  -func Register(r *http.Request) (bool, error) {
    66  - username := r.FormValue("username")
    67  - newPassword := r.FormValue("password")
    68  - confirmPassword := r.FormValue("confirm_password")
    69  - u, err := models.GetUserByUsername(username)
    70  - // If the given username already exists, throw an error and return false
    71  - if err == nil {
    72  - return false, ErrUsernameTaken
    73  - }
    74  - 
    75  - // If we have an error which is not simply indicating that no user was found, report it
    76  - if err != nil && err != gorm.ErrRecordNotFound {
    77  - log.Warn(err)
    78  - return false, err
    79  - }
    80  - 
    81  - u = models.User{}
    82  - // If we've made it here, we should have a valid username given
    83  - // Check that the passsword isn't blank
    84  - if newPassword == "" {
    85  - return false, ErrEmptyPassword
    86  - }
    87  - // Make sure passwords match
    88  - if newPassword != confirmPassword {
    89  - return false, ErrPasswordMismatch
    90  - }
    91  - // Let's create the password hash
    92  - h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
    93  - if err != nil {
    94  - return false, err
    95  - }
    96  - u.Username = username
    97  - u.Hash = string(h)
    98  - u.ApiKey = GenerateSecureKey()
    99  - err = models.PutUser(&u)
    100  - return true, nil
    101  -}
    102  - 
    103  -// GenerateSecureKey creates a secure key to use
    104  -// as an API key
    105  -func GenerateSecureKey() string {
    106  - // Inspired from gorilla/securecookie
    107  - k := make([]byte, 32)
    108  - io.ReadFull(rand.Reader, k)
    109  - return fmt.Sprintf("%x", k)
    110 37  }
    111 38   
    112 39  // ChangePassword verifies the current password provided in the request and,
    skipped 31 lines
  • ■ ■ ■ ■ ■ ■
    controllers/api/api_test.go
    skipped 18 lines
    19 19   apiKey string
    20 20   config *config.Config
    21 21   apiServer *Server
     22 + admin models.User
    22 23  }
    23 24   
    24 25  func (s *APISuite) SetupSuite() {
    skipped 12 lines
    37 38   u, err := models.GetUser(1)
    38 39   s.Nil(err)
    39 40   s.apiKey = u.ApiKey
     41 + s.admin = u
    40 42   // Move our cwd up to the project root for help with resolving
    41 43   // static assets
    42 44   err = os.Chdir("../")
    skipped 5 lines
    48 50   campaigns, _ := models.GetCampaigns(1)
    49 51   for _, campaign := range campaigns {
    50 52   models.DeleteCampaign(campaign.Id)
     53 + }
     54 + // Cleanup all users except the original admin
     55 + users, _ := models.GetUsers()
     56 + for _, user := range users {
     57 + if user.Id == 1 {
     58 + continue
     59 + }
     60 + err := models.DeleteUser(user.Id)
     61 + s.Nil(err)
    51 62   }
    52 63  }
    53 64   
    skipped 70 lines
  • ■ ■ ■ ■ ■ ■
    controllers/api/reset.go
    skipped 2 lines
    3 3  import (
    4 4   "net/http"
    5 5   
    6  - "github.com/gophish/gophish/auth"
    7 6   ctx "github.com/gophish/gophish/context"
    8 7   "github.com/gophish/gophish/models"
     8 + "github.com/gophish/gophish/util"
    9 9  )
    10 10   
    11 11  // Reset (/api/reset) resets the currently authenticated user's API key
    skipped 1 lines
    13 13   switch {
    14 14   case r.Method == "POST":
    15 15   u := ctx.Get(r, "user").(models.User)
    16  - u.ApiKey = auth.GenerateSecureKey()
     16 + u.ApiKey = util.GenerateSecureKey()
    17 17   err := models.PutUser(&u)
    18 18   if err != nil {
    19 19   http.Error(w, "Error setting API Key", http.StatusInternalServerError)
    skipped 6 lines
  • ■ ■ ■ ■ ■ ■
    controllers/api/server.go
    skipped 3 lines
    4 4   "net/http"
    5 5   
    6 6   mid "github.com/gophish/gophish/middleware"
     7 + "github.com/gophish/gophish/models"
    7 8   "github.com/gophish/gophish/worker"
    8 9   "github.com/gorilla/mux"
    9 10  )
    skipped 54 lines
    64 65   router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
    65 66   router.HandleFunc("/smtp/", as.SendingProfiles)
    66 67   router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile)
     68 + router.HandleFunc("/users/", mid.Use(as.Users, mid.RequirePermission(models.PermissionModifySystem)))
     69 + router.HandleFunc("/users/{id:[0-9]+}", mid.Use(as.User))
    67 70   router.HandleFunc("/util/send_test_email", as.SendTestEmail)
    68 71   router.HandleFunc("/import/group", as.ImportGroup)
    69 72   router.HandleFunc("/import/email", as.ImportEmail)
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    controllers/api/user.go
     1 +package api
     2 + 
     3 +import (
     4 + "encoding/json"
     5 + "errors"
     6 + "net/http"
     7 + "strconv"
     8 + 
     9 + ctx "github.com/gophish/gophish/context"
     10 + log "github.com/gophish/gophish/logger"
     11 + "github.com/gophish/gophish/models"
     12 + "github.com/gophish/gophish/util"
     13 + "github.com/gorilla/mux"
     14 + "github.com/jinzhu/gorm"
     15 +)
     16 + 
     17 +// ErrEmptyPassword is thrown when a user provides a blank password to the register
     18 +// or change password functions
     19 +var ErrEmptyPassword = errors.New("No password provided")
     20 + 
     21 +// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
     22 +var ErrUsernameTaken = errors.New("Username already taken")
     23 + 
     24 +// ErrEmptyUsername is thrown when a user attempts to register a username that is taken.
     25 +var ErrEmptyUsername = errors.New("No username provided")
     26 + 
     27 +// ErrEmptyRole is throws when no role is provided when creating or modifying a user.
     28 +var ErrEmptyRole = errors.New("No role specified")
     29 + 
     30 +// ErrInsufficientPermission is thrown when a user attempts to change an
     31 +// attribute (such as the role) for which they don't have permission.
     32 +var ErrInsufficientPermission = errors.New("Permission denied")
     33 + 
     34 +// userRequest is the payload which represents the creation of a new user.
     35 +type userRequest struct {
     36 + Username string `json:"username"`
     37 + Password string `json:"password"`
     38 + Role string `json:"role"`
     39 +}
     40 + 
     41 +func (ur *userRequest) Validate(existingUser *models.User) error {
     42 + switch {
     43 + case ur.Username == "":
     44 + return ErrEmptyUsername
     45 + case ur.Role == "":
     46 + return ErrEmptyRole
     47 + }
     48 + // Verify that the username isn't already taken. We consider two cases:
     49 + // * We're creating a new user, in which case any match is a conflict
     50 + // * We're modifying a user, in which case any match with a different ID is
     51 + // a conflict.
     52 + possibleConflict, err := models.GetUserByUsername(ur.Username)
     53 + if err == nil {
     54 + if existingUser == nil {
     55 + return ErrUsernameTaken
     56 + }
     57 + if possibleConflict.Id != existingUser.Id {
     58 + return ErrUsernameTaken
     59 + }
     60 + }
     61 + // If we have an error which is not simply indicating that no user was found, report it
     62 + if err != nil && err != gorm.ErrRecordNotFound {
     63 + return err
     64 + }
     65 + return nil
     66 +}
     67 + 
     68 +// Users contains functions to retrieve a list of existing users or create a
     69 +// new user. Users with the ModifySystem permissions can view and create users.
     70 +func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
     71 + switch {
     72 + case r.Method == "GET":
     73 + us, err := models.GetUsers()
     74 + if err != nil {
     75 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     76 + return
     77 + }
     78 + JSONResponse(w, us, http.StatusOK)
     79 + return
     80 + case r.Method == "POST":
     81 + ur := &userRequest{}
     82 + err := json.NewDecoder(r.Body).Decode(ur)
     83 + if err != nil {
     84 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
     85 + return
     86 + }
     87 + err = ur.Validate(nil)
     88 + if err != nil {
     89 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
     90 + return
     91 + }
     92 + if ur.Password == "" {
     93 + JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
     94 + return
     95 + }
     96 + hash, err := util.NewHash(ur.Password)
     97 + if err != nil {
     98 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     99 + return
     100 + }
     101 + role, err := models.GetRoleBySlug(ur.Role)
     102 + if err != nil {
     103 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     104 + return
     105 + }
     106 + user := models.User{
     107 + Username: ur.Username,
     108 + Hash: hash,
     109 + ApiKey: util.GenerateSecureKey(),
     110 + Role: role,
     111 + RoleID: role.ID,
     112 + }
     113 + err = models.PutUser(&user)
     114 + if err != nil {
     115 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     116 + return
     117 + }
     118 + JSONResponse(w, user, http.StatusOK)
     119 + return
     120 + }
     121 +}
     122 + 
     123 +// User contains functions to retrieve or delete a single user. Users with
     124 +// the ModifySystem permission can view and modify any user. Otherwise, users
     125 +// may only view or delete their own account.
     126 +func (as *Server) User(w http.ResponseWriter, r *http.Request) {
     127 + vars := mux.Vars(r)
     128 + id, _ := strconv.ParseInt(vars["id"], 0, 64)
     129 + // If the user doesn't have ModifySystem permissions, we need to verify
     130 + // that they're only taking action on their account.
     131 + currentUser := ctx.Get(r, "user").(models.User)
     132 + hasSystem, err := currentUser.HasPermission(models.PermissionModifySystem)
     133 + if err != nil {
     134 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     135 + return
     136 + }
     137 + if !hasSystem && currentUser.Id != id {
     138 + JSONResponse(w, models.Response{Success: false, Message: http.StatusText(http.StatusForbidden)}, http.StatusForbidden)
     139 + return
     140 + }
     141 + existingUser, err := models.GetUser(id)
     142 + if err != nil {
     143 + JSONResponse(w, models.Response{Success: false, Message: "User not found"}, http.StatusNotFound)
     144 + return
     145 + }
     146 + switch {
     147 + case r.Method == "GET":
     148 + JSONResponse(w, existingUser, http.StatusOK)
     149 + case r.Method == "DELETE":
     150 + err = models.DeleteUser(id)
     151 + if err != nil {
     152 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     153 + return
     154 + }
     155 + log.Infof("Deleted user account for %s", existingUser.Username)
     156 + JSONResponse(w, models.Response{Success: true, Message: "User deleted Successfully!"}, http.StatusOK)
     157 + case r.Method == "PUT":
     158 + ur := &userRequest{}
     159 + err = json.NewDecoder(r.Body).Decode(ur)
     160 + if err != nil {
     161 + log.Errorf("error decoding user request: %v", err)
     162 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
     163 + return
     164 + }
     165 + err = ur.Validate(&existingUser)
     166 + if err != nil {
     167 + log.Errorf("invalid user request received: %v", err)
     168 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
     169 + return
     170 + }
     171 + existingUser.Username = ur.Username
     172 + // Only users with the ModifySystem permission are able to update a
     173 + // user's role. This prevents a privilege escalation letting users
     174 + // upgrade their own account.
     175 + if !hasSystem && ur.Role != existingUser.Role.Slug {
     176 + JSONResponse(w, models.Response{Success: false, Message: ErrInsufficientPermission.Error()}, http.StatusBadRequest)
     177 + return
     178 + }
     179 + role, err := models.GetRoleBySlug(ur.Role)
     180 + if err != nil {
     181 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     182 + return
     183 + }
     184 + // If our user is trying to change the role of an admin, we need to
     185 + // ensure that it isn't the last user account with the Admin role.
     186 + if existingUser.Role.Slug == models.RoleAdmin && existingUser.Role.ID != role.ID {
     187 + err = models.EnsureEnoughAdmins()
     188 + if err != nil {
     189 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     190 + return
     191 + }
     192 + }
     193 + existingUser.Role = role
     194 + existingUser.RoleID = role.ID
     195 + // We don't force the password to be provided, since it may be an admin
     196 + // managing the user's account, and making a simple change like
     197 + // updating the username or role. However, if it _is_ provided, we'll
     198 + // update the stored hash.
     199 + //
     200 + // Note that we don't force the current password to be provided. The
     201 + // assumption here is that the API key is a proper bearer token proving
     202 + // authenticated access to the account.
     203 + if ur.Password != "" {
     204 + hash, err := util.NewHash(ur.Password)
     205 + if err != nil {
     206 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     207 + return
     208 + }
     209 + existingUser.Hash = hash
     210 + }
     211 + err = models.PutUser(&existingUser)
     212 + if err != nil {
     213 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     214 + return
     215 + }
     216 + JSONResponse(w, existingUser, http.StatusOK)
     217 + }
     218 +}
     219 + 
  • ■ ■ ■ ■ ■ ■
    controllers/api/user_test.go
     1 +package api
     2 + 
     3 +import (
     4 + "bytes"
     5 + "encoding/json"
     6 + "fmt"
     7 + "net/http"
     8 + "net/http/httptest"
     9 + 
     10 + "golang.org/x/crypto/bcrypt"
     11 + 
     12 + ctx "github.com/gophish/gophish/context"
     13 + "github.com/gophish/gophish/models"
     14 +)
     15 + 
     16 +func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
     17 + role, err := models.GetRoleBySlug(slug)
     18 + s.Nil(err)
     19 + unauthorizedUser := &models.User{
     20 + Username: "foo",
     21 + Hash: "bar",
     22 + ApiKey: "12345",
     23 + Role: role,
     24 + RoleID: role.ID,
     25 + }
     26 + err = models.PutUser(unauthorizedUser)
     27 + s.Nil(err)
     28 + return unauthorizedUser
     29 +}
     30 + 
     31 +func (s *APISuite) TestGetUsers() {
     32 + r := httptest.NewRequest(http.MethodGet, "/api/users", nil)
     33 + r = ctx.Set(r, "user", s.admin)
     34 + w := httptest.NewRecorder()
     35 + 
     36 + s.apiServer.Users(w, r)
     37 + s.Equal(w.Code, http.StatusOK)
     38 + 
     39 + got := []models.User{}
     40 + err := json.NewDecoder(w.Body).Decode(&got)
     41 + s.Nil(err)
     42 + 
     43 + // We only expect one user
     44 + s.Equal(1, len(got))
     45 + // And it should be the admin user
     46 + s.Equal(s.admin.Id, got[0].Id)
     47 +}
     48 + 
     49 +func (s *APISuite) TestCreateUser() {
     50 + payload := &userRequest{
     51 + Username: "foo",
     52 + Password: "bar",
     53 + Role: models.RoleUser,
     54 + }
     55 + body, err := json.Marshal(payload)
     56 + s.Nil(err)
     57 + 
     58 + r := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(body))
     59 + r.Header.Set("Content-Type", "application/json")
     60 + r = ctx.Set(r, "user", s.admin)
     61 + w := httptest.NewRecorder()
     62 + 
     63 + s.apiServer.Users(w, r)
     64 + s.Equal(w.Code, http.StatusOK)
     65 + 
     66 + got := &models.User{}
     67 + err = json.NewDecoder(w.Body).Decode(got)
     68 + s.Nil(err)
     69 + s.Equal(got.Username, payload.Username)
     70 + s.Equal(got.Role.Slug, payload.Role)
     71 +}
     72 + 
     73 +// TestModifyUser tests that a user with the appropriate access is able to
     74 +// modify their username and password.
     75 +func (s *APISuite) TestModifyUser() {
     76 + unpriviledgedUser := s.createUnpriviledgedUser(models.RoleUser)
     77 + newPassword := "new-password"
     78 + newUsername := "new-username"
     79 + payload := userRequest{
     80 + Username: newUsername,
     81 + Password: newPassword,
     82 + Role: unpriviledgedUser.Role.Slug,
     83 + }
     84 + body, err := json.Marshal(payload)
     85 + s.Nil(err)
     86 + url := fmt.Sprintf("/api/users/%d", unpriviledgedUser.Id)
     87 + r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
     88 + r.Header.Set("Content-Type", "application/json")
     89 + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unpriviledgedUser.ApiKey))
     90 + w := httptest.NewRecorder()
     91 + 
     92 + s.apiServer.ServeHTTP(w, r)
     93 + response := &models.User{}
     94 + err = json.NewDecoder(w.Body).Decode(response)
     95 + s.Nil(err)
     96 + s.Equal(w.Code, http.StatusOK)
     97 + s.Equal(response.Username, newUsername)
     98 + got, err := models.GetUser(unpriviledgedUser.Id)
     99 + s.Nil(err)
     100 + s.Equal(response.Username, got.Username)
     101 + s.Equal(newUsername, got.Username)
     102 + err = bcrypt.CompareHashAndPassword([]byte(got.Hash), []byte(newPassword))
     103 + s.Nil(err)
     104 +}
     105 + 
     106 +// TestUnauthorizedListUsers ensures that users without the ModifySystem
     107 +// permission are unable to list the users registered in Gophish.
     108 +func (s *APISuite) TestUnauthorizedListUsers() {
     109 + // First, let's create a standard user which doesn't
     110 + // have ModifySystem permissions.
     111 + unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
     112 + // We'll try to make a request to the various users API endpoints to
     113 + // ensure that they fail. Previously, we could hit the handlers directly
     114 + // but we need to go through the router for this test to ensure the
     115 + // middleware gets applied.
     116 + r := httptest.NewRequest(http.MethodGet, "/api/users/", nil)
     117 + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
     118 + w := httptest.NewRecorder()
     119 + 
     120 + s.apiServer.ServeHTTP(w, r)
     121 + s.Equal(w.Code, http.StatusForbidden)
     122 +}
     123 + 
     124 +// TestUnauthorizedModifyUsers verifies that users without ModifySystem
     125 +// permission (a "standard" user) can only get or modify their own information.
     126 +func (s *APISuite) TestUnauthorizedGetUser() {
     127 + // First, we'll make sure that a user with the "user" role is unable to
     128 + // get the information of another user (in this case, the main admin).
     129 + unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
     130 + url := fmt.Sprintf("/api/users/%d", s.admin.Id)
     131 + r := httptest.NewRequest(http.MethodGet, url, nil)
     132 + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
     133 + w := httptest.NewRecorder()
     134 + 
     135 + s.apiServer.ServeHTTP(w, r)
     136 + s.Equal(w.Code, http.StatusForbidden)
     137 +}
     138 + 
     139 +// TestUnauthorizedModifyRole ensures that users without the ModifySystem
     140 +// privilege are unable to modify their own role, preventing a potential
     141 +// privilege escalation issue.
     142 +func (s *APISuite) TestUnauthorizedSetRole() {
     143 + unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
     144 + url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
     145 + payload := &userRequest{
     146 + Username: unauthorizedUser.Username,
     147 + Role: models.RoleAdmin,
     148 + }
     149 + body, err := json.Marshal(payload)
     150 + s.Nil(err)
     151 + r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
     152 + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
     153 + w := httptest.NewRecorder()
     154 + 
     155 + s.apiServer.ServeHTTP(w, r)
     156 + s.Equal(w.Code, http.StatusBadRequest)
     157 + response := &models.Response{}
     158 + err = json.NewDecoder(w.Body).Decode(response)
     159 + s.Nil(err)
     160 + s.Equal(response.Message, ErrInsufficientPermission.Error())
     161 +}
     162 + 
     163 +// TestModifyWithExistingUsername verifies that it's not possible to modify
     164 +// an user's username to one which already exists.
     165 +func (s *APISuite) TestModifyWithExistingUsername() {
     166 + unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
     167 + payload := &userRequest{
     168 + Username: s.admin.Username,
     169 + Role: unauthorizedUser.Role.Slug,
     170 + }
     171 + body, err := json.Marshal(payload)
     172 + s.Nil(err)
     173 + url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
     174 + r := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(body))
     175 + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
     176 + w := httptest.NewRecorder()
     177 + 
     178 + s.apiServer.ServeHTTP(w, r)
     179 + s.Equal(w.Code, http.StatusBadRequest)
     180 + expected := &models.Response{
     181 + Message: ErrUsernameTaken.Error(),
     182 + Success: false,
     183 + }
     184 + got := &models.Response{}
     185 + err = json.NewDecoder(w.Body).Decode(got)
     186 + s.Nil(err)
     187 + s.Equal(got.Message, expected.Message)
     188 +}
     189 + 
  • ■ ■ ■ ■ ■ ■
    controllers/route.go
    skipped 94 lines
    95 95  func (as *AdminServer) registerRoutes() {
    96 96   router := mux.NewRouter()
    97 97   // Base Front-end routes
    98  - router.HandleFunc("/", Use(as.Base, mid.RequireLogin))
     98 + router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
    99 99   router.HandleFunc("/login", as.Login)
    100  - router.HandleFunc("/logout", Use(as.Logout, mid.RequireLogin))
    101  - router.HandleFunc("/campaigns", Use(as.Campaigns, mid.RequireLogin))
    102  - router.HandleFunc("/campaigns/{id:[0-9]+}", Use(as.CampaignID, mid.RequireLogin))
    103  - router.HandleFunc("/templates", Use(as.Templates, mid.RequireLogin))
    104  - router.HandleFunc("/users", Use(as.Users, mid.RequireLogin))
    105  - router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin))
    106  - router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin))
    107  - router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
    108  - router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem)))
     100 + router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
     101 + router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
     102 + router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
     103 + router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
     104 + router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin))
     105 + router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin))
     106 + router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
     107 + router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
     108 + router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
    109 109   // Create the API routes
    110 110   api := api.NewServer(api.WithWorker(as.worker))
    111 111   router.PathPrefix("/api/").Handler(api)
    skipped 2 lines
    114 114   router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
    115 115   
    116 116   // Setup CSRF Protection
    117  - csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()),
     117 + csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
    118 118   csrf.FieldName("csrf_token"),
    119 119   csrf.Secure(as.config.UseTLS))
    120 120   adminHandler := csrfHandler(router)
    121  - adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
     121 + adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
    122 122   
    123 123   // Setup GZIP compression
    124 124   gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
    skipped 4 lines
    129 129   as.server.Handler = adminHandler
    130 130  }
    131 131   
    132  -// Use allows us to stack middleware to process the request
    133  -// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
    134  -func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
    135  - for _, m := range mid {
    136  - handler = m(handler)
    137  - }
    138  - return handler
    139  -}
    140  - 
    141 132  type templateParams struct {
    142 133   Title string
    143 134   Flashes []interface{}
    skipped 16 lines
    160 151   }
    161 152  }
    162 153   
    163  -// Register creates a new user
    164  -func (as *AdminServer) Register(w http.ResponseWriter, r *http.Request) {
    165  - // If it is a post request, attempt to register the account
    166  - // Now that we are all registered, we can log the user in
    167  - params := templateParams{Title: "Register", Token: csrf.Token(r)}
    168  - session := ctx.Get(r, "session").(*sessions.Session)
    169  - switch {
    170  - case r.Method == "GET":
    171  - params.Flashes = session.Flashes()
    172  - session.Save(r, w)
    173  - templates := template.New("template")
    174  - _, err := templates.ParseFiles("templates/register.html", "templates/flashes.html")
    175  - if err != nil {
    176  - log.Error(err)
    177  - }
    178  - template.Must(templates, err).ExecuteTemplate(w, "base", params)
    179  - case r.Method == "POST":
    180  - //Attempt to register
    181  - succ, err := auth.Register(r)
    182  - //If we've registered, redirect to the login page
    183  - if succ {
    184  - Flash(w, r, "success", "Registration successful!")
    185  - session.Save(r, w)
    186  - http.Redirect(w, r, "/login", 302)
    187  - return
    188  - }
    189  - // Check the error
    190  - m := err.Error()
    191  - log.Error(err)
    192  - Flash(w, r, "danger", m)
    193  - session.Save(r, w)
    194  - http.Redirect(w, r, "/register", 302)
    195  - return
    196  - }
    197  -}
    198  - 
    199 154  // Base handles the default path and template execution
    200 155  func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
    201 156   params := newTemplateParams(r)
    skipped 22 lines
    224 179   getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
    225 180  }
    226 181   
    227  -// Users handles the default path and template execution
    228  -func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) {
     182 +// Groups handles the default path and template execution
     183 +func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) {
    229 184   params := newTemplateParams(r)
    230 185   params.Title = "Users & Groups"
    231  - getTemplate(w, "users").ExecuteTemplate(w, "base", params)
     186 + getTemplate(w, "groups").ExecuteTemplate(w, "base", params)
    232 187  }
    233 188   
    234 189  // LandingPages handles the default path and template execution
    skipped 34 lines
    269 224   }
    270 225   api.JSONResponse(w, msg, http.StatusOK)
    271 226   }
     227 +}
     228 + 
     229 +// UserManagement is an admin-only handler that allows for the registration
     230 +// and management of user accounts within Gophish.
     231 +func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
     232 + params := newTemplateParams(r)
     233 + params.Title = "User Management"
     234 + getTemplate(w, "users").ExecuteTemplate(w, "base", params)
    272 235  }
    273 236   
    274 237  // Login handles the authentication flow for a user. If credentials are valid,
    skipped 81 lines
  • ■ ■ ■ ■ ■ ■
    gophish.go
    skipped 31 lines
    32 32   
    33 33   "gopkg.in/alecthomas/kingpin.v2"
    34 34   
    35  - "github.com/gophish/gophish/auth"
    36 35   "github.com/gophish/gophish/config"
    37 36   "github.com/gophish/gophish/controllers"
    38 37   log "github.com/gophish/gophish/logger"
     38 + "github.com/gophish/gophish/middleware"
    39 39   "github.com/gophish/gophish/models"
    40 40  )
    41 41   
    skipped 52 lines
    94 94   }
    95 95   adminConfig := conf.AdminConf
    96 96   adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
    97  - auth.Store.Options.Secure = adminConfig.UseTLS
     97 + middleware.Store.Options.Secure = adminConfig.UseTLS
    98 98   
    99 99   phishConfig := conf.PhishConf
    100 100   phishServer := controllers.NewPhishingServer(phishConfig)
    skipped 13 lines
  • ■ ■ ■ ■ ■
    gulpfile.js
    skipped 8 lines
    9 9   concat = require('gulp-concat'),
    10 10   uglify = require('gulp-uglify'),
    11 11   cleanCSS = require('gulp-clean-css'),
     12 + babel = require('gulp-babel'),
    12 13   
    13 14   js_directory = 'static/js/src/',
    14 15   css_directory = 'static/css/',
    15 16   vendor_directory = js_directory + 'vendor/',
    16  - app_directory = js_directory + 'app/**/*.js',
     17 + app_directory = js_directory + 'app/',
    17 18   dest_js_directory = 'static/js/dist/',
    18 19   dest_css_directory = 'static/css/dist/';
    19 20   
    skipped 28 lines
    48 49  }
    49 50   
    50 51  scripts = function () {
    51  - // Gophish app files
    52  - return gulp.src(app_directory)
     52 + // Gophish app files - non-ES6
     53 + return gulp.src([
     54 + app_directory + 'autocomplete.js',
     55 + app_directory + 'campaign_results.js',
     56 + app_directory + 'campaigns.js',
     57 + app_directory + 'dashboard.js',
     58 + app_directory + 'groups.js',
     59 + app_directory + 'landing_pages.js',
     60 + app_directory + 'sending_profiles.js',
     61 + app_directory + 'settings.js',
     62 + app_directory + 'templates.js',
     63 + app_directory + 'gophish.js',
     64 + ])
    53 65   .pipe(rename({
    54 66   suffix: '.min'
    55 67   }))
    skipped 33 lines
  • ■ ■ ■ ■ ■ ■
    middleware/middleware.go
    skipped 5 lines
    6 6   "net/http"
    7 7   "strings"
    8 8   
    9  - "github.com/gophish/gophish/auth"
    10 9   ctx "github.com/gophish/gophish/context"
    11 10   "github.com/gophish/gophish/models"
    12 11   "github.com/gorilla/csrf"
    skipped 18 lines
    31 30   }
    32 31  }
    33 32   
     33 +// Use allows us to stack middleware to process the request
     34 +// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
     35 +func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
     36 + for _, m := range mid {
     37 + handler = m(handler)
     38 + }
     39 + return handler
     40 +}
     41 + 
    34 42  // GetContext wraps each request in a function which fills in the context for a given request.
    35 43  // This includes setting the User and Session keys and values as necessary for use in later functions.
    36 44  func GetContext(handler http.Handler) http.HandlerFunc {
    skipped 6 lines
    43 51   }
    44 52   // Set the context appropriately here.
    45 53   // Set the session
    46  - session, _ := auth.Store.Get(r, "gophish")
     54 + session, _ := Store.Get(r, "gophish")
    47 55   // Put the session in the context so that we can
    48 56   // reuse the values in different handlers
    49 57   r = ctx.Set(r, "session", session)
    skipped 57 lines
    107 115   return func(w http.ResponseWriter, r *http.Request) {
    108 116   if u := ctx.Get(r, "user"); u != nil {
    109 117   handler.ServeHTTP(w, r)
    110  - } else {
    111  - q := r.URL.Query()
    112  - q.Set("next", r.URL.Path)
    113  - http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
     118 + return
    114 119   }
     120 + q := r.URL.Query()
     121 + q.Set("next", r.URL.Path)
     122 + http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
     123 + return
    115 124   }
    116 125  }
    117 126   
    skipped 53 lines
  • ■ ■ ■ ■ ■ ■
    middleware/session.go
     1 +package middleware
     2 + 
     3 +import (
     4 + "encoding/gob"
     5 + 
     6 + "github.com/gophish/gophish/models"
     7 + "github.com/gorilla/securecookie"
     8 + "github.com/gorilla/sessions"
     9 +)
     10 + 
     11 +// init registers the necessary models to be saved in the session later
     12 +func init() {
     13 + gob.Register(&models.User{})
     14 + gob.Register(&models.Flash{})
     15 + Store.Options.HttpOnly = true
     16 + // This sets the maxAge to 5 days for all cookies
     17 + Store.MaxAge(86400 * 5)
     18 +}
     19 + 
     20 +// Store contains the session information for the request
     21 +var Store = sessions.NewCookieStore(
     22 + []byte(securecookie.GenerateRandomKey(64)), //Signing key
     23 + []byte(securecookie.GenerateRandomKey(32)))
     24 + 
  • ■ ■ ■ ■
    models/rbac.go
    skipped 47 lines
    48 48  // Role represents a user role within Gophish. Each user has a single role
    49 49  // which maps to a set of permissions.
    50 50  type Role struct {
    51  - ID int64 `json:"id"`
     51 + ID int64 `json:"-"`
    52 52   Slug string `json:"slug"`
    53 53   Name string `json:"name"`
    54 54   Description string `json:"description"`
    skipped 35 lines
  • ■ ■ ■ ■ ■ ■
    models/user.go
    1 1  package models
    2 2   
     3 +import (
     4 + "errors"
     5 + 
     6 + log "github.com/gophish/gophish/logger"
     7 +)
     8 + 
     9 +// ErrModifyingOnlyAdmin occurs when there is an attempt to modify the only
     10 +// user account with the Admin role in such a way that there will be no user
     11 +// accounts left in Gophish with that role.
     12 +var ErrModifyingOnlyAdmin = errors.New("Cannot remove the only administrator")
     13 + 
    3 14  // User represents the user model for gophish.
    4 15  type User struct {
    5 16   Id int64 `json:"id"`
    6 17   Username string `json:"username" sql:"not null;unique"`
    7 18   Hash string `json:"-"`
    8  - ApiKey string `json:"api_key" sql:"not null;unique"`
     19 + ApiKey string `json:"-" sql:"not null;unique"`
    9 20   Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
    10 21   RoleID int64 `json:"-"`
    11 22  }
    skipped 6 lines
    18 29   return u, err
    19 30  }
    20 31   
     32 +// GetUsers returns the users registered in Gophish
     33 +func GetUsers() ([]User, error) {
     34 + us := []User{}
     35 + err := db.Preload("Role").Find(&us).Error
     36 + return us, err
     37 +}
     38 + 
    21 39  // GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an
    22 40  // error is thrown.
    23 41  func GetUserByAPIKey(key string) (User, error) {
    skipped 16 lines
    40 58   return err
    41 59  }
    42 60   
     61 +// EnsureEnoughAdmins ensures that there is more than one user account in
     62 +// Gophish with the Admin role. This function is meant to be called before
     63 +// modifying a user account with the Admin role in a non-revokable way.
     64 +func EnsureEnoughAdmins() error {
     65 + role, err := GetRoleBySlug(RoleAdmin)
     66 + if err != nil {
     67 + return err
     68 + }
     69 + var adminCount int
     70 + err = db.Model(&User{}).Where("role_id=?", role.ID).Count(&adminCount).Error
     71 + if err != nil {
     72 + return err
     73 + }
     74 + if adminCount == 1 {
     75 + return ErrModifyingOnlyAdmin
     76 + }
     77 + return nil
     78 +}
     79 + 
     80 +// DeleteUser deletes the given user. To ensure that there is always at least
     81 +// one user account with the Admin role, this function will refuse to delete
     82 +// the last Admin.
     83 +func DeleteUser(id int64) error {
     84 + existing, err := GetUser(id)
     85 + if err != nil {
     86 + return err
     87 + }
     88 + // If the user is an admin, we need to verify that it's not the last one.
     89 + if existing.Role.Slug == RoleAdmin {
     90 + err = EnsureEnoughAdmins()
     91 + if err != nil {
     92 + return err
     93 + }
     94 + }
     95 + campaigns, err := GetCampaigns(id)
     96 + if err != nil {
     97 + return err
     98 + }
     99 + // Delete the campaigns
     100 + log.Infof("Deleting campaigns for user ID %d", id)
     101 + for _, campaign := range campaigns {
     102 + err = DeleteCampaign(campaign.Id)
     103 + if err != nil {
     104 + return err
     105 + }
     106 + }
     107 + log.Infof("Deleting pages for user ID %d", id)
     108 + // Delete the landing pages
     109 + pages, err := GetPages(id)
     110 + if err != nil {
     111 + return err
     112 + }
     113 + for _, page := range pages {
     114 + err = DeletePage(page.Id, id)
     115 + if err != nil {
     116 + return err
     117 + }
     118 + }
     119 + // Delete the templates
     120 + log.Infof("Deleting templates for user ID %d", id)
     121 + templates, err := GetTemplates(id)
     122 + if err != nil {
     123 + return err
     124 + }
     125 + for _, template := range templates {
     126 + err = DeleteTemplate(template.Id, id)
     127 + if err != nil {
     128 + return err
     129 + }
     130 + }
     131 + // Delete the groups
     132 + log.Infof("Deleting groups for user ID %d", id)
     133 + groups, err := GetGroups(id)
     134 + if err != nil {
     135 + return err
     136 + }
     137 + for _, group := range groups {
     138 + err = DeleteGroup(&group)
     139 + if err != nil {
     140 + return err
     141 + }
     142 + }
     143 + // Delete the sending profiles
     144 + log.Infof("Deleting sending profiles for user ID %d", id)
     145 + profiles, err := GetSMTPs(id)
     146 + if err != nil {
     147 + return err
     148 + }
     149 + for _, profile := range profiles {
     150 + err = DeleteSMTP(profile.Id, id)
     151 + if err != nil {
     152 + return err
     153 + }
     154 + }
     155 + // Finally, delete the user
     156 + err = db.Where("id=?", id).Delete(&User{}).Error
     157 + return err
     158 +}
     159 + 
  • ■ ■ ■ ■ ■ ■
    models/user_test.go
    skipped 59 lines
    60 60   c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012")
    61 61  }
    62 62   
     63 +func (s *ModelsSuite) verifyRoleCount(c *check.C, roleID, expected int64) {
     64 + var adminCount int64
     65 + err := db.Model(&User{}).Where("role_id=?", roleID).Count(&adminCount).Error
     66 + c.Assert(err, check.Equals, nil)
     67 + c.Assert(adminCount, check.Equals, expected)
     68 +}
     69 + 
     70 +func (s *ModelsSuite) TestDeleteLastAdmin(c *check.C) {
     71 + // Create a new admin user
     72 + role, err := GetRoleBySlug(RoleAdmin)
     73 + c.Assert(err, check.Equals, nil)
     74 + newAdmin := User{
     75 + Username: "new-admin",
     76 + Hash: "123456",
     77 + ApiKey: "123456",
     78 + Role: role,
     79 + RoleID: role.ID,
     80 + }
     81 + err = PutUser(&newAdmin)
     82 + c.Assert(err, check.Equals, nil)
     83 + 
     84 + // Ensure that there are two admins
     85 + s.verifyRoleCount(c, role.ID, 2)
     86 + 
     87 + // Delete the newly created admin - this should work since we have more
     88 + // than one current admin.
     89 + err = DeleteUser(newAdmin.Id)
     90 + c.Assert(err, check.Equals, nil)
     91 + 
     92 + // Verify that we now have one admin
     93 + s.verifyRoleCount(c, role.ID, 1)
     94 + 
     95 + // Try to delete the last admin - this should fail since we always want at
     96 + // least one admin active in Gophish.
     97 + err = DeleteUser(1)
     98 + c.Assert(err, check.Equals, ErrModifyingOnlyAdmin)
     99 + 
     100 + // Verify that the admin wasn't deleted
     101 + s.verifyRoleCount(c, role.ID, 1)
     102 +}
     103 + 
  • ■ ■ ■ ■ ■
    package.json
    skipped 11 lines
    12 12   },
    13 13   "homepage": "https://getgophish.com",
    14 14   "devDependencies": {
     15 + "@babel/core": "^7.4.5",
     16 + "@babel/preset-env": "^7.4.5",
     17 + "babel-loader": "^8.0.6",
    15 18   "clean-css": "^4.2.1",
    16 19   "gulp": "^4.0.0",
     20 + "gulp-babel": "^8.0.0",
    17 21   "gulp-clean-css": "^4.0.0",
    18 22   "gulp-cli": "^2.2.0",
    19 23   "gulp-concat": "^2.6.1",
    skipped 2 lines
    22 26   "gulp-uglify": "^3.0.2",
    23 27   "gulp-wrap": "^0.15.0",
    24 28   "jshint": "^2.10.2",
    25  - "jshint-stylish": "^2.2.1"
     29 + "jshint-stylish": "^2.2.1",
     30 + "webpack": "^4.32.2",
     31 + "webpack-cli": "^3.3.2"
    26 32   }
    27 33  }
    28 34   
  • static/css/dist/gophish.css
    Unable to diff as some line is too long.
  • ■ ■ ■ ■ ■ ■
    static/css/main.css
    skipped 701 lines
    702 702   background-color: #37485a;
    703 703  }
    704 704   
     705 +.nav-badge {
     706 + margin-top: 5px;
     707 +}
     708 + 
    705 709  #resultsMapContainer {
    706 710   display: none;
    707 711  }
    skipped 17 lines
  • ■ ■ ■ ■
    static/js/dist/app/gophish.min.js
    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()});
     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,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()});
  • ■ ■ ■ ■ ■
    static/js/dist/app/groups.min.js
     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:"[email protected]",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)});
  • ■ ■ ■ ■
    static/js/dist/app/users.min.js
    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:"[email protected]",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download",e),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(s){var e=groups.find(function(e){return e.id===s});e&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(s).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});
     1 +!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()})})})})}]);
  • ■ ■ ■ ■ ■ ■
    static/js/src/app/gophish.js
    skipped 31 lines
    32 32  function escapeHtml(text) {
    33 33   return $("<div/>").text(text).html()
    34 34  }
     35 +window.escapeHtml = escapeHtml
    35 36   
    36 37  function unescapeHtml(html) {
    37 38   return $("<div/>").html(html).text()
    skipped 158 lines
    196 197   return query("/smtp/" + id, "DELETE", {}, false)
    197 198   }
    198 199   },
     200 + // users contains the endpoints for /users
     201 + users: {
     202 + // get() - Queries the API for GET /users
     203 + get: function () {
     204 + return query("/users/", "GET", {}, true)
     205 + },
     206 + // post() - Posts a user to POST /users
     207 + post: function (user) {
     208 + return query("/users/", "POST", user, true)
     209 + }
     210 + },
     211 + // userId contains the endpoints for /users/:id
     212 + userId: {
     213 + // get() - Queries the API for GET /users/:id
     214 + get: function (id) {
     215 + return query("/users/" + id, "GET", {}, true)
     216 + },
     217 + // put() - Puts a user to PUT /users/:id
     218 + put: function (user) {
     219 + return query("/users/" + user.id, "PUT", user, true)
     220 + },
     221 + // delete() - Deletes a user at DELETE /users/:id
     222 + delete: function (id) {
     223 + return query("/users/" + id, "DELETE", {}, true)
     224 + }
     225 + },
    199 226   // import handles all of the "import" functions in the api
    200 227   import_email: function (req) {
    201 228   return query("/import/email", "POST", req, false)
    skipped 10 lines
    212 239   return query("/reset", "POST", {}, true)
    213 240   }
    214 241  }
     242 +window.api = api
    215 243   
    216 244  // Register our moment.js datatables listeners
    217 245  $(document).ready(function () {
    skipped 13 lines
  • ■ ■ ■ ■ ■ ■
    static/js/src/app/groups.js
     1 +var groups = []
     2 + 
     3 +// Save attempts to POST or PUT to /groups/
     4 +function save(id) {
     5 + var targets = []
     6 + $.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
     7 + targets.push({
     8 + first_name: unescapeHtml(target[0]),
     9 + last_name: unescapeHtml(target[1]),
     10 + email: unescapeHtml(target[2]),
     11 + position: unescapeHtml(target[3])
     12 + })
     13 + })
     14 + var group = {
     15 + name: $("#name").val(),
     16 + targets: targets
     17 + }
     18 + // Submit the group
     19 + if (id != -1) {
     20 + // If we're just editing an existing group,
     21 + // we need to PUT /groups/:id
     22 + group.id = id
     23 + api.groupId.put(group)
     24 + .success(function (data) {
     25 + successFlash("Group updated successfully!")
     26 + load()
     27 + dismiss()
     28 + $("#modal").modal('hide')
     29 + })
     30 + .error(function (data) {
     31 + modalError(data.responseJSON.message)
     32 + })
     33 + } else {
     34 + // Else, if this is a new group, POST it
     35 + // to /groups
     36 + api.groups.post(group)
     37 + .success(function (data) {
     38 + successFlash("Group added successfully!")
     39 + load()
     40 + dismiss()
     41 + $("#modal").modal('hide')
     42 + })
     43 + .error(function (data) {
     44 + modalError(data.responseJSON.message)
     45 + })
     46 + }
     47 +}
     48 + 
     49 +function dismiss() {
     50 + $("#targetsTable").dataTable().DataTable().clear().draw()
     51 + $("#name").val("")
     52 + $("#modal\\.flashes").empty()
     53 +}
     54 + 
     55 +function edit(id) {
     56 + targets = $("#targetsTable").dataTable({
     57 + destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
     58 + columnDefs: [{
     59 + orderable: false,
     60 + targets: "no-sort"
     61 + }]
     62 + })
     63 + $("#modalSubmit").unbind('click').click(function () {
     64 + save(id)
     65 + })
     66 + if (id == -1) {
     67 + var group = {}
     68 + } else {
     69 + api.groupId.get(id)
     70 + .success(function (group) {
     71 + $("#name").val(group.name)
     72 + $.each(group.targets, function (i, record) {
     73 + targets.DataTable()
     74 + .row.add([
     75 + escapeHtml(record.first_name),
     76 + escapeHtml(record.last_name),
     77 + escapeHtml(record.email),
     78 + escapeHtml(record.position),
     79 + '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
     80 + ]).draw()
     81 + });
     82 + 
     83 + })
     84 + .error(function () {
     85 + errorFlash("Error fetching group")
     86 + })
     87 + }
     88 + // Handle file uploads
     89 + $("#csvupload").fileupload({
     90 + url: "/api/import/group",
     91 + dataType: "json",
     92 + beforeSend: function (xhr) {
     93 + xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
     94 + },
     95 + add: function (e, data) {
     96 + $("#modal\\.flashes").empty()
     97 + var acceptFileTypes = /(csv|txt)$/i;
     98 + var filename = data.originalFiles[0]['name']
     99 + if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
     100 + modalError("Unsupported file extension (use .csv or .txt)")
     101 + return false;
     102 + }
     103 + data.submit();
     104 + },
     105 + done: function (e, data) {
     106 + $.each(data.result, function (i, record) {
     107 + addTarget(
     108 + record.first_name,
     109 + record.last_name,
     110 + record.email,
     111 + record.position);
     112 + });
     113 + targets.DataTable().draw();
     114 + }
     115 + })
     116 +}
     117 + 
     118 +var downloadCSVTemplate = function () {
     119 + var csvScope = [{
     120 + 'First Name': 'Example',
     121 + 'Last Name': 'User',
     122 + 'Email': '[email protected]',
     123 + 'Position': 'Systems Administrator'
     124 + }]
     125 + var filename = 'group_template.csv'
     126 + var csvString = Papa.unparse(csvScope, {})
     127 + var csvData = new Blob([csvString], {
     128 + type: 'text/csv;charset=utf-8;'
     129 + });
     130 + if (navigator.msSaveBlob) {
     131 + navigator.msSaveBlob(csvData, filename);
     132 + } else {
     133 + var csvURL = window.URL.createObjectURL(csvData);
     134 + var dlLink = document.createElement('a');
     135 + dlLink.href = csvURL;
     136 + dlLink.setAttribute('download', filename)
     137 + document.body.appendChild(dlLink)
     138 + dlLink.click();
     139 + document.body.removeChild(dlLink)
     140 + }
     141 +}
     142 + 
     143 + 
     144 +var deleteGroup = function (id) {
     145 + var group = groups.find(function (x) {
     146 + return x.id === id
     147 + })
     148 + if (!group) {
     149 + return
     150 + }
     151 + swal({
     152 + title: "Are you sure?",
     153 + text: "This will delete the group. This can't be undone!",
     154 + type: "warning",
     155 + animation: false,
     156 + showCancelButton: true,
     157 + confirmButtonText: "Delete " + escapeHtml(group.name),
     158 + confirmButtonColor: "#428bca",
     159 + reverseButtons: true,
     160 + allowOutsideClick: false,
     161 + preConfirm: function () {
     162 + return new Promise(function (resolve, reject) {
     163 + api.groupId.delete(id)
     164 + .success(function (msg) {
     165 + resolve()
     166 + })
     167 + .error(function (data) {
     168 + reject(data.responseJSON.message)
     169 + })
     170 + })
     171 + }
     172 + }).then(function () {
     173 + swal(
     174 + 'Group Deleted!',
     175 + 'This group has been deleted!',
     176 + 'success'
     177 + );
     178 + $('button:contains("OK")').on('click', function () {
     179 + location.reload()
     180 + })
     181 + })
     182 +}
     183 + 
     184 +function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
     185 + // Create new data row.
     186 + var email = escapeHtml(emailInput).toLowerCase();
     187 + var newRow = [
     188 + escapeHtml(firstNameInput),
     189 + escapeHtml(lastNameInput),
     190 + email,
     191 + escapeHtml(positionInput),
     192 + '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
     193 + ];
     194 + 
     195 + // Check table to see if email already exists.
     196 + var targetsTable = targets.DataTable();
     197 + var existingRowIndex = targetsTable
     198 + .column(2, {
     199 + order: "index"
     200 + }) // Email column has index of 2
     201 + .data()
     202 + .indexOf(email);
     203 + // Update or add new row as necessary.
     204 + if (existingRowIndex >= 0) {
     205 + targetsTable
     206 + .row(existingRowIndex, {
     207 + order: "index"
     208 + })
     209 + .data(newRow);
     210 + } else {
     211 + targetsTable.row.add(newRow);
     212 + }
     213 +}
     214 + 
     215 +function load() {
     216 + $("#groupTable").hide()
     217 + $("#emptyMessage").hide()
     218 + $("#loading").show()
     219 + api.groups.summary()
     220 + .success(function (response) {
     221 + $("#loading").hide()
     222 + if (response.total > 0) {
     223 + groups = response.groups
     224 + $("#emptyMessage").hide()
     225 + $("#groupTable").show()
     226 + var groupTable = $("#groupTable").DataTable({
     227 + destroy: true,
     228 + columnDefs: [{
     229 + orderable: false,
     230 + targets: "no-sort"
     231 + }]
     232 + });
     233 + groupTable.clear();
     234 + $.each(groups, function (i, group) {
     235 + groupTable.row.add([
     236 + escapeHtml(group.name),
     237 + escapeHtml(group.num_targets),
     238 + moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
     239 + "<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
     240 + <i class='fa fa-pencil'></i>\
     241 + </button>\
     242 + <button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
     243 + <i class='fa fa-trash-o'></i>\
     244 + </button></div>"
     245 + ]).draw()
     246 + })
     247 + } else {
     248 + $("#emptyMessage").show()
     249 + }
     250 + })
     251 + .error(function () {
     252 + errorFlash("Error fetching groups")
     253 + })
     254 +}
     255 + 
     256 +$(document).ready(function () {
     257 + load()
     258 + // Setup the event listeners
     259 + // Handle manual additions
     260 + $("#targetForm").submit(function () {
     261 + addTarget(
     262 + $("#firstName").val(),
     263 + $("#lastName").val(),
     264 + $("#email").val(),
     265 + $("#position").val());
     266 + targets.DataTable().draw();
     267 + 
     268 + // Reset user input.
     269 + $("#targetForm>div>input").val('');
     270 + $("#firstName").focus();
     271 + return false;
     272 + });
     273 + // Handle Deletion
     274 + $("#targetsTable").on("click", "span>i.fa-trash-o", function () {
     275 + targets.DataTable()
     276 + .row($(this).parents('tr'))
     277 + .remove()
     278 + .draw();
     279 + });
     280 + $("#modal").on("hide.bs.modal", function () {
     281 + dismiss();
     282 + });
     283 + $("#csv-template").click(downloadCSVTemplate)
     284 +});
  • ■ ■ ■ ■ ■ ■
    static/js/src/app/users.js
    1  -var groups = []
     1 +let users = []
    2 2   
    3  -// Save attempts to POST or PUT to /groups/
    4  -function save(id) {
    5  - var targets = []
    6  - $.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
    7  - targets.push({
    8  - first_name: unescapeHtml(target[0]),
    9  - last_name: unescapeHtml(target[1]),
    10  - email: unescapeHtml(target[2]),
    11  - position: unescapeHtml(target[3])
    12  - })
    13  - })
    14  - var group = {
    15  - name: $("#name").val(),
    16  - targets: targets
     3 +// Save attempts to POST or PUT to /users/
     4 +const save = (id) => {
     5 + // Validate that the passwords match
     6 + if ($("#password").val() !== $("#confirm_password").val()) {
     7 + modalError("Passwords must match.")
     8 + return
    17 9   }
    18  - // Submit the group
     10 + let user = {
     11 + username: $("#username").val(),
     12 + password: $("#password").val(),
     13 + role: $("#role").val()
     14 + }
     15 + // Submit the user
    19 16   if (id != -1) {
    20  - // If we're just editing an existing group,
    21  - // we need to PUT /groups/:id
    22  - group.id = id
    23  - api.groupId.put(group)
     17 + // If we're just editing an existing user,
     18 + // we need to PUT /user/:id
     19 + user.id = id
     20 + api.userId.put(user)
    24 21   .success(function (data) {
    25  - successFlash("Group updated successfully!")
     22 + successFlash(`User ${user.username} updated successfully!`)
    26 23   load()
    27 24   dismiss()
    28 25   $("#modal").modal('hide')
    skipped 2 lines
    31 28   modalError(data.responseJSON.message)
    32 29   })
    33 30   } else {
    34  - // Else, if this is a new group, POST it
    35  - // to /groups
    36  - api.groups.post(group)
     31 + // Else, if this is a new user, POST it
     32 + // to /user
     33 + api.users.post(user)
    37 34   .success(function (data) {
    38  - successFlash("Group added successfully!")
     35 + successFlash(`User ${user.username} registered successfully!`)
    39 36   load()
    40 37   dismiss()
    41 38   $("#modal").modal('hide')
    skipped 4 lines
    46 43   }
    47 44  }
    48 45   
    49  -function dismiss() {
    50  - $("#targetsTable").dataTable().DataTable().clear().draw()
    51  - $("#name").val("")
     46 +const dismiss = () => {
     47 + $("#username").val("")
     48 + $("#password").val("")
     49 + $("#confirm_password").val("")
     50 + $("#role").val("")
    52 51   $("#modal\\.flashes").empty()
    53 52  }
    54 53   
    55  -function edit(id) {
    56  - targets = $("#targetsTable").dataTable({
    57  - destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
    58  - columnDefs: [{
    59  - orderable: false,
    60  - targets: "no-sort"
    61  - }]
    62  - })
    63  - $("#modalSubmit").unbind('click').click(function () {
     54 +const edit = (id) => {
     55 + $("#modalSubmit").unbind('click').click(() => {
    64 56   save(id)
    65 57   })
     58 + $("#role").select2()
    66 59   if (id == -1) {
    67  - var group = {}
     60 + $("#role").val("user")
     61 + $("#role").trigger("change")
    68 62   } else {
    69  - api.groupId.get(id)
    70  - .success(function (group) {
    71  - $("#name").val(group.name)
    72  - $.each(group.targets, function (i, record) {
    73  - targets.DataTable()
    74  - .row.add([
    75  - escapeHtml(record.first_name),
    76  - escapeHtml(record.last_name),
    77  - escapeHtml(record.email),
    78  - escapeHtml(record.position),
    79  - '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
    80  - ]).draw()
    81  - });
    82  - 
     63 + api.userId.get(id)
     64 + .success(function (user) {
     65 + $("#username").val(user.username)
     66 + $("#role").val(user.role.slug)
     67 + $("#role").trigger("change")
    83 68   })
    84 69   .error(function () {
    85  - errorFlash("Error fetching group")
     70 + errorFlash("Error fetching user")
    86 71   })
    87 72   }
    88  - // Handle file uploads
    89  - $("#csvupload").fileupload({
    90  - url: "/api/import/group",
    91  - dataType: "json",
    92  - beforeSend: function (xhr) {
    93  - xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
    94  - },
    95  - add: function (e, data) {
    96  - $("#modal\\.flashes").empty()
    97  - var acceptFileTypes = /(csv|txt)$/i;
    98  - var filename = data.originalFiles[0]['name']
    99  - if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
    100  - modalError("Unsupported file extension (use .csv or .txt)")
    101  - return false;
    102  - }
    103  - data.submit();
    104  - },
    105  - done: function (e, data) {
    106  - $.each(data.result, function (i, record) {
    107  - addTarget(
    108  - record.first_name,
    109  - record.last_name,
    110  - record.email,
    111  - record.position);
    112  - });
    113  - targets.DataTable().draw();
    114  - }
    115  - })
    116 73  }
    117 74   
    118  -var downloadCSVTemplate = function () {
    119  - var csvScope = [{
    120  - 'First Name': 'Example',
    121  - 'Last Name': 'User',
    122  - 'Email': '[email protected]',
    123  - 'Position': 'Systems Administrator'
    124  - }]
    125  - var filename = 'group_template.csv'
    126  - var csvString = Papa.unparse(csvScope, {})
    127  - var csvData = new Blob([csvString], {
    128  - type: 'text/csv;charset=utf-8;'
    129  - });
    130  - if (navigator.msSaveBlob) {
    131  - navigator.msSaveBlob(csvData, filename);
    132  - } else {
    133  - var csvURL = window.URL.createObjectURL(csvData);
    134  - var dlLink = document.createElement('a');
    135  - dlLink.href = csvURL;
    136  - dlLink.setAttribute('download', filename)
    137  - document.body.appendChild(dlLink)
    138  - dlLink.click();
    139  - document.body.removeChild(dlLink)
    140  - }
    141  -}
    142  - 
    143  - 
    144  -var deleteGroup = function (id) {
    145  - var group = groups.find(function (x) {
    146  - return x.id === id
    147  - })
    148  - if (!group) {
     75 +const deleteUser = (id) => {
     76 + var user = users.find(x => x.id == id)
     77 + if (!user) {
    149 78   return
    150 79   }
    151 80   swal({
    152 81   title: "Are you sure?",
    153  - text: "This will delete the group. This can't be undone!",
     82 + 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!`,
    154 83   type: "warning",
    155 84   animation: false,
    156 85   showCancelButton: true,
    157  - confirmButtonText: "Delete " + escapeHtml(group.name),
     86 + confirmButtonText: "Delete",
    158 87   confirmButtonColor: "#428bca",
    159 88   reverseButtons: true,
    160 89   allowOutsideClick: false,
    161 90   preConfirm: function () {
    162  - return new Promise(function (resolve, reject) {
    163  - api.groupId.delete(id)
    164  - .success(function (msg) {
     91 + return new Promise((resolve, reject) => {
     92 + api.userId.delete(id)
     93 + .success((msg) => {
    165 94   resolve()
    166 95   })
    167  - .error(function (data) {
     96 + .error((data) => {
    168 97   reject(data.responseJSON.message)
    169 98   })
    170 99   })
    171 100   }
    172 101   }).then(function () {
    173 102   swal(
    174  - 'Group Deleted!',
    175  - 'This group has been deleted!',
     103 + 'User Deleted!',
     104 + `The user account for ${user.username} and all associated objects have been deleted!`,
    176 105   'success'
    177 106   );
    178 107   $('button:contains("OK")').on('click', function () {
    skipped 2 lines
    181 110   })
    182 111  }
    183 112   
    184  -function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
    185  - // Create new data row.
    186  - var email = escapeHtml(emailInput).toLowerCase();
    187  - var newRow = [
    188  - escapeHtml(firstNameInput),
    189  - escapeHtml(lastNameInput),
    190  - email,
    191  - escapeHtml(positionInput),
    192  - '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
    193  - ];
    194 113   
    195  - // Check table to see if email already exists.
    196  - var targetsTable = targets.DataTable();
    197  - var existingRowIndex = targetsTable
    198  - .column(2, {
    199  - order: "index"
    200  - }) // Email column has index of 2
    201  - .data()
    202  - .indexOf(email);
    203  - // Update or add new row as necessary.
    204  - if (existingRowIndex >= 0) {
    205  - targetsTable
    206  - .row(existingRowIndex, {
    207  - order: "index"
    208  - })
    209  - .data(newRow);
    210  - } else {
    211  - targetsTable.row.add(newRow);
    212  - }
    213  -}
    214  - 
    215  -function load() {
    216  - $("#groupTable").hide()
    217  - $("#emptyMessage").hide()
     114 +const load = () => {
     115 + $("#userTable").hide()
    218 116   $("#loading").show()
    219  - api.groups.summary()
    220  - .success(function (response) {
     117 + api.users.get()
     118 + .success((us) => {
     119 + users = us
    221 120   $("#loading").hide()
    222  - if (response.total > 0) {
    223  - groups = response.groups
    224  - $("#emptyMessage").hide()
    225  - $("#groupTable").show()
    226  - var groupTable = $("#groupTable").DataTable({
    227  - destroy: true,
    228  - columnDefs: [{
    229  - orderable: false,
    230  - targets: "no-sort"
    231  - }]
    232  - });
    233  - groupTable.clear();
    234  - $.each(groups, function (i, group) {
    235  - groupTable.row.add([
    236  - escapeHtml(group.name),
    237  - escapeHtml(group.num_targets),
    238  - moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
    239  - "<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
     121 + $("#userTable").show()
     122 + let userTable = $("#userTable").DataTable({
     123 + destroy: true,
     124 + columnDefs: [{
     125 + orderable: false,
     126 + targets: "no-sort"
     127 + }]
     128 + });
     129 + userTable.clear();
     130 + $.each(users, (i, user) => {
     131 + userTable.row.add([
     132 + escapeHtml(user.username),
     133 + escapeHtml(user.role.name),
     134 + "<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 + "'>\
    240 135   <i class='fa fa-pencil'></i>\
    241 136   </button>\
    242  - <button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
     137 + <button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
    243 138   <i class='fa fa-trash-o'></i>\
    244 139   </button></div>"
    245  - ]).draw()
    246  - })
    247  - } else {
    248  - $("#emptyMessage").show()
    249  - }
     140 + ]).draw()
     141 + })
    250 142   })
    251  - .error(function () {
    252  - errorFlash("Error fetching groups")
     143 + .error(() => {
     144 + errorFlash("Error fetching users")
    253 145   })
    254 146  }
    255 147   
    256 148  $(document).ready(function () {
    257 149   load()
    258 150   // Setup the event listeners
    259  - // Handle manual additions
    260  - $("#targetForm").submit(function () {
    261  - addTarget(
    262  - $("#firstName").val(),
    263  - $("#lastName").val(),
    264  - $("#email").val(),
    265  - $("#position").val());
    266  - targets.DataTable().draw();
    267  - 
    268  - // Reset user input.
    269  - $("#targetForm>div>input").val('');
    270  - $("#firstName").focus();
    271  - return false;
    272  - });
    273  - // Handle Deletion
    274  - $("#targetsTable").on("click", "span>i.fa-trash-o", function () {
    275  - targets.DataTable()
    276  - .row($(this).parents('tr'))
    277  - .remove()
    278  - .draw();
    279  - });
    280 151   $("#modal").on("hide.bs.modal", function () {
    281 152   dismiss();
    282 153   });
    283  - $("#csv-template").click(downloadCSVTemplate)
     154 + // Select2 Defaults
     155 + $.fn.select2.defaults.set("width", "100%");
     156 + $.fn.select2.defaults.set("dropdownParent", $("#role-select"));
     157 + $.fn.select2.defaults.set("theme", "bootstrap");
     158 + $.fn.select2.defaults.set("sorter", function (data) {
     159 + return data.sort(function (a, b) {
     160 + if (a.text.toLowerCase() > b.text.toLowerCase()) {
     161 + return 1;
     162 + }
     163 + if (a.text.toLowerCase() < b.text.toLowerCase()) {
     164 + return -1;
     165 + }
     166 + return 0;
     167 + });
     168 + })
     169 + $("#new_button").on("click", function () {
     170 + edit(-1)
     171 + })
     172 + $("#userTable").on('click', '.edit_button', function (e) {
     173 + edit($(this).attr('data-user-id'))
     174 + })
     175 + $("#userTable").on('click', '.delete_button', function (e) {
     176 + deleteUser($(this).attr('data-user-id'))
     177 + })
    284 178  });
  • ■ ■ ■ ■ ■ ■
    templates/groups.html
     1 +{{define "body"}}
     2 +<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
     3 + <div class="row">
     4 + <h1 class="page-header">
     5 + Users &amp; Groups
     6 + </h1>
     7 + </div>
     8 + <div id="flashes" class="row"></div>
     9 + <div class="row">
     10 + <button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
     11 + data-target="#modal">
     12 + <i class="fa fa-plus"></i> New Group</button>
     13 + </div>
     14 + &nbsp;
     15 + <div id="loading">
     16 + <i class="fa fa-spinner fa-spin fa-4x"></i>
     17 + </div>
     18 + <div id="emptyMessage" class="row" style="display:none;">
     19 + <div class="alert alert-info">
     20 + No groups created yet. Let's create one!
     21 + </div>
     22 + </div>
     23 + <div class="row">
     24 + <table id="groupTable" class="table" style="display:none;">
     25 + <thead>
     26 + <tr>
     27 + <th>Name</th>
     28 + <th># of Members</th>
     29 + <th>Modified Date</th>
     30 + <th class="col-md-2 no-sort"></th>
     31 + </tr>
     32 + </thead>
     33 + <tbody>
     34 + </tbody>
     35 + </table>
     36 + </div>
     37 +</div>
     38 +<!-- Modal -->
     39 +<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
     40 + <div class="modal-dialog" role="document">
     41 + <div class="modal-content">
     42 + <div class="modal-header">
     43 + <button type="button" class="close" data-dismiss="modal" aria-label="Close">
     44 + <span aria-hidden="true">&times;</span>
     45 + </button>
     46 + <h4 class="modal-title" id="groupModalLabel">New Group</h4>
     47 + </div>
     48 + <div class="modal-body">
     49 + <div class="row" id="modal.flashes"></div>
     50 + <label class="control-label" for="name">Name:</label>
     51 + <div class="form-group">
     52 + <input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
     53 + autofocus />
     54 + </div>
     55 + <div class="form-group">
     56 + <span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
     57 + title="Supports CSV files" id="fileUpload">
     58 + <i class="fa fa-plus"></i> Bulk Import Users
     59 + <input type="file" id="csvupload" multiple>
     60 + </span>
     61 + <span id="csv-template" class="text-muted small">
     62 + <i class="fa fa-file-excel-o"></i> Download CSV Template</span>
     63 + </div>
     64 + <div class="row">
     65 + <form id="targetForm">
     66 + <div class="col-sm-2">
     67 + <input type="text" class="form-control" placeholder="First Name" id="firstName">
     68 + </div>
     69 + <div class="col-sm-2">
     70 + <input type="text" class="form-control" placeholder="Last Name" id="lastName">
     71 + </div>
     72 + <div class="col-sm-3">
     73 + <input type="email" class="form-control" placeholder="Email" id="email" required>
     74 + </div>
     75 + <div class="col-sm-3">
     76 + <input type="text" class="form-control" placeholder="Position" id="position">
     77 + </div>
     78 + <div class="col-sm-1">
     79 + <button type="submit" class="btn btn-danger btn-lg">
     80 + <i class="fa fa-plus"></i> Add</button>
     81 + </div>
     82 + </form>
     83 + </div>
     84 + <br />
     85 + <table id="targetsTable" class="table table-hover table-striped table-condensed">
     86 + <thead>
     87 + <tr>
     88 + <th>First Name</th>
     89 + <th>Last Name</th>
     90 + <th>Email</th>
     91 + <th>Position</th>
     92 + <th class="no-sort"></th>
     93 + <tbody>
     94 + </tbody>
     95 + </table>
     96 + </div>
     97 + <div class="modal-footer">
     98 + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
     99 + <button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
     100 + </div>
     101 + </div>
     102 + </div>
     103 +</div>
     104 +{{end}} {{define "scripts"}}
     105 +<script src="/js/dist/app/groups.min.js"></script>
     106 +{{end}}
  • ■ ■ ■ ■ ■
    templates/nav.html
    skipped 9 lines
    10 10   <a href="/campaigns">Campaigns</a>
    11 11   </li>
    12 12   <li>
    13  - <a href="/users">Users &amp; Groups</a>
     13 + <a href="/groups">Users &amp; Groups</a>
    14 14   </li>
    15 15   <li> <a href="/templates">Email Templates</a>
    16 16   </li>
    skipped 4 lines
    21 21   <a href="/sending_profiles">Sending Profiles</a>
    22 22   </li>
    23 23   <li>
    24  - <a href="/settings">Settings <span class="badge pull-right">Admin</span></a>
     24 + <a href="/settings">Account Settings</span></a>
     25 + </li>
     26 + {{if .ModifySystem}}
     27 + <li>
     28 + <a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
    25 29   </li>
     30 + {{end}}
    26 31   <li>
    27 32   <hr>
    28 33   </li>
    skipped 11 lines
  • ■ ■ ■ ■ ■ ■
    templates/register.html
    1  -{{ define "base" }}
    2  -<!DOCTYPE html>
    3  -<html lang="en">
    4  - 
    5  -<head>
    6  - <meta charset="utf-8">
    7  - <meta http-equiv="X-UA-Compatible" content="IE=edge">
    8  - <meta name="viewport" content="width=device-width, initial-scale=1.0">
    9  - <meta name="description" content="Gophish - Open-Source Phishing Toolkit">
    10  - <meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
    11  - <link rel="shortcut icon" href="/favicon.png">
    12  - 
    13  - <title>Gophish - {{ .Title }}</title>
    14  - 
    15  - <link href="/css/dist/gophish.css" rel='stylesheet' type='text/css'>
    16  - <link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
    17  -</head>
    18  - 
    19  -<body>
    20  - <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    21  - <div class="container-fluid">
    22  - <div class="navbar-header">
    23  - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
    24  - <span class="sr-only">Toggle navigation</span>
    25  - <span class="icon-bar"></span>
    26  - <span class="icon-bar"></span>
    27  - <span class="icon-bar"></span>
    28  - </button>
    29  - <img class="navbar-logo" src="/images/logo_inv_small.png" />
    30  - <a class="navbar-brand" href="/">&nbsp;gophish</a>
    31  - </div>
    32  - <div class="navbar-collapse collapse">
    33  - <ul class="nav navbar-nav navbar-right">
    34  - <li>
    35  - <a id="login-button" href="/login">
    36  - <button type="button" class="btn btn-primary">Login</button>
    37  - </a>
    38  - </li>
    39  - </ul>
    40  - </div>
    41  - </div>
    42  - </div>
    43  - <div class="container">
    44  - <form class="form-signin" action="/register" method="POST">
    45  - <img id="logo" src="/images/logo_purple.png" />
    46  - <h2 class="form-signin-heading">Please register below</h2>
    47  - {{template "flashes" .Flashes}}
    48  - <input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus />
    49  - <input type="password" name="password" class="form-control middle-input" placeholder="Password"
    50  - autocomplete="off" required />
    51  - <input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password"
    52  - autocomplete="off" required />
    53  - <input type="hidden" name="csrf_token" value="{{.Token}}" />
    54  - <button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
    55  - </form>
    56  - </div>
    57  - <!-- Placed at the end of the document so the pages load faster -->
    58  - <script src="/js/dist/vendor.min.js"></script>
    59  -</body>
    60  - 
    61  -</html>
    62  -{{ end }}
  • ■ ■ ■ ■ ■ ■
    templates/settings.html
    skipped 7 lines
    8 8   <ul class="nav nav-tabs" role="tablist">
    9 9   <li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
    10 10   data-toggle="tab">Account Settings</a></li>
    11  - <li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI Settings</a></li>
     11 + <li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
     12 + Settings</a></li>
    12 13   </ul>
    13 14   <!-- Tab Panes -->
    14 15   <div class="tab-content">
    skipped 7 lines
    22 23   </div>
    23 24   </div>
    24 25   <br />
    25  - <div class="row">
    26  - <label class="col-sm-2 control-label form-label">Register a New User</label>
    27  - <div class="col-md-6">
    28  - <a href="/register" class="btn btn-primary"><i class="fa fa-plus"></i> Add User</a>
    29  - </div>
    30  - </div>
    31  - <br />
    32 26   {{end}}
    33 27   <div class="row">
    34 28   <label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
    35 29   <div class="col-md-6">
    36  - <input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}" class="form-control"
    37  - readonly />
     30 + <input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}"
     31 + class="form-control" readonly />
    38 32   </div>
    39 33   <form id="apiResetForm">
    40 34   <button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
    skipped 5 lines
    46 40   <div class="row">
    47 41   <label for="username" class="col-sm-2 control-label form-label">Username:</label>
    48 42   <div class="col-md-6">
    49  - <input type="text" id="username" name="username" value="{{.User.Username}}" class="form-control" />
     43 + <input type="text" id="username" name="username" value="{{.User.Username}}"
     44 + class="form-control" />
    50 45   </div>
    51 46   </div>
    52 47   <br />
    53 48   <div class="row">
    54 49   <label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
    55 50   <div class="col-md-6">
    56  - <input type="password" id="current_password" name="current_password" autocomplete="off" class="form-control" />
     51 + <input type="password" id="current_password" name="current_password" autocomplete="off"
     52 + class="form-control" />
    57 53   </div>
    58 54   </div>
    59 55   <br />
    60 56   <div class="row">
    61 57   <label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
    62 58   <div class="col-md-6">
    63  - <input type="password" id="new_password" name="new_password" autocomplete="off" class="form-control" />
     59 + <input type="password" id="new_password" name="new_password" autocomplete="off"
     60 + class="form-control" />
    64 61   </div>
    65 62   </div>
    66 63   <br />
    67 64   <div class="row">
    68  - <label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New Password:</label>
     65 + <label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New
     66 + Password:</label>
    69 67   <div class="col-md-6">
    70 68   <input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
    71 69   class="form-control" />
    skipped 20 lines
  • ■ ■ ■ ■ ■ ■
    templates/users.html
    skipped 1 lines
    2 2  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    3 3   <div class="row">
    4 4   <h1 class="page-header">
    5  - Users &amp; Groups
     5 + {{.Title}}
    6 6   </h1>
    7 7   </div>
    8 8   <div id="flashes" class="row"></div>
    9 9   <div class="row">
    10  - <button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
    11  - data-target="#modal">
    12  - <i class="fa fa-plus"></i> New Group</button>
     10 + <button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
     11 + data-user-id="-1" data-target="#modal">
     12 + <i class="fa fa-plus"></i> New User</button>
    13 13   </div>
    14 14   &nbsp;
    15 15   <div id="loading">
    16 16   <i class="fa fa-spinner fa-spin fa-4x"></i>
    17 17   </div>
    18  - <div id="emptyMessage" class="row" style="display:none;">
    19  - <div class="alert alert-info">
    20  - No groups created yet. Let's create one!
    21  - </div>
    22  - </div>
    23 18   <div class="row">
    24  - <table id="groupTable" class="table" style="display:none;">
     19 + <table id="userTable" class="table" style="display:none;">
    25 20   <thead>
    26 21   <tr>
    27  - <th>Name</th>
    28  - <th># of Members</th>
    29  - <th>Modified Date</th>
     22 + <th>Username</th>
     23 + <th>Role</th>
    30 24   <th class="col-md-2 no-sort"></th>
    31 25   </tr>
    32 26   </thead>
    skipped 10 lines
    43 37   <button type="button" class="close" data-dismiss="modal" aria-label="Close">
    44 38   <span aria-hidden="true">&times;</span>
    45 39   </button>
    46  - <h4 class="modal-title" id="groupModalLabel">New Group</h4>
     40 + <h4 class="modal-title" id="groupModalLabel">New User</h4>
    47 41   </div>
    48  - <div class="modal-body">
     42 + <div class="modal-body" id="modal_body">
    49 43   <div class="row" id="modal.flashes"></div>
    50  - <label class="control-label" for="name">Name:</label>
     44 + <label class="control-label" for="username">Username:</label>
    51 45   <div class="form-group">
    52  - <input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
    53  - autofocus />
     46 + <input type="text" class="form-control" placeholder="Username" id="username" autofocus />
    54 47   </div>
     48 + <label class="control-label" for="password">Password:</label>
    55 49   <div class="form-group">
    56  - <span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right" title="Supports CSV files"
    57  - id="fileUpload">
    58  - <i class="fa fa-plus"></i> Bulk Import Users
    59  - <input type="file" id="csvupload" multiple>
    60  - </span>
    61  - <span id="csv-template" class="text-muted small">
    62  - <i class="fa fa-file-excel-o"></i> Download CSV Template</span>
     50 + <input type="password" class="form-control" placeholder="Password" id="password" required />
    63 51   </div>
    64  - <div class="row">
    65  - <form id="targetForm">
    66  - <div class="col-sm-2">
    67  - <input type="text" class="form-control" placeholder="First Name" id="firstName">
    68  - </div>
    69  - <div class="col-sm-2">
    70  - <input type="text" class="form-control" placeholder="Last Name" id="lastName">
    71  - </div>
    72  - <div class="col-sm-3">
    73  - <input type="email" class="form-control" placeholder="Email" id="email" required>
    74  - </div>
    75  - <div class="col-sm-3">
    76  - <input type="text" class="form-control" placeholder="Position" id="position">
    77  - </div>
    78  - <div class="col-sm-1">
    79  - <button type="submit" class="btn btn-danger btn-lg">
    80  - <i class="fa fa-plus"></i> Add</button>
    81  - </div>
    82  - </form>
     52 + <label class="control-label" for="confirm_password">Confirm Password:</label>
     53 + <div class="form-group">
     54 + <input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
     55 + required />
    83 56   </div>
    84  - <br />
    85  - <table id="targetsTable" class="table table-hover table-striped table-condensed">
    86  - <thead>
    87  - <tr>
    88  - <th>First Name</th>
    89  - <th>Last Name</th>
    90  - <th>Email</th>
    91  - <th>Position</th>
    92  - <th class="no-sort"></th>
    93  - <tbody>
    94  - </tbody>
    95  - </table>
     57 + <label class="control-label" for="role">Role:</label>
     58 + <div class="form-group" id="role-select">
     59 + <select class="form-control" placeholder="" id="role" />
     60 + <option value="admin">Admin</option>
     61 + <option value="user">User</option>
     62 + </select>
     63 + </div>
    96 64   </div>
    97 65   <div class="modal-footer">
    98 66   <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    util/util.go
    skipped 20 lines
    21 21   log "github.com/gophish/gophish/logger"
    22 22   "github.com/gophish/gophish/models"
    23 23   "github.com/jordan-wright/email"
     24 + "golang.org/x/crypto/bcrypt"
    24 25  )
    25 26   
    26 27  var (
    skipped 164 lines
    191 192   return nil
    192 193  }
    193 194   
     195 +// GenerateSecureKey creates a secure key to use as an API key
     196 +func GenerateSecureKey() string {
     197 + // Inspired from gorilla/securecookie
     198 + k := make([]byte, 32)
     199 + io.ReadFull(rand.Reader, k)
     200 + return fmt.Sprintf("%x", k)
     201 +}
     202 + 
     203 +// NewHash hashes the provided password and returns the bcrypt hash (using the
     204 +// default 10 rounds) as a string.
     205 +func NewHash(pass string) (string, error) {
     206 + hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
     207 + if err != nil {
     208 + return "", err
     209 + }
     210 + return string(hash), nil
     211 +}
     212 + 
  • ■ ■ ■ ■ ■ ■
    webpack.config.js
     1 +const path = require('path');
     2 + 
     3 +module.exports = {
     4 + context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
     5 + entry: {
     6 + users: './users',
     7 + },
     8 + output: {
     9 + path: path.resolve(__dirname, 'static', 'js', 'dist', 'app'),
     10 + filename: '[name].min.js'
     11 + },
     12 + module: {
     13 + rules: [{
     14 + test: /\.js$/,
     15 + exclude: /node_modules/,
     16 + use: {
     17 + loader: "babel-loader"
     18 + }
     19 + }]
     20 + }
     21 +}
  • yarn.lock
    Diff is too large to be displayed.
Please wait...
Page is in error, reload to recover