package auth import ( "context" "time" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" "gorm.io/gorm" mpauth "git.esin.io/lab/weixin/clientapi/mp/auth" "git.esin.io/lab/weixin/pkg/pubsub" pb "git.esin.io/lab/weixin/protobuf/clientapi/mp/auth" ) type Config struct { WeixinAppID string WeiXinAppSecret string DB *gorm.DB Publisher pubsub.Publisher } type Service struct { pb.UnimplementedAuthServiceServer client *mpauth.Client db *gorm.DB publisher pubsub.Publisher } func NewServiceServer(cfg *Config) pb.AuthServiceServer { return &Service{ db: cfg.DB, client: mpauth.NewClient(&mpauth.Config{ ClientID: cfg.WeixinAppID, ClientSecret: cfg.WeiXinAppSecret, }), publisher: cfg.Publisher, } } func RegisterAuthServiceServer(s *grpc.Server, srv pb.AuthServiceServer) { pb.RegisterAuthServiceServer(s, srv) } func (srv Service) PublishEvent(ctx context.Context, subject string, message interface{}) error { if srv.publisher != nil { return srv.publisher.Publish(ctx, subject, message) } return nil } func (srv Service) GetCodeURL(ctx context.Context, req *pb.GetCodeURLRequest) (*pb.GetCodeURLResponse, error) { resp := srv.client.GetCodeURL(req.RedirectUrl, req.State, mpauth.Scope(req.Scope.String())) return &pb.GetCodeURLResponse{ Url: resp, }, nil } func (srv Service) ExchangeToken(ctx context.Context, req *pb.ExchangeTokenRequest) (*pb.ExchangeTokenResponse, error) { resp, err := srv.client.ExchangeToken(ctx, req.Code) if err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "exchange token from weixin failed").Error()) } var token Token if err := srv.db.Last(&token, "open_id = ?", resp.OpenID).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(codes.Internal, err.Error()) } token = Token{Token: *resp} } token.Assign(resp) if err := srv.db.Save(&token).Error; err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "save token to database failed").Error()) } go srv.PublishEvent(ctx, "auth.token.exchanged", &token) return &pb.ExchangeTokenResponse{ Token: token.Proto(), Timestamp: timestamppb.New(token.CreatedAt), }, nil } func (srv Service) RefreshToken(ctx context.Context, req *pb.RefreshTokenRequest) (*pb.RefreshTokenResponse, error) { var token Token if err := srv.db.Last(&token, "open_id = ?", req.OpenId).Error; err != nil { return nil, status.Errorf(codes.Internal, err.Error()) } resp, err := srv.client.RefreshToken(ctx, token.RefreshToken) if err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "refresh refreshed token from weixin failed").Error()) } token.Assign(resp) if err := srv.db.Save(&token).Error; err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "save refreshed token to database failed").Error()) } go srv.PublishEvent(ctx, "auth.token.refreshed", &token) return &pb.RefreshTokenResponse{ Token: token.Proto(), Timestamp: timestamppb.New(token.CreatedAt), }, nil } func (srv Service) GetUserinfo(ctx context.Context, req *pb.GetUserinfoRequest) (*pb.GetUserinfoResponse, error) { var token Token if err := srv.db.Last(&token, "open_id = ?", req.OpenId).Error; err != nil { return nil, status.Errorf(codes.Internal, err.Error()) } var userinfo Userinfo if err := srv.db.Last(&userinfo, "open_id = ?", req.OpenId).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(codes.Internal, err.Error()) } resp, err := srv.client.GetUserinfo(ctx, token.AccessToken, req.OpenId, mpauth.Lang(req.Lang.String())) if err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "get userinfo token from weixin failed").Error()) } userinfo = Userinfo{Userinfo: *resp} if err := srv.db.Create(&userinfo).Error; err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "save userinfo to database failed").Error()) } go srv.PublishEvent(ctx, "auth.userinfo.created", &userinfo) } return &pb.GetUserinfoResponse{ Userinfo: userinfo.Proto(), CreatedTime: timestamppb.New(userinfo.CreatedAt), UpdatedTime: timestamppb.New(userinfo.UpdatedAt), }, nil } func (srv Service) SyncUserinfo(ctx context.Context, req *pb.SyncUserinfoRequest) (*pb.SyncUserinfoResponse, error) { var token Token if err := srv.db.Last(&token, "open_id = ?", req.OpenId).Error; err != nil { return nil, status.Errorf(codes.Internal, err.Error()) } var userinfo Userinfo if err := srv.db.Last(&userinfo, "open_id = ?", req.OpenId).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(codes.Internal, err.Error()) } } resp, err := srv.client.GetUserinfo(ctx, token.AccessToken, req.OpenId, mpauth.Lang(req.Lang.String())) if err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "get userinfo token from weixin failed").Error()) } userinfo.Userinfo = *resp if err := srv.db.Save(&userinfo).Error; err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "sync userinfo and save to database failed").Error()) } go srv.PublishEvent(ctx, "auth.userinfo.synchronized", &userinfo) return &pb.SyncUserinfoResponse{ Userinfo: userinfo.Proto(), CreatedTime: timestamppb.New(userinfo.CreatedAt), UpdatedTime: timestamppb.New(userinfo.UpdatedAt), }, nil } func (srv Service) GetClientCredential(ctx context.Context, _ *emptypb.Empty) (*pb.GetClientCredentialResponse, error) { var cred ClientCredential err := srv.db.Last(&cred).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, status.Errorf(codes.Internal, err.Error()) } // get new cred when cred not found or cred expired if errors.Is(err, gorm.ErrRecordNotFound) || cred.Expired() { resp, err := srv.client.GetClientCredential(ctx) if err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "get client credential token from weixin failed").Error()) } cred = ClientCredential{ ClientCredential: *resp, } if err := srv.db.Create(&cred).Error; err != nil { return nil, status.Errorf(codes.Internal, errors.Wrap(err, "save client credential to database failed").Error()) } go srv.PublishEvent(ctx, "auth.clientcredential.created", &cred) } return &pb.GetClientCredentialResponse{ ClientCredential: cred.Proto(), Timestamp: timestamppb.New(cred.CreatedAt), }, nil } type Token struct { gorm.Model mpauth.Token } func (Token) TableName() string { return "auth_token" } func (token Token) Expired() bool { expTime := token.UpdatedAt.Add(time.Second * time.Duration(token.ExpiresIn)) return expTime.Before(time.Now()) } func (token Token) Proto() *pb.Token { return &pb.Token{ AccessToken: token.AccessToken, ExpiresIn: token.ExpiresIn, RefreshToken: token.RefreshToken, Scope: token.Scope, OpenId: token.OpenID, } } func (token *Token) Assign(t *mpauth.Token) { token.Token = *t } type Userinfo struct { gorm.Model mpauth.Userinfo } func (Userinfo) TableName() string { return "auth_userinfo" } func (userinfo Userinfo) Proto() *pb.Userinfo { return &pb.Userinfo{ OpenId: userinfo.OpenID, NickName: userinfo.NickName, Sex: userinfo.Sex, Province: userinfo.Province, City: userinfo.City, Country: userinfo.Country, HeadImgUrl: userinfo.HeadImgURL, Privilege: userinfo.Privilege, UnionId: userinfo.UnionID, } } type ClientCredential struct { gorm.Model mpauth.ClientCredential } func (cred ClientCredential) Expired() bool { expTime := cred.UpdatedAt.Add(time.Second * time.Duration(cred.ExpiresIn)) return expTime.Before(time.Now()) } func (cred ClientCredential) Proto() *pb.ClientCredential { return &pb.ClientCredential{ AccessToken: cred.AccessToken, ExpiresIn: cred.ExpiresIn, } }