aeus-admin/service/auth.go

200 lines
4.7 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,
Admin: model.Admin,
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,
}
}