From 3f227772fd0c4ce51749ba09ab917b9461eeff85 Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 10 Jan 2025 14:39:36 +0800 Subject: [PATCH] fix: user wechat auth --- backend/Makefile | 29 +++----- .../app/events/publishers/user_register.go | 23 +++++++ .../events/{ => subscribers}/provider.gen.go | 8 ++- .../app/events/subscribers/user_register.go | 66 +++++++++++++++++++ backend/app/events/user_register.go | 29 +------- backend/app/http/auth/controller_wechat.go | 32 ++------- backend/app/http/auth/provider.gen.go | 16 +++++ backend/app/http/auth/routes.gen.go | 45 +++++++++++++ backend/app/http/users/provider.gen.go | 6 +- backend/app/http/users/service.go | 39 +++++++++-- backend/app/service/event/event.go | 4 +- .../models/qvyun_v2/public/model/users.go | 27 ++++---- backend/go.mod | 4 +- backend/pkg/f/bind.go | 7 ++ backend/providers/jwt/jwt.go | 8 +-- backend/providers/wechat/wechat.go | 30 +++++++++ 16 files changed, 273 insertions(+), 100 deletions(-) create mode 100644 backend/app/events/publishers/user_register.go rename backend/app/events/{ => subscribers}/provider.gen.go (82%) create mode 100644 backend/app/events/subscribers/user_register.go create mode 100644 backend/app/http/auth/routes.gen.go diff --git a/backend/Makefile b/backend/Makefile index 92d0ecd..f64f780 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,23 +1,7 @@ -buildAt=`date +%Y/%m/%d-%H:%M:%S` -gitHash=`git rev-parse HEAD` -version=`git rev-parse --abbrev-ref HEAD | grep -v HEAD || git describe --exact-match HEAD || git rev-parse HEAD` ## todo: use current release git tag -flags="-X 'atom/utils.Version=${version}' -X 'atom/utils.BuildAt=${buildAt}' -X 'atom/utils.GitHash=${gitHash}'" -release_flags="-w -s ${flags}" - -GOPATH:=$(shell go env GOPATH) - .PHONY: tidy tidy: @go mod tidy -.PHONY: release -release: - @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=${flags} -o bin/release/backend . - @cp config.toml bin/release/ - -.PHONY: test -test: - @go test -v ./... -cover .PHONY: lint lint: @@ -26,8 +10,15 @@ lint: .PHONY: init init: go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway - go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 - go install google.golang.org/protobuf/cmd/protoc-gen-go - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc + go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 + go install google.golang.org/protobuf/cmd/protoc-gen-go + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc go install github.com/bufbuild/buf/cmd/buf go install github.com/golangci/golangci-lint/cmd/golangci-lint + + +.PHONY: all +all: tidy + @atomctl gen enum + @atomctl gen route + @atomctl gen provider \ No newline at end of file diff --git a/backend/app/events/publishers/user_register.go b/backend/app/events/publishers/user_register.go new file mode 100644 index 0000000..b0f487d --- /dev/null +++ b/backend/app/events/publishers/user_register.go @@ -0,0 +1,23 @@ +package publishers + +import ( + "encoding/json" + + "backend/app/events" + + "git.ipao.vip/rogeecn/atom/contracts" +) + +var _ contracts.EventPublisher = (*UserRegister)(nil) + +type UserRegister struct { + ID int64 `json:"id"` +} + +func (e *UserRegister) Marshal() ([]byte, error) { + return json.Marshal(e) +} + +func (e *UserRegister) Topic() string { + return events.TopicUserRegister.String() +} diff --git a/backend/app/events/provider.gen.go b/backend/app/events/subscribers/provider.gen.go similarity index 82% rename from backend/app/events/provider.gen.go rename to backend/app/events/subscribers/provider.gen.go index 635ed0b..59e5f12 100755 --- a/backend/app/events/provider.gen.go +++ b/backend/app/events/subscribers/provider.gen.go @@ -1,6 +1,7 @@ -package events +package subscribers import ( + "backend/app/http/users" "backend/providers/event" "git.ipao.vip/rogeecn/atom" @@ -12,8 +13,11 @@ import ( func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( __event *event.PubSub, + userSvc *users.Service, ) (contracts.Initial, error) { - obj := &UserRegister{} + obj := &UserRegister{ + userSvc: userSvc, + } if err := obj.Prepare(); err != nil { return nil, err } diff --git a/backend/app/events/subscribers/user_register.go b/backend/app/events/subscribers/user_register.go new file mode 100644 index 0000000..8addc3d --- /dev/null +++ b/backend/app/events/subscribers/user_register.go @@ -0,0 +1,66 @@ +package subscribers + +import ( + "context" + "encoding/json" + + "backend/app/events" + "backend/app/events/publishers" + "backend/app/http/users" + "backend/database/fields" + + "git.ipao.vip/rogeecn/atom/contracts" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var _ contracts.EventHandler = (*UserRegister)(nil) + +// @provider(event) +type UserRegister struct { + log *logrus.Entry `inject:"false"` + + userSvc *users.Service +} + +func (e *UserRegister) Prepare() error { + e.log = logrus.WithField("module", "events.subscribers.user_register") + return nil +} + +// PublishToTopic implements contracts.EventHandler. +func (e *UserRegister) PublishToTopic() string { + return events.TopicProcessed.String() +} + +// Topic implements contracts.EventHandler. +func (e *UserRegister) Topic() string { + return events.TopicUserRegister.String() +} + +// Handler implements contracts.EventHandler. +func (e *UserRegister) Handler(msg *message.Message) ([]*message.Message, error) { + var payload publishers.UserRegister + err := json.Unmarshal(msg.Payload, &payload) + if err != nil { + return nil, err + } + e.log.Infof("received event %s", msg.Payload) + + user, err := e.userSvc.GetUserByID(context.Background(), payload.ID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get user by id: %d", payload.ID) + } + + if user.Status != fields.UserStatusPending { + return nil, nil + } + + err = e.userSvc.SetUserStatusByID(context.Background(), payload.ID, fields.UserStatusVerified) + if err != nil { + return nil, errors.Wrapf(err, "failed to set user status to %s by id: %d", fields.UserStatusVerified, payload.ID) + } + + return nil, nil +} diff --git a/backend/app/events/user_register.go b/backend/app/events/user_register.go index 35a9f75..21698fd 100644 --- a/backend/app/events/user_register.go +++ b/backend/app/events/user_register.go @@ -4,19 +4,12 @@ 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) -) +var _ contracts.EventPublisher = (*UserRegister)(nil) -// @provider(event) type UserRegister struct { - log *logrus.Entry `inject:"false" json:"-"` - ID int64 `json:"id"` + ID int64 `json:"id"` } func (e *UserRegister) Prepare() error { @@ -28,25 +21,7 @@ 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 index e3d4341..e1116bf 100644 --- a/backend/app/http/auth/controller_wechat.go +++ b/backend/app/http/auth/controller_wechat.go @@ -3,11 +3,11 @@ package auth import ( "fmt" "net/url" - "strings" "time" "backend/app/consts" "backend/app/http/users" + "backend/database/fields" "backend/providers/jwt" "backend/providers/otel" "backend/providers/wechat" @@ -33,8 +33,7 @@ func (ctl *Controller) Prepare() error { return nil } -// @Router /v1/auth/wechat/jump/:tenant [get] -// @Bind tenant path +// @Router /v1/auth/wechat/jump [get] // @Bind redirectUri query func (ctl *Controller) JumpToAuth(ctx fiber.Ctx, tenant, redirectUri string) error { _, span := otel.Start(ctx.Context(), "auth.controller.wechat") @@ -68,21 +67,13 @@ func (ctl *Controller) JumpToAuth(ctx fiber.Ctx, tenant, redirectUri string) err return ctx.Redirect().To(to.String()) } -// @Router /v1/auth/login/:tenant [get] -// @Bind tenant path +// @Router /v1/auth/login [get] // @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 { @@ -90,31 +81,22 @@ func (ctl *Controller) Login(ctx fiber.Ctx, code, state, tenant, redirectUri str } ctl.log.Debugf("tokenInfo %+v", token) - user, err := ctl.userSvc.GetOrNewFromChannel(ctx.Context(), consts.AuthChannelWeChat, token.OpenID, tenant) + userID, err := ctl.userSvc.GetUserIDByOpenID(ctx.Context(), fields.AuthChannelWeChat, token.GetOpenID()) 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) + jwtToken, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{UserID: userID})) if err != nil { return errors.Wrap(err, "failed to create token") } ctx.Cookie(&fiber.Cookie{ - Name: "token", + Name: consts.TokenTypeUser.String(), 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) + return ctx.Redirect().To(redirectUri) } diff --git a/backend/app/http/auth/provider.gen.go b/backend/app/http/auth/provider.gen.go index 983a7f7..8d2fc1d 100755 --- a/backend/app/http/auth/provider.gen.go +++ b/backend/app/http/auth/provider.gen.go @@ -7,7 +7,9 @@ import ( "backend/providers/jwt" "backend/providers/wechat" + "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/utils/opt" ) @@ -32,6 +34,20 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func( + controller *Controller, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + controller: controller, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } if err := container.Container.Provide(func( db *sql.DB, ) (*Service, error) { diff --git a/backend/app/http/auth/routes.gen.go b/backend/app/http/auth/routes.gen.go new file mode 100644 index 0000000..055e86a --- /dev/null +++ b/backend/app/http/auth/routes.gen.go @@ -0,0 +1,45 @@ +// Code generated by the atomctl ; DO NOT EDIT. + +package auth + +import ( + . "backend/pkg/f" + + _ "git.ipao.vip/rogeecn/atom" + _ "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" +) + +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + controller *Controller +} + +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.auth") + return nil +} + +func (r *Routes) Name() string { + return "auth" +} + +func (r *Routes) Register(router fiber.Router) { + // 注册路由组: Controller + router.Get("/v1/auth/wechat/jump/:tenant", Func2( + r.controller.JumpToAuth, + Path[string]("tenant"), + QueryParam[string]("redirectUri"), + )) + + router.Get("/v1/auth/login/:tenant", Func4( + r.controller.Login, + Path[string]("tenant"), + QueryParam[string]("code"), + QueryParam[string]("state"), + QueryParam[string]("redirectUri"), + )) + +} diff --git a/backend/app/http/users/provider.gen.go b/backend/app/http/users/provider.gen.go index db6358e..a6effcf 100755 --- a/backend/app/http/users/provider.gen.go +++ b/backend/app/http/users/provider.gen.go @@ -3,6 +3,8 @@ package users import ( "database/sql" + "backend/providers/event" + "git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/utils/opt" ) @@ -24,9 +26,11 @@ func Provide(opts ...opt.Option) error { } if err := container.Container.Provide(func( db *sql.DB, + event *event.PubSub, ) (*Service, error) { obj := &Service{ - db: db, + db: db, + event: event, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/http/users/service.go b/backend/app/http/users/service.go index 5ba472f..7c457af 100644 --- a/backend/app/http/users/service.go +++ b/backend/app/http/users/service.go @@ -5,7 +5,7 @@ import ( "database/sql" "time" - "backend/app/events" + "backend/app/events/publishers" "backend/database/fields" "backend/database/models/qvyun_v2/public/model" "backend/database/models/qvyun_v2/public/table" @@ -14,6 +14,7 @@ import ( "backend/providers/otel" . "github.com/go-jet/jet/v2/postgres" + "github.com/pkg/errors" "github.com/samber/lo" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" @@ -35,7 +36,7 @@ func (svc *Service) Prepare() error { } // GetUsersByOpenID Get user by open id -func (svc *Service) GetUserByOpenID(ctx context.Context, channel fields.AuthChannel, openID string) (*model.Users, error) { +func (svc *Service) GetUserByOpenIDOfChannel(ctx context.Context, channel fields.AuthChannel, openID string) (*model.Users, error) { _, span := otel.Start(ctx, "users.service.GetUsersByOpenID") defer span.End() @@ -106,7 +107,7 @@ func (svc *Service) CreateUser(ctx context.Context, user *model.Users) (*model.U user.UpdatedAt = time.Now() } - user.Status = int16(fields.UserStatusPending) + user.Status = fields.UserStatusPending // use bcrypt to hash password pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) @@ -126,9 +127,11 @@ func (svc *Service) CreateUser(ctx context.Context, user *model.Users) (*model.U // if user created successfully, trigger event span.AddEvent("user created") - svc.event.Publish(&events.UserRegister{ID: m.ID}) + if err := svc.event.Publish(&publishers.UserRegister{ID: m.ID}); err != nil { + return nil, errors.Wrapf(err, "failed to publish user register event %d", m.ID) + } - return &m, err + return &m, nil } // GetUserByID @@ -199,3 +202,29 @@ func (svc *Service) AttachUserOAuth(ctx context.Context, user *model.Users, chan return nil } + +// SetUserStatusByID +func (svc *Service) SetUserStatusByID(ctx context.Context, userID int64, status fields.UserStatus) error { + _, span := otel.Start(ctx, "users.service.SetUserStatusByID") + defer span.End() + span.SetAttributes( + attribute.Int64("user.id", userID), + attribute.String("user.status", status.String()), + ) + + tbl := table.Users + stmt := tbl. + UPDATE(). + SET( + tbl.Status.SET(Int16(int16(status))), + ). + WHERE( + tbl.ID.EQ(Int64(userID)), + ) + 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/service/event/event.go b/backend/app/service/event/event.go index 60cfffb..32009ab 100644 --- a/backend/app/service/event/event.go +++ b/backend/app/service/event/event.go @@ -3,7 +3,7 @@ package event import ( "context" - "backend/app/events" + "backend/app/events/subscribers" "backend/app/service" "backend/providers/app" "backend/providers/event" @@ -31,7 +31,7 @@ func Command() atom.Option { atom.Providers( defaultProviders(). With( - events.Provide, + subscribers.Provide, ), ), ) diff --git a/backend/database/models/qvyun_v2/public/model/users.go b/backend/database/models/qvyun_v2/public/model/users.go index a0cf8f9..15dcc36 100644 --- a/backend/database/models/qvyun_v2/public/model/users.go +++ b/backend/database/models/qvyun_v2/public/model/users.go @@ -8,21 +8,22 @@ package model import ( + "backend/database/fields" "time" ) type Users 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"` - Status int16 `json:"status"` - Email string `json:"email"` - Phone string `json:"phone"` - Username string `json:"username"` - Nickname *string `json:"nickname"` - Password string `json:"password"` - Age int16 `json:"age"` - Sex int16 `json:"sex"` - Avatar *string `json:"avatar"` + 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"` + Status fields.UserStatus `json:"status"` + Email string `json:"email"` + Phone string `json:"phone"` + Username string `json:"username"` + Nickname *string `json:"nickname"` + Password string `json:"password"` + Age int16 `json:"age"` + Sex int16 `json:"sex"` + Avatar *string `json:"avatar"` } diff --git a/backend/go.mod b/backend/go.mod index 5ae9e10..1ba0ef8 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -31,6 +31,7 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/speps/go-hashids/v2 v2.0.1 github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.10.0 github.com/swaggo/files/v2 v2.0.2 github.com/swaggo/swag v1.16.4 github.com/uber/jaeger-client-go v2.30.0+incompatible @@ -46,6 +47,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.33.0 go.opentelemetry.io/otel/trace v1.33.0 go.uber.org/dig v1.18.0 + golang.org/x/crypto v0.32.0 golang.org/x/net v0.34.0 golang.org/x/sync v0.10.0 google.golang.org/grpc v1.69.2 @@ -127,7 +129,6 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.19.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -146,7 +147,6 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sys v0.29.0 // indirect diff --git a/backend/pkg/f/bind.go b/backend/pkg/f/bind.go index a0ce329..7a92fdd 100644 --- a/backend/pkg/f/bind.go +++ b/backend/pkg/f/bind.go @@ -34,6 +34,13 @@ func Body[T any](name string) func(fiber.Ctx) (*T, error) { } } +func QueryParam[T fiber.GenericType](key string) func(fiber.Ctx) (T, error) { + return func(ctx fiber.Ctx) (T, error) { + v := fiber.Query[T](ctx, key) + return v, nil + } +} + func Query[T any](name string) func(fiber.Ctx) (*T, error) { return func(ctx fiber.Ctx) (*T, error) { p := new(T) diff --git a/backend/providers/jwt/jwt.go b/backend/providers/jwt/jwt.go index ee39a95..c208e0a 100644 --- a/backend/providers/jwt/jwt.go +++ b/backend/providers/jwt/jwt.go @@ -18,10 +18,10 @@ const ( ) type BaseClaims struct { - OpenID string `json:"open_id,omitempty"` - Tenant string `json:"tenant,omitempty"` - UserID int64 `json:"user_id,omitempty"` - TenantID int64 `json:"tenant_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` + TenantID *int64 `json:"tenant_id,omitempty"` + OpenID *string `json:"open_id,omitempty"` + Tenant *string `json:"tenant,omitempty"` } // Custom claims structure diff --git a/backend/providers/wechat/wechat.go b/backend/providers/wechat/wechat.go index 6ae91fd..d26b404 100644 --- a/backend/providers/wechat/wechat.go +++ b/backend/providers/wechat/wechat.go @@ -6,6 +6,9 @@ import ( "net/url" "sort" "strings" + "time" + + "backend/pkg/oauth" "github.com/imroc/req/v3" "github.com/pkg/errors" @@ -123,6 +126,8 @@ func (we *Client) ScopeAuthorizeURL(opts ...ScopeAuthorizeURLOptions) (*url.URL, return u, nil } +var _ oauth.OAuthInfo = (*AuthorizeAccessToken)(nil) + type AuthorizeAccessToken struct { ErrorResponse AccessToken string `json:"access_token,omitempty"` @@ -134,6 +139,31 @@ type AuthorizeAccessToken struct { Unionid string `json:"unionid,omitempty"` } +// GetAccessToken implements oauth.OAuthInfo. +func (a *AuthorizeAccessToken) GetAccessToken() string { + return a.AccessToken +} + +// GetExpiredAt implements oauth.OAuthInfo. +func (a *AuthorizeAccessToken) GetExpiredAt() time.Time { + return time.Now().Add(time.Duration(a.ExpiresIn) * time.Second) +} + +// GetOpenID implements oauth.OAuthInfo. +func (a *AuthorizeAccessToken) GetOpenID() string { + return a.Openid +} + +// GetRefreshToken implements oauth.OAuthInfo. +func (a *AuthorizeAccessToken) GetRefreshToken() string { + return a.RefreshToken +} + +// GetUnionID implements oauth.OAuthInfo. +func (a *AuthorizeAccessToken) GetUnionID() string { + return a.Unionid +} + func (we *Client) AuthorizeCode2Token(code string) (*AuthorizeAccessToken, error) { params := we.wrapParams(map[string]string{ "code": code,