From 0d35aa15dec46f1f390af6ec39ab2939b6d3852d Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 10 Jan 2025 11:42:12 +0800 Subject: [PATCH] fix: issues --- backend/app/consts/consts.gen.go | 174 +++++++++++ backend/app/consts/consts.go | 7 + backend/app/events/event_demo.go | 71 ----- backend/app/events/provider.gen.go | 4 +- backend/app/events/topics.gen.go | 174 +++++++++++ backend/app/events/topics.go | 10 + backend/app/events/user_register.go | 52 ++++ backend/app/http/auth/controller_wechat.go | 120 ++++++++ backend/app/http/auth/dto.go | 1 + backend/app/http/auth/provider.gen.go | 50 ++++ backend/app/http/auth/service.go | 20 ++ backend/app/http/auth/service_test.go | 37 +++ backend/app/http/tenants/controller.go | 30 ++ backend/app/http/tenants/dto.go | 1 + backend/app/http/tenants/provider.gen.go | 40 +++ backend/app/http/tenants/service.go | 20 ++ backend/app/http/tenants/service_test.go | 37 +++ backend/app/http/users/controller.go | 30 ++ backend/app/http/users/dto.go | 1 + backend/app/http/users/provider.gen.go | 40 +++ backend/app/http/users/service.go | 201 +++++++++++++ backend/app/http/users/service_test.go | 109 +++++++ backend/app/middlewares/provider.gen.go | 19 +- backend/app/service/event/event.go | 4 +- backend/app/service/http/http.go | 5 + backend/app/service/service.go | 4 +- backend/app/service/testx/testing.go | 11 +- backend/database/fields/user_oauths.gen.go | 277 ++++++++++++++++++ backend/database/fields/user_oauths.go | 5 + backend/database/fields/users.gen.go | 241 +++++++++++++++ backend/database/fields/users.go | 5 + .../20250109084432_create_users.sql | 1 - .../qvyun_v2/public/model/user_oauths.go | 27 +- backend/database/transform.yaml | 10 +- backend/pkg/consts/consts.go | 8 - backend/pkg/oauth/contracts.go | 11 + backend/pkg/oauth/wechat.go | 39 +++ backend/providers/{events => event}/config.go | 19 +- .../{events => event}/logrus_adapter.go | 2 +- .../providers/{events => event}/provider.go | 2 +- .../{events => event}/provider_kafka.go | 2 +- .../{events => event}/provider_redis.go | 2 +- .../{events => event}/provider_sql.go | 2 +- backend/providers/otel/config.go | 7 + backend/providers/otel/funcs.go | 2 +- readme.md | 1 + 46 files changed, 1822 insertions(+), 113 deletions(-) create mode 100644 backend/app/consts/consts.gen.go create mode 100644 backend/app/consts/consts.go delete mode 100644 backend/app/events/event_demo.go create mode 100644 backend/app/events/topics.gen.go create mode 100644 backend/app/events/topics.go create mode 100644 backend/app/events/user_register.go create mode 100644 backend/app/http/auth/controller_wechat.go create mode 100644 backend/app/http/auth/dto.go create mode 100755 backend/app/http/auth/provider.gen.go create mode 100644 backend/app/http/auth/service.go create mode 100644 backend/app/http/auth/service_test.go create mode 100644 backend/app/http/tenants/controller.go create mode 100644 backend/app/http/tenants/dto.go create mode 100755 backend/app/http/tenants/provider.gen.go create mode 100644 backend/app/http/tenants/service.go create mode 100644 backend/app/http/tenants/service_test.go create mode 100644 backend/app/http/users/controller.go create mode 100644 backend/app/http/users/dto.go create mode 100755 backend/app/http/users/provider.gen.go create mode 100644 backend/app/http/users/service.go create mode 100644 backend/app/http/users/service_test.go create mode 100644 backend/database/fields/user_oauths.gen.go create mode 100644 backend/database/fields/user_oauths.go create mode 100644 backend/database/fields/users.gen.go create mode 100644 backend/database/fields/users.go delete mode 100644 backend/pkg/consts/consts.go create mode 100644 backend/pkg/oauth/contracts.go create mode 100644 backend/pkg/oauth/wechat.go rename backend/providers/{events => event}/config.go (71%) rename backend/providers/{events => event}/logrus_adapter.go (99%) rename backend/providers/{events => event}/provider.go (97%) rename backend/providers/{events => event}/provider_kafka.go (98%) rename backend/providers/{events => event}/provider_redis.go (98%) rename backend/providers/{events => event}/provider_sql.go (98%) create mode 100644 readme.md diff --git a/backend/app/consts/consts.gen.go b/backend/app/consts/consts.gen.go new file mode 100644 index 0000000..db760b8 --- /dev/null +++ b/backend/app/consts/consts.gen.go @@ -0,0 +1,174 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package consts + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // TokenTypeUser is a TokenType of type User. + TokenTypeUser TokenType = "__tu" + // TokenTypeTenant is a TokenType of type Tenant. + TokenTypeTenant TokenType = "__tt" +) + +var ErrInvalidTokenType = fmt.Errorf("not a valid TokenType, try [%s]", strings.Join(_TokenTypeNames, ", ")) + +var _TokenTypeNames = []string{ + string(TokenTypeUser), + string(TokenTypeTenant), +} + +// TokenTypeNames returns a list of possible string values of TokenType. +func TokenTypeNames() []string { + tmp := make([]string, len(_TokenTypeNames)) + copy(tmp, _TokenTypeNames) + return tmp +} + +// TokenTypeValues returns a list of the values for TokenType +func TokenTypeValues() []TokenType { + return []TokenType{ + TokenTypeUser, + TokenTypeTenant, + } +} + +// String implements the Stringer interface. +func (x TokenType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x TokenType) IsValid() bool { + _, err := ParseTokenType(string(x)) + return err == nil +} + +var _TokenTypeValue = map[string]TokenType{ + "__tu": TokenTypeUser, + "__tt": TokenTypeTenant, +} + +// ParseTokenType attempts to convert a string to a TokenType. +func ParseTokenType(name string) (TokenType, error) { + if x, ok := _TokenTypeValue[name]; ok { + return x, nil + } + return TokenType(""), fmt.Errorf("%s is %w", name, ErrInvalidTokenType) +} + +var errTokenTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *TokenType) Scan(value interface{}) (err error) { + if value == nil { + *x = TokenType("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseTokenType(v) + case []byte: + *x, err = ParseTokenType(string(v)) + case TokenType: + *x = v + case *TokenType: + if v == nil { + return errTokenTypeNilPtr + } + *x = *v + case *string: + if v == nil { + return errTokenTypeNilPtr + } + *x, err = ParseTokenType(*v) + default: + return errors.New("invalid type for TokenType") + } + + return +} + +// Value implements the driver Valuer interface. +func (x TokenType) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *TokenType) Set(val string) error { + v, err := ParseTokenType(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *TokenType) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *TokenType) Type() string { + return "TokenType" +} + +type NullTokenType struct { + TokenType TokenType + Valid bool +} + +func NewNullTokenType(val interface{}) (x NullTokenType) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullTokenType) Scan(value interface{}) (err error) { + if value == nil { + x.TokenType, x.Valid = TokenType(""), false + return + } + + err = x.TokenType.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullTokenType) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.TokenType), nil +} + +type NullTokenTypeStr struct { + NullTokenType +} + +func NewNullTokenTypeStr(val interface{}) (x NullTokenTypeStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullTokenTypeStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.TokenType.String(), nil +} diff --git a/backend/app/consts/consts.go b/backend/app/consts/consts.go new file mode 100644 index 0000000..1a8c986 --- /dev/null +++ b/backend/app/consts/consts.go @@ -0,0 +1,7 @@ +package consts + +// swagger:enum TokenType +// ENUM( +// User = "__tu", Tenant = "__tt" +// ) +type TokenType string diff --git a/backend/app/events/event_demo.go b/backend/app/events/event_demo.go deleted file mode 100644 index 57de3d7..0000000 --- a/backend/app/events/event_demo.go +++ /dev/null @@ -1,71 +0,0 @@ -package events - -import ( - "encoding/json" - "fmt" - "time" - - "git.ipao.vip/rogeecn/atom/contracts" - "github.com/ThreeDotsLabs/watermill" - "github.com/ThreeDotsLabs/watermill/message" - "github.com/sirupsen/logrus" -) - -var _ contracts.EventHandler = (*UserRegister)(nil) - -type Event struct { - ID int `json:"id"` -} - -type ProcessedEvent struct { - ProcessedID int `json:"processed_id"` - Time time.Time `json:"time"` -} - -// @provider(event) -type UserRegister struct { - log *logrus.Entry `inject:"false"` -} - -func (u *UserRegister) Prepare() error { - return nil -} - -// Handler implements contracts.EventHandler. -func (u *UserRegister) Handler(msg *message.Message) ([]*message.Message, error) { - consumedPayload := Event{} - err := json.Unmarshal(msg.Payload, &consumedPayload) - if err != nil { - // When a handler returns an error, the default behavior is to send a Nack (negative-acknowledgement). - // The message will be processed again. - // - // You can change the default behaviour by using middlewares, like Retry or PoisonQueue. - // You can also implement your own middleware. - return nil, err - } - - fmt.Printf("received event %+v\n", consumedPayload) - - newPayload, err := json.Marshal(ProcessedEvent{ - ProcessedID: consumedPayload.ID, - Time: time.Now(), - }) - if err != nil { - return nil, err - } - - newMessage := message.NewMessage(watermill.NewUUID(), newPayload) - - return nil, nil - return []*message.Message{newMessage}, nil -} - -// PublishToTopic implements contracts.EventHandler. -func (u *UserRegister) PublishToTopic() string { - return "event:processed" -} - -// Topic implements contracts.EventHandler. -func (u *UserRegister) Topic() string { - return "event:user-register" -} diff --git a/backend/app/events/provider.gen.go b/backend/app/events/provider.gen.go index 793703e..635ed0b 100755 --- a/backend/app/events/provider.gen.go +++ b/backend/app/events/provider.gen.go @@ -1,7 +1,7 @@ package events import ( - "backend/providers/events" + "backend/providers/event" "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" @@ -11,7 +11,7 @@ import ( func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( - __event *events.PubSub, + __event *event.PubSub, ) (contracts.Initial, error) { obj := &UserRegister{} if err := obj.Prepare(); err != nil { diff --git a/backend/app/events/topics.gen.go b/backend/app/events/topics.gen.go new file mode 100644 index 0000000..1edf6eb --- /dev/null +++ b/backend/app/events/topics.gen.go @@ -0,0 +1,174 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package events + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // TopicProcessed is a Topic of type Processed. + TopicProcessed Topic = "event:processed" + // TopicUserRegister is a Topic of type UserRegister. + TopicUserRegister Topic = "user:register" +) + +var ErrInvalidTopic = fmt.Errorf("not a valid Topic, try [%s]", strings.Join(_TopicNames, ", ")) + +var _TopicNames = []string{ + string(TopicProcessed), + string(TopicUserRegister), +} + +// TopicNames returns a list of possible string values of Topic. +func TopicNames() []string { + tmp := make([]string, len(_TopicNames)) + copy(tmp, _TopicNames) + return tmp +} + +// TopicValues returns a list of the values for Topic +func TopicValues() []Topic { + return []Topic{ + TopicProcessed, + TopicUserRegister, + } +} + +// String implements the Stringer interface. +func (x Topic) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x Topic) IsValid() bool { + _, err := ParseTopic(string(x)) + return err == nil +} + +var _TopicValue = map[string]Topic{ + "event:processed": TopicProcessed, + "user:register": TopicUserRegister, +} + +// ParseTopic attempts to convert a string to a Topic. +func ParseTopic(name string) (Topic, error) { + if x, ok := _TopicValue[name]; ok { + return x, nil + } + return Topic(""), fmt.Errorf("%s is %w", name, ErrInvalidTopic) +} + +var errTopicNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *Topic) Scan(value interface{}) (err error) { + if value == nil { + *x = Topic("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseTopic(v) + case []byte: + *x, err = ParseTopic(string(v)) + case Topic: + *x = v + case *Topic: + if v == nil { + return errTopicNilPtr + } + *x = *v + case *string: + if v == nil { + return errTopicNilPtr + } + *x, err = ParseTopic(*v) + default: + return errors.New("invalid type for Topic") + } + + return +} + +// Value implements the driver Valuer interface. +func (x Topic) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *Topic) Set(val string) error { + v, err := ParseTopic(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *Topic) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *Topic) Type() string { + return "Topic" +} + +type NullTopic struct { + Topic Topic + Valid bool +} + +func NewNullTopic(val interface{}) (x NullTopic) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullTopic) Scan(value interface{}) (err error) { + if value == nil { + x.Topic, x.Valid = Topic(""), false + return + } + + err = x.Topic.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullTopic) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.Topic), nil +} + +type NullTopicStr struct { + NullTopic +} + +func NewNullTopicStr(val interface{}) (x NullTopicStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullTopicStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.Topic.String(), nil +} diff --git a/backend/app/events/topics.go b/backend/app/events/topics.go new file mode 100644 index 0000000..bf9c96a --- /dev/null +++ b/backend/app/events/topics.go @@ -0,0 +1,10 @@ +package events + +// swagger:enum Topic +// ENUM( +// +// Processed = "event:processed" +// UserRegister = "user:register" +// +// ) +type Topic string diff --git a/backend/app/events/user_register.go b/backend/app/events/user_register.go new file mode 100644 index 0000000..35a9f75 --- /dev/null +++ b/backend/app/events/user_register.go @@ -0,0 +1,52 @@ +package events + +import ( + "encoding/json" + + "git.ipao.vip/rogeecn/atom/contracts" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/sirupsen/logrus" +) + +var ( + _ contracts.EventHandler = (*UserRegister)(nil) + _ contracts.EventPublisher = (*UserRegister)(nil) +) + +// @provider(event) +type UserRegister struct { + log *logrus.Entry `inject:"false" json:"-"` + ID int64 `json:"id"` +} + +func (e *UserRegister) Prepare() error { + return nil +} + +// Marshal implements contracts.EventPublisher. +func (e *UserRegister) Marshal() ([]byte, error) { + return json.Marshal(e) +} + +// PublishToTopic implements contracts.EventHandler. +func (e *UserRegister) PublishToTopic() string { + return TopicProcessed.String() +} + +// Topic implements contracts.EventHandler. +func (e *UserRegister) Topic() string { + return TopicUserRegister.String() +} + +// Handler implements contracts.EventHandler. +func (e *UserRegister) Handler(msg *message.Message) ([]*message.Message, error) { + var payload UserRegister + err := json.Unmarshal(msg.Payload, &payload) + if err != nil { + return nil, err + } + + e.log.Infof("received event %+v\n", payload) + + return nil, nil +} diff --git a/backend/app/http/auth/controller_wechat.go b/backend/app/http/auth/controller_wechat.go new file mode 100644 index 0000000..e3d4341 --- /dev/null +++ b/backend/app/http/auth/controller_wechat.go @@ -0,0 +1,120 @@ +package auth + +import ( + "fmt" + "net/url" + "strings" + "time" + + "backend/app/consts" + "backend/app/http/users" + "backend/providers/jwt" + "backend/providers/otel" + "backend/providers/wechat" + + "github.com/gofiber/fiber/v3" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +const StatePrefix = "sns_basic_auth" + +// @provider +type Controller struct { + svc *Service + userSvc *users.Service + jwt *jwt.JWT + wechat *wechat.Client + log *log.Entry `inject:"false"` +} + +func (ctl *Controller) Prepare() error { + ctl.log = log.WithField("module", "auth.Controller") + return nil +} + +// @Router /v1/auth/wechat/jump/:tenant [get] +// @Bind tenant path +// @Bind redirectUri query +func (ctl *Controller) JumpToAuth(ctx fiber.Ctx, tenant, redirectUri string) error { + _, span := otel.Start(ctx.Context(), "auth.controller.wechat") + defer span.End() + + ctl.log.Debugf("%s, query: %v", ctx.OriginalURL(), ctx.Queries()) + + paramRedirect := ctx.Query("redirect") + + // 添加 redirect 参数 + u, err := url.Parse(string(ctx.Request().URI().FullURI())) + if err != nil { + return err + } + query := u.Query() + query.Set("redirect", paramRedirect) + u.RawQuery = query.Encode() + u.Path = "/v1/auth/wechat/login/" + tenant + fullUrl := u.String() + + ctl.log.WithField("module", "middleware.SilentAuth").Debug("redirect_uri: ", fullUrl) + + to, err := ctl.wechat.ScopeAuthorizeURL( + wechat.ScopeAuthorizeURLWithRedirectURI(fullUrl), + wechat.ScopeAuthorizeURLWithState(fmt.Sprintf("%s-%d", StatePrefix, time.Now().UnixNano())), + ) + if err != nil { + return errors.Wrap(err, "failed to get wechat auth url") + } + + return ctx.Redirect().To(to.String()) +} + +// @Router /v1/auth/login/:tenant [get] +// @Bind tenant path +// @Bind code query +// @Bind state query +// @Bind redirectUri query +func (ctl *Controller) Login(ctx fiber.Ctx, code, state, tenant, redirectUri string) error { + ctl.log.Debugf("code: %s, state: %s", code, state) + + ctx.Cookie(&fiber.Cookie{ + Name: consts.TokenTypeUser.String(), + Value: "", + Expires: time.Now().Add(12 * time.Hour), + HTTPOnly: true, + }) + + // get the openid + token, err := ctl.wechat.AuthorizeCode2Token(code) + if err != nil { + return errors.Wrap(err, "failed to get openid") + } + ctl.log.Debugf("tokenInfo %+v", token) + + user, err := ctl.userSvc.GetOrNewFromChannel(ctx.Context(), consts.AuthChannelWeChat, token.OpenID, tenant) + if err != nil { + return errors.Wrap(err, "failed to get user") + } + + claim := c.jwt.CreateClaims(jwt.BaseClaims{ + OpenID: user.OpenID, + Tenant: tenantSlug, + UserID: user.ID, + TenantID: tenant.ID, + }) + jwtToken, err = c.jwt.CreateToken(claim) + if err != nil { + return errors.Wrap(err, "failed to create token") + } + + ctx.Cookie(&fiber.Cookie{ + Name: "token", + Value: jwtToken, + Expires: time.Now().Add(6 * time.Hour), + HTTPOnly: true, + }) + + html := strings.ReplaceAll(string(b), "{{JWT}}", jwtToken) + return ctx.SendString(html) + + return ctx.Redirect().To(paramRedirect) +} diff --git a/backend/app/http/auth/dto.go b/backend/app/http/auth/dto.go new file mode 100644 index 0000000..8832b06 --- /dev/null +++ b/backend/app/http/auth/dto.go @@ -0,0 +1 @@ +package auth diff --git a/backend/app/http/auth/provider.gen.go b/backend/app/http/auth/provider.gen.go new file mode 100755 index 0000000..983a7f7 --- /dev/null +++ b/backend/app/http/auth/provider.gen.go @@ -0,0 +1,50 @@ +package auth + +import ( + "database/sql" + + "backend/app/http/users" + "backend/providers/jwt" + "backend/providers/wechat" + + "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( + jwt *jwt.JWT, + svc *Service, + userSvc *users.Service, + wechat *wechat.Client, + ) (*Controller, error) { + obj := &Controller{ + jwt: jwt, + svc: svc, + userSvc: userSvc, + wechat: wechat, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + db *sql.DB, + ) (*Service, error) { + obj := &Service{ + db: db, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/auth/service.go b/backend/app/http/auth/service.go new file mode 100644 index 0000000..6352797 --- /dev/null +++ b/backend/app/http/auth/service.go @@ -0,0 +1,20 @@ +package auth + +import ( + "database/sql" + + . "github.com/go-jet/jet/v2/postgres" + log "github.com/sirupsen/logrus" +) + +// @provider:except +type Service struct { + db *sql.DB + log *log.Entry `inject:"false"` +} + +func (svc *Service) Prepare() error { + svc.log = log.WithField("module", "auth.service") + _ = Int(1) + return nil +} diff --git a/backend/app/http/auth/service_test.go b/backend/app/http/auth/service_test.go new file mode 100644 index 0000000..2643b51 --- /dev/null +++ b/backend/app/http/auth/service_test.go @@ -0,0 +1,37 @@ +package auth + +import ( + "testing" + + "backend/app/service/testx" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type ServiceInjectParams struct { + dig.In + Svc *Service +} + +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} + +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default().With( + Provide, + ) + + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} + +func (s *ServiceTestSuite) Test_Service() { + Convey("Test Service", s.T(), func() { + So(s.Svc, ShouldNotBeNil) + }) +} diff --git a/backend/app/http/tenants/controller.go b/backend/app/http/tenants/controller.go new file mode 100644 index 0000000..9b4a890 --- /dev/null +++ b/backend/app/http/tenants/controller.go @@ -0,0 +1,30 @@ +package tenants + +import ( + log "github.com/sirupsen/logrus" +) + +// @provider +type Controller struct { + svc *Service + log *log.Entry `inject:"false"` +} + +func (c *Controller) Prepare() error { + c.log = log.WithField("module", "tenants.Controller") + return nil +} + +// Test godoc +// +// @Summary Test +// @Description Test +// @Tags Test +// @Accept json +// @Produce json +// @Param id path int true "AccountID" +// @Param queryFilter query dto.AlarmListQuery true "AlarmListQueryFilter" +// @Param pageFilter query common.PageQueryFilter true "PageQueryFilter" +// @Param sortFilter query common.SortQueryFilter true "SortQueryFilter" +// @Success 200 {object} common.PageDataResponse{list=dto.AlarmItem} +// @Router /v1/test/:id [get] diff --git a/backend/app/http/tenants/dto.go b/backend/app/http/tenants/dto.go new file mode 100644 index 0000000..d3a5f5d --- /dev/null +++ b/backend/app/http/tenants/dto.go @@ -0,0 +1 @@ +package tenants diff --git a/backend/app/http/tenants/provider.gen.go b/backend/app/http/tenants/provider.gen.go new file mode 100755 index 0000000..c626c17 --- /dev/null +++ b/backend/app/http/tenants/provider.gen.go @@ -0,0 +1,40 @@ +package tenants + +import ( + "database/sql" + + "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( + svc *Service, + ) (*Controller, error) { + obj := &Controller{ + svc: svc, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + db *sql.DB, + ) (*Service, error) { + obj := &Service{ + db: db, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/tenants/service.go b/backend/app/http/tenants/service.go new file mode 100644 index 0000000..da443bb --- /dev/null +++ b/backend/app/http/tenants/service.go @@ -0,0 +1,20 @@ +package tenants + +import ( + "database/sql" + + . "github.com/go-jet/jet/v2/postgres" + log "github.com/sirupsen/logrus" +) + +// @provider:except +type Service struct { + db *sql.DB + log *log.Entry `inject:"false"` +} + +func (svc *Service) Prepare() error { + svc.log = log.WithField("module", "tenants.service") + _ = Int(1) + return nil +} diff --git a/backend/app/http/tenants/service_test.go b/backend/app/http/tenants/service_test.go new file mode 100644 index 0000000..4600f33 --- /dev/null +++ b/backend/app/http/tenants/service_test.go @@ -0,0 +1,37 @@ +package tenants + +import ( + "testing" + + "backend/app/service/testx" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type ServiceInjectParams struct { + dig.In + Svc *Service +} + +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} + +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default().With( + Provide, + ) + + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} + +func (s *ServiceTestSuite) Test_Service() { + Convey("Test Service", s.T(), func() { + So(s.Svc, ShouldNotBeNil) + }) +} diff --git a/backend/app/http/users/controller.go b/backend/app/http/users/controller.go new file mode 100644 index 0000000..88add18 --- /dev/null +++ b/backend/app/http/users/controller.go @@ -0,0 +1,30 @@ +package users + +import ( + log "github.com/sirupsen/logrus" +) + +// @provider +type Controller struct { + svc *Service + log *log.Entry `inject:"false"` +} + +func (c *Controller) Prepare() error { + c.log = log.WithField("module", "users.Controller") + return nil +} + +// Test godoc +// +// @Summary Test +// @Description Test +// @Tags Test +// @Accept json +// @Produce json +// @Param id path int true "AccountID" +// @Param queryFilter query dto.AlarmListQuery true "AlarmListQueryFilter" +// @Param pageFilter query common.PageQueryFilter true "PageQueryFilter" +// @Param sortFilter query common.SortQueryFilter true "SortQueryFilter" +// @Success 200 {object} common.PageDataResponse{list=dto.AlarmItem} +// @Router /v1/test/:id [get] diff --git a/backend/app/http/users/dto.go b/backend/app/http/users/dto.go new file mode 100644 index 0000000..82abcb9 --- /dev/null +++ b/backend/app/http/users/dto.go @@ -0,0 +1 @@ +package users diff --git a/backend/app/http/users/provider.gen.go b/backend/app/http/users/provider.gen.go new file mode 100755 index 0000000..db6358e --- /dev/null +++ b/backend/app/http/users/provider.gen.go @@ -0,0 +1,40 @@ +package users + +import ( + "database/sql" + + "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( + svc *Service, + ) (*Controller, error) { + obj := &Controller{ + svc: svc, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + db *sql.DB, + ) (*Service, error) { + obj := &Service{ + db: db, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/users/service.go b/backend/app/http/users/service.go new file mode 100644 index 0000000..5ba472f --- /dev/null +++ b/backend/app/http/users/service.go @@ -0,0 +1,201 @@ +package users + +import ( + "context" + "database/sql" + "time" + + "backend/app/events" + "backend/database/fields" + "backend/database/models/qvyun_v2/public/model" + "backend/database/models/qvyun_v2/public/table" + "backend/pkg/oauth" + "backend/providers/event" + "backend/providers/otel" + + . "github.com/go-jet/jet/v2/postgres" + "github.com/samber/lo" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.15.0" + "golang.org/x/crypto/bcrypt" +) + +// @provider:except +type Service struct { + db *sql.DB + event *event.PubSub + log *log.Entry `inject:"false"` +} + +func (svc *Service) Prepare() error { + svc.log = log.WithField("module", "users.service") + _ = Int(1) + return nil +} + +// GetUsersByOpenID Get user by open id +func (svc *Service) GetUserByOpenID(ctx context.Context, channel fields.AuthChannel, openID string) (*model.Users, error) { + _, span := otel.Start(ctx, "users.service.GetUsersByOpenID") + defer span.End() + + userId, err := svc.GetUserIDByOpenID(ctx, channel, openID) + if err != nil { + // span 添加用户不存在事件 + span.AddEvent("user not found") + return nil, err + } + + tbl := table.Users + stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.ID.EQ(Int64(userId))) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var user model.Users + if err := stmt.QueryContext(ctx, svc.db, &user); err != nil { + return nil, err + } + + return &user, nil +} + +func (svc *Service) GetUserIDByOpenID(ctx context.Context, channel fields.AuthChannel, openID string) (int64, error) { + _, span := otel.Start(ctx, "users.service.GetUserIDByOpenID") + defer span.End() + + span.SetAttributes( + attribute.String("channel", channel.String()), + attribute.String("openID", openID), + ) + + tbl := table.UserOauths + + stmt := tbl. + SELECT(tbl.UserID.AS("user_id")). + WHERE( + tbl.Channel.EQ(Int16(int16(channel))). + AND(tbl.OpenID.EQ(String(openID))), + ) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var result struct { + UserID int64 + } + + if err := stmt.QueryContext(ctx, svc.db, &result); err != nil { + return 0, err + } + + return result.UserID, nil +} + +// CreateUser +func (svc *Service) CreateUser(ctx context.Context, user *model.Users) (*model.Users, error) { + _, span := otel.Start(ctx, "users.service.CreateUser") + defer span.End() + span.SetAttributes( + attribute.String("user.username", user.Username), + attribute.String("user.email", user.Email), + attribute.String("user.phone", user.Phone), + ) + + if user.CreatedAt.IsZero() { + user.CreatedAt = time.Now() + } + + if user.UpdatedAt.IsZero() { + user.UpdatedAt = time.Now() + } + + user.Status = int16(fields.UserStatusPending) + + // use bcrypt to hash password + pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + user.Password = string(pwd) + + tbl := table.Users + stmt := tbl.INSERT(tbl.MutableColumns).MODEL(user).ON_CONFLICT(tbl.Email, tbl.Phone, tbl.Username).DO_NOTHING().RETURNING(tbl.AllColumns) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var m model.Users + if err = stmt.QueryContext(ctx, svc.db, &m); err != nil { + return nil, err + } + + // if user created successfully, trigger event + span.AddEvent("user created") + svc.event.Publish(&events.UserRegister{ID: m.ID}) + + return &m, err +} + +// GetUserByID +func (svc *Service) GetUserByID(ctx context.Context, userID int64) (*model.Users, error) { + _, span := otel.Start(ctx, "users.service.GetUserByID") + defer span.End() + span.SetAttributes( + attribute.Int64("user.id", userID), + ) + + tbl := table.Users + stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.ID.EQ(Int64(userID))) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var user model.Users + if err := stmt.QueryContext(ctx, svc.db, &user); err != nil { + return nil, err + } + + return &user, nil +} + +// AttachUserOAuth +func (svc *Service) AttachUserOAuth(ctx context.Context, user *model.Users, channel fields.AuthChannel, oauthInfo oauth.OAuthInfo) error { + _, span := otel.Start(ctx, "users.service.AttachUserOAuth") + defer span.End() + span.SetAttributes( + attribute.Int64("user.id", user.ID), + attribute.String("channel", channel.String()), + attribute.String("openID", oauthInfo.GetOpenID()), + ) + + m := &model.UserOauths{ + ID: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeletedAt: nil, + Channel: channel, + UserID: user.ID, + UnionID: lo.ToPtr(oauthInfo.GetUnionID()), + OpenID: oauthInfo.GetOpenID(), + AccessToken: oauthInfo.GetAccessToken(), + RefreshToken: oauthInfo.GetRefreshToken(), + ExpireAt: oauthInfo.GetExpiredAt(), + Meta: new(string), + } + + tbl := table.UserOauths + stmt := tbl. + INSERT(tbl.MutableColumns). + MODEL(m). + ON_CONFLICT(tbl.Channel, tbl.UserID). + DO_UPDATE( + SET( + tbl.UnionID.SET(String(oauthInfo.GetUnionID())), + tbl.OpenID.SET(String(oauthInfo.GetOpenID())), + tbl.AccessToken.SET(String(oauthInfo.GetAccessToken())), + tbl.RefreshToken.SET(String(oauthInfo.GetRefreshToken())), + tbl.ExpireAt.SET(TimestampT(oauthInfo.GetExpiredAt())), + ), + ) + + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + if _, err := stmt.ExecContext(ctx, svc.db); err != nil { + return err + } + + return nil +} diff --git a/backend/app/http/users/service_test.go b/backend/app/http/users/service_test.go new file mode 100644 index 0000000..9daafca --- /dev/null +++ b/backend/app/http/users/service_test.go @@ -0,0 +1,109 @@ +package users + +import ( + "context" + "testing" + "time" + + "backend/app/service/testx" + "backend/database" + "backend/database/fields" + "backend/database/models/qvyun_v2/public/model" + "backend/database/models/qvyun_v2/public/table" + + "git.ipao.vip/rogeecn/atom/contracts" + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type ServiceInjectParams struct { + dig.In + Initials []contracts.Initial `group:"initials"` + + Svc *Service +} + +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} + +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default().With( + Provide, + ) + + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} + +func (s *ServiceTestSuite) Test_GetUserIDByOpenID() { + Convey("Test GetUserIDByOpenID", s.T(), func() { + v, err := fields.AuthChannelWeChat.Value() + So(err, ShouldBeNil) + So(v, ShouldEqual, 1) + + Convey("truncate tables", func() { + err := database.Truncate(context.Background(), s.Svc.db, table.UserOauths.TableName()) + So(err, ShouldBeNil) + }) + + Convey("insert data", func() { + m := model.UserOauths{ + Channel: fields.AuthChannelWeChat, + UserID: 1, + OpenID: "test_open_id", + AccessKey: "test_access_key", + AccessToken: "test_access_token", + RefreshToken: "test_refresh_token", + ExpireAt: time.Now().Add(time.Hour), + } + + tbl := table.UserOauths + stmt := tbl.INSERT( + tbl.MutableColumns.Except( + tbl.CreatedAt, + tbl.UpdatedAt, + tbl.DeletedAt, + tbl.UnionID, + tbl.Meta, + ), + ).MODEL(m) + s.T().Log(stmt.Sql()) + + _, err := stmt.ExecContext(context.Background(), s.Svc.db) + So(err, ShouldBeNil) + }) + + Convey("get user id by open id", func() { + userID, err := s.Svc.GetUserIDByOpenID(context.Background(), fields.AuthChannelWeChat, "test_open_id") + So(err, ShouldBeNil) + So(userID, ShouldEqual, 1) + }) + }) +} + +// CreateUser +func (s *ServiceTestSuite) Test_CreateUser() { + Convey("Test CreateUser", s.T(), func() { + Convey("truncate tables", func() { + err := database.Truncate(context.Background(), s.Svc.db, table.Users.TableName()) + So(err, ShouldBeNil) + }) + + Convey("create user", func() { + user := &model.Users{ + Email: "test@qq.com", + Phone: "12345678901", + Username: "test", + Password: "test", + Age: 18, + } + + _, err := s.Svc.CreateUser(context.Background(), user) + So(err, ShouldBeNil) + }) + }) +} diff --git a/backend/app/middlewares/provider.gen.go b/backend/app/middlewares/provider.gen.go index 9bd3ba4..ba3ace9 100755 --- a/backend/app/middlewares/provider.gen.go +++ b/backend/app/middlewares/provider.gen.go @@ -1,13 +1,28 @@ package middlewares import ( + "backend/providers/app" + "backend/providers/jwt" + "backend/providers/storage" + "backend/providers/wechat" + "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() (*Middlewares, error) { - obj := &Middlewares{} + if err := container.Container.Provide(func( + app *app.Config, + client *wechat.Client, + jwt *jwt.JWT, + storagePath *storage.Config, + ) (*Middlewares, error) { + obj := &Middlewares{ + app: app, + client: client, + jwt: jwt, + storagePath: storagePath, + } if err := obj.Prepare(); err != nil { return nil, err } diff --git a/backend/app/service/event/event.go b/backend/app/service/event/event.go index 2edb9a7..60cfffb 100644 --- a/backend/app/service/event/event.go +++ b/backend/app/service/event/event.go @@ -6,7 +6,7 @@ import ( "backend/app/events" "backend/app/service" "backend/providers/app" - providerEvents "backend/providers/events" + "backend/providers/event" "backend/providers/postgres" "git.ipao.vip/rogeecn/atom" @@ -41,7 +41,7 @@ type Service struct { dig.In App *app.Config - PubSub *providerEvents.PubSub + PubSub *event.PubSub Initials []contracts.Initial `group:"initials"` } diff --git a/backend/app/service/http/http.go b/backend/app/service/http/http.go index 82921e7..c989ca3 100644 --- a/backend/app/service/http/http.go +++ b/backend/app/service/http/http.go @@ -17,6 +17,7 @@ import ( "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/favicon" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -69,6 +70,10 @@ func Serve(cmd *cobra.Command, args []string) error { svc.Http.Engine.Get("/swagger/*", swagger.HandlerDefault) } + svc.Http.Engine.Get("MP_verify_dEF9kn8rJlBsuLKk.txt", func(c fiber.Ctx) error { + return c.SendString("dEF9kn8rJlBsuLKk") + }) + svc.Http.Engine.Use(svc.Middlewares.WeChatVerify) svc.Http.Engine.Use(errorx.Middleware) svc.Http.Engine.Use(favicon.New(favicon.Config{ diff --git a/backend/app/service/service.go b/backend/app/service/service.go index fea0242..32f775b 100644 --- a/backend/app/service/service.go +++ b/backend/app/service/service.go @@ -2,7 +2,7 @@ package service import ( "backend/providers/app" - "backend/providers/events" + "backend/providers/event" "git.ipao.vip/rogeecn/atom/container" ) @@ -10,6 +10,6 @@ import ( func Default(providers ...container.ProviderContainer) container.Providers { return append(container.Providers{ app.DefaultProvider(), - events.DefaultProvider(), + event.DefaultProvider(), }, providers...) } diff --git a/backend/app/service/testx/testing.go b/backend/app/service/testx/testing.go index 8d6b50e..e9e3426 100644 --- a/backend/app/service/testx/testing.go +++ b/backend/app/service/testx/testing.go @@ -4,18 +4,27 @@ import ( "os" "testing" + "backend/providers/otel" + "backend/providers/postgres" + "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" "github.com/rogeecn/fabfile" + log "github.com/sirupsen/logrus" . "github.com/smartystreets/goconvey/convey" ) func Default(providers ...container.ProviderContainer) container.Providers { - return append(container.Providers{}, providers...) + return append(container.Providers{ + otel.DefaultProvider(), + postgres.DefaultProvider(), + }, providers...) } func Serve(providers container.Providers, t *testing.T, invoke any) { Convey("tests boot up", t, func() { + log.SetLevel(log.DebugLevel) + file := fabfile.MustFind("config.toml") localEnv := os.Getenv("ENV_LOCAL") diff --git a/backend/database/fields/user_oauths.gen.go b/backend/database/fields/user_oauths.gen.go new file mode 100644 index 0000000..8aa2138 --- /dev/null +++ b/backend/database/fields/user_oauths.gen.go @@ -0,0 +1,277 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package fields + +import ( + "database/sql/driver" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // AuthChannelWeChat is a AuthChannel of type WeChat. + AuthChannelWeChat AuthChannel = iota + 1 + // AuthChannelWeiBo is a AuthChannel of type WeiBo. + AuthChannelWeiBo + // AuthChannelQQ is a AuthChannel of type QQ. + AuthChannelQQ + // AuthChannelGithub is a AuthChannel of type Github. + AuthChannelGithub + // AuthChannelGoogle is a AuthChannel of type Google. + AuthChannelGoogle + // AuthChannelFacebook is a AuthChannel of type Facebook. + AuthChannelFacebook + // AuthChannelTwitter is a AuthChannel of type Twitter. + AuthChannelTwitter + // AuthChannelLinkedIn is a AuthChannel of type LinkedIn. + AuthChannelLinkedIn + // AuthChannelApple is a AuthChannel of type Apple. + AuthChannelApple +) + +var ErrInvalidAuthChannel = fmt.Errorf("not a valid AuthChannel, try [%s]", strings.Join(_AuthChannelNames, ", ")) + +const _AuthChannelName = "WeChatWeiBoQQGithubGoogleFacebookTwitterLinkedInApple" + +var _AuthChannelNames = []string{ + _AuthChannelName[0:6], + _AuthChannelName[6:11], + _AuthChannelName[11:13], + _AuthChannelName[13:19], + _AuthChannelName[19:25], + _AuthChannelName[25:33], + _AuthChannelName[33:40], + _AuthChannelName[40:48], + _AuthChannelName[48:53], +} + +// AuthChannelNames returns a list of possible string values of AuthChannel. +func AuthChannelNames() []string { + tmp := make([]string, len(_AuthChannelNames)) + copy(tmp, _AuthChannelNames) + return tmp +} + +// AuthChannelValues returns a list of the values for AuthChannel +func AuthChannelValues() []AuthChannel { + return []AuthChannel{ + AuthChannelWeChat, + AuthChannelWeiBo, + AuthChannelQQ, + AuthChannelGithub, + AuthChannelGoogle, + AuthChannelFacebook, + AuthChannelTwitter, + AuthChannelLinkedIn, + AuthChannelApple, + } +} + +var _AuthChannelMap = map[AuthChannel]string{ + AuthChannelWeChat: _AuthChannelName[0:6], + AuthChannelWeiBo: _AuthChannelName[6:11], + AuthChannelQQ: _AuthChannelName[11:13], + AuthChannelGithub: _AuthChannelName[13:19], + AuthChannelGoogle: _AuthChannelName[19:25], + AuthChannelFacebook: _AuthChannelName[25:33], + AuthChannelTwitter: _AuthChannelName[33:40], + AuthChannelLinkedIn: _AuthChannelName[40:48], + AuthChannelApple: _AuthChannelName[48:53], +} + +// String implements the Stringer interface. +func (x AuthChannel) String() string { + if str, ok := _AuthChannelMap[x]; ok { + return str + } + return fmt.Sprintf("AuthChannel(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AuthChannel) IsValid() bool { + _, ok := _AuthChannelMap[x] + return ok +} + +var _AuthChannelValue = map[string]AuthChannel{ + _AuthChannelName[0:6]: AuthChannelWeChat, + _AuthChannelName[6:11]: AuthChannelWeiBo, + _AuthChannelName[11:13]: AuthChannelQQ, + _AuthChannelName[13:19]: AuthChannelGithub, + _AuthChannelName[19:25]: AuthChannelGoogle, + _AuthChannelName[25:33]: AuthChannelFacebook, + _AuthChannelName[33:40]: AuthChannelTwitter, + _AuthChannelName[40:48]: AuthChannelLinkedIn, + _AuthChannelName[48:53]: AuthChannelApple, +} + +// ParseAuthChannel attempts to convert a string to a AuthChannel. +func ParseAuthChannel(name string) (AuthChannel, error) { + if x, ok := _AuthChannelValue[name]; ok { + return x, nil + } + return AuthChannel(0), fmt.Errorf("%s is %w", name, ErrInvalidAuthChannel) +} + +var errAuthChannelNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AuthChannel) Scan(value interface{}) (err error) { + if value == nil { + *x = AuthChannel(0) + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case int64: + *x = AuthChannel(v) + case string: + *x, err = ParseAuthChannel(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = AuthChannel(val), nil + } + } + case []byte: + *x, err = ParseAuthChannel(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = AuthChannel(val), nil + } + } + case AuthChannel: + *x = v + case int: + *x = AuthChannel(v) + case *AuthChannel: + if v == nil { + return errAuthChannelNilPtr + } + *x = *v + case uint: + *x = AuthChannel(v) + case uint64: + *x = AuthChannel(v) + case *int: + if v == nil { + return errAuthChannelNilPtr + } + *x = AuthChannel(*v) + case *int64: + if v == nil { + return errAuthChannelNilPtr + } + *x = AuthChannel(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = AuthChannel(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errAuthChannelNilPtr + } + *x = AuthChannel(*v) + case *uint: + if v == nil { + return errAuthChannelNilPtr + } + *x = AuthChannel(*v) + case *uint64: + if v == nil { + return errAuthChannelNilPtr + } + *x = AuthChannel(*v) + case *string: + if v == nil { + return errAuthChannelNilPtr + } + *x, err = ParseAuthChannel(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = AuthChannel(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x AuthChannel) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *AuthChannel) Set(val string) error { + v, err := ParseAuthChannel(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *AuthChannel) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *AuthChannel) Type() string { + return "AuthChannel" +} + +type NullAuthChannel struct { + AuthChannel AuthChannel + Valid bool +} + +func NewNullAuthChannel(val interface{}) (x NullAuthChannel) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullAuthChannel) Scan(value interface{}) (err error) { + if value == nil { + x.AuthChannel, x.Valid = AuthChannel(0), false + return + } + + err = x.AuthChannel.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullAuthChannel) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.AuthChannel), nil +} + +type NullAuthChannelStr struct { + NullAuthChannel +} + +func NewNullAuthChannelStr(val interface{}) (x NullAuthChannelStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullAuthChannelStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.AuthChannel.String(), nil +} diff --git a/backend/database/fields/user_oauths.go b/backend/database/fields/user_oauths.go new file mode 100644 index 0000000..b65ec75 --- /dev/null +++ b/backend/database/fields/user_oauths.go @@ -0,0 +1,5 @@ +package fields + +// swagger:enum AuthChannel +// ENUM( WeChat = 1 , WeiBo, QQ, Github, Google, Facebook, Twitter, LinkedIn, Apple) +type AuthChannel int16 diff --git a/backend/database/fields/users.gen.go b/backend/database/fields/users.gen.go new file mode 100644 index 0000000..79eec95 --- /dev/null +++ b/backend/database/fields/users.gen.go @@ -0,0 +1,241 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package fields + +import ( + "database/sql/driver" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // UserStatusPending is a UserStatus of type Pending. + UserStatusPending UserStatus = iota + // UserStatusVerified is a UserStatus of type Verified. + UserStatusVerified + // UserStatusBlocked is a UserStatus of type Blocked. + UserStatusBlocked +) + +var ErrInvalidUserStatus = fmt.Errorf("not a valid UserStatus, try [%s]", strings.Join(_UserStatusNames, ", ")) + +const _UserStatusName = "PendingVerifiedBlocked" + +var _UserStatusNames = []string{ + _UserStatusName[0:7], + _UserStatusName[7:15], + _UserStatusName[15:22], +} + +// UserStatusNames returns a list of possible string values of UserStatus. +func UserStatusNames() []string { + tmp := make([]string, len(_UserStatusNames)) + copy(tmp, _UserStatusNames) + return tmp +} + +// UserStatusValues returns a list of the values for UserStatus +func UserStatusValues() []UserStatus { + return []UserStatus{ + UserStatusPending, + UserStatusVerified, + UserStatusBlocked, + } +} + +var _UserStatusMap = map[UserStatus]string{ + UserStatusPending: _UserStatusName[0:7], + UserStatusVerified: _UserStatusName[7:15], + UserStatusBlocked: _UserStatusName[15:22], +} + +// String implements the Stringer interface. +func (x UserStatus) String() string { + if str, ok := _UserStatusMap[x]; ok { + return str + } + return fmt.Sprintf("UserStatus(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x UserStatus) IsValid() bool { + _, ok := _UserStatusMap[x] + return ok +} + +var _UserStatusValue = map[string]UserStatus{ + _UserStatusName[0:7]: UserStatusPending, + _UserStatusName[7:15]: UserStatusVerified, + _UserStatusName[15:22]: UserStatusBlocked, +} + +// ParseUserStatus attempts to convert a string to a UserStatus. +func ParseUserStatus(name string) (UserStatus, error) { + if x, ok := _UserStatusValue[name]; ok { + return x, nil + } + return UserStatus(0), fmt.Errorf("%s is %w", name, ErrInvalidUserStatus) +} + +var errUserStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *UserStatus) Scan(value interface{}) (err error) { + if value == nil { + *x = UserStatus(0) + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case int64: + *x = UserStatus(v) + case string: + *x, err = ParseUserStatus(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = UserStatus(val), nil + } + } + case []byte: + *x, err = ParseUserStatus(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = UserStatus(val), nil + } + } + case UserStatus: + *x = v + case int: + *x = UserStatus(v) + case *UserStatus: + if v == nil { + return errUserStatusNilPtr + } + *x = *v + case uint: + *x = UserStatus(v) + case uint64: + *x = UserStatus(v) + case *int: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *int64: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = UserStatus(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *uint: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *uint64: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *string: + if v == nil { + return errUserStatusNilPtr + } + *x, err = ParseUserStatus(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = UserStatus(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x UserStatus) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *UserStatus) Set(val string) error { + v, err := ParseUserStatus(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *UserStatus) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *UserStatus) Type() string { + return "UserStatus" +} + +type NullUserStatus struct { + UserStatus UserStatus + Valid bool +} + +func NewNullUserStatus(val interface{}) (x NullUserStatus) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullUserStatus) Scan(value interface{}) (err error) { + if value == nil { + x.UserStatus, x.Valid = UserStatus(0), false + return + } + + err = x.UserStatus.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatus) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.UserStatus), nil +} + +type NullUserStatusStr struct { + NullUserStatus +} + +func NewNullUserStatusStr(val interface{}) (x NullUserStatusStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatusStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.UserStatus.String(), nil +} diff --git a/backend/database/fields/users.go b/backend/database/fields/users.go new file mode 100644 index 0000000..5c4cb28 --- /dev/null +++ b/backend/database/fields/users.go @@ -0,0 +1,5 @@ +package fields + +// swagger:enum UserStatus +// ENUM( Pending, Verified, Blocked) +type UserStatus int16 diff --git a/backend/database/migrations/20250109084432_create_users.sql b/backend/database/migrations/20250109084432_create_users.sql index f943195..b1de60e 100644 --- a/backend/database/migrations/20250109084432_create_users.sql +++ b/backend/database/migrations/20250109084432_create_users.sql @@ -33,7 +33,6 @@ CREATE TABLE user_id INT8 NOT NULL, union_id VARCHAR(128), open_id VARCHAR(128) NOT NULL UNIQUE, - access_key VARCHAR(256) NOT NULL default '', access_token VARCHAR(256) NOT NULL default '', refresh_token VARCHAR(256) NOT NULL default '', expire_at timestamp NOT NULL, diff --git a/backend/database/models/qvyun_v2/public/model/user_oauths.go b/backend/database/models/qvyun_v2/public/model/user_oauths.go index 1c2a3c6..d05f3b5 100644 --- a/backend/database/models/qvyun_v2/public/model/user_oauths.go +++ b/backend/database/models/qvyun_v2/public/model/user_oauths.go @@ -8,21 +8,22 @@ package model import ( + "backend/database/fields" "time" ) type UserOauths struct { - ID int64 `sql:"primary_key" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at"` - Channel int16 `json:"channel"` - UserID int64 `json:"user_id"` - UnionID *string `json:"union_id"` - OpenID string `json:"open_id"` - AccessKey string `json:"access_key"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpireAt time.Time `json:"expire_at"` - Meta *string `json:"meta"` + ID int64 `sql:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at"` + Channel fields.AuthChannel `json:"channel"` + UserID int64 `json:"user_id"` + UnionID *string `json:"union_id"` + OpenID string `json:"open_id"` + AccessKey string `json:"access_key"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpireAt time.Time `json:"expire_at"` + Meta *string `json:"meta"` } diff --git a/backend/database/transform.yaml b/backend/database/transform.yaml index 4a46153..d8e856b 100644 --- a/backend/database/transform.yaml +++ b/backend/database/transform.yaml @@ -5,7 +5,9 @@ ignores: - river_job - river_leader - river_queue -# types: -# users: # table name -# meta: UserMeta -# meta: Json[UserMeta] +types: + users: # table name + status: UserStatus + + user_oauths: + channel: AuthChannel diff --git a/backend/pkg/consts/consts.go b/backend/pkg/consts/consts.go deleted file mode 100644 index 85fa520..0000000 --- a/backend/pkg/consts/consts.go +++ /dev/null @@ -1,8 +0,0 @@ -package consts - -// Format -// -// // swagger:enum CacheKey -// // ENUM( -// // VerifyCode = "code:__CHANNEL__:%s", -// // ) diff --git a/backend/pkg/oauth/contracts.go b/backend/pkg/oauth/contracts.go new file mode 100644 index 0000000..85f5b99 --- /dev/null +++ b/backend/pkg/oauth/contracts.go @@ -0,0 +1,11 @@ +package oauth + +import "time" + +type OAuthInfo interface { + GetOpenID() string + GetUnionID() string + GetAccessToken() string + GetRefreshToken() string + GetExpiredAt() time.Time +} diff --git a/backend/pkg/oauth/wechat.go b/backend/pkg/oauth/wechat.go new file mode 100644 index 0000000..71e3c2a --- /dev/null +++ b/backend/pkg/oauth/wechat.go @@ -0,0 +1,39 @@ +package oauth + +import "time" + +var _ OAuthInfo = (*WechatOAuthInfo)(nil) + +type WechatOAuthInfo struct { + Scope string `json:"scope,omitempty"` + OpenID string `json:"openid,omitempty"` + UnionID string `json:"unionid,omitempty"` + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` +} + +// GetAccessToken implements OAuthInfo. +func (w *WechatOAuthInfo) GetAccessToken() string { + return w.AccessToken +} + +// GetExpiredAt implements OAuthInfo. +func (w *WechatOAuthInfo) GetExpiredAt() time.Time { + return time.Now().Add(time.Duration(w.ExpiresIn) * time.Second) +} + +// GetOpenID implements OAuthInfo. +func (w *WechatOAuthInfo) GetOpenID() string { + return w.OpenID +} + +// GetRefreshToken implements OAuthInfo. +func (w *WechatOAuthInfo) GetRefreshToken() string { + return w.RefreshToken +} + +// GetUnionID implements OAuthInfo. +func (w *WechatOAuthInfo) GetUnionID() string { + return w.UnionID +} diff --git a/backend/providers/events/config.go b/backend/providers/event/config.go similarity index 71% rename from backend/providers/events/config.go rename to backend/providers/event/config.go index 69c79a2..bc87043 100644 --- a/backend/providers/events/config.go +++ b/backend/providers/event/config.go @@ -1,10 +1,12 @@ -package events +package event import ( "context" "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/utils/opt" + "github.com/ThreeDotsLabs/watermill" "github.com/ThreeDotsLabs/watermill/message" ) @@ -46,3 +48,18 @@ func (ps *PubSub) Handle( ) { ps.Router.AddHandler(handlerName, consumerTopic, ps.Subscriber, publisherTopic, ps.Publisher, handler) } + +// publish +func (ps *PubSub) Publish(e contracts.EventPublisher) error { + if e == nil { + return nil + } + + payload, err := e.Marshal() + if err != nil { + return err + } + + msg := message.NewMessage(watermill.NewUUID(), payload) + return ps.Publisher.Publish(e.Topic(), msg) +} diff --git a/backend/providers/events/logrus_adapter.go b/backend/providers/event/logrus_adapter.go similarity index 99% rename from backend/providers/events/logrus_adapter.go rename to backend/providers/event/logrus_adapter.go index a47ab64..b4cdd41 100644 --- a/backend/providers/events/logrus_adapter.go +++ b/backend/providers/event/logrus_adapter.go @@ -1,4 +1,4 @@ -package events +package event import ( "github.com/ThreeDotsLabs/watermill" diff --git a/backend/providers/events/provider.go b/backend/providers/event/provider.go similarity index 97% rename from backend/providers/events/provider.go rename to backend/providers/event/provider.go index c228926..f189156 100644 --- a/backend/providers/events/provider.go +++ b/backend/providers/event/provider.go @@ -1,4 +1,4 @@ -package events +package event import ( "git.ipao.vip/rogeecn/atom/container" diff --git a/backend/providers/events/provider_kafka.go b/backend/providers/event/provider_kafka.go similarity index 98% rename from backend/providers/events/provider_kafka.go rename to backend/providers/event/provider_kafka.go index 6f58f97..52e4a31 100644 --- a/backend/providers/events/provider_kafka.go +++ b/backend/providers/event/provider_kafka.go @@ -1,4 +1,4 @@ -package events +package event import ( "git.ipao.vip/rogeecn/atom/container" diff --git a/backend/providers/events/provider_redis.go b/backend/providers/event/provider_redis.go similarity index 98% rename from backend/providers/events/provider_redis.go rename to backend/providers/event/provider_redis.go index 0d48bb4..e6fd5bb 100644 --- a/backend/providers/events/provider_redis.go +++ b/backend/providers/event/provider_redis.go @@ -1,4 +1,4 @@ -package events +package event import ( "git.ipao.vip/rogeecn/atom/container" diff --git a/backend/providers/events/provider_sql.go b/backend/providers/event/provider_sql.go similarity index 98% rename from backend/providers/events/provider_sql.go rename to backend/providers/event/provider_sql.go index b862313..e48e520 100644 --- a/backend/providers/events/provider_sql.go +++ b/backend/providers/event/provider_sql.go @@ -1,4 +1,4 @@ -package events +package event import ( sqlDB "database/sql" diff --git a/backend/providers/otel/config.go b/backend/providers/otel/config.go index afd2782..6108985 100644 --- a/backend/providers/otel/config.go +++ b/backend/providers/otel/config.go @@ -51,4 +51,11 @@ func (c *Config) format() { c.Env = "unknown" } } + + if c.EndpointGRPC == "" { + c.EndpointGRPC = os.Getenv("OTEL_ENDPOINT_GRPC") + if c.EndpointGRPC == "" { + c.EndpointGRPC = "localhost:4317" + } + } } diff --git a/backend/providers/otel/funcs.go b/backend/providers/otel/funcs.go index 34db99f..66bde02 100644 --- a/backend/providers/otel/funcs.go +++ b/backend/providers/otel/funcs.go @@ -13,7 +13,7 @@ var ( ) func Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return tracer.Start(ctx, spanName, opts...) + return tracer.Start(ctx, spanName) } func Int64Counter(name string, options ...metric.Int64CounterOption) (metric.Int64Counter, error) { diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..897fa35 --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +# QvYun \ No newline at end of file