注册代码

This commit is contained in:
xmdhs 2023-09-02 00:33:00 +08:00
parent 6153ccee14
commit 109f284a11
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
17 changed files with 390 additions and 59 deletions

5
config/config.go Normal file
View File

@ -0,0 +1,5 @@
package config
type Config struct {
OfflineUUID bool
}

View File

@ -25,12 +25,18 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil {
return nil, fmt.Errorf("error preparing query CreateUser: %w", err) return nil, fmt.Errorf("error preparing query CreateUser: %w", err)
} }
if q.createUserProfileStmt, err = db.PrepareContext(ctx, createUserProfile); err != nil {
return nil, fmt.Errorf("error preparing query CreateUserProfile: %w", err)
}
if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil {
return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) return nil, fmt.Errorf("error preparing query DeleteUser: %w", err)
} }
if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil { if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil {
return nil, fmt.Errorf("error preparing query GetUser: %w", err) return nil, fmt.Errorf("error preparing query GetUser: %w", err)
} }
if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil {
return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err)
}
if q.listUserStmt, err = db.PrepareContext(ctx, listUser); err != nil { if q.listUserStmt, err = db.PrepareContext(ctx, listUser); err != nil {
return nil, fmt.Errorf("error preparing query ListUser: %w", err) return nil, fmt.Errorf("error preparing query ListUser: %w", err)
} }
@ -44,6 +50,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing createUserStmt: %w", cerr) err = fmt.Errorf("error closing createUserStmt: %w", cerr)
} }
} }
if q.createUserProfileStmt != nil {
if cerr := q.createUserProfileStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createUserProfileStmt: %w", cerr)
}
}
if q.deleteUserStmt != nil { if q.deleteUserStmt != nil {
if cerr := q.deleteUserStmt.Close(); cerr != nil { if cerr := q.deleteUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteUserStmt: %w", cerr) err = fmt.Errorf("error closing deleteUserStmt: %w", cerr)
@ -54,6 +65,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing getUserStmt: %w", cerr) err = fmt.Errorf("error closing getUserStmt: %w", cerr)
} }
} }
if q.getUserByEmailStmt != nil {
if cerr := q.getUserByEmailStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr)
}
}
if q.listUserStmt != nil { if q.listUserStmt != nil {
if cerr := q.listUserStmt.Close(); cerr != nil { if cerr := q.listUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listUserStmt: %w", cerr) err = fmt.Errorf("error closing listUserStmt: %w", cerr)
@ -99,8 +115,10 @@ type Queries struct {
db DBTX db DBTX
tx *sql.Tx tx *sql.Tx
createUserStmt *sql.Stmt createUserStmt *sql.Stmt
createUserProfileStmt *sql.Stmt
deleteUserStmt *sql.Stmt deleteUserStmt *sql.Stmt
getUserStmt *sql.Stmt getUserStmt *sql.Stmt
getUserByEmailStmt *sql.Stmt
listUserStmt *sql.Stmt listUserStmt *sql.Stmt
} }
@ -109,8 +127,10 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
db: tx, db: tx,
tx: tx, tx: tx,
createUserStmt: q.createUserStmt, createUserStmt: q.createUserStmt,
createUserProfileStmt: q.createUserProfileStmt,
deleteUserStmt: q.deleteUserStmt, deleteUserStmt: q.deleteUserStmt,
getUserStmt: q.getUserStmt, getUserStmt: q.getUserStmt,
getUserByEmailStmt: q.getUserByEmailStmt,
listUserStmt: q.listUserStmt, listUserStmt: q.listUserStmt,
} }
} }

View File

@ -5,6 +5,7 @@ package mysql
import () import ()
type Skin struct { type Skin struct {
ID int64 `db:"id"`
UserID int64 `db:"user_id"` UserID int64 `db:"user_id"`
SkinHash string `db:"skin_hash"` SkinHash string `db:"skin_hash"`
Type string `db:"type"` Type string `db:"type"`
@ -16,7 +17,22 @@ type User struct {
Email string `db:"email"` Email string `db:"email"`
Password string `db:"password"` Password string `db:"password"`
Salt string `db:"salt"` Salt string `db:"salt"`
Disabled int32 `db:"disabled"` State int32 `db:"state"`
Admin int32 `db:"admin"`
RegTime int64 `db:"reg_time"` RegTime int64 `db:"reg_time"`
} }
type UserProfile struct {
UserID int64 `db:"user_id"`
Name string `db:"name"`
Uuid string `db:"uuid"`
}
type UserSkin struct {
UserID int64 `db:"user_id"`
SkinID int64 `db:"skin_id"`
}
type UserToken struct {
UserID int64 `db:"user_id"`
TokenID int32 `db:"token_id"`
}

View File

@ -9,8 +9,10 @@ import (
type Querier interface { type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error)
CreateUserProfile(ctx context.Context, arg CreateUserProfileParams) (sql.Result, error)
DeleteUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error
GetUser(ctx context.Context, id int64) (User, error) GetUser(ctx context.Context, id int64) (User, error)
GetUserByEmail(ctx context.Context, email string) (User, error)
ListUser(ctx context.Context) ([]User, error) ListUser(ctx context.Context) ([]User, error)
} }

View File

@ -9,18 +9,16 @@ import (
) )
const createUser = `-- name: CreateUser :execresult const createUser = `-- name: CreateUser :execresult
INSERT INTO REPLACE INTO user (
user (
id, id,
email, email,
password, password,
salt, salt,
disabled, state,
admin,
reg_time reg_time
) )
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?) (?, ?, ?, ?, ?, ?)
` `
type CreateUserParams struct { type CreateUserParams struct {
@ -28,8 +26,7 @@ type CreateUserParams struct {
Email string `db:"email"` Email string `db:"email"`
Password string `db:"password"` Password string `db:"password"`
Salt string `db:"salt"` Salt string `db:"salt"`
Disabled int32 `db:"disabled"` State int32 `db:"state"`
Admin int32 `db:"admin"`
RegTime int64 `db:"reg_time"` RegTime int64 `db:"reg_time"`
} }
@ -39,12 +36,27 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Res
arg.Email, arg.Email,
arg.Password, arg.Password,
arg.Salt, arg.Salt,
arg.Disabled, arg.State,
arg.Admin,
arg.RegTime, arg.RegTime,
) )
} }
const createUserProfile = `-- name: CreateUserProfile :execresult
REPLACE INTO ` + "`" + `user_profile` + "`" + ` (` + "`" + `user_id` + "`" + `, ` + "`" + `name` + "`" + `, ` + "`" + `uuid` + "`" + `)
VALUES
(?, ?, ?)
`
type CreateUserProfileParams struct {
UserID int64 `db:"user_id"`
Name string `db:"name"`
Uuid string `db:"uuid"`
}
func (q *Queries) CreateUserProfile(ctx context.Context, arg CreateUserProfileParams) (sql.Result, error) {
return q.exec(ctx, q.createUserProfileStmt, createUserProfile, arg.UserID, arg.Name, arg.Uuid)
}
const deleteUser = `-- name: DeleteUser :exec const deleteUser = `-- name: DeleteUser :exec
DELETE FROM DELETE FROM
user user
@ -59,7 +71,7 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
const getUser = `-- name: GetUser :one const getUser = `-- name: GetUser :one
SELECT SELECT
id, email, password, salt, disabled, admin, reg_time id, email, password, salt, state, reg_time
FROM FROM
user user
WHERE WHERE
@ -76,8 +88,32 @@ func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
&i.Email, &i.Email,
&i.Password, &i.Password,
&i.Salt, &i.Salt,
&i.Disabled, &i.State,
&i.Admin, &i.RegTime,
)
return i, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT
id, email, password, salt, state, reg_time
FROM
user
WHERE
email = ?
LIMIT
1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Password,
&i.Salt,
&i.State,
&i.RegTime, &i.RegTime,
) )
return i, err return i, err
@ -85,7 +121,7 @@ func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
const listUser = `-- name: ListUser :many const listUser = `-- name: ListUser :many
SELECT SELECT
id, email, password, salt, disabled, admin, reg_time id, email, password, salt, state, reg_time
FROM FROM
user user
ORDER BY ORDER BY
@ -106,8 +142,7 @@ func (q *Queries) ListUser(ctx context.Context) ([]User, error) {
&i.Email, &i.Email,
&i.Password, &i.Password,
&i.Salt, &i.Salt,
&i.Disabled, &i.State,
&i.Admin,
&i.RegTime, &i.RegTime,
); err != nil { ); err != nil {
return nil, err return nil, err

View File

@ -17,21 +17,35 @@ ORDER BY
reg_time; reg_time;
-- name: CreateUser :execresult -- name: CreateUser :execresult
INSERT INTO REPLACE INTO user (
user (
id, id,
email, email,
password, password,
salt, salt,
disabled, state,
admin,
reg_time reg_time
) )
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?); (?, ?, ?, ?, ?, ?);
-- name: DeleteUser :exec -- name: DeleteUser :exec
DELETE FROM DELETE FROM
user user
WHERE WHERE
id = ?; id = ?;
-- name: CreateUserProfile :execresult
REPLACE INTO `user_profile` (`user_id`, `name`, `uuid`)
VALUES
(?, ?, ?);
-- name: GetUserByEmail :one
SELECT
*
FROM
user
WHERE
email = ?
LIMIT
1;

View File

@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS `user` (
email VARCHAR(20) NOT NULL, email VARCHAR(20) NOT NULL,
password text NOT NULL, password text NOT NULL,
salt text NOT NULL, salt text NOT NULL,
-- 二进制状态位,暂无作用
state INT NOT NULL, state INT NOT NULL,
reg_time BIGINT NOT NULL reg_time BIGINT NOT NULL
); );

2
go.mod
View File

@ -8,9 +8,11 @@ require (
) )
require ( require (
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
golang.org/x/crypto v0.7.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect golang.org/x/net v0.8.0 // indirect

4
go.sum
View File

@ -1,3 +1,5 @@
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -11,6 +13,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=

View File

@ -1 +1,25 @@
package handle package handle
import (
"log/slog"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/julienschmidt/httprouter"
"github.com/xmdhs/authlib-skin/db/mysql"
"github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/utils"
)
func Reg(l *slog.Logger, q mysql.Querier, v *validator.Validate) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := r.Context()
u, err := utils.DeCodeBody[model.User](r.Body, v)
if err != nil {
l.InfoContext(ctx, err.Error())
}
_ = u
}
}

View File

@ -1,33 +1,25 @@
package yggdrasil package yggdrasil
import ( import (
"encoding/json"
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/xmdhs/authlib-skin/model/yggdrasil" "github.com/xmdhs/authlib-skin/model/yggdrasil"
"github.com/xmdhs/authlib-skin/utils"
) )
func Authenticate(l *slog.Logger, v *validator.Validate) httprouter.Handle { func Authenticate(l *slog.Logger, v *validator.Validate) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
cxt := r.Context() cxt := r.Context()
jr := json.NewDecoder(r.Body) a, err := utils.DeCodeBody[yggdrasil.Authenticate](r.Body, v)
var a yggdrasil.Authenticate
err := jr.Decode(&a)
if err != nil { if err != nil {
l.Info(err.Error()) l.InfoContext(cxt, err.Error())
handleYgError(cxt, w, yggdrasil.Error{ErrorMessage: err.Error()}, 400)
return
}
err = v.Struct(a)
if err != nil {
l.Info(err.Error())
handleYgError(cxt, w, yggdrasil.Error{ErrorMessage: err.Error()}, 400) handleYgError(cxt, w, yggdrasil.Error{ErrorMessage: err.Error()}, 400)
return return
} }
_ = a
} }
} }

View File

@ -5,3 +5,9 @@ type API[T any] struct {
Data T `json:"data"` Data T `json:"data"`
Msg string `json:"msg"` Msg string `json:"msg"`
} }
type User struct {
Email string `validate:"required,email"`
Password string `validate:"required,sha256"`
Name string `validate:"required,min=3,max=16"`
}

42
server/slog.go Normal file
View File

@ -0,0 +1,42 @@
package server
import (
"context"
"log/slog"
)
type reqInfo struct {
URL string
IP string
TrackId uint64
}
type reqInfoKeyType string
var reqinfoKey reqInfoKeyType = "reqinfoKey"
func setCtx(ctx context.Context, r *reqInfo) context.Context {
return context.WithValue(ctx, reqinfoKey, r)
}
func getFromCtx(ctx context.Context) *reqInfo {
v := ctx.Value(reqinfoKey)
if v == nil {
return nil
}
return v.(*reqInfo)
}
type warpSlogHandle struct {
slog.Handler
}
func (w *warpSlogHandle) Handle(ctx context.Context, r slog.Record) error {
if w.Enabled(ctx, slog.LevelDebug) {
ri := getFromCtx(ctx)
if ri != nil {
r.AddAttrs(slog.String("ip", ri.IP), slog.String("url", ri.URL), slog.Uint64("trackID", ri.TrackId))
}
}
return w.Handler.Handle(ctx, r)
}

79
service/user.go Normal file
View File

@ -0,0 +1,79 @@
package service
import (
"context"
"crypto/md5"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/bwmarrin/snowflake"
"github.com/google/uuid"
"github.com/xmdhs/authlib-skin/config"
"github.com/xmdhs/authlib-skin/db/mysql"
"github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/utils"
)
var ErrExistUser = errors.New("用户已存在")
func Reg(ctx context.Context, u model.User, q mysql.Querier, db *sql.DB, snow *snowflake.Node,
c config.Config,
) error {
ou, err := q.GetUserByEmail(ctx, u.Email)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("Reg: %w", err)
}
if ou.Email != "" {
return fmt.Errorf("Reg: %w", ErrExistUser)
}
err = utils.WithTx(ctx, &sql.TxOptions{}, q, db, func(q mysql.Querier) error {
p, s := utils.Argon2ID(u.Password)
userID := snow.Generate().Int64()
_, err := q.CreateUser(ctx, mysql.CreateUserParams{
ID: userID,
Email: u.Email,
Password: p,
Salt: s,
State: 0,
RegTime: time.Now().Unix(),
})
if err != nil {
return err
}
var userUuid string
if c.OfflineUUID {
userUuid = uuidGen(u.Name)
} else {
userUuid = strings.ReplaceAll(uuid.New().String(), "-", "")
}
_, err = q.CreateUserProfile(ctx, mysql.CreateUserProfileParams{
UserID: userID,
Name: u.Name,
Uuid: userUuid,
})
if err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("Reg: %w", err)
}
return nil
}
func uuidGen(t string) string {
data := []byte("OfflinePlayer:" + t)
h := md5.New()
h.Write(data)
uuid := h.Sum(nil)
uuid[6] = (uuid[6] & 0x0f) | uint8((3&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80
return hex.EncodeToString(uuid)
}

28
utils/argon2id.go Normal file
View File

@ -0,0 +1,28 @@
package utils
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"golang.org/x/crypto/argon2"
)
func Argon2ID(pass string) (password string, salt string) {
s := make([]byte, 16)
_, err := rand.Read(s)
if err != nil {
panic(err)
}
b := argon2.IDKey([]byte(pass), s, 1, 64*1024, 1, 32)
return base64.StdEncoding.EncodeToString(b), base64.StdEncoding.EncodeToString(s)
}
func Argon2Compare(pass, hashPass string, salt []byte) bool {
b := argon2.IDKey([]byte(pass), salt, 1, 64*1024, 1, 32)
hb, err := base64.StdEncoding.DecodeString(hashPass)
if err != nil {
return false
}
return subtle.ConstantTimeCompare(b, hb) == 1
}

24
utils/decode.go Normal file
View File

@ -0,0 +1,24 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"github.com/go-playground/validator/v10"
)
func DeCodeBody[T any](r io.Reader, v *validator.Validate) (T, error) {
jr := json.NewDecoder(r)
var a T
err := jr.Decode(&a)
if err != nil {
return a, fmt.Errorf("DeCodeBody: %w", err)
}
err = v.Struct(a)
if err != nil {
return a, fmt.Errorf("DeCodeBody: %w", err)
}
return a, nil
}

37
utils/tx.go Normal file
View File

@ -0,0 +1,37 @@
package utils
import (
"context"
"database/sql"
"fmt"
"github.com/xmdhs/authlib-skin/db/mysql"
)
func WithTx(ctx context.Context, opts *sql.TxOptions, q mysql.Querier, db *sql.DB, f func(mysql.Querier) error) error {
w, ok := q.(interface {
WithTx(tx *sql.Tx) *mysql.Queries
})
var tx *sql.Tx
if ok {
fmt.Println("事务开启") // remove me
var err error
tx, err = db.BeginTx(ctx, opts)
if err != nil {
return fmt.Errorf("WithTx: %w", err)
}
defer tx.Rollback()
q = w.WithTx(tx)
}
err := f(q)
if err != nil {
return fmt.Errorf("WithTx: %w", err)
}
if tx != nil {
err := tx.Commit()
if err != nil {
return fmt.Errorf("WithTx: %w", err)
}
}
return nil
}