Compare commits

..

3 Commits

Author SHA1 Message Date
50187f5a34 Set user password 2023-10-26 17:40:05 +02:00
dd671d561c Restructure again 2023-10-26 16:49:24 +02:00
7512d75a4d Readme 2023-10-26 16:03:20 +02:00
10 changed files with 104 additions and 30 deletions

View File

@ -16,6 +16,11 @@
- [Password Hashing (bcrypt)](https://gowebexamples.com/password-hashing/) - [Password Hashing (bcrypt)](https://gowebexamples.com/password-hashing/)
## Tools
- [Bombardier benchmarking](https://github.com/codesenberg/bombardier)
## Bombardier benchmark ## Bombardier benchmark
Pandora - Jet templating engine Pandora - Jet templating engine
@ -56,3 +61,5 @@ Statistics Avg Stdev Max
others - 0 others - 0
Throughput: 19.91MB/s Throughput: 19.91MB/s
``` ```

1
app/lib/auth/auth.go Normal file
View File

@ -0,0 +1 @@
package auth

41
app/lib/auth/passwords.go Normal file
View File

@ -0,0 +1,41 @@
package auth
import (
"regexp"
"golang.org/x/crypto/bcrypt"
)
// about bcrypt cost: https://docs.laminas.dev/laminas-crypt/password/#bcrypt
// bcrypt cost benchmarks: https://github.com/nsmithuk/bcrypt-cost-go
const BCRYPT_COST = 10
const MIN_PASSWORD_LENGTH = 10
func IsPasswordGoodEnough(password string) bool {
var re *regexp.Regexp
passwordBytes := []byte(password)
if len(password) < MIN_PASSWORD_LENGTH {
return false
}
re, _ = regexp.Compile("[a-z]")
if re.Find(passwordBytes) == nil {
return false
}
re, _ = regexp.Compile("[A-Z]")
if re.Find(passwordBytes) == nil {
return false
}
re, _ = regexp.Compile("[0-9]")
//lint:ignore S1008 allow early exit instead optimization
if re.Find(passwordBytes) == nil {
return false
}
return true
}
func HashPassword(password string, secretKey string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password+secretKey), BCRYPT_COST)
return string(bytes), err
}

View File

@ -1,4 +1,4 @@
package common package cfg
import ( import (
"fmt" "fmt"

View File

@ -1,7 +1,9 @@
package common package db
import ( import (
"fmt" "fmt"
"iris-test/app/lib/cfg"
"iris-test/app/lib/logging"
"iris-test/app/repository" "iris-test/app/repository"
"strconv" "strconv"
"strings" "strings"
@ -20,18 +22,18 @@ var DBConn *gorm.DB
func InitDB() *gorm.DB { func InitDB() *gorm.DB {
var connectionString = strings.Join([]string{ var connectionString = strings.Join([]string{
"postgres://", "postgres://",
Config.Database.Username, ":", cfg.Config.Database.Username, ":",
Config.Database.Password, "@", cfg.Config.Database.Password, "@",
Config.Database.Host, ":", cfg.Config.Database.Host, ":",
Config.Database.Port, "/", cfg.Config.Database.Port, "/",
Config.Database.Name, cfg.Config.Database.Name,
"?sslmode=disable", "?sslmode=disable",
"&TimeZone=UTC", "&TimeZone=UTC",
"&connect_timeout=", strconv.Itoa(DB_CONNECTION_TIMEOUT), "&connect_timeout=", strconv.Itoa(DB_CONNECTION_TIMEOUT),
}, "") }, "")
var logLevel = gormLogger.Silent var logLevel = gormLogger.Silent
if Config.Application.DebugSQL { if cfg.Config.Application.DebugSQL {
logLevel = gormLogger.Info logLevel = gormLogger.Info
} }
@ -41,7 +43,7 @@ func InitDB() *gorm.DB {
}) })
if err != nil { if err != nil {
msg := fmt.Sprintf("Error connecting to database: %s. Terminating!", err) msg := fmt.Sprintf("Error connecting to database: %s. Terminating!", err)
Log.Error(msg) logging.Error(msg)
panic(msg) panic(msg)
} }

View File

@ -1,9 +1,10 @@
package common package logging
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"iris-test/app/lib/cfg"
"os" "os"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -28,9 +29,9 @@ func Warn(message string) {
} }
func InitLogging() { func InitLogging() {
logLevel, err := logrus.ParseLevel(Config.Application.LogLevel) logLevel, err := logrus.ParseLevel(cfg.Config.Application.LogLevel)
if err != nil { if err != nil {
panic(fmt.Sprintf("Invalid configured logLevel: %s\n", Config.Application.LogLevel)) panic(fmt.Sprintf("Invalid configured logLevel: %s\n", cfg.Config.Application.LogLevel))
} }
Log.SetLevel(logLevel) Log.SetLevel(logLevel)
@ -42,14 +43,14 @@ func InitLogging() {
DisableQuote: true, DisableQuote: true,
}) })
LogFile := Config.Application.LogFile LogFile := cfg.Config.Application.LogFile
file, err := os.OpenFile( file, err := os.OpenFile(
LogFile, LogFile,
os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.O_CREATE|os.O_WRONLY|os.O_APPEND,
0655, 0655,
) )
if err != nil { if err != nil {
msg := fmt.Sprintf("Failed to log to file %s: %s", Config.Application.LogFile, err) msg := fmt.Sprintf("Failed to log to file %s: %s", cfg.Config.Application.LogFile, err)
Log.Warning(msg) Log.Warning(msg)
panic(msg) panic(msg)
} }
@ -57,7 +58,7 @@ func InitLogging() {
mw := io.MultiWriter(os.Stdout, file) mw := io.MultiWriter(os.Stdout, file)
Log.SetOutput(mw) Log.SetOutput(mw)
configJson, err := json.Marshal(Config) configJson, err := json.Marshal(cfg.Config)
if err == nil { if err == nil {
Info(fmt.Sprintf("Using config: %s", configJson)) Info(fmt.Sprintf("Using config: %s", configJson))
} }

View File

@ -2,7 +2,9 @@ package main
import ( import (
"fmt" "fmt"
"iris-test/app/common" "iris-test/app/lib/cfg"
"iris-test/app/lib/db"
"iris-test/app/lib/logging"
"iris-test/app/views" "iris-test/app/views"
"os" "os"
"time" "time"
@ -16,17 +18,17 @@ import (
var redisDB *redis.Database var redisDB *redis.Database
func createSessionEngine() *sessions.Sessions { func createSessionEngine() *sessions.Sessions {
redisAddr := fmt.Sprintf("%s:%d", common.Config.Redis.Host, common.Config.Redis.Port) redisAddr := fmt.Sprintf("%s:%d", cfg.Config.Redis.Host, cfg.Config.Redis.Port)
redisDB = redis.New(redis.Config{ redisDB = redis.New(redis.Config{
Network: "tcp", Network: "tcp",
Addr: redisAddr, Addr: redisAddr,
Timeout: time.Duration(30) * time.Second, Timeout: time.Duration(30) * time.Second,
MaxActive: 10, MaxActive: 10,
Username: common.Config.Redis.Username, Username: cfg.Config.Redis.Username,
Password: common.Config.Redis.Password, Password: cfg.Config.Redis.Password,
Database: common.Config.Redis.Database, Database: cfg.Config.Redis.Database,
Prefix: common.Config.Redis.Prefix, Prefix: cfg.Config.Redis.Prefix,
Driver: redis.GoRedis(), // defaults to this driver. Driver: redis.GoRedis(), // defaults to this driver.
// To set a custom, existing go-redis client, use the "SetClient" method: // To set a custom, existing go-redis client, use the "SetClient" method:
// Driver: redis.GoRedis().SetClient(customGoRedisClient) // Driver: redis.GoRedis().SetClient(customGoRedisClient)
@ -84,7 +86,7 @@ func createApp() *iris.Application {
accessLog := createAccessLog() accessLog := createAccessLog()
app := iris.New() app := iris.New()
app.Logger().SetLevel(common.Config.Application.LogLevel) app.Logger().SetLevel(cfg.Config.Application.LogLevel)
app.Use(sessionsEngine.Handler()) app.Use(sessionsEngine.Handler())
app.UseRouter(accessLog.Handler) app.UseRouter(accessLog.Handler)
app.RegisterView(iris.Jet("./app/templates", ".jet").Reload(true)) app.RegisterView(iris.Jet("./app/templates", ".jet").Reload(true))
@ -93,9 +95,9 @@ func createApp() *iris.Application {
} }
func main() { func main() {
common.InitCfg() cfg.InitCfg()
common.InitLogging() logging.InitLogging()
common.InitDB() db.InitDB()
app := createApp() app := createApp()
defer redisDB.Close() defer redisDB.Close()

View File

@ -1,6 +1,8 @@
package models package models
import ( import (
"iris-test/app/lib/auth"
"iris-test/app/lib/cfg"
"time" "time"
) )
@ -19,8 +21,12 @@ func (u *User) TableName() string {
return "users" return "users"
} }
// func (u *User) SetPassword(password string) (string, error) { func (u *User) SetPassword(password string) error {
// secretKey := Config.Application.SecretKey secretKey := cfg.Config.Application.SecretKey
// bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) hashedPassword, err := auth.HashPassword(password, secretKey)
// return string(bytes), err if err != nil {
// } return err
}
u.Password = hashedPassword
return nil
}

View File

@ -20,6 +20,10 @@
<label class="form-label">Email</label> <label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="{{ user.Email }}" required> <input type="email" name="email" class="form-control" value="{{ user.Email }}" required>
</div> </div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="text" name="password" class="form-control">
</div>
<div class="d-flex"> <div class="d-flex">
<a href="/users" class="btn btn-outline-secondary ms-auto me-2"> <a href="/users" class="btn btn-outline-secondary ms-auto me-2">

View File

@ -1,6 +1,8 @@
package views package views
import ( import (
"fmt"
"iris-test/app/lib/auth"
"iris-test/app/repository" "iris-test/app/repository"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
@ -10,6 +12,7 @@ type editUserForm struct {
FirstName string `form:"first-name"` FirstName string `form:"first-name"`
LastName string `form:"last-name"` LastName string `form:"last-name"`
Email string `form:"email"` Email string `form:"email"`
Password string `form:"password"`
} }
func GetUsersPage(ctx iris.Context) { func GetUsersPage(ctx iris.Context) {
@ -66,6 +69,13 @@ func SaveUserPage(ctx iris.Context) {
user.LastName = form.LastName user.LastName = form.LastName
user.Email = form.Email user.Email = form.Email
if len(form.Password) > 0 {
user.SetPassword(form.Password)
fmt.Printf("Set password: %s\n", user.Password)
fmt.Printf("IsPasswordGoodEnough: %v\n", auth.IsPasswordGoodEnough(form.Password))
}
userRepository.Save(user) userRepository.Save(user)
ctx.Redirect("/users") ctx.Redirect("/users")