Implement User Management API (#1473)

This implements the first pass for a user management API allowing users with the `ModifySystem` permission to create, modify, and delete users. In addition to this, any user is able to use the API to view or modify their own account information.
1257-lets-encrypt
Jordan Wright 2019-05-31 13:58:18 -05:00 committed by GitHub
parent faadf0c850
commit 84096b8724
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 3595 additions and 532 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -1,49 +1,24 @@
package auth package auth
import ( import (
"encoding/gob"
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"crypto/rand"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
//init registers the necessary models to be saved in the session later
func init() {
gob.Register(&models.User{})
gob.Register(&models.Flash{})
Store.Options.HttpOnly = true
// This sets the maxAge to 5 days for all cookies
Store.MaxAge(86400 * 5)
}
// Store contains the session information for the request
var Store = sessions.NewCookieStore(
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
[]byte(securecookie.GenerateRandomKey(32)))
// ErrInvalidPassword is thrown when a user provides an incorrect password. // ErrInvalidPassword is thrown when a user provides an incorrect password.
var ErrInvalidPassword = errors.New("Invalid Password") var ErrInvalidPassword = errors.New("Invalid Password")
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
// or change password functions
var ErrPasswordMismatch = errors.New("Password cannot be blank")
// ErrEmptyPassword is thrown when a user provides a blank password to the register // ErrEmptyPassword is thrown when a user provides a blank password to the register
// or change password functions // or change password functions
var ErrEmptyPassword = errors.New("Password cannot be blank") var ErrEmptyPassword = errors.New("No password provided")
// ErrPasswordMismatch is thrown when a user provides passwords that do not match
var ErrPasswordMismatch = errors.New("Passwords must match")
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
var ErrUsernameTaken = errors.New("Username already taken")
// Login attempts to login the user given a request. // Login attempts to login the user given a request.
func Login(r *http.Request) (bool, models.User, error) { func Login(r *http.Request) (bool, models.User, error) {
@ -61,54 +36,6 @@ func Login(r *http.Request) (bool, models.User, error) {
return true, u, nil return true, u, nil
} }
// Register attempts to register the user given a request.
func Register(r *http.Request) (bool, error) {
username := r.FormValue("username")
newPassword := r.FormValue("password")
confirmPassword := r.FormValue("confirm_password")
u, err := models.GetUserByUsername(username)
// If the given username already exists, throw an error and return false
if err == nil {
return false, ErrUsernameTaken
}
// If we have an error which is not simply indicating that no user was found, report it
if err != nil && err != gorm.ErrRecordNotFound {
log.Warn(err)
return false, err
}
u = models.User{}
// If we've made it here, we should have a valid username given
// Check that the passsword isn't blank
if newPassword == "" {
return false, ErrEmptyPassword
}
// Make sure passwords match
if newPassword != confirmPassword {
return false, ErrPasswordMismatch
}
// Let's create the password hash
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return false, err
}
u.Username = username
u.Hash = string(h)
u.ApiKey = GenerateSecureKey()
err = models.PutUser(&u)
return true, nil
}
// GenerateSecureKey creates a secure key to use
// as an API key
func GenerateSecureKey() string {
// Inspired from gorilla/securecookie
k := make([]byte, 32)
io.ReadFull(rand.Reader, k)
return fmt.Sprintf("%x", k)
}
// ChangePassword verifies the current password provided in the request and, // ChangePassword verifies the current password provided in the request and,
// if it's valid, changes the password for the authenticated user. // if it's valid, changes the password for the authenticated user.
func ChangePassword(r *http.Request) error { func ChangePassword(r *http.Request) error {

View File

@ -19,6 +19,7 @@ type APISuite struct {
apiKey string apiKey string
config *config.Config config *config.Config
apiServer *Server apiServer *Server
admin models.User
} }
func (s *APISuite) SetupSuite() { func (s *APISuite) SetupSuite() {
@ -37,6 +38,7 @@ func (s *APISuite) SetupSuite() {
u, err := models.GetUser(1) u, err := models.GetUser(1)
s.Nil(err) s.Nil(err)
s.apiKey = u.ApiKey s.apiKey = u.ApiKey
s.admin = u
// Move our cwd up to the project root for help with resolving // Move our cwd up to the project root for help with resolving
// static assets // static assets
err = os.Chdir("../") err = os.Chdir("../")
@ -49,6 +51,15 @@ func (s *APISuite) TearDownTest() {
for _, campaign := range campaigns { for _, campaign := range campaigns {
models.DeleteCampaign(campaign.Id) models.DeleteCampaign(campaign.Id)
} }
// Cleanup all users except the original admin
users, _ := models.GetUsers()
for _, user := range users {
if user.Id == 1 {
continue
}
err := models.DeleteUser(user.Id)
s.Nil(err)
}
} }
func (s *APISuite) SetupTest() { func (s *APISuite) SetupTest() {

View File

@ -3,9 +3,9 @@ package api
import ( import (
"net/http" "net/http"
"github.com/gophish/gophish/auth"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
) )
// Reset (/api/reset) resets the currently authenticated user's API key // Reset (/api/reset) resets the currently authenticated user's API key
@ -13,7 +13,7 @@ func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
switch { switch {
case r.Method == "POST": case r.Method == "POST":
u := ctx.Get(r, "user").(models.User) u := ctx.Get(r, "user").(models.User)
u.ApiKey = auth.GenerateSecureKey() u.ApiKey = util.GenerateSecureKey()
err := models.PutUser(&u) err := models.PutUser(&u)
if err != nil { if err != nil {
http.Error(w, "Error setting API Key", http.StatusInternalServerError) http.Error(w, "Error setting API Key", http.StatusInternalServerError)

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
mid "github.com/gophish/gophish/middleware" mid "github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/worker" "github.com/gophish/gophish/worker"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -64,6 +65,8 @@ func (as *Server) registerRoutes() {
router.HandleFunc("/pages/{id:[0-9]+}", as.Page) router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
router.HandleFunc("/smtp/", as.SendingProfiles) router.HandleFunc("/smtp/", as.SendingProfiles)
router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile) router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile)
router.HandleFunc("/users/", mid.Use(as.Users, mid.RequirePermission(models.PermissionModifySystem)))
router.HandleFunc("/users/{id:[0-9]+}", mid.Use(as.User))
router.HandleFunc("/util/send_test_email", as.SendTestEmail) router.HandleFunc("/util/send_test_email", as.SendTestEmail)
router.HandleFunc("/import/group", as.ImportGroup) router.HandleFunc("/import/group", as.ImportGroup)
router.HandleFunc("/import/email", as.ImportEmail) router.HandleFunc("/import/email", as.ImportEmail)

218
controllers/api/user.go Normal file
View File

@ -0,0 +1,218 @@
package api
import (
"encoding/json"
"errors"
"net/http"
"strconv"
ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
)
// ErrEmptyPassword is thrown when a user provides a blank password to the register
// or change password functions
var ErrEmptyPassword = errors.New("No password provided")
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
var ErrUsernameTaken = errors.New("Username already taken")
// ErrEmptyUsername is thrown when a user attempts to register a username that is taken.
var ErrEmptyUsername = errors.New("No username provided")
// ErrEmptyRole is throws when no role is provided when creating or modifying a user.
var ErrEmptyRole = errors.New("No role specified")
// ErrInsufficientPermission is thrown when a user attempts to change an
// attribute (such as the role) for which they don't have permission.
var ErrInsufficientPermission = errors.New("Permission denied")
// userRequest is the payload which represents the creation of a new user.
type userRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
func (ur *userRequest) Validate(existingUser *models.User) error {
switch {
case ur.Username == "":
return ErrEmptyUsername
case ur.Role == "":
return ErrEmptyRole
}
// Verify that the username isn't already taken. We consider two cases:
// * We're creating a new user, in which case any match is a conflict
// * We're modifying a user, in which case any match with a different ID is
// a conflict.
possibleConflict, err := models.GetUserByUsername(ur.Username)
if err == nil {
if existingUser == nil {
return ErrUsernameTaken
}
if possibleConflict.Id != existingUser.Id {
return ErrUsernameTaken
}
}
// If we have an error which is not simply indicating that no user was found, report it
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
return nil
}
// Users contains functions to retrieve a list of existing users or create a
// new user. Users with the ModifySystem permissions can view and create users.
func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
us, err := models.GetUsers()
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, us, http.StatusOK)
return
case r.Method == "POST":
ur := &userRequest{}
err := json.NewDecoder(r.Body).Decode(ur)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
err = ur.Validate(nil)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
if ur.Password == "" {
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
return
}
hash, err := util.NewHash(ur.Password)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
role, err := models.GetRoleBySlug(ur.Role)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
user := models.User{
Username: ur.Username,
Hash: hash,
ApiKey: util.GenerateSecureKey(),
Role: role,
RoleID: role.ID,
}
err = models.PutUser(&user)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, user, http.StatusOK)
return
}
}
// User contains functions to retrieve or delete a single user. Users with
// the ModifySystem permission can view and modify any user. Otherwise, users
// may only view or delete their own account.
func (as *Server) User(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64)
// If the user doesn't have ModifySystem permissions, we need to verify
// that they're only taking action on their account.
currentUser := ctx.Get(r, "user").(models.User)
hasSystem, err := currentUser.HasPermission(models.PermissionModifySystem)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
if !hasSystem && currentUser.Id != id {
JSONResponse(w, models.Response{Success: false, Message: http.StatusText(http.StatusForbidden)}, http.StatusForbidden)
return
}
existingUser, err := models.GetUser(id)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "User not found"}, http.StatusNotFound)
return
}
switch {
case r.Method == "GET":
JSONResponse(w, existingUser, http.StatusOK)
case r.Method == "DELETE":
err = models.DeleteUser(id)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
log.Infof("Deleted user account for %s", existingUser.Username)
JSONResponse(w, models.Response{Success: true, Message: "User deleted Successfully!"}, http.StatusOK)
case r.Method == "PUT":
ur := &userRequest{}
err = json.NewDecoder(r.Body).Decode(ur)
if err != nil {
log.Errorf("error decoding user request: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
err = ur.Validate(&existingUser)
if err != nil {
log.Errorf("invalid user request received: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
existingUser.Username = ur.Username
// Only users with the ModifySystem permission are able to update a
// user's role. This prevents a privilege escalation letting users
// upgrade their own account.
if !hasSystem && ur.Role != existingUser.Role.Slug {
JSONResponse(w, models.Response{Success: false, Message: ErrInsufficientPermission.Error()}, http.StatusBadRequest)
return
}
role, err := models.GetRoleBySlug(ur.Role)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
// If our user is trying to change the role of an admin, we need to
// ensure that it isn't the last user account with the Admin role.
if existingUser.Role.Slug == models.RoleAdmin && existingUser.Role.ID != role.ID {
err = models.EnsureEnoughAdmins()
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
}
existingUser.Role = role
existingUser.RoleID = role.ID
// We don't force the password to be provided, since it may be an admin
// managing the user's account, and making a simple change like
// updating the username or role. However, if it _is_ provided, we'll
// update the stored hash.
//
// Note that we don't force the current password to be provided. The
// assumption here is that the API key is a proper bearer token proving
// authenticated access to the account.
if ur.Password != "" {
hash, err := util.NewHash(ur.Password)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
existingUser.Hash = hash
}
err = models.PutUser(&existingUser)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, existingUser, http.StatusOK)
}
}

View File

@ -0,0 +1,188 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"golang.org/x/crypto/bcrypt"
ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/models"
)
func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
role, err := models.GetRoleBySlug(slug)
s.Nil(err)
unauthorizedUser := &models.User{
Username: "foo",
Hash: "bar",
ApiKey: "12345",
Role: role,
RoleID: role.ID,
}
err = models.PutUser(unauthorizedUser)
s.Nil(err)
return unauthorizedUser
}
func (s *APISuite) TestGetUsers() {
r := httptest.NewRequest(http.MethodGet, "/api/users", nil)
r = ctx.Set(r, "user", s.admin)
w := httptest.NewRecorder()
s.apiServer.Users(w, r)
s.Equal(w.Code, http.StatusOK)
got := []models.User{}
err := json.NewDecoder(w.Body).Decode(&got)
s.Nil(err)
// We only expect one user
s.Equal(1, len(got))
// And it should be the admin user
s.Equal(s.admin.Id, got[0].Id)
}
func (s *APISuite) TestCreateUser() {
payload := &userRequest{
Username: "foo",
Password: "bar",
Role: models.RoleUser,
}
body, err := json.Marshal(payload)
s.Nil(err)
r := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(body))
r.Header.Set("Content-Type", "application/json")
r = ctx.Set(r, "user", s.admin)
w := httptest.NewRecorder()
s.apiServer.Users(w, r)
s.Equal(w.Code, http.StatusOK)
got := &models.User{}
err = json.NewDecoder(w.Body).Decode(got)
s.Nil(err)
s.Equal(got.Username, payload.Username)
s.Equal(got.Role.Slug, payload.Role)
}
// TestModifyUser tests that a user with the appropriate access is able to
// modify their username and password.
func (s *APISuite) TestModifyUser() {
unpriviledgedUser := s.createUnpriviledgedUser(models.RoleUser)
newPassword := "new-password"
newUsername := "new-username"
payload := userRequest{
Username: newUsername,
Password: newPassword,
Role: unpriviledgedUser.Role.Slug,
}
body, err := json.Marshal(payload)
s.Nil(err)
url := fmt.Sprintf("/api/users/%d", unpriviledgedUser.Id)
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unpriviledgedUser.ApiKey))
w := httptest.NewRecorder()
s.apiServer.ServeHTTP(w, r)
response := &models.User{}
err = json.NewDecoder(w.Body).Decode(response)
s.Nil(err)
s.Equal(w.Code, http.StatusOK)
s.Equal(response.Username, newUsername)
got, err := models.GetUser(unpriviledgedUser.Id)
s.Nil(err)
s.Equal(response.Username, got.Username)
s.Equal(newUsername, got.Username)
err = bcrypt.CompareHashAndPassword([]byte(got.Hash), []byte(newPassword))
s.Nil(err)
}
// TestUnauthorizedListUsers ensures that users without the ModifySystem
// permission are unable to list the users registered in Gophish.
func (s *APISuite) TestUnauthorizedListUsers() {
// First, let's create a standard user which doesn't
// have ModifySystem permissions.
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
// We'll try to make a request to the various users API endpoints to
// ensure that they fail. Previously, we could hit the handlers directly
// but we need to go through the router for this test to ensure the
// middleware gets applied.
r := httptest.NewRequest(http.MethodGet, "/api/users/", nil)
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
w := httptest.NewRecorder()
s.apiServer.ServeHTTP(w, r)
s.Equal(w.Code, http.StatusForbidden)
}
// TestUnauthorizedModifyUsers verifies that users without ModifySystem
// permission (a "standard" user) can only get or modify their own information.
func (s *APISuite) TestUnauthorizedGetUser() {
// First, we'll make sure that a user with the "user" role is unable to
// get the information of another user (in this case, the main admin).
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
url := fmt.Sprintf("/api/users/%d", s.admin.Id)
r := httptest.NewRequest(http.MethodGet, url, nil)
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
w := httptest.NewRecorder()
s.apiServer.ServeHTTP(w, r)
s.Equal(w.Code, http.StatusForbidden)
}
// TestUnauthorizedModifyRole ensures that users without the ModifySystem
// privilege are unable to modify their own role, preventing a potential
// privilege escalation issue.
func (s *APISuite) TestUnauthorizedSetRole() {
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
payload := &userRequest{
Username: unauthorizedUser.Username,
Role: models.RoleAdmin,
}
body, err := json.Marshal(payload)
s.Nil(err)
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
w := httptest.NewRecorder()
s.apiServer.ServeHTTP(w, r)
s.Equal(w.Code, http.StatusBadRequest)
response := &models.Response{}
err = json.NewDecoder(w.Body).Decode(response)
s.Nil(err)
s.Equal(response.Message, ErrInsufficientPermission.Error())
}
// TestModifyWithExistingUsername verifies that it's not possible to modify
// an user's username to one which already exists.
func (s *APISuite) TestModifyWithExistingUsername() {
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
payload := &userRequest{
Username: s.admin.Username,
Role: unauthorizedUser.Role.Slug,
}
body, err := json.Marshal(payload)
s.Nil(err)
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
r := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(body))
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
w := httptest.NewRecorder()
s.apiServer.ServeHTTP(w, r)
s.Equal(w.Code, http.StatusBadRequest)
expected := &models.Response{
Message: ErrUsernameTaken.Error(),
Success: false,
}
got := &models.Response{}
err = json.NewDecoder(w.Body).Decode(got)
s.Nil(err)
s.Equal(got.Message, expected.Message)
}

View File

@ -95,17 +95,17 @@ func (as *AdminServer) Shutdown() error {
func (as *AdminServer) registerRoutes() { func (as *AdminServer) registerRoutes() {
router := mux.NewRouter() router := mux.NewRouter()
// Base Front-end routes // Base Front-end routes
router.HandleFunc("/", Use(as.Base, mid.RequireLogin)) router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
router.HandleFunc("/login", as.Login) router.HandleFunc("/login", as.Login)
router.HandleFunc("/logout", Use(as.Logout, mid.RequireLogin)) router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
router.HandleFunc("/campaigns", Use(as.Campaigns, mid.RequireLogin)) router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
router.HandleFunc("/campaigns/{id:[0-9]+}", Use(as.CampaignID, mid.RequireLogin)) router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
router.HandleFunc("/templates", Use(as.Templates, mid.RequireLogin)) router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
router.HandleFunc("/users", Use(as.Users, mid.RequireLogin)) router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin))
router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin)) router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin))
router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin)) router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin)) router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem))) router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
// Create the API routes // Create the API routes
api := api.NewServer(api.WithWorker(as.worker)) api := api.NewServer(api.WithWorker(as.worker))
router.PathPrefix("/api/").Handler(api) router.PathPrefix("/api/").Handler(api)
@ -114,11 +114,11 @@ func (as *AdminServer) registerRoutes() {
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/"))) router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
// Setup CSRF Protection // Setup CSRF Protection
csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()), csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
csrf.FieldName("csrf_token"), csrf.FieldName("csrf_token"),
csrf.Secure(as.config.UseTLS)) csrf.Secure(as.config.UseTLS))
adminHandler := csrfHandler(router) adminHandler := csrfHandler(router)
adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext) adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
// Setup GZIP compression // Setup GZIP compression
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression) gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
@ -129,15 +129,6 @@ func (as *AdminServer) registerRoutes() {
as.server.Handler = adminHandler as.server.Handler = adminHandler
} }
// Use allows us to stack middleware to process the request
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
for _, m := range mid {
handler = m(handler)
}
return handler
}
type templateParams struct { type templateParams struct {
Title string Title string
Flashes []interface{} Flashes []interface{}
@ -160,42 +151,6 @@ func newTemplateParams(r *http.Request) templateParams {
} }
} }
// Register creates a new user
func (as *AdminServer) Register(w http.ResponseWriter, r *http.Request) {
// If it is a post request, attempt to register the account
// Now that we are all registered, we can log the user in
params := templateParams{Title: "Register", Token: csrf.Token(r)}
session := ctx.Get(r, "session").(*sessions.Session)
switch {
case r.Method == "GET":
params.Flashes = session.Flashes()
session.Save(r, w)
templates := template.New("template")
_, err := templates.ParseFiles("templates/register.html", "templates/flashes.html")
if err != nil {
log.Error(err)
}
template.Must(templates, err).ExecuteTemplate(w, "base", params)
case r.Method == "POST":
//Attempt to register
succ, err := auth.Register(r)
//If we've registered, redirect to the login page
if succ {
Flash(w, r, "success", "Registration successful!")
session.Save(r, w)
http.Redirect(w, r, "/login", 302)
return
}
// Check the error
m := err.Error()
log.Error(err)
Flash(w, r, "danger", m)
session.Save(r, w)
http.Redirect(w, r, "/register", 302)
return
}
}
// Base handles the default path and template execution // Base handles the default path and template execution
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) { func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r) params := newTemplateParams(r)
@ -224,11 +179,11 @@ func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) {
getTemplate(w, "templates").ExecuteTemplate(w, "base", params) getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
} }
// Users handles the default path and template execution // Groups handles the default path and template execution
func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) { func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r) params := newTemplateParams(r)
params.Title = "Users & Groups" params.Title = "Users & Groups"
getTemplate(w, "users").ExecuteTemplate(w, "base", params) getTemplate(w, "groups").ExecuteTemplate(w, "base", params)
} }
// LandingPages handles the default path and template execution // LandingPages handles the default path and template execution
@ -271,6 +226,14 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
} }
} }
// UserManagement is an admin-only handler that allows for the registration
// and management of user accounts within Gophish.
func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "User Management"
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
}
// Login handles the authentication flow for a user. If credentials are valid, // Login handles the authentication flow for a user. If credentials are valid,
// a session is created // a session is created
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) { func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {

View File

@ -32,10 +32,10 @@ import (
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/config" "github.com/gophish/gophish/config"
"github.com/gophish/gophish/controllers" "github.com/gophish/gophish/controllers"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
) )
@ -94,7 +94,7 @@ func main() {
} }
adminConfig := conf.AdminConf adminConfig := conf.AdminConf
adminServer := controllers.NewAdminServer(adminConfig, adminOptions...) adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
auth.Store.Options.Secure = adminConfig.UseTLS middleware.Store.Options.Secure = adminConfig.UseTLS
phishConfig := conf.PhishConf phishConfig := conf.PhishConf
phishServer := controllers.NewPhishingServer(phishConfig) phishServer := controllers.NewPhishingServer(phishConfig)

View File

@ -9,11 +9,12 @@ var gulp = require('gulp'),
concat = require('gulp-concat'), concat = require('gulp-concat'),
uglify = require('gulp-uglify'), uglify = require('gulp-uglify'),
cleanCSS = require('gulp-clean-css'), cleanCSS = require('gulp-clean-css'),
babel = require('gulp-babel'),
js_directory = 'static/js/src/', js_directory = 'static/js/src/',
css_directory = 'static/css/', css_directory = 'static/css/',
vendor_directory = js_directory + 'vendor/', vendor_directory = js_directory + 'vendor/',
app_directory = js_directory + 'app/**/*.js', app_directory = js_directory + 'app/',
dest_js_directory = 'static/js/dist/', dest_js_directory = 'static/js/dist/',
dest_css_directory = 'static/css/dist/'; dest_css_directory = 'static/css/dist/';
@ -48,8 +49,19 @@ vendorjs = function () {
} }
scripts = function () { scripts = function () {
// Gophish app files // Gophish app files - non-ES6
return gulp.src(app_directory) return gulp.src([
app_directory + 'autocomplete.js',
app_directory + 'campaign_results.js',
app_directory + 'campaigns.js',
app_directory + 'dashboard.js',
app_directory + 'groups.js',
app_directory + 'landing_pages.js',
app_directory + 'sending_profiles.js',
app_directory + 'settings.js',
app_directory + 'templates.js',
app_directory + 'gophish.js',
])
.pipe(rename({ .pipe(rename({
suffix: '.min' suffix: '.min'
})) }))

View File

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/gophish/gophish/auth"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
@ -31,6 +30,15 @@ func CSRFExceptions(handler http.Handler) http.HandlerFunc {
} }
} }
// Use allows us to stack middleware to process the request
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
for _, m := range mid {
handler = m(handler)
}
return handler
}
// GetContext wraps each request in a function which fills in the context for a given request. // GetContext wraps each request in a function which fills in the context for a given request.
// This includes setting the User and Session keys and values as necessary for use in later functions. // This includes setting the User and Session keys and values as necessary for use in later functions.
func GetContext(handler http.Handler) http.HandlerFunc { func GetContext(handler http.Handler) http.HandlerFunc {
@ -43,7 +51,7 @@ func GetContext(handler http.Handler) http.HandlerFunc {
} }
// Set the context appropriately here. // Set the context appropriately here.
// Set the session // Set the session
session, _ := auth.Store.Get(r, "gophish") session, _ := Store.Get(r, "gophish")
// Put the session in the context so that we can // Put the session in the context so that we can
// reuse the values in different handlers // reuse the values in different handlers
r = ctx.Set(r, "session", session) r = ctx.Set(r, "session", session)
@ -107,11 +115,12 @@ func RequireLogin(handler http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if u := ctx.Get(r, "user"); u != nil { if u := ctx.Get(r, "user"); u != nil {
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
} else { return
}
q := r.URL.Query() q := r.URL.Query()
q.Set("next", r.URL.Path) q.Set("next", r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect) http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
} return
} }
} }

23
middleware/session.go Normal file
View File

@ -0,0 +1,23 @@
package middleware
import (
"encoding/gob"
"github.com/gophish/gophish/models"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// init registers the necessary models to be saved in the session later
func init() {
gob.Register(&models.User{})
gob.Register(&models.Flash{})
Store.Options.HttpOnly = true
// This sets the maxAge to 5 days for all cookies
Store.MaxAge(86400 * 5)
}
// Store contains the session information for the request
var Store = sessions.NewCookieStore(
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
[]byte(securecookie.GenerateRandomKey(32)))

View File

@ -48,7 +48,7 @@ const (
// Role represents a user role within Gophish. Each user has a single role // Role represents a user role within Gophish. Each user has a single role
// which maps to a set of permissions. // which maps to a set of permissions.
type Role struct { type Role struct {
ID int64 `json:"id"` ID int64 `json:"-"`
Slug string `json:"slug"` Slug string `json:"slug"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`

View File

@ -1,11 +1,22 @@
package models package models
import (
"errors"
log "github.com/gophish/gophish/logger"
)
// ErrModifyingOnlyAdmin occurs when there is an attempt to modify the only
// user account with the Admin role in such a way that there will be no user
// accounts left in Gophish with that role.
var ErrModifyingOnlyAdmin = errors.New("Cannot remove the only administrator")
// User represents the user model for gophish. // User represents the user model for gophish.
type User struct { type User struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Username string `json:"username" sql:"not null;unique"` Username string `json:"username" sql:"not null;unique"`
Hash string `json:"-"` Hash string `json:"-"`
ApiKey string `json:"api_key" sql:"not null;unique"` ApiKey string `json:"-" sql:"not null;unique"`
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"` Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
RoleID int64 `json:"-"` RoleID int64 `json:"-"`
} }
@ -18,6 +29,13 @@ func GetUser(id int64) (User, error) {
return u, err return u, err
} }
// GetUsers returns the users registered in Gophish
func GetUsers() ([]User, error) {
us := []User{}
err := db.Preload("Role").Find(&us).Error
return us, err
}
// GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an // GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an
// error is thrown. // error is thrown.
func GetUserByAPIKey(key string) (User, error) { func GetUserByAPIKey(key string) (User, error) {
@ -39,3 +57,102 @@ func PutUser(u *User) error {
err := db.Save(u).Error err := db.Save(u).Error
return err return err
} }
// EnsureEnoughAdmins ensures that there is more than one user account in
// Gophish with the Admin role. This function is meant to be called before
// modifying a user account with the Admin role in a non-revokable way.
func EnsureEnoughAdmins() error {
role, err := GetRoleBySlug(RoleAdmin)
if err != nil {
return err
}
var adminCount int
err = db.Model(&User{}).Where("role_id=?", role.ID).Count(&adminCount).Error
if err != nil {
return err
}
if adminCount == 1 {
return ErrModifyingOnlyAdmin
}
return nil
}
// DeleteUser deletes the given user. To ensure that there is always at least
// one user account with the Admin role, this function will refuse to delete
// the last Admin.
func DeleteUser(id int64) error {
existing, err := GetUser(id)
if err != nil {
return err
}
// If the user is an admin, we need to verify that it's not the last one.
if existing.Role.Slug == RoleAdmin {
err = EnsureEnoughAdmins()
if err != nil {
return err
}
}
campaigns, err := GetCampaigns(id)
if err != nil {
return err
}
// Delete the campaigns
log.Infof("Deleting campaigns for user ID %d", id)
for _, campaign := range campaigns {
err = DeleteCampaign(campaign.Id)
if err != nil {
return err
}
}
log.Infof("Deleting pages for user ID %d", id)
// Delete the landing pages
pages, err := GetPages(id)
if err != nil {
return err
}
for _, page := range pages {
err = DeletePage(page.Id, id)
if err != nil {
return err
}
}
// Delete the templates
log.Infof("Deleting templates for user ID %d", id)
templates, err := GetTemplates(id)
if err != nil {
return err
}
for _, template := range templates {
err = DeleteTemplate(template.Id, id)
if err != nil {
return err
}
}
// Delete the groups
log.Infof("Deleting groups for user ID %d", id)
groups, err := GetGroups(id)
if err != nil {
return err
}
for _, group := range groups {
err = DeleteGroup(&group)
if err != nil {
return err
}
}
// Delete the sending profiles
log.Infof("Deleting sending profiles for user ID %d", id)
profiles, err := GetSMTPs(id)
if err != nil {
return err
}
for _, profile := range profiles {
err = DeleteSMTP(profile.Id, id)
if err != nil {
return err
}
}
// Finally, delete the user
err = db.Where("id=?", id).Delete(&User{}).Error
return err
}

View File

@ -59,3 +59,44 @@ func (s *ModelsSuite) TestGeneratedAPIKey(c *check.C) {
c.Assert(err, check.Equals, nil) c.Assert(err, check.Equals, nil)
c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012") c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012")
} }
func (s *ModelsSuite) verifyRoleCount(c *check.C, roleID, expected int64) {
var adminCount int64
err := db.Model(&User{}).Where("role_id=?", roleID).Count(&adminCount).Error
c.Assert(err, check.Equals, nil)
c.Assert(adminCount, check.Equals, expected)
}
func (s *ModelsSuite) TestDeleteLastAdmin(c *check.C) {
// Create a new admin user
role, err := GetRoleBySlug(RoleAdmin)
c.Assert(err, check.Equals, nil)
newAdmin := User{
Username: "new-admin",
Hash: "123456",
ApiKey: "123456",
Role: role,
RoleID: role.ID,
}
err = PutUser(&newAdmin)
c.Assert(err, check.Equals, nil)
// Ensure that there are two admins
s.verifyRoleCount(c, role.ID, 2)
// Delete the newly created admin - this should work since we have more
// than one current admin.
err = DeleteUser(newAdmin.Id)
c.Assert(err, check.Equals, nil)
// Verify that we now have one admin
s.verifyRoleCount(c, role.ID, 1)
// Try to delete the last admin - this should fail since we always want at
// least one admin active in Gophish.
err = DeleteUser(1)
c.Assert(err, check.Equals, ErrModifyingOnlyAdmin)
// Verify that the admin wasn't deleted
s.verifyRoleCount(c, role.ID, 1)
}

View File

@ -12,8 +12,12 @@
}, },
"homepage": "https://getgophish.com", "homepage": "https://getgophish.com",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"babel-loader": "^8.0.6",
"clean-css": "^4.2.1", "clean-css": "^4.2.1",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.0.0", "gulp-clean-css": "^4.0.0",
"gulp-cli": "^2.2.0", "gulp-cli": "^2.2.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
@ -22,6 +26,8 @@
"gulp-uglify": "^3.0.2", "gulp-uglify": "^3.0.2",
"gulp-wrap": "^0.15.0", "gulp-wrap": "^0.15.0",
"jshint": "^2.10.2", "jshint": "^2.10.2",
"jshint-stylish": "^2.2.1" "jshint-stylish": "^2.2.1",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2"
} }
} }

File diff suppressed because one or more lines are too long

4
static/css/main.css vendored
View File

@ -702,6 +702,10 @@ table.dataTable {
background-color: #37485a; background-color: #37485a;
} }
.nav-badge {
margin-top: 5px;
}
#resultsMapContainer { #resultsMapContainer {
display: none; display: none;
} }

View File

@ -1 +1 @@
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function query(e,t,n,r){return $.ajax({url:"/api"+e,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)}})}function escapeHtml(e){return $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};$(document).ready(function(){var t=location.pathname;$(".nav-sidebar li").each(function(){var e=$(this);e.find("a").attr("href")===t&&e.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()}); function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function query(e,t,r,n){return $.ajax({url:"/api"+e,async:n,method:t,data:JSON.stringify(r),dataType:"json",contentType:"application/json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)}})}function escapeHtml(e){return $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}window.escapeHtml=escapeHtml;var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},users:{get:function(){return query("/users/","GET",{},!0)},post:function(e){return query("/users/","POST",e,!0)}},userId:{get:function(e){return query("/users/"+e,"GET",{},!0)},put:function(e){return query("/users/"+e.id,"PUT",e,!0)},delete:function(e){return query("/users/"+e,"DELETE",{},!0)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};window.api=api,$(document).ready(function(){var t=location.pathname;$(".nav-sidebar li").each(function(){var e=$(this);e.find("a").attr("href")===t&&e.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});

1
static/js/dist/app/groups.min.js vendored Normal file
View File

@ -0,0 +1 @@
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download",e),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(s){var e=groups.find(function(e){return e.id===s});e&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(s).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});

View File

@ -1 +1 @@
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download",e),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(s){var e=groups.find(function(e){return e.id===s});e&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(s).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)}); !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){var r=[],n=function(){$("#username").val(""),$("#password").val(""),$("#confirm_password").val(""),$("#role").val(""),$("#modal\\.flashes").empty()},o=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){if($("#password").val()===$("#confirm_password").val()){var t={username:$("#username").val(),password:$("#password").val(),role:$("#role").val()};-1!=e?(t.id=e,api.userId.put(t).success(function(e){successFlash("User ".concat(t.username," updated successfully!")),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.users.post(t).success(function(e){successFlash("User ".concat(t.username," registered successfully!")),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}else modalError("Passwords must match.")}(e)}),$("#role").select2(),-1==e?($("#role").val("user"),$("#role").trigger("change")):api.userId.get(e).success(function(e){$("#username").val(e.username),$("#role").val(e.role.slug),$("#role").trigger("change")}).error(function(){errorFlash("Error fetching user")})},s=function(){$("#userTable").hide(),$("#loading").show(),api.users.get().success(function(e){r=e,$("#loading").hide(),$("#userTable").show();var t=$("#userTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(r,function(e,r){t.row.add([escapeHtml(r.username),escapeHtml(r.role.name),"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='"+r.id+"'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger delete_button' data-user-id='"+r.id+"'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}).error(function(){errorFlash("Error fetching users")})};$(document).ready(function(){s(),$("#modal").on("hide.bs.modal",function(){n()}),$.fn.select2.defaults.set("width","100%"),$.fn.select2.defaults.set("dropdownParent",$("#role-select")),$.fn.select2.defaults.set("theme","bootstrap"),$.fn.select2.defaults.set("sorter",function(e){return e.sort(function(e,t){return e.text.toLowerCase()>t.text.toLowerCase()?1:e.text.toLowerCase()<t.text.toLowerCase()?-1:0})}),$("#new_button").on("click",function(){o(-1)}),$("#userTable").on("click",".edit_button",function(e){o($(this).attr("data-user-id"))}),$("#userTable").on("click",".delete_button",function(e){var t,n;t=$(this).attr("data-user-id"),(n=r.find(function(e){return e.id==t}))&&swal({title:"Are you sure?",text:"This will delete the account for ".concat(n.username," as well as all of the objects they have created.\n\nThis can't be undone!"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,r){api.userId.delete(t).success(function(t){e()}).error(function(e){r(e.responseJSON.message)})})}}).then(function(){swal("User Deleted!","The user account for ".concat(n.username," and all associated objects have been deleted!"),"success"),$('button:contains("OK")').on("click",function(){location.reload()})})})})}]);

View File

@ -32,6 +32,7 @@ function query(endpoint, method, data, async) {
function escapeHtml(text) { function escapeHtml(text) {
return $("<div/>").text(text).html() return $("<div/>").text(text).html()
} }
window.escapeHtml = escapeHtml
function unescapeHtml(html) { function unescapeHtml(html) {
return $("<div/>").html(html).text() return $("<div/>").html(html).text()
@ -196,6 +197,32 @@ var api = {
return query("/smtp/" + id, "DELETE", {}, false) return query("/smtp/" + id, "DELETE", {}, false)
} }
}, },
// users contains the endpoints for /users
users: {
// get() - Queries the API for GET /users
get: function () {
return query("/users/", "GET", {}, true)
},
// post() - Posts a user to POST /users
post: function (user) {
return query("/users/", "POST", user, true)
}
},
// userId contains the endpoints for /users/:id
userId: {
// get() - Queries the API for GET /users/:id
get: function (id) {
return query("/users/" + id, "GET", {}, true)
},
// put() - Puts a user to PUT /users/:id
put: function (user) {
return query("/users/" + user.id, "PUT", user, true)
},
// delete() - Deletes a user at DELETE /users/:id
delete: function (id) {
return query("/users/" + id, "DELETE", {}, true)
}
},
// import handles all of the "import" functions in the api // import handles all of the "import" functions in the api
import_email: function (req) { import_email: function (req) {
return query("/import/email", "POST", req, false) return query("/import/email", "POST", req, false)
@ -212,6 +239,7 @@ var api = {
return query("/reset", "POST", {}, true) return query("/reset", "POST", {}, true)
} }
} }
window.api = api
// Register our moment.js datatables listeners // Register our moment.js datatables listeners
$(document).ready(function () { $(document).ready(function () {

284
static/js/src/app/groups.js Normal file
View File

@ -0,0 +1,284 @@
var groups = []
// Save attempts to POST or PUT to /groups/
function save(id) {
var targets = []
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
targets.push({
first_name: unescapeHtml(target[0]),
last_name: unescapeHtml(target[1]),
email: unescapeHtml(target[2]),
position: unescapeHtml(target[3])
})
})
var group = {
name: $("#name").val(),
targets: targets
}
// Submit the group
if (id != -1) {
// If we're just editing an existing group,
// we need to PUT /groups/:id
group.id = id
api.groupId.put(group)
.success(function (data) {
successFlash("Group updated successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function (data) {
modalError(data.responseJSON.message)
})
} else {
// Else, if this is a new group, POST it
// to /groups
api.groups.post(group)
.success(function (data) {
successFlash("Group added successfully!")
load()
dismiss()
$("#modal").modal('hide')
})
.error(function (data) {
modalError(data.responseJSON.message)
})
}
}
function dismiss() {
$("#targetsTable").dataTable().DataTable().clear().draw()
$("#name").val("")
$("#modal\\.flashes").empty()
}
function edit(id) {
targets = $("#targetsTable").dataTable({
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
})
$("#modalSubmit").unbind('click').click(function () {
save(id)
})
if (id == -1) {
var group = {}
} else {
api.groupId.get(id)
.success(function (group) {
$("#name").val(group.name)
$.each(group.targets, function (i, record) {
targets.DataTable()
.row.add([
escapeHtml(record.first_name),
escapeHtml(record.last_name),
escapeHtml(record.email),
escapeHtml(record.position),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
});
})
.error(function () {
errorFlash("Error fetching group")
})
}
// Handle file uploads
$("#csvupload").fileupload({
url: "/api/import/group",
dataType: "json",
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
},
add: function (e, data) {
$("#modal\\.flashes").empty()
var acceptFileTypes = /(csv|txt)$/i;
var filename = data.originalFiles[0]['name']
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
modalError("Unsupported file extension (use .csv or .txt)")
return false;
}
data.submit();
},
done: function (e, data) {
$.each(data.result, function (i, record) {
addTarget(
record.first_name,
record.last_name,
record.email,
record.position);
});
targets.DataTable().draw();
}
})
}
var downloadCSVTemplate = function () {
var csvScope = [{
'First Name': 'Example',
'Last Name': 'User',
'Email': 'foobar@example.com',
'Position': 'Systems Administrator'
}]
var filename = 'group_template.csv'
var csvString = Papa.unparse(csvScope, {})
var csvData = new Blob([csvString], {
type: 'text/csv;charset=utf-8;'
});
if (navigator.msSaveBlob) {
navigator.msSaveBlob(csvData, filename);
} else {
var csvURL = window.URL.createObjectURL(csvData);
var dlLink = document.createElement('a');
dlLink.href = csvURL;
dlLink.setAttribute('download', filename)
document.body.appendChild(dlLink)
dlLink.click();
document.body.removeChild(dlLink)
}
}
var deleteGroup = function (id) {
var group = groups.find(function (x) {
return x.id === id
})
if (!group) {
return
}
swal({
title: "Are you sure?",
text: "This will delete the group. This can't be undone!",
type: "warning",
animation: false,
showCancelButton: true,
confirmButtonText: "Delete " + escapeHtml(group.name),
confirmButtonColor: "#428bca",
reverseButtons: true,
allowOutsideClick: false,
preConfirm: function () {
return new Promise(function (resolve, reject) {
api.groupId.delete(id)
.success(function (msg) {
resolve()
})
.error(function (data) {
reject(data.responseJSON.message)
})
})
}
}).then(function () {
swal(
'Group Deleted!',
'This group has been deleted!',
'success'
);
$('button:contains("OK")').on('click', function () {
location.reload()
})
})
}
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
// Create new data row.
var email = escapeHtml(emailInput).toLowerCase();
var newRow = [
escapeHtml(firstNameInput),
escapeHtml(lastNameInput),
email,
escapeHtml(positionInput),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
];
// Check table to see if email already exists.
var targetsTable = targets.DataTable();
var existingRowIndex = targetsTable
.column(2, {
order: "index"
}) // Email column has index of 2
.data()
.indexOf(email);
// Update or add new row as necessary.
if (existingRowIndex >= 0) {
targetsTable
.row(existingRowIndex, {
order: "index"
})
.data(newRow);
} else {
targetsTable.row.add(newRow);
}
}
function load() {
$("#groupTable").hide()
$("#emptyMessage").hide()
$("#loading").show()
api.groups.summary()
.success(function (response) {
$("#loading").hide()
if (response.total > 0) {
groups = response.groups
$("#emptyMessage").hide()
$("#groupTable").show()
var groupTable = $("#groupTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
groupTable.clear();
$.each(groups, function (i, group) {
groupTable.row.add([
escapeHtml(group.name),
escapeHtml(group.num_targets),
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
<i class='fa fa-pencil'></i>\
</button>\
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
})
} else {
$("#emptyMessage").show()
}
})
.error(function () {
errorFlash("Error fetching groups")
})
}
$(document).ready(function () {
load()
// Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function () {
addTarget(
$("#firstName").val(),
$("#lastName").val(),
$("#email").val(),
$("#position").val());
targets.DataTable().draw();
// Reset user input.
$("#targetForm>div>input").val('');
$("#firstName").focus();
return false;
});
// Handle Deletion
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
targets.DataTable()
.row($(this).parents('tr'))
.remove()
.draw();
});
$("#modal").on("hide.bs.modal", function () {
dismiss();
});
$("#csv-template").click(downloadCSVTemplate)
});

View File

@ -1,28 +1,25 @@
var groups = [] let users = []
// Save attempts to POST or PUT to /groups/ // Save attempts to POST or PUT to /users/
function save(id) { const save = (id) => {
var targets = [] // Validate that the passwords match
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) { if ($("#password").val() !== $("#confirm_password").val()) {
targets.push({ modalError("Passwords must match.")
first_name: unescapeHtml(target[0]), return
last_name: unescapeHtml(target[1]),
email: unescapeHtml(target[2]),
position: unescapeHtml(target[3])
})
})
var group = {
name: $("#name").val(),
targets: targets
} }
// Submit the group let user = {
username: $("#username").val(),
password: $("#password").val(),
role: $("#role").val()
}
// Submit the user
if (id != -1) { if (id != -1) {
// If we're just editing an existing group, // If we're just editing an existing user,
// we need to PUT /groups/:id // we need to PUT /user/:id
group.id = id user.id = id
api.groupId.put(group) api.userId.put(user)
.success(function (data) { .success(function (data) {
successFlash("Group updated successfully!") successFlash(`User ${user.username} updated successfully!`)
load() load()
dismiss() dismiss()
$("#modal").modal('hide') $("#modal").modal('hide')
@ -31,11 +28,11 @@ function save(id) {
modalError(data.responseJSON.message) modalError(data.responseJSON.message)
}) })
} else { } else {
// Else, if this is a new group, POST it // Else, if this is a new user, POST it
// to /groups // to /user
api.groups.post(group) api.users.post(user)
.success(function (data) { .success(function (data) {
successFlash("Group added successfully!") successFlash(`User ${user.username} registered successfully!`)
load() load()
dismiss() dismiss()
$("#modal").modal('hide') $("#modal").modal('hide')
@ -46,133 +43,65 @@ function save(id) {
} }
} }
function dismiss() { const dismiss = () => {
$("#targetsTable").dataTable().DataTable().clear().draw() $("#username").val("")
$("#name").val("") $("#password").val("")
$("#confirm_password").val("")
$("#role").val("")
$("#modal\\.flashes").empty() $("#modal\\.flashes").empty()
} }
function edit(id) { const edit = (id) => {
targets = $("#targetsTable").dataTable({ $("#modalSubmit").unbind('click').click(() => {
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
})
$("#modalSubmit").unbind('click').click(function () {
save(id) save(id)
}) })
$("#role").select2()
if (id == -1) { if (id == -1) {
var group = {} $("#role").val("user")
$("#role").trigger("change")
} else { } else {
api.groupId.get(id) api.userId.get(id)
.success(function (group) { .success(function (user) {
$("#name").val(group.name) $("#username").val(user.username)
$.each(group.targets, function (i, record) { $("#role").val(user.role.slug)
targets.DataTable() $("#role").trigger("change")
.row.add([
escapeHtml(record.first_name),
escapeHtml(record.last_name),
escapeHtml(record.email),
escapeHtml(record.position),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw()
});
}) })
.error(function () { .error(function () {
errorFlash("Error fetching group") errorFlash("Error fetching user")
}) })
} }
// Handle file uploads
$("#csvupload").fileupload({
url: "/api/import/group",
dataType: "json",
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
},
add: function (e, data) {
$("#modal\\.flashes").empty()
var acceptFileTypes = /(csv|txt)$/i;
var filename = data.originalFiles[0]['name']
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
modalError("Unsupported file extension (use .csv or .txt)")
return false;
}
data.submit();
},
done: function (e, data) {
$.each(data.result, function (i, record) {
addTarget(
record.first_name,
record.last_name,
record.email,
record.position);
});
targets.DataTable().draw();
}
})
}
var downloadCSVTemplate = function () {
var csvScope = [{
'First Name': 'Example',
'Last Name': 'User',
'Email': 'foobar@example.com',
'Position': 'Systems Administrator'
}]
var filename = 'group_template.csv'
var csvString = Papa.unparse(csvScope, {})
var csvData = new Blob([csvString], {
type: 'text/csv;charset=utf-8;'
});
if (navigator.msSaveBlob) {
navigator.msSaveBlob(csvData, filename);
} else {
var csvURL = window.URL.createObjectURL(csvData);
var dlLink = document.createElement('a');
dlLink.href = csvURL;
dlLink.setAttribute('download', filename)
document.body.appendChild(dlLink)
dlLink.click();
document.body.removeChild(dlLink)
}
} }
const deleteUser = (id) => {
var deleteGroup = function (id) { var user = users.find(x => x.id == id)
var group = groups.find(function (x) { if (!user) {
return x.id === id
})
if (!group) {
return return
} }
swal({ swal({
title: "Are you sure?", title: "Are you sure?",
text: "This will delete the group. This can't be undone!", text: `This will delete the account for ${user.username} as well as all of the objects they have created.\n\nThis can't be undone!`,
type: "warning", type: "warning",
animation: false, animation: false,
showCancelButton: true, showCancelButton: true,
confirmButtonText: "Delete " + escapeHtml(group.name), confirmButtonText: "Delete",
confirmButtonColor: "#428bca", confirmButtonColor: "#428bca",
reverseButtons: true, reverseButtons: true,
allowOutsideClick: false, allowOutsideClick: false,
preConfirm: function () { preConfirm: function () {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
api.groupId.delete(id) api.userId.delete(id)
.success(function (msg) { .success((msg) => {
resolve() resolve()
}) })
.error(function (data) { .error((data) => {
reject(data.responseJSON.message) reject(data.responseJSON.message)
}) })
}) })
} }
}).then(function () { }).then(function () {
swal( swal(
'Group Deleted!', 'User Deleted!',
'This group has been deleted!', `The user account for ${user.username} and all associated objects have been deleted!`,
'success' 'success'
); );
$('button:contains("OK")').on('click', function () { $('button:contains("OK")').on('click', function () {
@ -181,104 +110,69 @@ var deleteGroup = function (id) {
}) })
} }
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
// Create new data row.
var email = escapeHtml(emailInput).toLowerCase();
var newRow = [
escapeHtml(firstNameInput),
escapeHtml(lastNameInput),
email,
escapeHtml(positionInput),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
];
// Check table to see if email already exists. const load = () => {
var targetsTable = targets.DataTable(); $("#userTable").hide()
var existingRowIndex = targetsTable
.column(2, {
order: "index"
}) // Email column has index of 2
.data()
.indexOf(email);
// Update or add new row as necessary.
if (existingRowIndex >= 0) {
targetsTable
.row(existingRowIndex, {
order: "index"
})
.data(newRow);
} else {
targetsTable.row.add(newRow);
}
}
function load() {
$("#groupTable").hide()
$("#emptyMessage").hide()
$("#loading").show() $("#loading").show()
api.groups.summary() api.users.get()
.success(function (response) { .success((us) => {
users = us
$("#loading").hide() $("#loading").hide()
if (response.total > 0) { $("#userTable").show()
groups = response.groups let userTable = $("#userTable").DataTable({
$("#emptyMessage").hide()
$("#groupTable").show()
var groupTable = $("#groupTable").DataTable({
destroy: true, destroy: true,
columnDefs: [{ columnDefs: [{
orderable: false, orderable: false,
targets: "no-sort" targets: "no-sort"
}] }]
}); });
groupTable.clear(); userTable.clear();
$.each(groups, function (i, group) { $.each(users, (i, user) => {
groupTable.row.add([ userTable.row.add([
escapeHtml(group.name), escapeHtml(user.username),
escapeHtml(group.num_targets), escapeHtml(user.role.name),
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'), "<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
<i class='fa fa-pencil'></i>\ <i class='fa fa-pencil'></i>\
</button>\ </button>\
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\ <button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ]).draw()
}) })
} else {
$("#emptyMessage").show()
}
}) })
.error(function () { .error(() => {
errorFlash("Error fetching groups") errorFlash("Error fetching users")
}) })
} }
$(document).ready(function () { $(document).ready(function () {
load() load()
// Setup the event listeners // Setup the event listeners
// Handle manual additions
$("#targetForm").submit(function () {
addTarget(
$("#firstName").val(),
$("#lastName").val(),
$("#email").val(),
$("#position").val());
targets.DataTable().draw();
// Reset user input.
$("#targetForm>div>input").val('');
$("#firstName").focus();
return false;
});
// Handle Deletion
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
targets.DataTable()
.row($(this).parents('tr'))
.remove()
.draw();
});
$("#modal").on("hide.bs.modal", function () { $("#modal").on("hide.bs.modal", function () {
dismiss(); dismiss();
}); });
$("#csv-template").click(downloadCSVTemplate) // Select2 Defaults
$.fn.select2.defaults.set("width", "100%");
$.fn.select2.defaults.set("dropdownParent", $("#role-select"));
$.fn.select2.defaults.set("theme", "bootstrap");
$.fn.select2.defaults.set("sorter", function (data) {
return data.sort(function (a, b) {
if (a.text.toLowerCase() > b.text.toLowerCase()) {
return 1;
}
if (a.text.toLowerCase() < b.text.toLowerCase()) {
return -1;
}
return 0;
});
})
$("#new_button").on("click", function () {
edit(-1)
})
$("#userTable").on('click', '.edit_button', function (e) {
edit($(this).attr('data-user-id'))
})
$("#userTable").on('click', '.delete_button', function (e) {
deleteUser($(this).attr('data-user-id'))
})
}); });

106
templates/groups.html Normal file
View File

@ -0,0 +1,106 @@
{{define "body"}}
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row">
<h1 class="page-header">
Users &amp; Groups
</h1>
</div>
<div id="flashes" class="row"></div>
<div class="row">
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
data-target="#modal">
<i class="fa fa-plus"></i> New Group</button>
</div>
&nbsp;
<div id="loading">
<i class="fa fa-spinner fa-spin fa-4x"></i>
</div>
<div id="emptyMessage" class="row" style="display:none;">
<div class="alert alert-info">
No groups created yet. Let's create one!
</div>
</div>
<div class="row">
<table id="groupTable" class="table" style="display:none;">
<thead>
<tr>
<th>Name</th>
<th># of Members</th>
<th>Modified Date</th>
<th class="col-md-2 no-sort"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
</div>
<div class="modal-body">
<div class="row" id="modal.flashes"></div>
<label class="control-label" for="name">Name:</label>
<div class="form-group">
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
autofocus />
</div>
<div class="form-group">
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
title="Supports CSV files" id="fileUpload">
<i class="fa fa-plus"></i> Bulk Import Users
<input type="file" id="csvupload" multiple>
</span>
<span id="csv-template" class="text-muted small">
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
</div>
<div class="row">
<form id="targetForm">
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="First Name" id="firstName">
</div>
<div class="col-sm-2">
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
</div>
<div class="col-sm-3">
<input type="email" class="form-control" placeholder="Email" id="email" required>
</div>
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Position" id="position">
</div>
<div class="col-sm-1">
<button type="submit" class="btn btn-danger btn-lg">
<i class="fa fa-plus"></i> Add</button>
</div>
</form>
</div>
<br />
<table id="targetsTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Position</th>
<th class="no-sort"></th>
<tbody>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
</div>
</div>
</div>
</div>
{{end}} {{define "scripts"}}
<script src="/js/dist/app/groups.min.js"></script>
{{end}}

View File

@ -10,7 +10,7 @@
<a href="/campaigns">Campaigns</a> <a href="/campaigns">Campaigns</a>
</li> </li>
<li> <li>
<a href="/users">Users &amp; Groups</a> <a href="/groups">Users &amp; Groups</a>
</li> </li>
<li> <a href="/templates">Email Templates</a> <li> <a href="/templates">Email Templates</a>
</li> </li>
@ -21,8 +21,13 @@
<a href="/sending_profiles">Sending Profiles</a> <a href="/sending_profiles">Sending Profiles</a>
</li> </li>
<li> <li>
<a href="/settings">Settings <span class="badge pull-right">Admin</span></a> <a href="/settings">Account Settings</span></a>
</li> </li>
{{if .ModifySystem}}
<li>
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
</li>
{{end}}
<li> <li>
<hr> <hr>
</li> </li>

View File

@ -1,62 +0,0 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
<link rel="shortcut icon" href="/favicon.png">
<title>Gophish - {{ .Title }}</title>
<link href="/css/dist/gophish.css" rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<img class="navbar-logo" src="/images/logo_inv_small.png" />
<a class="navbar-brand" href="/">&nbsp;gophish</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a id="login-button" href="/login">
<button type="button" class="btn btn-primary">Login</button>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<form class="form-signin" action="/register" method="POST">
<img id="logo" src="/images/logo_purple.png" />
<h2 class="form-signin-heading">Please register below</h2>
{{template "flashes" .Flashes}}
<input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus />
<input type="password" name="password" class="form-control middle-input" placeholder="Password"
autocomplete="off" required />
<input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password"
autocomplete="off" required />
<input type="hidden" name="csrf_token" value="{{.Token}}" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
</form>
</div>
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/dist/vendor.min.js"></script>
</body>
</html>
{{ end }}

View File

@ -8,7 +8,8 @@
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab" <li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
data-toggle="tab">Account Settings</a></li> data-toggle="tab">Account Settings</a></li>
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI Settings</a></li> <li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
Settings</a></li>
</ul> </ul>
<!-- Tab Panes --> <!-- Tab Panes -->
<div class="tab-content"> <div class="tab-content">
@ -22,19 +23,12 @@
</div> </div>
</div> </div>
<br /> <br />
<div class="row">
<label class="col-sm-2 control-label form-label">Register a New User</label>
<div class="col-md-6">
<a href="/register" class="btn btn-primary"><i class="fa fa-plus"></i> Add User</a>
</div>
</div>
<br />
{{end}} {{end}}
<div class="row"> <div class="row">
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label> <label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}" class="form-control" <input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}"
readonly /> class="form-control" readonly />
</div> </div>
<form id="apiResetForm"> <form id="apiResetForm">
<button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button> <button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
@ -46,26 +40,30 @@
<div class="row"> <div class="row">
<label for="username" class="col-sm-2 control-label form-label">Username:</label> <label for="username" class="col-sm-2 control-label form-label">Username:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="text" id="username" name="username" value="{{.User.Username}}" class="form-control" /> <input type="text" id="username" name="username" value="{{.User.Username}}"
class="form-control" />
</div> </div>
</div> </div>
<br /> <br />
<div class="row"> <div class="row">
<label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label> <label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="password" id="current_password" name="current_password" autocomplete="off" class="form-control" /> <input type="password" id="current_password" name="current_password" autocomplete="off"
class="form-control" />
</div> </div>
</div> </div>
<br /> <br />
<div class="row"> <div class="row">
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label> <label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="password" id="new_password" name="new_password" autocomplete="off" class="form-control" /> <input type="password" id="new_password" name="new_password" autocomplete="off"
class="form-control" />
</div> </div>
</div> </div>
<br /> <br />
<div class="row"> <div class="row">
<label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New Password:</label> <label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New
Password:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off" <input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
class="form-control" /> class="form-control" />

View File

@ -2,31 +2,25 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="row"> <div class="row">
<h1 class="page-header"> <h1 class="page-header">
Users &amp; Groups {{.Title}}
</h1> </h1>
</div> </div>
<div id="flashes" class="row"></div> <div id="flashes" class="row"></div>
<div class="row"> <div class="row">
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static" <button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
data-target="#modal"> data-user-id="-1" data-target="#modal">
<i class="fa fa-plus"></i> New Group</button> <i class="fa fa-plus"></i> New User</button>
</div> </div>
&nbsp; &nbsp;
<div id="loading"> <div id="loading">
<i class="fa fa-spinner fa-spin fa-4x"></i> <i class="fa fa-spinner fa-spin fa-4x"></i>
</div> </div>
<div id="emptyMessage" class="row" style="display:none;">
<div class="alert alert-info">
No groups created yet. Let's create one!
</div>
</div>
<div class="row"> <div class="row">
<table id="groupTable" class="table" style="display:none;"> <table id="userTable" class="table" style="display:none;">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Username</th>
<th># of Members</th> <th>Role</th>
<th>Modified Date</th>
<th class="col-md-2 no-sort"></th> <th class="col-md-2 no-sort"></th>
</tr> </tr>
</thead> </thead>
@ -43,56 +37,30 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<h4 class="modal-title" id="groupModalLabel">New Group</h4> <h4 class="modal-title" id="groupModalLabel">New User</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body" id="modal_body">
<div class="row" id="modal.flashes"></div> <div class="row" id="modal.flashes"></div>
<label class="control-label" for="name">Name:</label> <label class="control-label" for="username">Username:</label>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name" <input type="text" class="form-control" placeholder="Username" id="username" autofocus />
autofocus />
</div> </div>
<label class="control-label" for="password">Password:</label>
<div class="form-group"> <div class="form-group">
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right" title="Supports CSV files" <input type="password" class="form-control" placeholder="Password" id="password" required />
id="fileUpload">
<i class="fa fa-plus"></i> Bulk Import Users
<input type="file" id="csvupload" multiple>
</span>
<span id="csv-template" class="text-muted small">
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
</div> </div>
<div class="row"> <label class="control-label" for="confirm_password">Confirm Password:</label>
<form id="targetForm"> <div class="form-group">
<div class="col-sm-2"> <input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
<input type="text" class="form-control" placeholder="First Name" id="firstName"> required />
</div> </div>
<div class="col-sm-2"> <label class="control-label" for="role">Role:</label>
<input type="text" class="form-control" placeholder="Last Name" id="lastName"> <div class="form-group" id="role-select">
<select class="form-control" placeholder="" id="role" />
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div> </div>
<div class="col-sm-3">
<input type="email" class="form-control" placeholder="Email" id="email" required>
</div>
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Position" id="position">
</div>
<div class="col-sm-1">
<button type="submit" class="btn btn-danger btn-lg">
<i class="fa fa-plus"></i> Add</button>
</div>
</form>
</div>
<br />
<table id="targetsTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Position</th>
<th class="no-sort"></th>
<tbody>
</tbody>
</table>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View File

@ -21,6 +21,7 @@ import (
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/jordan-wright/email" "github.com/jordan-wright/email"
"golang.org/x/crypto/bcrypt"
) )
var ( var (
@ -190,3 +191,21 @@ func CheckAndCreateSSL(cp string, kp string) error {
log.Info("TLS Certificate Generation complete") log.Info("TLS Certificate Generation complete")
return nil return nil
} }
// GenerateSecureKey creates a secure key to use as an API key
func GenerateSecureKey() string {
// Inspired from gorilla/securecookie
k := make([]byte, 32)
io.ReadFull(rand.Reader, k)
return fmt.Sprintf("%x", k)
}
// NewHash hashes the provided password and returns the bcrypt hash (using the
// default 10 rounds) as a string.
func NewHash(pass string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}

21
webpack.config.js Normal file
View File

@ -0,0 +1,21 @@
const path = require('path');
module.exports = {
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
entry: {
users: './users',
},
output: {
path: path.resolve(__dirname, 'static', 'js', 'dist', 'app'),
filename: '[name].min.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}]
}
}

2352
yarn.lock

File diff suppressed because it is too large Load Diff