feat: add tenant commands
This commit is contained in:
BIN
backend/__debug_bin1551459525
Executable file
BIN
backend/__debug_bin1551459525
Executable file
Binary file not shown.
BIN
backend/backend
BIN
backend/backend
Binary file not shown.
@@ -18,8 +18,8 @@ const (
|
||||
CtxKeyTx CtxKey = "__ctx_db:"
|
||||
// CtxKeyJwt is a CtxKey of type Jwt.
|
||||
CtxKeyJwt CtxKey = "__jwt_token:"
|
||||
// CtxKeySession is a CtxKey of type Session.
|
||||
CtxKeySession CtxKey = "__session_user:"
|
||||
// CtxKeyClaim is a CtxKey of type Claim.
|
||||
CtxKeyClaim CtxKey = "__jwt_claim:"
|
||||
)
|
||||
|
||||
var ErrInvalidCtxKey = fmt.Errorf("not a valid CtxKey, try [%s]", strings.Join(_CtxKeyNames, ", "))
|
||||
@@ -27,7 +27,7 @@ var ErrInvalidCtxKey = fmt.Errorf("not a valid CtxKey, try [%s]", strings.Join(_
|
||||
var _CtxKeyNames = []string{
|
||||
string(CtxKeyTx),
|
||||
string(CtxKeyJwt),
|
||||
string(CtxKeySession),
|
||||
string(CtxKeyClaim),
|
||||
}
|
||||
|
||||
// CtxKeyNames returns a list of possible string values of CtxKey.
|
||||
@@ -42,7 +42,7 @@ func CtxKeyValues() []CtxKey {
|
||||
return []CtxKey{
|
||||
CtxKeyTx,
|
||||
CtxKeyJwt,
|
||||
CtxKeySession,
|
||||
CtxKeyClaim,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ func (x CtxKey) IsValid() bool {
|
||||
}
|
||||
|
||||
var _CtxKeyValue = map[string]CtxKey{
|
||||
"__ctx_db:": CtxKeyTx,
|
||||
"__jwt_token:": CtxKeyJwt,
|
||||
"__session_user:": CtxKeySession,
|
||||
"__ctx_db:": CtxKeyTx,
|
||||
"__jwt_token:": CtxKeyJwt,
|
||||
"__jwt_claim:": CtxKeyClaim,
|
||||
}
|
||||
|
||||
// ParseCtxKey attempts to convert a string to a CtxKey.
|
||||
|
||||
@@ -4,6 +4,6 @@ package consts
|
||||
// ENUM(
|
||||
// Tx = "__ctx_db:",
|
||||
// Jwt = "__jwt_token:",
|
||||
// Session = "__session_user:",
|
||||
// Claim = "__jwt_claim:",
|
||||
// )
|
||||
type CtxKey string
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"backend/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func GetJwtToken(ctx fiber.Ctx) (string, error) {
|
||||
headers, ok := ctx.GetReqHeaders()[jwt.HttpHeader]
|
||||
if !ok {
|
||||
return "", ctx.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return "", ctx.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
token := headers[0]
|
||||
|
||||
token = token[len(jwt.TokenPrefix):]
|
||||
return token, nil
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func Serve(cmd *cobra.Command, args []string) error {
|
||||
http.Service.Engine.Use(mid.WeChatVerify)
|
||||
http.Service.Engine.Use(mid.WeChatAuthUserInfo)
|
||||
http.Service.Engine.Use(mid.WeChatSilentAuth)
|
||||
http.Service.Engine.Use(mid.JwtParse)
|
||||
http.Service.Engine.Use(mid.ParseJWT)
|
||||
|
||||
mounts := map[string][]string{
|
||||
"/t/{tenant}": {"users", "medias"},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"backend/modules/commands/discover"
|
||||
"backend/modules/commands/store"
|
||||
"backend/modules/medias"
|
||||
"backend/modules/tasks/discover"
|
||||
"backend/modules/tasks/store"
|
||||
"backend/providers/app"
|
||||
"backend/providers/postgres"
|
||||
"backend/providers/storage"
|
||||
|
||||
73
backend/common/service/tenants/tenants.go
Normal file
73
backend/common/service/tenants/tenants.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package tenants
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"backend/modules/commands/tenant"
|
||||
"backend/modules/medias"
|
||||
"backend/modules/users"
|
||||
"backend/providers/app"
|
||||
"backend/providers/postgres"
|
||||
"backend/providers/storage"
|
||||
|
||||
"git.ipao.vip/rogeecn/atom"
|
||||
"git.ipao.vip/rogeecn/atom/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func defaultProviders(providers ...container.ProviderContainer) container.Providers {
|
||||
return append(container.Providers{
|
||||
app.DefaultProvider(),
|
||||
storage.DefaultProvider(),
|
||||
postgres.DefaultProvider(),
|
||||
}, providers...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("tenants"),
|
||||
atom.Short("run tenants"),
|
||||
atom.Command(
|
||||
atom.Name("create"),
|
||||
atom.Providers(defaultProviders().With(
|
||||
medias.Provide,
|
||||
users.Provide,
|
||||
tenant.Provide,
|
||||
)),
|
||||
atom.Arguments(func(cmd *cobra.Command) {
|
||||
cmd.Flags().String("slug", "", "slug")
|
||||
}),
|
||||
atom.RunE(func(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(t *tenant.Create) error {
|
||||
slug := cmd.Flag("slug").Value.String()
|
||||
return t.RunE(args[0], slug)
|
||||
})
|
||||
}),
|
||||
),
|
||||
atom.Command(
|
||||
atom.Name("expire"),
|
||||
atom.Long("expire [slug] [2024-01-01]"),
|
||||
atom.Providers(defaultProviders().With(
|
||||
medias.Provide,
|
||||
users.Provide,
|
||||
tenant.Provide,
|
||||
)),
|
||||
atom.Arguments(func(cmd *cobra.Command) {
|
||||
}),
|
||||
atom.RunE(func(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(t *tenant.Expire) error {
|
||||
slug := args[0]
|
||||
expireStr := args[1] // format 2024-01-01
|
||||
// parse expire string as time.Time
|
||||
expire, err := time.Parse("2006-01-02", expireStr)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "parse expire time failed: %s", expireStr)
|
||||
}
|
||||
|
||||
return t.RunE(slug, expire)
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
1
backend/common/session.go
Normal file
1
backend/common/session.go
Normal file
@@ -0,0 +1 @@
|
||||
package common
|
||||
@@ -20,7 +20,7 @@ CREATE TABLE
|
||||
tenants (
|
||||
id SERIAL8 PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(128) NOT NULL UNIQUE,
|
||||
description VARCHAR(128),
|
||||
expire_at timestamp NOT NULL,
|
||||
created_at timestamp NOT NULL default now(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"backend/common/service/migrate"
|
||||
"backend/common/service/model"
|
||||
"backend/common/service/tasks"
|
||||
"backend/common/service/tenants"
|
||||
|
||||
"git.ipao.vip/rogeecn/atom"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -20,6 +21,7 @@ func main() {
|
||||
migrate.Command(),
|
||||
model.Command(),
|
||||
tasks.Command(),
|
||||
tenants.Command(),
|
||||
}
|
||||
|
||||
if err := atom.Serve(opts...); err != nil {
|
||||
|
||||
@@ -44,6 +44,10 @@ func (d *DiscoverMedias) RunE(from, to string) error {
|
||||
return errors.Wrapf(err, "glob videos: %s", from)
|
||||
}
|
||||
|
||||
if err := d.ensureDirectory(to); err != nil {
|
||||
return errors.Wrapf(err, "ensure directory: %s", to)
|
||||
}
|
||||
|
||||
store, err := media_store.NewStore(to)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "new store: %s", to)
|
||||
@@ -271,7 +275,7 @@ func (d *DiscoverMedias) ffmpegVideoToPoster(video string, output string) error
|
||||
// ffmpeg -i input_video.mp4 -ss N -vframes 1 -vf "scale=width:height" output_image.jpg
|
||||
args := []string{
|
||||
"-i", video,
|
||||
"-ss", "00:01:00",
|
||||
"-ss", "00:00:01",
|
||||
"-vframes", "1",
|
||||
"-vf", "scale=640:360",
|
||||
output,
|
||||
34
backend/modules/commands/tenant/create.go
Normal file
34
backend/modules/commands/tenant/create.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/modules/users"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type Create struct {
|
||||
userSvc *users.Service
|
||||
log *log.Entry `inject:"false"`
|
||||
}
|
||||
|
||||
// Prepare
|
||||
func (d *Create) Prepare() error {
|
||||
d.log = log.WithField("module", "tenants.create")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Create) RunE(name, slug string) error {
|
||||
d.log.Infof("create tenant %s(%s)", name, slug)
|
||||
|
||||
err := d.userSvc.CreateTenant(context.Background(), name, slug)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "create tenant: %s(%s)", name, slug)
|
||||
}
|
||||
|
||||
d.log.Infof("create tenant success: %s(%s)", name, slug)
|
||||
return nil
|
||||
}
|
||||
35
backend/modules/commands/tenant/expire.go
Normal file
35
backend/modules/commands/tenant/expire.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"backend/modules/users"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type Expire struct {
|
||||
userSvc *users.Service
|
||||
log *log.Entry `inject:"false"`
|
||||
}
|
||||
|
||||
// Prepare
|
||||
func (d *Expire) Prepare() error {
|
||||
d.log = log.WithField("module", "tenants.create")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Expire) RunE(slug string, expire time.Time) error {
|
||||
d.log.Infof("renew tenant %s expire at s %s", slug, expire)
|
||||
|
||||
err := d.userSvc.SetTenantExpireAtBySlug(context.Background(), slug, expire)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "renew tenant: %s expire at %s", slug, expire)
|
||||
}
|
||||
|
||||
d.log.Infof("renew tenant success: %s expire at %s", slug, expire)
|
||||
return nil
|
||||
}
|
||||
40
backend/modules/commands/tenant/provider.gen.go
Executable file
40
backend/modules/commands/tenant/provider.gen.go
Executable file
@@ -0,0 +1,40 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"backend/modules/users"
|
||||
|
||||
"git.ipao.vip/rogeecn/atom/container"
|
||||
"git.ipao.vip/rogeecn/atom/utils/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
userSvc *users.Service,
|
||||
) (*Create, error) {
|
||||
obj := &Create{
|
||||
userSvc: userSvc,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := container.Container.Provide(func(
|
||||
userSvc *users.Service,
|
||||
) (*Expire, error) {
|
||||
obj := &Expire{
|
||||
userSvc: userSvc,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package medias
|
||||
|
||||
import (
|
||||
"backend/common/consts"
|
||||
"backend/common/errorx"
|
||||
"backend/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
. "github.com/spf13/cast"
|
||||
@@ -18,10 +20,9 @@ func (c *Controller) List(ctx fiber.Ctx) error {
|
||||
if err := ctx.Bind().Body(&filter); err != nil {
|
||||
return ctx.Status(fiber.StatusBadRequest).JSON(errorx.RequestParseError)
|
||||
}
|
||||
claim := ctx.Locals(consts.CtxKeyClaim).(*jwt.Claims)
|
||||
|
||||
tenantId, userId := ToInt64(ctx.Locals("tenantId")), ToInt64(ctx.Locals("userId"))
|
||||
|
||||
items, err := c.svc.List(ctx.Context(), tenantId, userId, &filter)
|
||||
items, err := c.svc.List(ctx.Context(), claim.TenantID, claim.UserID, &filter)
|
||||
if err != nil {
|
||||
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ func (r *Router) Prepare() error {
|
||||
|
||||
func (r *Router) Register() any {
|
||||
r.group.Get("", r.controller.List)
|
||||
r.group.Get("{id}", r.controller.Show)
|
||||
|
||||
return r.app
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/common/consts"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (f *Middlewares) JwtParse(c fiber.Ctx) error {
|
||||
func (f *Middlewares) ParseJWT(c fiber.Ctx) error {
|
||||
tokens := c.GetReqHeaders()["Authorization"]
|
||||
if len(tokens) == 0 {
|
||||
return c.Next()
|
||||
@@ -22,13 +20,20 @@ func (f *Middlewares) JwtParse(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// query user
|
||||
user, err := f.userSvc.GetByOpenID(c.Context(), claim.ID)
|
||||
user, err := f.userSvc.GetByOpenID(c.Context(), claim.OpenID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user")
|
||||
}
|
||||
claim.UserID = user.ID
|
||||
|
||||
c.SetUserContext(context.WithValue(c.UserContext(), consts.CtxKeyJwt, token))
|
||||
c.SetUserContext(context.WithValue(c.UserContext(), consts.CtxKeySession, user))
|
||||
tenantId, err := f.userSvc.GetTenantIDBySlug(c.Context(), claim.Tenant)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get tenant")
|
||||
}
|
||||
claim.TenantID = tenantId
|
||||
|
||||
c.Locals(consts.CtxKeyJwt, token)
|
||||
c.Locals(consts.CtxKeyClaim, claim)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import (
|
||||
)
|
||||
|
||||
func (f *Middlewares) WeChatAuthUserInfo(c fiber.Ctx) error {
|
||||
if len(c.GetReqHeaders()["Authorization"]) != 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
state := c.Query("state")
|
||||
code := c.Query("code")
|
||||
|
||||
@@ -40,13 +44,16 @@ func (f *Middlewares) WeChatAuthUserInfo(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var oauthInfo pg.UserOAuth
|
||||
copier.Copy(&oauthInfo, token)
|
||||
if err := copier.Copy(&oauthInfo, token); err != nil {
|
||||
return errors.Wrap(err, "failed to copy oauth info")
|
||||
}
|
||||
|
||||
user, err := f.userSvc.GetOrNew(c.Context(), tenantId, token.Openid, oauthInfo)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user")
|
||||
}
|
||||
|
||||
claim := f.jwt.CreateClaims(jwt.BaseClaims{UID: uint64(user.ID)})
|
||||
claim := f.jwt.CreateClaims(jwt.BaseClaims{OpenID: user.OpenID})
|
||||
claim.ID = user.OpenID
|
||||
jwtToken, err := f.jwt.CreateToken(claim)
|
||||
if err != nil {
|
||||
|
||||
@@ -208,3 +208,44 @@ func (svc *Service) GetTenantIDBySlug(ctx context.Context, slug string) (int64,
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// CreateTenant
|
||||
func (svc *Service) CreateTenant(ctx context.Context, name, slug string) error {
|
||||
log := svc.log.WithField("method", "CreateTenant")
|
||||
|
||||
expireAt := time.Now().Add(time.Hour * 24 * 366)
|
||||
// 仅保留天数
|
||||
expireAt = time.Date(expireAt.Year(), expireAt.Month(), expireAt.Day(), 0, 0, 0, 0, expireAt.Location())
|
||||
|
||||
tbl := table.Tenants
|
||||
stmt := tbl.
|
||||
INSERT(tbl.Name, tbl.Slug, tbl.ExpireAt).
|
||||
VALUES(String(name), String(slug), TimestampT(expireAt)).
|
||||
ON_CONFLICT(tbl.Slug).
|
||||
DO_NOTHING()
|
||||
log.Debug(stmt.DebugSql())
|
||||
|
||||
if _, err := stmt.ExecContext(ctx, svc.db); err != nil {
|
||||
return errors.Wrapf(err, "create tenant: %s(%s)", name, slug)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTenantExpireAtBySlug
|
||||
func (svc *Service) SetTenantExpireAtBySlug(ctx context.Context, slug string, expire time.Time) error {
|
||||
log := svc.log.WithField("method", "SetTenantExpireAtBySlug")
|
||||
|
||||
tbl := table.Tenants
|
||||
stmt := tbl.
|
||||
UPDATE(tbl.ExpireAt).
|
||||
SET(TimestampT(expire)).
|
||||
WHERE(tbl.Slug.EQ(String(slug)))
|
||||
log.Debug(stmt.DebugSql())
|
||||
|
||||
if _, err := stmt.ExecContext(ctx, svc.db); err != nil {
|
||||
return errors.Wrapf(err, "renew tenant: %s expire at %s", slug, expire)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ const (
|
||||
)
|
||||
|
||||
type BaseClaims struct {
|
||||
UID uint64 `json:"uid,omitempty"`
|
||||
Role uint64 `json:"role,omitempty"`
|
||||
OpenID string `json:"open_id,omitempty"`
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
TenantID int64 `json:"tenant_id,omitempty"`
|
||||
}
|
||||
|
||||
// Custom claims structure
|
||||
|
||||
Reference in New Issue
Block a user