diff --git a/backend/__debug_bin1551459525 b/backend/__debug_bin1551459525 new file mode 100755 index 0000000..80e6378 Binary files /dev/null and b/backend/__debug_bin1551459525 differ diff --git a/backend/backend b/backend/backend index 167f413..ad92b86 100755 Binary files a/backend/backend and b/backend/backend differ diff --git a/backend/common/consts/ctx.gen.go b/backend/common/consts/ctx.gen.go index b131d39..ba74cf5 100644 --- a/backend/common/consts/ctx.gen.go +++ b/backend/common/consts/ctx.gen.go @@ -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. diff --git a/backend/common/consts/ctx.go b/backend/common/consts/ctx.go index 26cec86..5a98c69 100644 --- a/backend/common/consts/ctx.go +++ b/backend/common/consts/ctx.go @@ -4,6 +4,6 @@ package consts // ENUM( // Tx = "__ctx_db:", // Jwt = "__jwt_token:", -// Session = "__session_user:", +// Claim = "__jwt_claim:", // ) type CtxKey string diff --git a/backend/common/jwt/jwt.go b/backend/common/jwt/jwt.go deleted file mode 100755 index acdd3c6..0000000 --- a/backend/common/jwt/jwt.go +++ /dev/null @@ -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 -} diff --git a/backend/common/service/http/http.go b/backend/common/service/http/http.go index fa60180..e3a981a 100644 --- a/backend/common/service/http/http.go +++ b/backend/common/service/http/http.go @@ -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"}, diff --git a/backend/common/service/tasks/tasks.go b/backend/common/service/tasks/tasks.go index 7e79342..cbd2ba2 100644 --- a/backend/common/service/tasks/tasks.go +++ b/backend/common/service/tasks/tasks.go @@ -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" diff --git a/backend/common/service/tenants/tenants.go b/backend/common/service/tenants/tenants.go new file mode 100644 index 0000000..b3d88c6 --- /dev/null +++ b/backend/common/service/tenants/tenants.go @@ -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) + }) + }), + ), + ) +} diff --git a/backend/common/session.go b/backend/common/session.go new file mode 100644 index 0000000..805d0c7 --- /dev/null +++ b/backend/common/session.go @@ -0,0 +1 @@ +package common diff --git a/backend/database/migrations/20241128075611_init.sql b/backend/database/migrations/20241128075611_init.sql index ecd74d5..21be68a 100644 --- a/backend/database/migrations/20241128075611_init.sql +++ b/backend/database/migrations/20241128075611_init.sql @@ -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(), diff --git a/backend/main.go b/backend/main.go index f8decc9..7563e09 100755 --- a/backend/main.go +++ b/backend/main.go @@ -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 { diff --git a/backend/modules/tasks/discover/discover_medias.go b/backend/modules/commands/discover/discover_medias.go similarity index 98% rename from backend/modules/tasks/discover/discover_medias.go rename to backend/modules/commands/discover/discover_medias.go index 5e1a3f0..0355fa3 100644 --- a/backend/modules/tasks/discover/discover_medias.go +++ b/backend/modules/commands/discover/discover_medias.go @@ -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, diff --git a/backend/modules/tasks/discover/discover_medias_test.go b/backend/modules/commands/discover/discover_medias_test.go similarity index 100% rename from backend/modules/tasks/discover/discover_medias_test.go rename to backend/modules/commands/discover/discover_medias_test.go diff --git a/backend/modules/tasks/discover/provider.gen.go b/backend/modules/commands/discover/provider.gen.go similarity index 100% rename from backend/modules/tasks/discover/provider.gen.go rename to backend/modules/commands/discover/provider.gen.go diff --git a/backend/modules/tasks/store/provider.gen.go b/backend/modules/commands/store/provider.gen.go similarity index 100% rename from backend/modules/tasks/store/provider.gen.go rename to backend/modules/commands/store/provider.gen.go diff --git a/backend/modules/tasks/store/store_medias.go b/backend/modules/commands/store/store_medias.go similarity index 100% rename from backend/modules/tasks/store/store_medias.go rename to backend/modules/commands/store/store_medias.go diff --git a/backend/modules/commands/tenant/create.go b/backend/modules/commands/tenant/create.go new file mode 100644 index 0000000..38833ff --- /dev/null +++ b/backend/modules/commands/tenant/create.go @@ -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 +} diff --git a/backend/modules/commands/tenant/expire.go b/backend/modules/commands/tenant/expire.go new file mode 100644 index 0000000..d63a7a7 --- /dev/null +++ b/backend/modules/commands/tenant/expire.go @@ -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 +} diff --git a/backend/modules/commands/tenant/provider.gen.go b/backend/modules/commands/tenant/provider.gen.go new file mode 100755 index 0000000..309ebd7 --- /dev/null +++ b/backend/modules/commands/tenant/provider.gen.go @@ -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 +} diff --git a/backend/modules/medias/controller.go b/backend/modules/medias/controller.go index 5c87e1d..df32094 100644 --- a/backend/modules/medias/controller.go +++ b/backend/modules/medias/controller.go @@ -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) } diff --git a/backend/modules/medias/router.go b/backend/modules/medias/router.go index 499ef57..c71c125 100755 --- a/backend/modules/medias/router.go +++ b/backend/modules/medias/router.go @@ -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 } diff --git a/backend/modules/middlewares/m_jwt_parse.go b/backend/modules/middlewares/m_jwt_parse.go index 3337a21..7e5d053 100644 --- a/backend/modules/middlewares/m_jwt_parse.go +++ b/backend/modules/middlewares/m_jwt_parse.go @@ -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() } diff --git a/backend/modules/middlewares/m_wechat_auth_userinfo.go b/backend/modules/middlewares/m_wechat_auth_userinfo.go index 6382d83..ce8034d 100644 --- a/backend/modules/middlewares/m_wechat_auth_userinfo.go +++ b/backend/modules/middlewares/m_wechat_auth_userinfo.go @@ -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 { diff --git a/backend/modules/users/service.go b/backend/modules/users/service.go index 6821bb1..a7f65e1 100644 --- a/backend/modules/users/service.go +++ b/backend/modules/users/service.go @@ -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 +} diff --git a/backend/providers/jwt/jwt.go b/backend/providers/jwt/jwt.go index 201ce3e..ee39a95 100644 --- a/backend/providers/jwt/jwt.go +++ b/backend/providers/jwt/jwt.go @@ -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