Compare commits

...

5 Commits

Author SHA1 Message Date
0188f97e60 Merge branch 'login' 2023-10-25 19:54:34 +02:00
b143983d52 User list 2023-10-25 19:54:21 +02:00
34b2f55cfc Restructure app 2023-10-25 17:55:36 +02:00
7fdf131d53 Create redis session engine 2023-10-25 10:56:38 +02:00
c7955104ae Login form 2023-10-25 08:28:44 +02:00
17 changed files with 303 additions and 78 deletions

View File

@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [] args_bin = []
bin = "./tmp/main" bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./app/main.go" cmd = "go build -o ./tmp/main ./app/."
delay = 1000 delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "build"] exclude_dir = ["assets", "tmp", "vendor", "testdata", "build"]
exclude_file = [] exclude_file = []

View File

@ -1,4 +1,4 @@
package cfg package main
import ( import (
"fmt" "fmt"
@ -10,13 +10,6 @@ import (
) )
type configStruct struct { type configStruct struct {
Database struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Name string `yaml:"name"`
Username string `yaml:"username"`
Password string `yaml:"password" json:"-"`
} `yaml:"database"`
Application struct { Application struct {
SecretKey string `yaml:"secretKey" json:"-"` SecretKey string `yaml:"secretKey" json:"-"`
LogLevel string `yaml:"logLevel"` LogLevel string `yaml:"logLevel"`
@ -27,6 +20,21 @@ type configStruct struct {
IsProduction bool `yaml:"isProduction"` IsProduction bool `yaml:"isProduction"`
LoopDelay uint32 `yaml:"loopDelay"` LoopDelay uint32 `yaml:"loopDelay"`
} `yaml:"application"` } `yaml:"application"`
Database struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Name string `yaml:"name"`
Username string `yaml:"username"`
Password string `yaml:"password" json:"-"`
} `yaml:"database"`
Redis struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password" json:"-"`
Database string `yaml:"database"`
Prefix string `yaml:"prefix"`
} `yaml:"redis"`
SMTP struct { SMTP struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
@ -70,7 +78,7 @@ func readEnv(cfg *configStruct) {
} }
} }
func Init() { func InitCfg() {
cfgFile := os.Getenv("MAILSENDER_CONFIG") cfgFile := os.Getenv("MAILSENDER_CONFIG")
if cfgFile == "" { if cfgFile == "" {
cfgFile = DEFAULT_CONFIG_FILE cfgFile = DEFAULT_CONFIG_FILE
@ -81,6 +89,7 @@ func Init() {
maskedCfg := Config maskedCfg := Config
maskedCfg.Database.Password = "**password hidden**" maskedCfg.Database.Password = "**password hidden**"
maskedCfg.Redis.Password = "**password hidden**"
maskedCfg.SMTP.Password = "**password hidden**" maskedCfg.SMTP.Password = "**password hidden**"
maskedCfg.Application.SecretKey = "**secret key hidden**" maskedCfg.Application.SecretKey = "**secret key hidden**"

View File

@ -1,9 +1,8 @@
package db package main
import ( import (
"fmt" "fmt"
"iris-test/app/cfg" "iris-test/app/repository"
"iris-test/app/logging"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -21,18 +20,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://",
cfg.Config.Database.Username, ":", Config.Database.Username, ":",
cfg.Config.Database.Password, "@", Config.Database.Password, "@",
cfg.Config.Database.Host, ":", Config.Database.Host, ":",
cfg.Config.Database.Port, "/", Config.Database.Port, "/",
cfg.Config.Database.Name, 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 cfg.Config.Application.DebugSQL { if Config.Application.DebugSQL {
logLevel = gormLogger.Info logLevel = gormLogger.Info
} }
@ -42,7 +41,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)
logging.Error(msg) Log.Error(msg)
panic(msg) panic(msg)
} }
@ -50,5 +49,7 @@ func InitDB() *gorm.DB {
db, _ := DBConn.DB() db, _ := DBConn.DB()
db.SetConnMaxIdleTime(CONNECTION_MAX_IDLE_TIME) db.SetConnMaxIdleTime(CONNECTION_MAX_IDLE_TIME)
repository.Dao = repository.CreateDAO(DBConn)
return DBConn return DBConn
} }

View File

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

View File

@ -1,26 +1,102 @@
package main package main
import ( import (
"iris-test/app/cfg" "fmt"
"iris-test/app/db"
"iris-test/app/logging"
"iris-test/app/views" "iris-test/app/views"
"os"
"time"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/accesslog"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/sessions/sessiondb/redis"
) )
var redisDB *redis.Database
func createSessionEngine() *sessions.Sessions {
redisAddr := fmt.Sprintf("%s:%d", Config.Redis.Host, Config.Redis.Port)
redisDB = redis.New(redis.Config{
Network: "tcp",
Addr: redisAddr,
Timeout: time.Duration(30) * time.Second,
MaxActive: 10,
Username: Config.Redis.Username,
Password: Config.Redis.Password,
Database: Config.Redis.Database,
Prefix: Config.Redis.Prefix,
Driver: redis.GoRedis(), // defaults to this driver.
// To set a custom, existing go-redis client, use the "SetClient" method:
// Driver: redis.GoRedis().SetClient(customGoRedisClient)
})
sessions_engine := sessions.New(sessions.Config{
Cookie: "_session_id",
Expires: 0, // defaults to 0: unlimited life. Another good value is: 45 * time.Minute,
AllowReclaim: true,
CookieSecureTLS: true,
})
sessions_engine.UseDatabase(redisDB)
return sessions_engine
}
func createAccessLog() *accesslog.AccessLog {
// Initialize a new access log middleware.
ac := accesslog.New(os.Stdout)
// Remove this line to disable logging to console:
// ac.AddOutput(os.Stdout)
// The default configuration:
ac.Delim = '|'
ac.TimeFormat = "2006-01-02 15:04:05"
ac.Async = false
ac.IP = true
ac.BytesReceivedBody = true
ac.BytesSentBody = true
ac.BytesReceived = false
ac.BytesSent = false
ac.BodyMinify = true
ac.RequestBody = true
ac.ResponseBody = false
ac.KeepMultiLineError = true
ac.PanicLog = accesslog.LogHandler
// Default line format if formatter is missing:
// Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response|
//
// Set Custom Formatter:
ac.SetFormatter(&accesslog.JSON{
Indent: " ",
HumanTime: true,
})
// ac.SetFormatter(&accesslog.CSV{})
// ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"})
return ac
}
func createApp() *iris.Application { func createApp() *iris.Application {
sessionsEngine := createSessionEngine()
accessLog := createAccessLog()
app := iris.New() app := iris.New()
app.Logger().SetLevel(Config.Application.LogLevel)
app.Use(sessionsEngine.Handler())
app.UseRouter(accessLog.Handler)
app.RegisterView(iris.Jet("./app/templates", ".jet").Reload(true)) app.RegisterView(iris.Jet("./app/templates", ".jet").Reload(true))
views.CreateRouter(app) views.CreateRouter(app)
return app return app
} }
func main() { func main() {
cfg.Init() InitCfg()
logging.Init() InitLogging()
db.InitDB() InitDB()
app := createApp() app := createApp()
defer redisDB.Close()
app.Listen(":8000") app.Listen(":8000")
} }

18
app/models/user.go Normal file
View File

@ -0,0 +1,18 @@
package models
import "time"
type User struct {
Id string `gorm:"type(uuid);unique"`
Email string `gorm:"unique"`
FirstName string
LastName string
Password string
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}
func (u *User) TableName() string {
return "users"
}

17
app/repository/dao.go Normal file
View File

@ -0,0 +1,17 @@
package repository
import "gorm.io/gorm"
type DAO struct {
db *gorm.DB
UsersRepository *UsersRepository
}
var Dao DAO
func CreateDAO(db *gorm.DB) DAO {
return DAO{
db: db,
UsersRepository: CreateUsersRepository(db),
}
}

View File

@ -0,0 +1,12 @@
package repository
type Pagination struct {
PageSize int
Page int
}
func (p *Pagination) New() *Pagination {
p.PageSize = 50
p.Page = 1
return p
}

45
app/repository/users.go Normal file
View File

@ -0,0 +1,45 @@
package repository
import (
"iris-test/app/models"
"gorm.io/gorm"
)
type UsersRepository struct {
db *gorm.DB
}
type UserFilter struct {
IsActive *bool
}
func CreateUsersRepository(db *gorm.DB) *UsersRepository {
return &UsersRepository{db}
}
func applyFilter(db *gorm.DB, filter *UserFilter) *gorm.DB {
query := db
// if filter.State != "" {
// query = query.Where("state = ?", filter.State)
// }
// if filter.SendAt_lt != nil {
// query = query.Where("send_at < ?", filter.SendAt_lt)
// }
// if filter.SendAt_notNull != nil {
// query = query.Not(map[string]interface{}{"send_at": nil})
// }
return query
}
func (repository *UsersRepository) List(filter *UserFilter) *[]models.User {
var users []models.User
query := repository.db.Model(&models.User{})
query = applyFilter(query, filter)
query.Find(&users)
return &users
}

View File

@ -5,9 +5,9 @@
<tbody> <tbody>
{{ range users }} {{ range users }}
<tr> <tr>
<td>{{ .firstName }}</td> <td>{{ .FirstName }}</td>
<td>{{ .lastName }}</td> <td>{{ .LastName }}</td>
<td>{{ .email }}</td> <td>{{ .Email }}</td>
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>

View File

@ -4,6 +4,23 @@
{{ block mainContent() }} {{ block mainContent() }}
<div class="row">
<form class="mb-5 col-4 ms-auto me-auto" method="post" action="/">
<div class="mb-3">
<label class="form-label">Email address</label>
<input type="email" name="email" class="form-control" value="edkirin@gmail.com">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="text" name="password" class="form-control" value="tralala">
</div>
<button type="submit" class="btn btn-success">
Submit
</button>
</form>
</div>
<div class="lead"> <div class="lead">
<p>Bacon ipsum dolor amet leberkas kevin meatball pork loin beef ribs prosciutto, turducken bacon bresaola tri-tip. Strip steak flank shankle, sirloin short ribs shoulder meatball pork chop kevin ribeye jowl ham pork belly turducken jerky. Flank tongue short loin ham hock brisket turducken tail filet mignon cupim. Pork capicola buffalo kevin jowl chicken. Filet mignon brisket pig, landjaeger sausage cow fatback drumstick chicken buffalo tenderloin spare ribs.</p> <p>Bacon ipsum dolor amet leberkas kevin meatball pork loin beef ribs prosciutto, turducken bacon bresaola tri-tip. Strip steak flank shankle, sirloin short ribs shoulder meatball pork chop kevin ribeye jowl ham pork belly turducken jerky. Flank tongue short loin ham hock brisket turducken tail filet mignon cupim. Pork capicola buffalo kevin jowl chicken. Filet mignon brisket pig, landjaeger sausage cow fatback drumstick chicken buffalo tenderloin spare ribs.</p>
<p>Swine shankle porchetta pancetta. Buffalo chicken turducken ground round kevin shoulder, salami pig t-bone beef ribs tri-tip tongue pork belly doner. Landjaeger meatloaf short loin biltong. Alcatra tongue shankle, tri-tip pancetta porchetta tenderloin corned beef pastrami rump. Bresaola chislic beef kielbasa sausage, ball tip burgdoggen boudin capicola short loin tenderloin buffalo landjaeger.</p> <p>Swine shankle porchetta pancetta. Buffalo chicken turducken ground round kevin shoulder, salami pig t-bone beef ribs tri-tip tongue pork belly doner. Landjaeger meatloaf short loin biltong. Alcatra tongue shankle, tri-tip pancetta porchetta tenderloin corned beef pastrami rump. Bresaola chislic beef kielbasa sausage, ball tip burgdoggen boudin capicola short loin tenderloin buffalo landjaeger.</p>

View File

@ -1,11 +1,33 @@
package views package views
import "github.com/kataras/iris/v12" import (
"fmt"
"github.com/kataras/iris/v12"
)
func GetIndexPage(ctx iris.Context) { func GetIndexPage(ctx iris.Context) {
params1 := []string{"param 1", "param 2", "param 3"} if err := ctx.View("pages/index.jet"); err != nil {
ctx.ViewData("params1", params1) showError(ctx, err)
ctx.ViewData("users", users) return
}
}
type loginForm struct {
Email string `form:"email"`
Password string `form:"password"`
}
func PostIndexPage(ctx iris.Context) {
var form loginForm
err := ctx.ReadForm(&form)
if err != nil {
ctx.StopWithError(iris.StatusBadRequest, err)
return
}
fmt.Println("email:", form.Email)
fmt.Println("password:", form.Password)
if err := ctx.View("pages/index.jet"); err != nil { if err := ctx.View("pages/index.jet"); err != nil {
showError(ctx, err) showError(ctx, err)

View File

@ -4,6 +4,8 @@ import "github.com/kataras/iris/v12"
func CreateRouter(app *iris.Application) { func CreateRouter(app *iris.Application) {
app.Get("/", GetIndexPage) app.Get("/", GetIndexPage)
app.Post("/", PostIndexPage)
app.Get("/users", GetUsersPage) app.Get("/users", GetUsersPage)
app.Get("/about", GetAboutPage) app.Get("/about", GetAboutPage)
} }

View File

@ -1,39 +1,19 @@
package views package views
import "github.com/kataras/iris/v12" import (
"iris-test/app/repository"
type User struct { "github.com/kataras/iris/v12"
firstName string )
lastName string
email string
}
var users = []User{
{
firstName: "Pero",
lastName: "Perić",
email: "pero@gmail.com",
},
{
firstName: "Mirko",
lastName: "Mirković",
email: "mirko@gmail.com",
},
{
firstName: "Ivo",
lastName: "Ivić",
email: "ivo@gmail.com",
},
{
firstName: "Slavko",
lastName: "Slavković",
email: "slavko@gmail.com",
},
}
func GetUsersPage(ctx iris.Context) { func GetUsersPage(ctx iris.Context) {
params1 := []string{"param 1", "param 2", "param 3"} params1 := []string{"param 1", "param 2", "param 3"}
ctx.ViewData("params1", params1) ctx.ViewData("params1", params1)
userRepository := repository.Dao.UsersRepository
users := userRepository.List(&repository.UserFilter{})
ctx.ViewData("users", users) ctx.ViewData("users", users)
if err := ctx.View("pages/users.jet"); err != nil { if err := ctx.View("pages/users.jet"); err != nil {

View File

@ -1,11 +1,3 @@
# Database credentials
database:
host: "localhost"
port: 5432
name: "iristest"
username: "iristest"
password: "iristest"
application: application:
secretKey: "secret-key" secretKey: "secret-key"
logLevel: info logLevel: info
@ -14,6 +6,21 @@ application:
isProduction: false isProduction: false
loopDelay: 3000 loopDelay: 3000
database:
host: "localhost"
port: 5432
name: "iristest"
username: "iristest"
password: "iristest"
redis:
host: "localhost"
port: 6379
username: "iristest"
password: "iristest"
database: ""
prefix: "myapp-"
smtp: smtp:
host: "smtp-host" host: "smtp-host"
port: 587 port: 587

6
go.mod
View File

@ -19,6 +19,8 @@ require (
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
@ -32,6 +34,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/blocks v0.0.8 // indirect
github.com/kataras/golog v0.1.9 // indirect github.com/kataras/golog v0.1.9 // indirect
github.com/kataras/pio v0.0.12 // indirect github.com/kataras/pio v0.0.12 // indirect
@ -41,6 +44,9 @@ require (
github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/redis/go-redis/v9 v9.2.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect

14
go.sum
View File

@ -16,9 +16,14 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@ -37,6 +42,7 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
@ -61,6 +67,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM=
github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk= github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
@ -95,9 +103,15 @@ github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3r
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.2.0 h1:zwMdX0A4eVzse46YN18QhuDiM4uf3JmkOB4VZrdt5uI=
github.com/redis/go-redis/v9 v9.2.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=