Feat: add session

This commit is contained in:
2025-12-07 11:27:54 +08:00
parent 9c30bc009d
commit fb1c47b321
12 changed files with 400 additions and 12 deletions

View File

@@ -0,0 +1,113 @@
package middlewares
import (
"context"
"net/http"
"gitea.konchin.com/go2025/backend/interfaces"
"gitea.konchin.com/go2025/backend/models"
"gitea.konchin.com/go2025/backend/tracing"
"gitea.konchin.com/go2025/backend/types"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"github.com/uptrace/bunrouter"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
func refreshAccessToken(
ctx context.Context,
db interfaces.Database,
w http.ResponseWriter,
req bunrouter.Request,
) (string, error) {
refreshTokenClaim, ok := req.Context().
Value(types.RefreshToken("")).(models.RefreshTokenClaim)
if !ok {
tracing.Logger.Ctx(ctx).
Warn("refresh token not exist")
return "", types.ContextNotExistError
}
session, err := db.GetSession(ctx, refreshTokenClaim.UserId)
if err != nil {
tracing.Logger.Ctx(ctx).
Warn("session not exist", zap.Error(err))
return "", err
}
ret, err := session.ToAccessToken()
if err != nil {
tracing.Logger.Ctx(ctx).
Warn("access token generate failed", zap.Error(err))
return "", err
}
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: ret,
Path: "/",
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
return ret, nil
}
func (self *Handlers) CheckAccessToken(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
ctx := req.Context()
var accessTokenString string
accessTokenCookie, err := req.Cookie("access_token")
if err != nil {
accessTokenString, err = refreshAccessToken(ctx, self.db, w, req)
if err != nil {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "access token refresh failed",
OriginError: err,
}
}
} else {
accessTokenString = accessTokenCookie.Value
}
var claim models.AccessTokenClaim
token, err := jwt.ParseWithClaims(accessTokenString, &claim,
func(*jwt.Token) (interface{}, error) {
return []byte(viper.GetString("ACCESS_TOKEN_SECRET")), nil
})
if err != nil || !token.Valid {
accessTokenString, err = refreshAccessToken(ctx, self.db, w, req)
if err != nil {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "access token refresh failed",
OriginError: err,
}
}
token, err := jwt.ParseWithClaims(accessTokenString, &claim,
func(*jwt.Token) (interface{}, error) {
return []byte(viper.GetString("ACCESS_TOKEN_SECRET")), nil
})
if err != nil || !token.Valid {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "access token jwt cannot parse or invalid",
OriginError: err,
}
}
}
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.String("owner.UserId", claim.UserId))
ctx = context.WithValue(ctx, types.AccessToken(""), claim)
return next(w, req.WithContext(ctx))
}
}

View File

@@ -0,0 +1,74 @@
package middlewares
import (
"context"
"net/http"
"time"
"gitea.konchin.com/go2025/backend/models"
"gitea.konchin.com/go2025/backend/types"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) CheckRefreshToken(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
ctx := req.Context()
refreshTokenCookie, err := req.Cookie("refresh_token")
if err != nil {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "user did not login",
OriginError: err,
}
}
var claim models.RefreshTokenClaim
token, err := jwt.ParseWithClaims(refreshTokenCookie.Value, &claim,
func(*jwt.Token) (interface{}, error) {
return []byte(viper.GetString("REFRESH_TOKEN_SECRET")), nil
})
if err != nil {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "refresh token jwt cannot parse",
OriginError: err,
}
}
if !token.Valid {
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "refresh token jwt invalid",
}
}
// check time and refresh
timeLeft := claim.ExpiresAt.Time.Sub(time.Now()) / time.Second
if int64(timeLeft) < viper.GetInt64("REFRESH_TOKEN_TIMEOUT")/2 {
session, err := self.db.UpdateRefreshToken(ctx, claim.UserId)
if err != nil {
return HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "upsert session failed",
OriginError: err,
}
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: session.RefreshToken,
Path: "/",
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
ctx = context.WithValue(ctx, types.RefreshToken(""), claim)
return next(w, req.WithContext(ctx))
}
}

11
middlewares/handlers.go Normal file
View File

@@ -0,0 +1,11 @@
package middlewares
import "gitea.konchin.com/go2025/backend/interfaces"
type Handlers struct {
db interfaces.Database
}
func NewHandlers(db interfaces.Database) *Handlers {
return &Handlers{db: db}
}