199 lines
4.6 KiB
Go
199 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.nobla.cn/golang/aeus-admin/models"
|
|
"git.nobla.cn/golang/aeus-admin/pb"
|
|
"git.nobla.cn/golang/aeus-admin/types"
|
|
"git.nobla.cn/golang/aeus/metadata"
|
|
"git.nobla.cn/golang/aeus/pkg/cache"
|
|
"git.nobla.cn/golang/aeus/pkg/errors"
|
|
"git.nobla.cn/golang/aeus/pkg/httpclient"
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
"github.com/mssola/useragent"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type (
|
|
authOptions struct {
|
|
db *gorm.DB
|
|
secret []byte
|
|
method string
|
|
ttl int64
|
|
cache cache.Cache
|
|
tokenStore TokenStore
|
|
turnstileValidateUrl string
|
|
turnstileSiteKey string
|
|
}
|
|
|
|
turnstileRequest struct {
|
|
Secret string `json:"secret"`
|
|
Response string `json:"response"`
|
|
}
|
|
|
|
turnstileResponse struct {
|
|
Success bool `json:"success"`
|
|
Hostname string `json:"hostname"`
|
|
ErrorCodes []string `json:"error-codes"`
|
|
Action string `json:"action"`
|
|
}
|
|
|
|
AuthOption func(o *authOptions)
|
|
|
|
TokenStore interface {
|
|
Put(ctx context.Context, token string, ttl int64) error
|
|
Del(ctx context.Context, token string) error
|
|
}
|
|
|
|
AuthService struct {
|
|
opts *authOptions
|
|
}
|
|
)
|
|
|
|
func WithAuthDB(db *gorm.DB) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.db = db
|
|
}
|
|
}
|
|
|
|
func WithAuthCache(cache cache.Cache) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.cache = cache
|
|
}
|
|
}
|
|
|
|
func WithTokenStore(store TokenStore) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.tokenStore = store
|
|
}
|
|
}
|
|
|
|
func WithAuthSecret(secret []byte) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.secret = secret
|
|
}
|
|
}
|
|
|
|
func WithAuthTranslate(key, url string) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.turnstileSiteKey = key
|
|
if url == "" {
|
|
url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
}
|
|
o.turnstileValidateUrl = url
|
|
}
|
|
}
|
|
|
|
func WithAuthMethod(method string) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.method = method
|
|
}
|
|
}
|
|
|
|
func WithAuthTTL(ttl int64) AuthOption {
|
|
return func(o *authOptions) {
|
|
o.ttl = ttl
|
|
}
|
|
}
|
|
|
|
func (s *AuthService) turnstileValidate(ctx context.Context, token string) (err error) {
|
|
if s.opts.turnstileSiteKey == "" || s.opts.turnstileValidateUrl == "" {
|
|
return nil
|
|
}
|
|
payload := &turnstileRequest{
|
|
Secret: s.opts.turnstileSiteKey,
|
|
Response: token,
|
|
}
|
|
result := &turnstileResponse{}
|
|
if err = httpclient.Do(
|
|
ctx,
|
|
s.opts.turnstileValidateUrl,
|
|
result,
|
|
httpclient.WithMethod(http.MethodPost),
|
|
httpclient.WithBody(payload),
|
|
); err != nil {
|
|
return
|
|
}
|
|
if !result.Success {
|
|
if len(result.ErrorCodes) == 0 {
|
|
err = errors.Format(errors.Unavailable, "turnstile validate failed")
|
|
} else {
|
|
err = errors.Format(errors.Unavailable, result.ErrorCodes[0])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *AuthService) Login(ctx context.Context, req *pb.LoginRequest) (res *pb.LoginResponse, err error) {
|
|
model := &models.User{}
|
|
tx := s.opts.db.WithContext(ctx)
|
|
if err = req.Validate(); err != nil {
|
|
return nil, errors.Format(errors.Invalid, err.Error())
|
|
}
|
|
if err = s.turnstileValidate(ctx, req.Token); err != nil {
|
|
return nil, errors.Format(errors.Invalid, err.Error())
|
|
}
|
|
if err = model.QueryOne(tx, "uid=?", req.Username); err != nil {
|
|
return nil, errors.ErrAccessDenied
|
|
}
|
|
if model.Status != types.UserStatusNormal {
|
|
return nil, errors.ErrAccessDenied
|
|
}
|
|
if model.Password != req.Password {
|
|
return nil, errors.ErrAccessDenied
|
|
}
|
|
claims := types.Claims{
|
|
Uid: model.Uid,
|
|
Role: model.Role,
|
|
IssuedAt: time.Now().Unix(),
|
|
ExpirationAt: time.Now().Add(time.Second * time.Duration(s.opts.ttl)).Unix(),
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
res = &pb.LoginResponse{}
|
|
if res.Token, err = token.SignedString(s.opts.secret); err == nil {
|
|
res.Uid = model.Uid
|
|
res.Username = model.Username
|
|
res.Expires = s.opts.ttl
|
|
}
|
|
loginModel := &models.Login{}
|
|
loginModel.Uid = model.Uid
|
|
loginModel.AccessToken = res.Token
|
|
md := metadata.FromContext(ctx)
|
|
if s.opts.tokenStore != nil {
|
|
s.opts.tokenStore.Put(ctx, res.Token, s.opts.ttl)
|
|
}
|
|
if userAgent, ok := md.Get("User-Agent"); ok {
|
|
ua := useragent.New(userAgent)
|
|
loginModel.Os = ua.OS()
|
|
loginModel.Platform = ua.Platform()
|
|
loginModel.UserAgent = userAgent
|
|
browser, browserVersion := ua.Browser()
|
|
loginModel.Browser = browser + "/" + browserVersion
|
|
tx.Save(loginModel)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *AuthService) Logout(ctx context.Context, req *pb.LogoutRequest) (res *pb.LogoutResponse, err error) {
|
|
if s.opts.tokenStore != nil {
|
|
err = s.opts.tokenStore.Del(ctx, req.Token)
|
|
}
|
|
return
|
|
}
|
|
|
|
func NewAuthService(cbs ...AuthOption) *AuthService {
|
|
opts := &authOptions{
|
|
ttl: 7200,
|
|
}
|
|
for _, cb := range cbs {
|
|
cb(opts)
|
|
}
|
|
return &AuthService{
|
|
opts: opts,
|
|
}
|
|
}
|