upgrade from go-pg to bun
This commit is contained in:
parent
f59f129354
commit
1886be62bc
23 changed files with 415 additions and 385 deletions
15
README.md
15
README.md
|
|
@ -14,7 +14,7 @@ The following feature set is a minimal selection of typical Web API requirements
|
|||
|
||||
- Configuration using [viper](https://github.com/spf13/viper)
|
||||
- CLI features using [cobra](https://github.com/spf13/cobra)
|
||||
- PostgreSQL support including migrations using [go-pg](https://github.com/go-pg/pg)
|
||||
- PostgreSQL support including migrations using [bun](https://github.com/uptrace/bun)
|
||||
- Structured logging with [Logrus](https://github.com/sirupsen/logrus)
|
||||
- Routing with [chi router](https://github.com/go-chi/chi) and middleware
|
||||
- JWT Authentication using [lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) with example passwordless email authentication
|
||||
|
|
@ -35,8 +35,8 @@ The following feature set is a minimal selection of typical Web API requirements
|
|||
### Using Docker Compose
|
||||
|
||||
- First start the database only: `docker compose up -d postgres`
|
||||
- Once initialize the database by running all migrations in database/migrate folder: `docker compose exec server ./main migrate`
|
||||
- Start the api server: `docker compose up server`
|
||||
- Once initialize the database by running all migrations in database/migrate folder: `docker compose run server ./main migrate`
|
||||
- Start the api server: `docker compose up`
|
||||
|
||||
## API Routes
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ For passwordless login following routes are available:
|
|||
|
||||
### Example API
|
||||
|
||||
Besides /auth/_ the API provides two main routes /api/_ and /admin/\*, as an example to separate application and administration context. The latter requires to be logged in as administrator by providing the respective JWT in Authorization Header.
|
||||
Besides /auth/_the API provides two main routes /api/_ and /admin/\*, as an example to separate application and administration context. The latter requires to be logged in as administrator by providing the respective JWT in Authorization Header.
|
||||
|
||||
Check [routes.md](routes.md) for a generated overview of the provided API routes.
|
||||
|
||||
|
|
@ -71,9 +71,10 @@ Outgoing emails containing the login token will be print to stdout if no valid e
|
|||
|
||||
Use one of the following bootstrapped users for login:
|
||||
|
||||
- admin@boot.io (has access to admin panel)
|
||||
- user@boot.io
|
||||
- <admin@example.com> (has access to admin panel)
|
||||
- <user@example.com>
|
||||
|
||||
TODO: deploy somewhere else...
|
||||
A deployed version can also be found on [Heroku](https://govue.herokuapp.com)
|
||||
|
||||
### Testing
|
||||
|
|
@ -94,7 +95,7 @@ By default viper will look at $HOME/.go-base.yaml for a config file. Setting you
|
|||
| DB_USER | string | postgres | database user name |
|
||||
| DB_PASSWORD | string | postgres | database user password |
|
||||
| DB_DATABASE | string | postgres | database shema name |
|
||||
| AUTH_LOGIN_URL | string | http://localhost:3000/login | client login url as sent in login token email |
|
||||
| AUTH_LOGIN_URL | string | <http://localhost:3000/login> | client login url as sent in login token email |
|
||||
| AUTH_LOGIN_TOKEN_LENGTH | int | 8 | length of login token |
|
||||
| AUTH_LOGIN_TOKEN_EXPIRY | time.Duration | 11m | login token expiry |
|
||||
| AUTH_JWT_SECRET | string | random | jwt sign and verify key - value "random" creates random 32 char secret at startup (and automatically invalidates existing tokens on app restarts, so during dev you might want to set a fixed value here) |
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-pg/pg"
|
||||
|
||||
"github.com/dhax/go-base/auth/authorize"
|
||||
"github.com/dhax/go-base/database"
|
||||
|
|
@ -30,8 +30,7 @@ type API struct {
|
|||
}
|
||||
|
||||
// NewAPI configures and returns admin application API.
|
||||
func NewAPI(db *pg.DB) (*API, error) {
|
||||
|
||||
func NewAPI(db *bun.DB) (*API, error) {
|
||||
accountStore := database.NewAdmAccountStore(db)
|
||||
accounts := NewAccountResource(accountStore)
|
||||
|
||||
|
|
|
|||
|
|
@ -82,8 +82,7 @@ func New(enableCORS bool) (*chi.Mux, error) {
|
|||
w.Write([]byte("pong"))
|
||||
})
|
||||
|
||||
client := "./public"
|
||||
r.Get("/*", SPAHandler(client))
|
||||
r.Get("/*", SPAHandler("public"))
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/dhax/go-base/database"
|
||||
"github.com/dhax/go-base/logging"
|
||||
|
|
@ -26,7 +26,7 @@ type API struct {
|
|||
}
|
||||
|
||||
// NewAPI configures and returns application API.
|
||||
func NewAPI(db *pg.DB) (*API, error) {
|
||||
func NewAPI(db *bun.DB) (*API, error) {
|
||||
accountStore := database.NewAccountStore(db)
|
||||
account := NewAccountResource(accountStore)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@ package jwt
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/orm"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// Token holds refresh jwt information.
|
||||
type Token struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
AccountID int `json:"-"`
|
||||
ID int `bun:"id,pk,autoincrement" json:"id,omitempty"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"`
|
||||
AccountID int `bun:"account_id,notnull" json:"-"`
|
||||
|
||||
Token string `json:"-"`
|
||||
Expiry time.Time `json:"-"`
|
||||
Mobile bool `sql:",notnull" json:"mobile"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
Token string `bun:"token,notnull" json:"-"`
|
||||
Expiry time.Time `bun:"expiry,notnull" json:"-"`
|
||||
Mobile bool `bun:"mobile,notnull" json:"mobile"`
|
||||
Identifier string `bun:"identifier" json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
// BeforeInsert hook executed before database insert operation.
|
||||
func (t *Token) BeforeInsert(db orm.DB) error {
|
||||
func (t *Token) BeforeInsert(db *bun.DB) error {
|
||||
now := time.Now()
|
||||
if t.CreatedAt.IsZero() {
|
||||
t.CreatedAt = now
|
||||
|
|
@ -30,7 +30,7 @@ func (t *Token) BeforeInsert(db orm.DB) error {
|
|||
}
|
||||
|
||||
// BeforeUpdate hook executed before database update operation.
|
||||
func (t *Token) BeforeUpdate(db orm.DB) error {
|
||||
func (t *Token) BeforeUpdate(db *bun.DB) error {
|
||||
t.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,28 +6,28 @@ import (
|
|||
|
||||
validation "github.com/go-ozzo/ozzo-validation"
|
||||
"github.com/go-ozzo/ozzo-validation/is"
|
||||
"github.com/go-pg/pg/orm"
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/dhax/go-base/auth/jwt"
|
||||
)
|
||||
|
||||
// Account represents an authenticated application user
|
||||
type Account struct {
|
||||
ID int `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
LastLogin time.Time `json:"last_login,omitempty"`
|
||||
ID int `bun:"id,pk,autoincrement" json:"id"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"`
|
||||
LastLogin time.Time `bun:"last_login" json:"last_login,omitempty"`
|
||||
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Active bool `sql:",notnull" json:"active"`
|
||||
Roles []string `pg:",array" json:"roles,omitempty"`
|
||||
Email string `bun:"email,notnull" json:"email"`
|
||||
Name string `bun:"name,notnull" json:"name"`
|
||||
Active bool `bun:"active,notnull" json:"active"`
|
||||
Roles []string `bun:"roles,array" json:"roles,omitempty"`
|
||||
|
||||
Token []jwt.Token `json:"token,omitempty"`
|
||||
Token []jwt.Token `bun:"rel:has-many" json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// BeforeInsert hook executed before database insert operation.
|
||||
func (a *Account) BeforeInsert(db orm.DB) error {
|
||||
func (a *Account) BeforeInsert(db *bun.DB) error {
|
||||
now := time.Now()
|
||||
if a.CreatedAt.IsZero() {
|
||||
a.CreatedAt = now
|
||||
|
|
@ -37,13 +37,13 @@ func (a *Account) BeforeInsert(db orm.DB) error {
|
|||
}
|
||||
|
||||
// BeforeUpdate hook executed before database update operation.
|
||||
func (a *Account) BeforeUpdate(db orm.DB) error {
|
||||
func (a *Account) BeforeUpdate(db *bun.DB) error {
|
||||
a.UpdatedAt = time.Now()
|
||||
return a.Validate()
|
||||
}
|
||||
|
||||
// BeforeDelete hook executed before database delete operation.
|
||||
func (a *Account) BeforeDelete(db orm.DB) error {
|
||||
func (a *Account) BeforeDelete(db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,30 +3,16 @@ package cmd
|
|||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/dhax/go-base/database/migrate"
|
||||
"github.com/dhax/go-base/database/migrations"
|
||||
)
|
||||
|
||||
var reset bool
|
||||
|
||||
// migrateCmd represents the migrate command
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "use go-pg migration tool",
|
||||
Long: `migrate uses go-pg migration tool under the hood supporting the same commands and an additional reset command`,
|
||||
Short: "use bun migration tool",
|
||||
Long: `run bun migrations`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
argsMig := args[:0]
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "migrate", "--db_debug", "--reset":
|
||||
default:
|
||||
argsMig = append(argsMig, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if reset {
|
||||
migrate.Reset()
|
||||
}
|
||||
migrate.Migrate(argsMig)
|
||||
migrations.Migrate()
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -42,5 +28,4 @@ func init() {
|
|||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// migrateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
migrateCmd.Flags().BoolVar(&reset, "reset", false, "migrate down to version 0 then up to latest. WARNING: all data will be lost!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/dhax/go-base/auth/jwt"
|
||||
"github.com/dhax/go-base/auth/pwdless"
|
||||
"github.com/dhax/go-base/models"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// AccountStore implements database operations for account management by user.
|
||||
type AccountStore struct {
|
||||
db *pg.DB
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// NewAccountStore returns an AccountStore.
|
||||
func NewAccountStore(db *pg.DB) *AccountStore {
|
||||
func NewAccountStore(db *bun.DB) *AccountStore {
|
||||
return &AccountStore{
|
||||
db: db,
|
||||
}
|
||||
|
|
@ -21,52 +24,71 @@ func NewAccountStore(db *pg.DB) *AccountStore {
|
|||
|
||||
// Get an account by ID.
|
||||
func (s *AccountStore) Get(id int) (*pwdless.Account, error) {
|
||||
a := pwdless.Account{ID: id}
|
||||
err := s.db.Model(&a).
|
||||
Where("account.id = ?id").
|
||||
Column("account.*", "Token").
|
||||
First()
|
||||
return &a, err
|
||||
a := &pwdless.Account{ID: id}
|
||||
err := s.db.NewSelect().
|
||||
Model(a).
|
||||
Where("id = ?", id).
|
||||
Relation("Token").
|
||||
Scan(context.Background())
|
||||
return a, err
|
||||
}
|
||||
|
||||
// Update an account.
|
||||
func (s *AccountStore) Update(a *pwdless.Account) error {
|
||||
_, err := s.db.Model(a).
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(a).
|
||||
Column("email", "name").
|
||||
WherePK().
|
||||
Update()
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete an account.
|
||||
func (s *AccountStore) Delete(a *pwdless.Account) error {
|
||||
err := s.db.RunInTransaction(func(tx *pg.Tx) error {
|
||||
if _, err := tx.Model(&jwt.Token{}).
|
||||
Where("account_id = ?", a.ID).
|
||||
Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Model(&models.Profile{}).
|
||||
Where("account_id = ?", a.ID).
|
||||
Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(a)
|
||||
})
|
||||
return err
|
||||
ctx := context.Background()
|
||||
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model((*jwt.Token)(nil)).
|
||||
Where("account_id = ?", a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model((*models.Profile)(nil)).
|
||||
Where("account_id = ?", a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model(a).
|
||||
WherePK().
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateToken updates a jwt refresh token.
|
||||
func (s *AccountStore) UpdateToken(t *jwt.Token) error {
|
||||
_, err := s.db.Model(t).
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(t).
|
||||
Column("identifier").
|
||||
WherePK().
|
||||
Update()
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteToken deletes a jwt refresh token.
|
||||
func (s *AccountStore) DeleteToken(t *jwt.Token) error {
|
||||
err := s.db.Delete(t)
|
||||
_, err := s.db.NewDelete().
|
||||
Model(t).
|
||||
WherePK().
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/dhax/go-base/auth/jwt"
|
||||
"github.com/dhax/go-base/auth/pwdless"
|
||||
"github.com/dhax/go-base/models"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/go-pg/pg/orm"
|
||||
"github.com/go-pg/pg/urlvalues"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -21,11 +21,11 @@ var (
|
|||
|
||||
// AdmAccountStore implements database operations for account management by admin.
|
||||
type AdmAccountStore struct {
|
||||
db *pg.DB
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// NewAdmAccountStore returns an AccountStore.
|
||||
func NewAdmAccountStore(db *pg.DB) *AdmAccountStore {
|
||||
func NewAdmAccountStore(db *bun.DB) *AdmAccountStore {
|
||||
return &AdmAccountStore{
|
||||
db: db,
|
||||
}
|
||||
|
|
@ -33,8 +33,9 @@ func NewAdmAccountStore(db *pg.DB) *AdmAccountStore {
|
|||
|
||||
// AccountFilter provides pagination and filtering options on accounts.
|
||||
type AccountFilter struct {
|
||||
Pager *urlvalues.Pager
|
||||
Filter *urlvalues.Filter
|
||||
Limit int
|
||||
Offset int
|
||||
Filter map[string]interface{}
|
||||
Order []string
|
||||
}
|
||||
|
||||
|
|
@ -44,29 +45,47 @@ func NewAccountFilter(params interface{}) (*AccountFilter, error) {
|
|||
if !ok {
|
||||
return nil, ErrBadParams
|
||||
}
|
||||
p := urlvalues.Values(v)
|
||||
f := &AccountFilter{
|
||||
Pager: urlvalues.NewPager(p),
|
||||
Filter: urlvalues.NewFilter(p),
|
||||
Order: p["order"],
|
||||
Limit: 10, // Default limit
|
||||
Offset: 0, // Default offset
|
||||
Filter: make(map[string]interface{}),
|
||||
Order: v["order"],
|
||||
}
|
||||
// Parse limit and offset
|
||||
if limit := v.Get("limit"); limit != "" {
|
||||
f.Limit = int(limit[0] - '0')
|
||||
}
|
||||
if offset := v.Get("offset"); offset != "" {
|
||||
f.Offset = int(offset[0] - '0')
|
||||
}
|
||||
// Parse filters
|
||||
for key, values := range v {
|
||||
if key != "limit" && key != "offset" && key != "order" {
|
||||
f.Filter[key] = values[0]
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Apply applies an AccountFilter on an orm.Query.
|
||||
func (f *AccountFilter) Apply(q *orm.Query) (*orm.Query, error) {
|
||||
q = q.Apply(f.Pager.Pagination)
|
||||
q = q.Apply(f.Filter.Filters)
|
||||
q = q.Order(f.Order...)
|
||||
return q, nil
|
||||
// Apply applies an AccountFilter on a bun.SelectQuery.
|
||||
func (f *AccountFilter) Apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
q = q.Limit(f.Limit).Offset(f.Offset)
|
||||
for key, value := range f.Filter {
|
||||
q = q.Where("? = ?", bun.Ident(key), value)
|
||||
}
|
||||
for _, order := range f.Order {
|
||||
q = q.Order(order)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// List applies a filter and returns paginated array of matching results and total count.
|
||||
func (s *AdmAccountStore) List(f *AccountFilter) ([]pwdless.Account, int, error) {
|
||||
a := []pwdless.Account{}
|
||||
count, err := s.db.Model(&a).
|
||||
var a []pwdless.Account
|
||||
count, err := s.db.NewSelect().
|
||||
Model(&a).
|
||||
Apply(f.Apply).
|
||||
SelectAndCount()
|
||||
ScanAndCount(context.Background())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -75,55 +94,90 @@ func (s *AdmAccountStore) List(f *AccountFilter) ([]pwdless.Account, int, error)
|
|||
|
||||
// Create creates a new account.
|
||||
func (s *AdmAccountStore) Create(a *pwdless.Account) error {
|
||||
count, _ := s.db.Model(a).
|
||||
Where("email = ?email").
|
||||
Count()
|
||||
|
||||
if count != 0 {
|
||||
return ErrUniqueEmailConstraint
|
||||
exists, err := s.db.NewSelect().
|
||||
Model((*pwdless.Account)(nil)).
|
||||
Where("email = ?", a.Email).
|
||||
Exists(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.db.RunInTransaction(func(tx *pg.Tx) error {
|
||||
err := tx.Insert(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := &models.Profile{
|
||||
AccountID: a.ID,
|
||||
}
|
||||
return tx.Insert(p)
|
||||
})
|
||||
if exists {
|
||||
return ErrUniqueEmailConstraint
|
||||
}
|
||||
ctx := context.Background()
|
||||
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewInsert().
|
||||
Model(a).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
p := &models.Profile{
|
||||
AccountID: a.ID,
|
||||
}
|
||||
if _, err := tx.NewInsert().
|
||||
Model(p).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get account by ID.
|
||||
func (s *AdmAccountStore) Get(id int) (*pwdless.Account, error) {
|
||||
a := pwdless.Account{ID: id}
|
||||
err := s.db.Select(&a)
|
||||
return &a, err
|
||||
a := &pwdless.Account{ID: id}
|
||||
err := s.db.NewSelect().
|
||||
Model(a).
|
||||
WherePK().
|
||||
Scan(context.Background())
|
||||
return a, err
|
||||
}
|
||||
|
||||
// Update account.
|
||||
func (s *AdmAccountStore) Update(a *pwdless.Account) error {
|
||||
err := s.db.Update(a)
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(a).
|
||||
WherePK().
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete account.
|
||||
func (s *AdmAccountStore) Delete(a *pwdless.Account) error {
|
||||
err := s.db.RunInTransaction(func(tx *pg.Tx) error {
|
||||
if _, err := tx.Model(&jwt.Token{}).
|
||||
Where("account_id = ?", a.ID).
|
||||
Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Model(&models.Profile{}).
|
||||
Where("account_id = ?", a.ID).
|
||||
Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(a)
|
||||
})
|
||||
return err
|
||||
ctx := context.Background()
|
||||
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model((*jwt.Token)(nil)).
|
||||
Where("account_id = ?", a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model((*models.Profile)(nil)).
|
||||
Where("account_id = ?", a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if _, err := tx.NewDelete().
|
||||
Model(a).
|
||||
WherePK().
|
||||
Exec(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dhax/go-base/auth/jwt"
|
||||
"github.com/dhax/go-base/auth/pwdless"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// AuthStore implements database operations for account pwdlessentication.
|
||||
type AuthStore struct {
|
||||
db *pg.DB
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// NewAuthStore return an AuthStore.
|
||||
func NewAuthStore(db *pg.DB) *AuthStore {
|
||||
func NewAuthStore(db *bun.DB) *AuthStore {
|
||||
return &AuthStore{
|
||||
db: db,
|
||||
}
|
||||
|
|
@ -22,65 +23,74 @@ func NewAuthStore(db *pg.DB) *AuthStore {
|
|||
|
||||
// GetAccount returns an account by ID.
|
||||
func (s *AuthStore) GetAccount(id int) (*pwdless.Account, error) {
|
||||
a := pwdless.Account{ID: id}
|
||||
err := s.db.Model(&a).
|
||||
Column("account.*").
|
||||
Where("id = ?id").
|
||||
First()
|
||||
return &a, err
|
||||
a := &pwdless.Account{ID: id}
|
||||
err := s.db.NewSelect().
|
||||
Model(a).
|
||||
Where("id = ?", id).
|
||||
Scan(context.Background())
|
||||
return a, err
|
||||
}
|
||||
|
||||
// GetAccountByEmail returns an account by email.
|
||||
func (s *AuthStore) GetAccountByEmail(e string) (*pwdless.Account, error) {
|
||||
a := pwdless.Account{Email: e}
|
||||
err := s.db.Model(&a).
|
||||
a := &pwdless.Account{Email: e}
|
||||
err := s.db.NewSelect().
|
||||
Model(a).
|
||||
Column("id", "active", "email", "name").
|
||||
Where("email = ?email").
|
||||
First()
|
||||
return &a, err
|
||||
Where("email = ?", e).
|
||||
Scan(context.Background())
|
||||
return a, err
|
||||
}
|
||||
|
||||
// UpdateAccount upates account data related to pwdlessentication.
|
||||
func (s *AuthStore) UpdateAccount(a *pwdless.Account) error {
|
||||
_, err := s.db.Model(a).
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(a).
|
||||
Column("last_login").
|
||||
WherePK().
|
||||
Update()
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// GetToken returns refresh token by token identifier.
|
||||
func (s *AuthStore) GetToken(t string) (*jwt.Token, error) {
|
||||
token := jwt.Token{Token: t}
|
||||
err := s.db.Model(&token).
|
||||
Where("token = ?token").
|
||||
First()
|
||||
|
||||
return &token, err
|
||||
token := &jwt.Token{Token: t}
|
||||
err := s.db.NewSelect().
|
||||
Model(token).
|
||||
Where("token = ?", t).
|
||||
Scan(context.Background())
|
||||
return token, err
|
||||
}
|
||||
|
||||
// CreateOrUpdateToken creates or updates an existing refresh token.
|
||||
func (s *AuthStore) CreateOrUpdateToken(t *jwt.Token) error {
|
||||
var err error
|
||||
if t.ID == 0 {
|
||||
err = s.db.Insert(t)
|
||||
} else {
|
||||
err = s.db.Update(t)
|
||||
_, err := s.db.NewInsert().
|
||||
Model(t).
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(t).
|
||||
WherePK().
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteToken deletes a refresh token.
|
||||
func (s *AuthStore) DeleteToken(t *jwt.Token) error {
|
||||
err := s.db.Delete(t)
|
||||
_, err := s.db.NewDelete().
|
||||
Model(t).
|
||||
WherePK().
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeExpiredToken deletes expired refresh token.
|
||||
func (s *AuthStore) PurgeExpiredToken() error {
|
||||
_, err := s.db.Model(&jwt.Token{}).
|
||||
_, err := s.db.NewDelete().
|
||||
Model((*jwt.Token)(nil)).
|
||||
Where("expiry < ?", time.Now()).
|
||||
Delete()
|
||||
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-pg/migrations"
|
||||
)
|
||||
|
||||
const bootstrapAdminAccount = `
|
||||
INSERT INTO accounts (id, email, name, active, roles)
|
||||
VALUES (DEFAULT, 'admin@boot.io', 'Admin Boot', true, '{admin}')
|
||||
`
|
||||
|
||||
const bootstrapUserAccount = `
|
||||
INSERT INTO accounts (id, email, name, active)
|
||||
VALUES (DEFAULT, 'user@boot.io', 'User Boot', true)
|
||||
`
|
||||
|
||||
func init() {
|
||||
up := []string{
|
||||
bootstrapAdminAccount,
|
||||
bootstrapUserAccount,
|
||||
}
|
||||
|
||||
down := []string{
|
||||
`TRUNCATE accounts CASCADE`,
|
||||
}
|
||||
|
||||
migrations.Register(func(db migrations.DB) error {
|
||||
fmt.Println("add bootstrap accounts")
|
||||
for _, q := range up {
|
||||
_, err := db.Exec(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, func(db migrations.DB) error {
|
||||
fmt.Println("truncate accounts cascading")
|
||||
for _, q := range down {
|
||||
_, err := db.Exec(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// Package migrate implements postgres migrations.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/dhax/go-base/database"
|
||||
"github.com/go-pg/migrations"
|
||||
"github.com/go-pg/pg"
|
||||
)
|
||||
|
||||
// Migrate runs go-pg migrations
|
||||
func Migrate(args []string) {
|
||||
db, err := database.DBConn()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.RunInTransaction(func(tx *pg.Tx) error {
|
||||
oldVersion, newVersion, err := migrations.Run(tx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newVersion != oldVersion {
|
||||
log.Printf("migrated from version %d to %d\n", oldVersion, newVersion)
|
||||
} else {
|
||||
log.Printf("version is %d\n", oldVersion)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reset runs reverts all migrations to version 0 and then applies all migrations to latest
|
||||
func Reset() {
|
||||
db, err := database.DBConn()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
version, err := migrations.Version(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.RunInTransaction(func(tx *pg.Tx) error {
|
||||
for version != 0 {
|
||||
oldVersion, newVersion, err := migrations.Run(tx, "down")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("migrated from version %d to %d\n", oldVersion, newVersion)
|
||||
version = newVersion
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package migrate
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-pg/migrations"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
const accountTable = `
|
||||
|
|
@ -43,7 +44,7 @@ func init() {
|
|||
`DROP TABLE accounts`,
|
||||
}
|
||||
|
||||
migrations.Register(func(db migrations.DB) error {
|
||||
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
|
||||
fmt.Println("creating initial tables")
|
||||
for _, q := range up {
|
||||
_, err := db.Exec(q)
|
||||
|
|
@ -52,7 +53,7 @@ func init() {
|
|||
}
|
||||
}
|
||||
return nil
|
||||
}, func(db migrations.DB) error {
|
||||
}, func(ctx context.Context, db *bun.DB) error {
|
||||
fmt.Println("dropping initial tables")
|
||||
for _, q := range down {
|
||||
_, err := db.Exec(q)
|
||||
1
database/migrations/2_bootstrap_users.tx.down.sql
Normal file
1
database/migrations/2_bootstrap_users.tx.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
TRUNCATE accounts CASCADE
|
||||
8
database/migrations/2_bootstrap_users.up.sql
Normal file
8
database/migrations/2_bootstrap_users.up.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
INSERT INTO accounts (id, email, name, active, roles)
|
||||
VALUES (DEFAULT, 'admin@example.com', 'Admin Example', true, '{admin}');
|
||||
|
||||
--bun:split
|
||||
|
||||
INSERT INTO accounts (id, email, name, active)
|
||||
VALUES (DEFAULT, 'user@example.com', 'User Example', true);
|
||||
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package migrate
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-pg/migrations"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
const profileTable = `
|
||||
|
|
@ -30,7 +31,7 @@ func init() {
|
|||
`DROP TABLE profiles`,
|
||||
}
|
||||
|
||||
migrations.Register(func(db migrations.DB) error {
|
||||
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
|
||||
fmt.Println("create profile table")
|
||||
for _, q := range up {
|
||||
_, err := db.Exec(q)
|
||||
|
|
@ -39,7 +40,7 @@ func init() {
|
|||
}
|
||||
}
|
||||
return nil
|
||||
}, func(db migrations.DB) error {
|
||||
}, func(ctx context.Context, db *bun.DB) error {
|
||||
fmt.Println("drop profile table")
|
||||
for _, q := range down {
|
||||
_, err := db.Exec(q)
|
||||
49
database/migrations/main.go
Normal file
49
database/migrations/main.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/dhax/go-base/database"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
//go:embed *.sql
|
||||
var sqlMigrations embed.FS
|
||||
|
||||
var Migrations = migrate.NewMigrations()
|
||||
|
||||
func init() {
|
||||
if err := Migrations.Discover(sqlMigrations); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate runs all migrations
|
||||
func Migrate() {
|
||||
db, err := database.DBConn()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
migrator := migrate.NewMigrator(db, Migrations)
|
||||
|
||||
err = migrator.Init(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
group, err := migrator.Migrate(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if group.ID == 0 {
|
||||
fmt.Printf("there are no new migrations to run\n")
|
||||
} else {
|
||||
fmt.Printf("migrated to %s\n", group)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,54 +2,42 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/driver/pgdriver"
|
||||
"github.com/uptrace/bun/extra/bundebug"
|
||||
)
|
||||
|
||||
// DBConn returns a postgres connection pool.
|
||||
func DBConn() (*pg.DB, error) {
|
||||
func DBConn() (*bun.DB, error) {
|
||||
viper.SetDefault("db_network", "tcp")
|
||||
viper.SetDefault("db_addr", "localhost:5432")
|
||||
viper.SetDefault("db_user", "postgres")
|
||||
viper.SetDefault("db_password", "postgres")
|
||||
viper.SetDefault("db_database", "postgres")
|
||||
|
||||
db := pg.Connect(&pg.Options{
|
||||
Network: viper.GetString("db_network"),
|
||||
Addr: viper.GetString("db_addr"),
|
||||
User: viper.GetString("db_user"),
|
||||
Password: viper.GetString("db_password"),
|
||||
Database: viper.GetString("db_database"),
|
||||
})
|
||||
dsn := "postgres://" + viper.GetString("db_user") + ":" + viper.GetString("db_password") + "@" + viper.GetString("db_addr") + "/" + viper.GetString("db_database") + "?sslmode=disable"
|
||||
|
||||
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
|
||||
|
||||
db := bun.NewDB(sqldb, pgdialect.New())
|
||||
|
||||
if err := checkConn(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if viper.GetBool("db_debug") {
|
||||
db.AddQueryHook(&logSQL{})
|
||||
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
type logSQL struct{}
|
||||
|
||||
func (l *logSQL) BeforeQuery(e *pg.QueryEvent) {}
|
||||
|
||||
func (l *logSQL) AfterQuery(e *pg.QueryEvent) {
|
||||
query, err := e.FormattedQuery()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println(query)
|
||||
}
|
||||
|
||||
func checkConn(db *pg.DB) error {
|
||||
func checkConn(db *bun.DB) error {
|
||||
var n int
|
||||
_, err := db.QueryOne(pg.Scan(&n), "SELECT 1")
|
||||
return err
|
||||
return db.NewSelect().ColumnExpr("1").Scan(context.Background(), &n)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/dhax/go-base/models"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// ProfileStore implements database operations for profile management.
|
||||
type ProfileStore struct {
|
||||
db *pg.DB
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// NewProfileStore returns a ProfileStore implementation.
|
||||
func NewProfileStore(db *pg.DB) *ProfileStore {
|
||||
func NewProfileStore(db *bun.DB) *ProfileStore {
|
||||
return &ProfileStore{
|
||||
db: db,
|
||||
}
|
||||
|
|
@ -19,16 +22,26 @@ func NewProfileStore(db *pg.DB) *ProfileStore {
|
|||
|
||||
// Get gets an profile by account ID.
|
||||
func (s *ProfileStore) Get(accountID int) (*models.Profile, error) {
|
||||
p := models.Profile{AccountID: accountID}
|
||||
_, err := s.db.Model(&p).
|
||||
p := &models.Profile{AccountID: accountID}
|
||||
err := s.db.NewSelect().
|
||||
Model(p).
|
||||
Where("account_id = ?", accountID).
|
||||
SelectOrInsert()
|
||||
Scan(context.Background())
|
||||
|
||||
return &p, err
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = s.db.NewInsert().
|
||||
Model(p).
|
||||
Exec(context.Background())
|
||||
}
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Update updates profile.
|
||||
func (s *ProfileStore) Update(p *models.Profile) error {
|
||||
err := s.db.Update(p)
|
||||
_, err := s.db.NewUpdate().
|
||||
Model(p).
|
||||
WherePK().
|
||||
Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
version: "3.8"
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
|
|
@ -40,3 +35,6 @@ services:
|
|||
- postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
|
|
|
|||
27
go.mod
27
go.mod
|
|
@ -1,6 +1,8 @@
|
|||
module github.com/dhax/go-base
|
||||
|
||||
go 1.18
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
|
|
@ -10,8 +12,6 @@ require (
|
|||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
|
||||
github.com/go-pg/migrations v6.7.3+incompatible
|
||||
github.com/go-pg/pg v8.0.7+incompatible
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.13
|
||||
|
|
@ -20,6 +20,10 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/uptrace/bun v1.2.3
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.3
|
||||
github.com/uptrace/bun/driver/pgdriver v1.2.3
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.3
|
||||
github.com/vanng822/go-premailer v1.20.2
|
||||
)
|
||||
|
||||
|
|
@ -29,6 +33,7 @@ require (
|
|||
github.com/andybalholm/cascadia v1.1.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
|
|
@ -41,12 +46,13 @@ require (
|
|||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.28.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
|
|
@ -54,11 +60,14 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/vanng822/css v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
|
|
|
|||
80
go.sum
80
go.sum
|
|
@ -68,9 +68,10 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
|
|
@ -92,11 +93,6 @@ github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX
|
|||
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
|
||||
github.com/go-pg/migrations v6.7.3+incompatible h1:mKayeWTNGhYA9P9wzZNSDoumJRhfB4fEmfAlxNTVwtA=
|
||||
github.com/go-pg/migrations v6.7.3+incompatible/go.mod h1:DtFiob3rFxsj0He8fye6Ta4eukFW80IfdY10zb2yH1c=
|
||||
github.com/go-pg/pg v8.0.7+incompatible h1:ty/sXL1OZLo+47KK9N8llRcmbA9tZasqbQ/OO4ld53g=
|
||||
github.com/go-pg/pg v8.0.7+incompatible/go.mod h1:a2oXow+aFOrvwcKs3eIA0lNFmMilrxK2sOkB5NWe0vA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
|
|
@ -138,6 +134,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
|
@ -162,7 +159,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
|
@ -177,9 +173,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
|
|
@ -195,6 +193,11 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB
|
|||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
|
|
@ -203,19 +206,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
|||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
||||
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
|
||||
github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -223,8 +217,11 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
|
|||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
|
|
@ -259,12 +256,26 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
|
||||
github.com/uptrace/bun v1.2.3 h1:6KDc6YiNlXde38j9ATKufb8o7MS8zllhAOeIyELKrk0=
|
||||
github.com/uptrace/bun v1.2.3/go.mod h1:8frYFHrO/Zol3I4FEjoXam0HoNk+t5k7aJRl3FXp0mk=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.3 h1:YyCxxqeL0lgFWRZzKCOt6mnxUsjqITcxSo0mLqgwMUA=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.3/go.mod h1:Vx9TscyEq1iN4tnirn6yYGwEflz0KG3rBZTBCLpKAjc=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.2.3 h1:VA5TKB0XW7EtreQq2R8Qu/vCAUX2ECaprxGKI9iDuDE=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.2.3/go.mod h1:yDiYTZYd4FfXFtV01m4I/RkI33IGj9N254jLStaeJLs=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw=
|
||||
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
|
||||
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/go-premailer v1.20.2 h1:vKs4VdtfXDqL7IXC2pkiBObc1bXM9bYH3Wa+wYw2DnI=
|
||||
github.com/vanng822/go-premailer v1.20.2/go.mod h1:RAxbRFp6M/B171gsKu8dsyq+Y5NGsUUvYfg+WQWusbE=
|
||||
github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -284,8 +295,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
@ -324,7 +336,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
|
@ -345,7 +356,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
|
|
@ -360,8 +370,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -384,7 +394,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -393,10 +402,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -417,7 +423,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -427,11 +432,14 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -447,8 +455,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
@ -496,7 +505,6 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
|
|||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
|
|
@ -597,19 +605,15 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
|||
|
|
@ -4,35 +4,34 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-ozzo/ozzo-validation"
|
||||
validation "github.com/go-ozzo/ozzo-validation"
|
||||
|
||||
"github.com/go-pg/pg/orm"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// Profile holds specific application settings linked to an Account.
|
||||
type Profile struct {
|
||||
ID int `json:"-"`
|
||||
AccountID int `json:"-"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
ID int `bun:"id,pk,autoincrement" json:"-"`
|
||||
AccountID int `bun:"account_id,notnull" json:"-"`
|
||||
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"`
|
||||
|
||||
Theme string `json:"theme,omitempty"`
|
||||
Theme string `bun:"theme" json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
// BeforeInsert hook executed before database insert operation.
|
||||
func (p *Profile) BeforeInsert(db orm.DB) error {
|
||||
func (p *Profile) BeforeInsert(db *bun.DB) error {
|
||||
p.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook executed before database update operation.
|
||||
func (p *Profile) BeforeUpdate(db orm.DB) error {
|
||||
func (p *Profile) BeforeUpdate(db *bun.DB) error {
|
||||
p.UpdatedAt = time.Now()
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// Validate validates Profile struct and returns validation errors.
|
||||
func (p *Profile) Validate() error {
|
||||
|
||||
return validation.ValidateStruct(p,
|
||||
validation.Field(&p.Theme, validation.Required, validation.In("default", "dark")),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue