diff --git a/Makefile b/Makefile index 3b714ae..872a37d 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,14 @@ build-go: ## Build all Go binaries. @echo "build go files" $(GO) run build.go build +build-debug-go: ## Build all Go binaries. + @echo "build debug go files" + $(GO) run build.go debug + build: build-go +debug: build-debug-go + run: ## Run Server @GO111MODULE=on ./bin/linux-amd64/godbledger-web diff --git a/backend/api/admin.go b/backend/api/admin.go index 94176c5..ca523c8 100644 --- a/backend/api/admin.go +++ b/backend/api/admin.go @@ -2,11 +2,12 @@ package api import ( m "github.com/darcys22/godbledger-web/backend/models" + "github.com/darcys22/godbledger-web/backend/auth" "github.com/gin-gonic/gin" "net/http" ) -func NewUser(c *gin.Context) { +func NewUser(ctx *gin.Context) { var new_user m.PostNewUserCommand if err := ctx.BindJSON(&new_user); err != nil { diff --git a/backend/api/api.go b/backend/api/api.go index 192e3e6..9c8f930 100644 --- a/backend/api/api.go +++ b/backend/api/api.go @@ -15,17 +15,17 @@ var log = logrus.WithField("prefix", "API") func mapStatic(m *gin.Engine, dir string, prefix string) { headers := func() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Cache-Control", "public, max-age=3600") - c.Next() + return func(ctx *gin.Context) { + ctx.Writer.Header().Set("Cache-Control", "public, max-age=3600") + ctx.Next() } } if setting.Env == setting.DEV { headers = func() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") - c.Next() + return func(ctx *gin.Context) { + ctx.Writer.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") + ctx.Next() } } } @@ -102,6 +102,8 @@ func NewGin() *gin.Engine { m.LoadHTMLGlob(path.Join(setting.StaticRootPath, "views/*.html")) + InitLoginHandler() + InitUsersDatabase() register(m) return m diff --git a/backend/api/index.go b/backend/api/index.go index 5432f46..3ff15fd 100644 --- a/backend/api/index.go +++ b/backend/api/index.go @@ -5,30 +5,34 @@ import ( "net/http" ) -func Index(c *gin.Context) { - c.HTML(http.StatusOK, "index.html", nil) +func Index(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "index.html", nil) } -func Reports(c *gin.Context) { - c.HTML(http.StatusOK, "reports.html", nil) +func Reports(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "reports.html", nil) } -func Reconcile(c *gin.Context) { - c.HTML(http.StatusOK, "reconcile.html", nil) +func Reconcile(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "reconcile.html", nil) } -func Accounts(c *gin.Context) { - c.HTML(http.StatusOK, "accounts.html", nil) +func Accounts(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "accounts.html", nil) } -func Modules(c *gin.Context) { - c.HTML(http.StatusOK, "modules.html", nil) +func Modules(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "modules.html", nil) } -func User(c *gin.Context) { - c.HTML(http.StatusOK, "user.html", nil) +func User(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "user.html", nil) } -func Admin(c *gin.Context) { - c.HTML(http.StatusOK, "admin.html", nil) +func Admin(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "admin.html", nil) +} + +func LoginView(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "login.view.html", nil) } diff --git a/backend/api/journal.go b/backend/api/journal.go index 83d4b18..6499886 100644 --- a/backend/api/journal.go +++ b/backend/api/journal.go @@ -6,66 +6,66 @@ import ( "net/http" ) -func GetJournals(c *gin.Context) { +func GetJournals(ctx *gin.Context) { journalsModel := m.NewJournalsListing() err := journalsModel.SearchJournals() if err != nil { log.Errorf("Could not get journal listing (%v)", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, journalsModel) + ctx.JSON(http.StatusOK, journalsModel) } -func PostJournal(c *gin.Context) { +func PostJournal(ctx *gin.Context) { var journal m.PostJournalCommand - if err := c.BindJSON(&journal); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := ctx.BindJSON(&journal); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := journal.Save(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, journal) + ctx.JSON(http.StatusOK, journal) } -func DeleteJournal(c *gin.Context) { - id := c.Params.ByName("id") +func DeleteJournal(ctx *gin.Context) { + id := ctx.Params.ByName("id") if err := m.DeleteJournalCommand(id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } - c.String(http.StatusOK, "Success") + ctx.String(http.StatusOK, "Success") } -func GetJournal(c *gin.Context) { - id := c.Params.ByName("id") +func GetJournal(ctx *gin.Context) { + id := ctx.Params.ByName("id") journal, err := m.GetJournalCommand(id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, journal) + ctx.JSON(http.StatusOK, journal) } -func EditJournal(c *gin.Context) { - id := c.Params.ByName("id") +func EditJournal(ctx *gin.Context) { + id := ctx.Params.ByName("id") var journal m.PostJournalCommand - if err := c.BindJSON(&journal); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := ctx.BindJSON(&journal); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := journal.Save(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } if err := m.DeleteJournalCommand(id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, journal) + ctx.JSON(http.StatusOK, journal) } diff --git a/backend/api/login.go b/backend/api/login.go index 37d85e1..8cee737 100644 --- a/backend/api/login.go +++ b/backend/api/login.go @@ -6,12 +6,13 @@ import ( "net/url" "github.com/darcys22/godbledger-web/backend/auth" + "github.com/darcys22/godbledger-web/backend/setting" ) var ( - loginService auth.LoginService = auth.StaticLoginService() - jwtService auth.JWTService = auth.JWTAuthService() - loginController LoginController = LoginHandler(loginService, jwtService) + loginService auth.LoginService + jwtService auth.JWTService + loginController LoginController ) type LoginController struct { @@ -19,9 +20,10 @@ type LoginController struct { jwtService auth.JWTService } -func LoginHandler(loginService auth.LoginService, - jwtService auth.JWTService) LoginController { - return LoginController{ +func InitLoginHandler() { + loginService = auth.StaticLoginService(setting.GetConfig()) + jwtService = auth.JWTAuthService() + loginController = LoginController{ loginService: loginService, jwtService: jwtService, } @@ -46,24 +48,6 @@ func (controller *LoginController) Login(ctx *gin.Context) string { return "" } -func (controller *LoginController) NewUser(ctx *gin.Context) string { - var credential LoginCredentials - err := ctx.ShouldBind(&credential) - if err != nil { - return "no data found" - } - //isUserAuthenticated := controller.loginService.NewUser(credential.Email, credential.Password) - //if isUserAuthenticated { - //return controller.jwtService.GenerateToken(credential.Email, true) - - //} - return "" -} - -func LoginView(ctx *gin.Context) { - ctx.HTML(http.StatusOK, "login.view.html", nil) -} - func Login(ctx *gin.Context) { token := loginController.Login(ctx) if token != "" { diff --git a/backend/api/reconcile.go b/backend/api/reconcile.go index 93c4b38..6bef08a 100644 --- a/backend/api/reconcile.go +++ b/backend/api/reconcile.go @@ -7,22 +7,22 @@ import ( "github.com/gin-gonic/gin" ) -func GetExternalAccountListing(c *gin.Context) { - m.GetExternalAccountListing(c) +func GetExternalAccountListing(ctx *gin.Context) { + m.GetExternalAccountListing(ctx) } -func GetUnreconciledTransactions(c *gin.Context) { +func GetUnreconciledTransactions(ctx *gin.Context) { var request m.UnreconciledTransactionsRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := ctx.BindJSON(&request); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err, unreconciledTransactionsResult := m.UnreconciledTransactions(request) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, unreconciledTransactionsResult) + ctx.JSON(http.StatusOK, unreconciledTransactionsResult) } diff --git a/backend/api/user.go b/backend/api/user.go index b80c2ec..f7cb5e3 100644 --- a/backend/api/user.go +++ b/backend/api/user.go @@ -7,6 +7,7 @@ import ( "github.com/darcys22/godbledger-web/backend/auth" m "github.com/darcys22/godbledger-web/backend/models" "github.com/darcys22/godbledger-web/backend/models/sqlite" + "github.com/darcys22/godbledger-web/backend/setting" "github.com/gin-gonic/gin" "github.com/dgrijalva/jwt-go" @@ -14,9 +15,13 @@ import ( ) var ( - users sqlite.UserModel = sqlite.New("sqlite.db") + users sqlite.UserModel ) +func InitUsersDatabase() { + users = sqlite.New("sqlite.db", setting.GetConfig()) +} + func respondWithError(ctx *gin.Context, message interface{}) { log.Debugf("Error processing JWT: %v", message) ctx.Abort() diff --git a/backend/auth/LoginService.go b/backend/auth/LoginService.go index c7fb250..803fa87 100644 --- a/backend/auth/LoginService.go +++ b/backend/auth/LoginService.go @@ -7,6 +7,7 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/darcys22/godbledger-web/backend/models/sqlite" + "github.com/darcys22/godbledger-web/backend/setting" ) type LoginService interface { @@ -16,8 +17,8 @@ type loginInformation struct { users *sqlite.UserModel } -func StaticLoginService() LoginService { - database := sqlite.New("sqlite.db") +func StaticLoginService(cfg *setting.Cfg) LoginService { + database := sqlite.New("sqlite.db", cfg) return &loginInformation{users: &database} } @@ -30,14 +31,6 @@ func (info *loginInformation) LoginUser(email string, password string) bool { return true } -func (info *loginInformation) NewUser(email string, password string) bool { - //_, err := info.users.New(email, password) - //if err != nil { - //return false - //} - return true -} - //JWT service type JWTService interface { GenerateToken(email string, isUser bool) string diff --git a/backend/models/sqlite/users.go b/backend/models/sqlite/users.go index 483a432..26d6747 100644 --- a/backend/models/sqlite/users.go +++ b/backend/models/sqlite/users.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/darcys22/godbledger-web/backend/models" + "github.com/darcys22/godbledger-web/backend/setting" ) var log = logrus.WithField("prefix", "SqliteUsers") @@ -18,13 +19,12 @@ var ErrNoRows = errors.New("sql: no rows in result set") type UserModel struct { DB *sql.DB + Cfg *setting.Cfg } -func New(path string) UserModel { +func New(path string, cfg *setting.Cfg) UserModel { database, _ := sql.Open("sqlite3", path) - log.Info("Users database at path: ", path) - statement, err := database.Prepare(` CREATE TABLE IF NOT EXISTS users ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -42,30 +42,48 @@ func New(path string) UserModel { log.Error("Error in prepare statement: ", err) } statement.Exec() - usersdb := UserModel{DB: database} + usersdb := UserModel{DB: database, Cfg: cfg} - //TODO this should be conditionally run from config + if !cfg.DisableInitialAdminCreation { + err = usersdb.CreateDefaultUser(cfg.AdminUser, cfg.AdminPassword) + if err != nil { + log.Error("Error creating default user: ", err) + } + } + + return usersdb +} + +func (m *UserModel) CreateDefaultUser(email, password string) error { defaultUserID := 0 - err = database.QueryRow(`SELECT id FROM users WHERE email = ? LIMIT 1`, "test@godbledger.com").Scan(&defaultUserID) + err := m.DB.QueryRow(`SELECT id FROM users WHERE email = ? LIMIT 1`, email).Scan(&defaultUserID) if err != nil { - if err.Error() == ErrNoRows.Error() { - log.Info("Inserting default user into users table") - err = usersdb.Insert("defaultuser", "test@godbledger.com", "password") - if err != nil { - log.Error("Error in adding default user: ", err) + if err == sql.ErrNoRows { + if err.Error() == ErrNoRows.Error() { + err = m.Insert("defaultuser", email , password) + if err != nil { + return err + } + defaultUser, err := m.Get(email) + if err != nil { + return err + } + err = m.ChangePermissions(defaultUser, "admin") + if err != nil { + return err + } + + } else { + return err } - - } else { - log.Error("Error in searching for default user: ", err) + log.Info("Created default user") } } - - return usersdb + return nil } func (m *UserModel) Insert(name, email, password string) error { // Create a bcrypt hash of the plain-text password. - log.Infof("Inserting user into users table, Name: %s Email: %s", name, email) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return err @@ -75,6 +93,7 @@ func (m *UserModel) Insert(name, email, password string) error { if err != nil { return err } + log.Infof("Inserted user into users table, Name: %s Email: %s", name, email) return nil } @@ -123,10 +142,10 @@ func (m *UserModel) NewUser(email, password string) (int, error) { } // We'll use the Get method to fetch details for a specific user based on their email/name -func (m *UserModel) Get(name string) (*models.User, error) { +func (m *UserModel) Get(email string) (*models.User, error) { var user models.User stmt := "SELECT * FROM users WHERE email = ? AND active = TRUE" - row := m.DB.QueryRow(stmt, name) + row := m.DB.QueryRow(stmt, email) err := row.Scan(&user.ID, &user.Name, &user.Email, &user.HashedPassword, &user.Created, &user.Active, &user.Currency, &user.DateLocale, &user.Role) if err != nil { return nil, err @@ -137,7 +156,17 @@ func (m *UserModel) Get(name string) (*models.User, error) { func (m *UserModel) Save(user *models.User) (error) { stmt := "UPDATE users SET currency = ?, locale = ? WHERE email = ? AND active = TRUE" _, err := m.DB.Exec(stmt, user.Currency, user.DateLocale, user.Email) + if err != nil { + return err + } log.Info("Users Saved: ", user) + return nil +} + +func (m *UserModel) ChangePermissions(user *models.User, role string) (error) { + stmt := "UPDATE users SET role = ? WHERE email = ? AND active = TRUE" + _, err := m.DB.Exec(stmt, role, user.Email) + log.Info("Users Permissions Updated: ", user) if err != nil { return err } @@ -146,7 +175,6 @@ func (m *UserModel) Save(user *models.User) (error) { func (m *UserModel) ChangePassword(user *models.User, password string) (error) { // Create a bcrypt hash of the plain-text password. - log.Infof("Updating user password, Name: %s Email: %s", user.Name , user.Email) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return err @@ -156,5 +184,6 @@ func (m *UserModel) ChangePassword(user *models.User, password string) (error) { if err != nil { return err } + log.Infof("Updated user password, Name: %s Email: %s", user.Name , user.Email) return nil } diff --git a/backend/models/users.go b/backend/models/users.go index 8b6a5db..c5153f3 100644 --- a/backend/models/users.go +++ b/backend/models/users.go @@ -48,5 +48,5 @@ type UserSettingsResponse struct { } func (u *User) Settings() UserSettingsResponse { - return UserSettingsResponse{u.Name, u.Currency, u.DateLocale, u.Role} + return UserSettingsResponse{u.Name, u.Role, u.DateLocale, u.Currency} } diff --git a/backend/setting/setting.go b/backend/setting/setting.go index 27e0b0f..9cab858 100644 --- a/backend/setting/setting.go +++ b/backend/setting/setting.go @@ -13,7 +13,10 @@ import ( ini "gopkg.in/ini.v1" ) -var log = logrus.WithField("prefix", "setting") +var ( + log = logrus.WithField("prefix", "setting") + globalcfg *Cfg +) type Scheme string @@ -80,6 +83,11 @@ type Cfg struct { BuildStamp int64 IsEnterprise bool + // security + DisableInitialAdminCreation bool + AdminUser string + AdminPassword string + } type CommandLineArgs struct { @@ -316,6 +324,10 @@ func setHomePath(args *CommandLineArgs) { } } +func GetConfig() *Cfg { + return globalcfg +} + func NewCfg() *Cfg { return &Cfg{ Raw: ini.Empty(), @@ -350,10 +362,15 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { if err := readServerSettings(iniFile, cfg); err != nil { return err } + if err := readSecuritySettings(iniFile, cfg); err != nil { + return err + } cfg.Protocol = Protocol + globalcfg = cfg + return nil } @@ -400,3 +417,22 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error { return nil } + +func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { + server := iniFile.Section("security") + var err error + + cfg.DisableInitialAdminCreation = server.Key("disable_initial_admin_creation").MustBool(false) + + cfg.AdminUser, err = valueAsString(server, "admin_user", "test@godbledger.com") + if err != nil { + return err + } + + cfg.AdminPassword, err = valueAsString(server, "admin_password", "password") + if err != nil { + return err + } + + return nil +} diff --git a/build.go b/build.go index b6ef991..645de28 100644 --- a/build.go +++ b/build.go @@ -115,6 +115,11 @@ func main() { doBuild(binary, "./backend/cmd/"+binary, []string{}) } + case "debug": + for _, binary := range binaries { + doBuild(binary, "./backend/cmd/"+binary, []string{"-gcflags=all='-N -l'"}) + } + case "build-frontend": grunt(gruntBuildArg("build")...) diff --git a/conf/defaults.ini b/conf/defaults.ini index cd4a4b6..5731adf 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -81,5 +81,5 @@ disable_initial_admin_creation = false # default admin user, created on startup admin_user = test@godbledger.com -# default admin password, can be changed before first start of GoDBLedger-Web, or in profile settings +# default admin password admin_password = password diff --git a/public/app/sidebar.js b/public/app/sidebar.js index 844f2ad..18606f5 100644 --- a/public/app/sidebar.js +++ b/public/app/sidebar.js @@ -22,6 +22,10 @@ function menuBtnChange() { } } +$('.adminonly').each(function(i, obj) { + obj.hidden = true; +}) + fetch('/api/user/settings', { method: 'GET', headers: { @@ -33,10 +37,16 @@ fetch('/api/user/settings', { let name = document.querySelector(".name"); let role = document.querySelector(".job"); window.user = data - name.innerText = data.name - role.innerText = data.role + name.innerText = data.name[0].toUpperCase() + data.name.slice(1); + role.innerText = data.role[0].toUpperCase() + data.role.slice(1); if (typeof Date.setLocale !== 'undefined') { Date.setLocale(data.datelocale); } + + if (data.role == "admin") { + $('.adminonly').each(function(i, obj) { + obj.hidden = false; + }) + } }) diff --git a/public/views/admin.html b/public/views/admin.html index 119c073..bd2267a 100644 --- a/public/views/admin.html +++ b/public/views/admin.html @@ -46,8 +46,9 @@