feat: add tenant commands

This commit is contained in:
Rogee
2024-12-07 18:48:05 +08:00
parent e15d8d9bb2
commit 8f88929575
25 changed files with 271 additions and 48 deletions

BIN
backend/__debug_bin1551459525 Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -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.

View File

@@ -4,6 +4,6 @@ package consts
// ENUM(
// Tx = "__ctx_db:",
// Jwt = "__jwt_token:",
// Session = "__session_user:",
// Claim = "__jwt_claim:",
// )
type CtxKey string

View File

@@ -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
}

View File

@@ -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"},

View File

@@ -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"

View 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)
})
}),
),
)
}

View File

@@ -0,0 +1 @@
package common

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -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,

View 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
}

View 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
}

View 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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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