Compare commits
14 Commits
264858b81e
...
pongo-temp
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a7584d628 | |||
| c1997aaab5 | |||
| e92a56c393 | |||
| 45f6f19441 | |||
| 2b13292e3b | |||
| da3deedb0c | |||
| 7024cd0de9 | |||
| 21607a5c48 | |||
| 30fccde9e5 | |||
| 0188f97e60 | |||
| b143983d52 | |||
| 34b2f55cfc | |||
| 7fdf131d53 | |||
| c7955104ae |
@ -5,7 +5,7 @@ tmp_dir = "tmp"
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./app/main.go"
|
||||
cmd = "go build -o ./tmp/main ./app/."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "build"]
|
||||
exclude_file = []
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/.vscode
|
||||
/__debug*
|
||||
/build
|
||||
/tmp
|
||||
/config.yaml
|
||||
|
||||
6
Makefile
6
Makefile
@ -1,2 +1,8 @@
|
||||
EXEC=iris-test
|
||||
|
||||
run:
|
||||
@air
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@go build -ldflags "-s -w" -o ./build/${EXEC} ./app/main.go
|
||||
|
||||
45
README.md
45
README.md
@ -11,3 +11,48 @@
|
||||
- [Source](https://github.com/CloudyKit/jet)
|
||||
- [Syntax reference](https://github.com/CloudyKit/jet/blob/master/docs/syntax.md)
|
||||
|
||||
## Howto
|
||||
|
||||
- [Password Hashing (bcrypt)](https://gowebexamples.com/password-hashing/)
|
||||
|
||||
|
||||
## Bombardier benchmark
|
||||
|
||||
Pandora - Jet templating engine
|
||||
|
||||
```
|
||||
eden@pandora:[~/apps/sandbox/golang/iris-web-framework]: bombardier -c 100 -d 10s -l http://localhost:8000 ±[A1][main]
|
||||
Bombarding http://localhost:8000 for 10s using 100 connection(s)
|
||||
Done!
|
||||
Statistics Avg Stdev Max
|
||||
Reqs/sec 10542.16 2631.23 17296.86
|
||||
Latency 9.50ms 3.22ms 42.93ms
|
||||
Latency Distribution
|
||||
50% 8.77ms
|
||||
75% 11.86ms
|
||||
90% 15.31ms
|
||||
95% 17.86ms
|
||||
99% 23.90ms
|
||||
HTTP codes:
|
||||
1xx - 0, 2xx - 105258, 3xx - 0, 4xx - 0, 5xx - 0
|
||||
others - 0
|
||||
Throughput: 28.08MB/s
|
||||
```
|
||||
```
|
||||
eden@pandora:[~/apps/sandbox/golang/iris-web-framework]: bombardier -c 100 -d 10s -l http://localhost:8000/users ±[A1][main]
|
||||
Bombarding http://localhost:8000/users for 10s using 100 connection(s)
|
||||
Done!
|
||||
Statistics Avg Stdev Max
|
||||
Reqs/sec 1096.26 427.09 3211.54
|
||||
Latency 91.08ms 80.06ms 471.41ms
|
||||
Latency Distribution
|
||||
50% 78.37ms
|
||||
75% 156.58ms
|
||||
90% 191.60ms
|
||||
95% 223.49ms
|
||||
99% 309.59ms
|
||||
HTTP codes:
|
||||
1xx - 0, 2xx - 11060, 3xx - 0, 4xx - 0, 5xx - 0
|
||||
others - 0
|
||||
Throughput: 19.91MB/s
|
||||
```
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package cfg
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -10,13 +10,6 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
SecretKey string `yaml:"secretKey" json:"-"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
@ -27,6 +20,21 @@ type configStruct struct {
|
||||
IsProduction bool `yaml:"isProduction"`
|
||||
LoopDelay uint32 `yaml:"loopDelay"`
|
||||
} `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 {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
@ -70,7 +78,7 @@ func readEnv(cfg *configStruct) {
|
||||
}
|
||||
}
|
||||
|
||||
func Init() {
|
||||
func InitCfg() {
|
||||
cfgFile := os.Getenv("MAILSENDER_CONFIG")
|
||||
if cfgFile == "" {
|
||||
cfgFile = DEFAULT_CONFIG_FILE
|
||||
@ -81,6 +89,7 @@ func Init() {
|
||||
|
||||
maskedCfg := Config
|
||||
maskedCfg.Database.Password = "**password hidden**"
|
||||
maskedCfg.Redis.Password = "**password hidden**"
|
||||
maskedCfg.SMTP.Password = "**password hidden**"
|
||||
maskedCfg.Application.SecretKey = "**secret key hidden**"
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
package db
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iris-test/app/cfg"
|
||||
"iris-test/app/logging"
|
||||
"iris-test/app/repository"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -21,18 +20,18 @@ var DBConn *gorm.DB
|
||||
func InitDB() *gorm.DB {
|
||||
var connectionString = strings.Join([]string{
|
||||
"postgres://",
|
||||
cfg.Config.Database.Username, ":",
|
||||
cfg.Config.Database.Password, "@",
|
||||
cfg.Config.Database.Host, ":",
|
||||
cfg.Config.Database.Port, "/",
|
||||
cfg.Config.Database.Name,
|
||||
Config.Database.Username, ":",
|
||||
Config.Database.Password, "@",
|
||||
Config.Database.Host, ":",
|
||||
Config.Database.Port, "/",
|
||||
Config.Database.Name,
|
||||
"?sslmode=disable",
|
||||
"&TimeZone=UTC",
|
||||
"&connect_timeout=", strconv.Itoa(DB_CONNECTION_TIMEOUT),
|
||||
}, "")
|
||||
|
||||
var logLevel = gormLogger.Silent
|
||||
if cfg.Config.Application.DebugSQL {
|
||||
if Config.Application.DebugSQL {
|
||||
logLevel = gormLogger.Info
|
||||
}
|
||||
|
||||
@ -42,7 +41,7 @@ func InitDB() *gorm.DB {
|
||||
})
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error connecting to database: %s. Terminating!", err)
|
||||
logging.Error(msg)
|
||||
Log.Error(msg)
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
@ -50,5 +49,7 @@ func InitDB() *gorm.DB {
|
||||
db, _ := DBConn.DB()
|
||||
db.SetConnMaxIdleTime(CONNECTION_MAX_IDLE_TIME)
|
||||
|
||||
repository.Dao = repository.CreateDAO(DBConn)
|
||||
|
||||
return DBConn
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
package logging
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"iris-test/app/cfg"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -28,10 +27,10 @@ func Warn(message string) {
|
||||
Log.Warn(message)
|
||||
}
|
||||
|
||||
func Init() {
|
||||
logLevel, err := logrus.ParseLevel(cfg.Config.Application.LogLevel)
|
||||
func InitLogging() {
|
||||
logLevel, err := logrus.ParseLevel(Config.Application.LogLevel)
|
||||
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)
|
||||
@ -43,14 +42,14 @@ func Init() {
|
||||
DisableQuote: true,
|
||||
})
|
||||
|
||||
LogFile := cfg.Config.Application.LogFile
|
||||
LogFile := Config.Application.LogFile
|
||||
file, err := os.OpenFile(
|
||||
LogFile,
|
||||
os.O_CREATE|os.O_WRONLY|os.O_APPEND,
|
||||
0655,
|
||||
)
|
||||
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)
|
||||
panic(msg)
|
||||
}
|
||||
@ -58,7 +57,7 @@ func Init() {
|
||||
mw := io.MultiWriter(os.Stdout, file)
|
||||
Log.SetOutput(mw)
|
||||
|
||||
configJson, err := json.Marshal(cfg.Config)
|
||||
configJson, err := json.Marshal(Config)
|
||||
if err == nil {
|
||||
Info(fmt.Sprintf("Using config: %s", configJson))
|
||||
}
|
||||
91
app/main.go
91
app/main.go
@ -1,26 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"iris-test/app/cfg"
|
||||
"iris-test/app/db"
|
||||
"iris-test/app/logging"
|
||||
"fmt"
|
||||
"iris-test/app/common"
|
||||
"iris-test/app/views"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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", common.Config.Redis.Host, common.Config.Redis.Port)
|
||||
|
||||
redisDB = redis.New(redis.Config{
|
||||
Network: "tcp",
|
||||
Addr: redisAddr,
|
||||
Timeout: time.Duration(30) * time.Second,
|
||||
MaxActive: 10,
|
||||
Username: common.Config.Redis.Username,
|
||||
Password: common.Config.Redis.Password,
|
||||
Database: common.Config.Redis.Database,
|
||||
Prefix: common.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)
|
||||
})
|
||||
|
||||
sessionsEngine := 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,
|
||||
})
|
||||
|
||||
sessionsEngine.UseDatabase(redisDB)
|
||||
|
||||
return sessionsEngine
|
||||
}
|
||||
|
||||
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 {
|
||||
sessionsEngine := createSessionEngine()
|
||||
accessLog := createAccessLog()
|
||||
|
||||
app := iris.New()
|
||||
app.RegisterView(iris.Jet("./app/templates", ".jet").Reload(true))
|
||||
app.Logger().SetLevel(common.Config.Application.LogLevel)
|
||||
app.Use(sessionsEngine.Handler())
|
||||
app.UseRouter(accessLog.Handler)
|
||||
app.RegisterView(iris.Django("./app/templates", ".html").Reload(true))
|
||||
views.CreateRouter(app)
|
||||
return app
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg.Init()
|
||||
logging.Init()
|
||||
db.InitDB()
|
||||
common.InitCfg()
|
||||
common.InitLogging()
|
||||
common.InitDB()
|
||||
|
||||
app := createApp()
|
||||
defer redisDB.Close()
|
||||
app.Listen(":8000")
|
||||
}
|
||||
|
||||
26
app/models/user.go
Normal file
26
app/models/user.go
Normal file
@ -0,0 +1,26 @@
|
||||
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"
|
||||
}
|
||||
|
||||
// func (u *User) SetPassword(password string) (string, error) {
|
||||
// secretKey := Config.Application.SecretKey
|
||||
// bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
// return string(bytes), err
|
||||
// }
|
||||
17
app/repository/dao.go
Normal file
17
app/repository/dao.go
Normal 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),
|
||||
}
|
||||
}
|
||||
16
app/repository/ordering.go
Normal file
16
app/repository/ordering.go
Normal file
@ -0,0 +1,16 @@
|
||||
package repository
|
||||
|
||||
const ORDERING_ASC = "asc"
|
||||
const ORDERING_DESC = "desc"
|
||||
|
||||
type Ordering struct {
|
||||
Field string
|
||||
Direction string
|
||||
}
|
||||
|
||||
func NewOrdering(field string, direction string) Ordering {
|
||||
return Ordering{
|
||||
Field: field,
|
||||
Direction: direction,
|
||||
}
|
||||
}
|
||||
13
app/repository/pagination.go
Normal file
13
app/repository/pagination.go
Normal file
@ -0,0 +1,13 @@
|
||||
package repository
|
||||
|
||||
type Pagination struct {
|
||||
PageSize int
|
||||
Page int
|
||||
}
|
||||
|
||||
func NewPagination() Pagination {
|
||||
return Pagination{
|
||||
PageSize: 50,
|
||||
Page: 1,
|
||||
}
|
||||
}
|
||||
92
app/repository/users.go
Normal file
92
app/repository/users.go
Normal file
@ -0,0 +1,92 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iris-test/app/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UsersRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type UserFilter struct {
|
||||
Id *string
|
||||
IsActive *bool
|
||||
}
|
||||
|
||||
func CreateUsersRepository(db *gorm.DB) *UsersRepository {
|
||||
return &UsersRepository{db}
|
||||
}
|
||||
|
||||
func applyFilter(db *gorm.DB, filter *UserFilter) *gorm.DB {
|
||||
query := db
|
||||
|
||||
if filter.Id != nil {
|
||||
query = query.Where("id = ?", filter.Id)
|
||||
}
|
||||
|
||||
if filter.IsActive != nil {
|
||||
query = query.Where(map[string]interface{}{"is_active": filter.IsActive})
|
||||
}
|
||||
|
||||
// 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 applyPagination(db *gorm.DB, pagination *Pagination) *gorm.DB {
|
||||
query := db
|
||||
|
||||
if pagination != nil {
|
||||
query.Limit(pagination.PageSize)
|
||||
query.Offset(pagination.PageSize * (pagination.Page - 1))
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func applyOrdering(db *gorm.DB, ordering *[]Ordering) *gorm.DB {
|
||||
query := db
|
||||
|
||||
if ordering != nil {
|
||||
for _, order := range *ordering {
|
||||
query.Order(fmt.Sprintf("%s %s", order.Field, order.Direction))
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (repository *UsersRepository) List(filter *UserFilter, pagination *Pagination, ordering *[]Ordering) *[]models.User {
|
||||
var users []models.User
|
||||
|
||||
query := repository.db.Model(&models.User{})
|
||||
query = applyFilter(query, filter)
|
||||
query = applyOrdering(query, ordering)
|
||||
query = applyPagination(query, pagination)
|
||||
query.Find(&users)
|
||||
|
||||
return &users
|
||||
}
|
||||
|
||||
func (repository *UsersRepository) Get(filter *UserFilter) *models.User {
|
||||
var user models.User
|
||||
|
||||
query := repository.db.Model(&models.User{})
|
||||
query = applyFilter(query, filter)
|
||||
query.First(&user)
|
||||
|
||||
return &user
|
||||
}
|
||||
|
||||
func (repository *UsersRepository) Save(user *models.User) *models.User {
|
||||
repository.db.Save(user)
|
||||
return user
|
||||
}
|
||||
33
app/templates/base/base.html
Normal file
33
app/templates/base/base.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% set title = "Hello world" %}
|
||||
|
||||
<!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">
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-3">
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link">Frontpage</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/users" class="nav-link">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about" class="nav-link">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<main>
|
||||
{% block mainContent %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,27 +0,0 @@
|
||||
{{ title := "Hello world" }}
|
||||
|
||||
<!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">
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<p>
|
||||
<a href="/">Frontpage</a>
|
||||
<a href="/users">Users</a>
|
||||
<a href="/about">About</a>
|
||||
</p>
|
||||
|
||||
<main>
|
||||
{{ yield mainContent() }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
24
app/templates/components/table_component.html
Normal file
24
app/templates/components/table_component.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% macro usersTable(users) %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>First name</th>
|
||||
<th>Last name</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/users/{{ user.Id }}">{{ user.Id }}</a>
|
||||
</td>
|
||||
<td>{{ user.FirstName }}</td>
|
||||
<td>{{ user.LastName }}</td>
|
||||
<td>{{ user.Email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
@ -1,15 +0,0 @@
|
||||
{{ block usersTable(users) }}
|
||||
<p>blablabla</p>
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{{ range users }}
|
||||
<tr>
|
||||
<td>{{ .firstName }}</td>
|
||||
<td>{{ .lastName }}</td>
|
||||
<td>{{ .email }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
28
app/templates/pages/index.html
Normal file
28
app/templates/pages/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% extends "/base/base.html" %}
|
||||
|
||||
|
||||
{% 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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,9 +1,26 @@
|
||||
{{ extends "/base/base.jet" }}
|
||||
{{ import "/components/table_component.jet" }}
|
||||
{{ extends "/base/base.html" }}
|
||||
{{ import "/components/table_component.html" }}
|
||||
|
||||
|
||||
{{ 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">
|
||||
<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>
|
||||
35
app/templates/pages/user-edit.jet
Normal file
35
app/templates/pages/user-edit.jet
Normal file
@ -0,0 +1,35 @@
|
||||
{{ extends "/base/base.jet" }}
|
||||
{{ import "/components/table_component.jet" }}
|
||||
|
||||
|
||||
{{ block mainContent() }}
|
||||
<h3>Edit user</h3>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<form class="mb-5 col-4 ms-auto me-auto" method="post" action="{{ .currentPath }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">First name</label>
|
||||
<input type="text" name="first-name" class="form-control" value="{{ user.FirstName }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Last name</label>
|
||||
<input type="text" name="last-name" class="form-control" value="{{ user.LastName }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ user.Email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<a href="/users" class="btn btn-outline-secondary ms-auto me-2">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
@ -3,13 +3,7 @@
|
||||
|
||||
|
||||
{{ block mainContent() }}
|
||||
<ul>
|
||||
{{ range params1 }}
|
||||
<li>{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
<h3>{{ title }}</h3>
|
||||
<h3>Users</h3>
|
||||
|
||||
{{ yield usersTable(users=users) }}
|
||||
{{ end }}
|
||||
|
||||
@ -1,11 +1,33 @@
|
||||
package views
|
||||
|
||||
import "github.com/kataras/iris/v12"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func GetIndexPage(ctx iris.Context) {
|
||||
params1 := []string{"param 1", "param 2", "param 3"}
|
||||
ctx.ViewData("params1", params1)
|
||||
ctx.ViewData("users", users)
|
||||
if err := ctx.View("pages/index.html"); err != nil {
|
||||
showError(ctx, err)
|
||||
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 {
|
||||
showError(ctx, err)
|
||||
|
||||
@ -4,6 +4,11 @@ import "github.com/kataras/iris/v12"
|
||||
|
||||
func CreateRouter(app *iris.Application) {
|
||||
app.Get("/", GetIndexPage)
|
||||
app.Post("/", PostIndexPage)
|
||||
|
||||
app.Get("/users", GetUsersPage)
|
||||
app.Get("/users/{userId:uuid}", EditUserPage)
|
||||
app.Post("/users/{userId:uuid}", SaveUserPage)
|
||||
|
||||
app.Get("/about", GetAboutPage)
|
||||
}
|
||||
|
||||
@ -1,39 +1,29 @@
|
||||
package views
|
||||
|
||||
import "github.com/kataras/iris/v12"
|
||||
import (
|
||||
"iris-test/app/repository"
|
||||
|
||||
type User struct {
|
||||
firstName string
|
||||
lastName string
|
||||
email string
|
||||
}
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
type editUserForm struct {
|
||||
FirstName string `form:"first-name"`
|
||||
LastName string `form:"last-name"`
|
||||
Email string `form:"email"`
|
||||
}
|
||||
|
||||
func GetUsersPage(ctx iris.Context) {
|
||||
params1 := []string{"param 1", "param 2", "param 3"}
|
||||
ctx.ViewData("params1", params1)
|
||||
userRepository := repository.Dao.UsersRepository
|
||||
|
||||
pagination := repository.NewPagination()
|
||||
ordering := []repository.Ordering{
|
||||
repository.NewOrdering("first_name", repository.ORDERING_ASC),
|
||||
repository.NewOrdering("last_name", repository.ORDERING_ASC),
|
||||
}
|
||||
|
||||
isActive := true
|
||||
users := userRepository.List(&repository.UserFilter{IsActive: &isActive}, &pagination, &ordering)
|
||||
|
||||
ctx.ViewData("users", users)
|
||||
|
||||
if err := ctx.View("pages/users.jet"); err != nil {
|
||||
@ -41,3 +31,42 @@ func GetUsersPage(ctx iris.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func EditUserPage(ctx iris.Context) {
|
||||
userId := ctx.Params().Get("userId")
|
||||
userRepository := repository.Dao.UsersRepository
|
||||
|
||||
filter := repository.UserFilter{Id: &userId}
|
||||
user := userRepository.Get(&filter)
|
||||
|
||||
ctx.ViewData("user", user)
|
||||
ctx.ViewData("currentPath", ctx.Path())
|
||||
|
||||
if err := ctx.View("pages/user-edit.jet"); err != nil {
|
||||
showError(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func SaveUserPage(ctx iris.Context) {
|
||||
var form editUserForm
|
||||
err := ctx.ReadForm(&form)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
userId := ctx.Params().Get("userId")
|
||||
userRepository := repository.Dao.UsersRepository
|
||||
|
||||
filter := repository.UserFilter{Id: &userId}
|
||||
user := userRepository.Get(&filter)
|
||||
|
||||
user.FirstName = form.FirstName
|
||||
user.LastName = form.LastName
|
||||
user.Email = form.Email
|
||||
|
||||
userRepository.Save(user)
|
||||
|
||||
ctx.Redirect("/users")
|
||||
}
|
||||
|
||||
@ -1,11 +1,3 @@
|
||||
# Database credentials
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
name: "iristest"
|
||||
username: "iristest"
|
||||
password: "iristest"
|
||||
|
||||
application:
|
||||
secretKey: "secret-key"
|
||||
logLevel: info
|
||||
@ -14,6 +6,21 @@ application:
|
||||
isProduction: false
|
||||
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:
|
||||
host: "smtp-host"
|
||||
port: 587
|
||||
|
||||
7
go.mod
7
go.mod
@ -19,8 +19,11 @@ require (
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // 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/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/flosch/pongo2/v6 v6.0.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
@ -32,6 +35,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // 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/golog v0.1.9 // indirect
|
||||
github.com/kataras/pio v0.0.12 // indirect
|
||||
@ -41,6 +45,9 @@ require (
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // 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/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@ -16,15 +16,22 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx
|
||||
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
||||
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
||||
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
|
||||
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@ -37,6 +44,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-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
@ -61,6 +69,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
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/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/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
|
||||
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
|
||||
@ -95,9 +105,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/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
|
||||
Reference in New Issue
Block a user