fx build fix
This commit is contained in:
56
api/server/internal/api/routers/authorized/handler.go
Normal file
56
api/server/internal/api/routers/authorized/handler.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type tokenHandlerFunc = func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc
|
||||
|
||||
func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler tokenHandlerFunc) {
|
||||
hndlr := func(r *http.Request) http.HandlerFunc {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
ar.logger.Debug("Authorization failed", zap.Error(err), zap.String("request", r.URL.Path))
|
||||
return response.Unauthorized(ar.logger, ar.service, "credentials required")
|
||||
}
|
||||
t, err := emodel.Claims2Token(claims)
|
||||
if err != nil {
|
||||
ar.logger.Debug("Failed to decode account token", zap.Error(err))
|
||||
return response.BadRequest(ar.logger, ar.service, "credentials_unreadable", "faild to parse credentials")
|
||||
}
|
||||
return handler(r, t)
|
||||
}
|
||||
ar.imp.InstallHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
|
||||
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
||||
var a model.Account
|
||||
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef))
|
||||
return response.NotFound(ar.logger, ar.service, err.Error())
|
||||
}
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
accessToken, err := ar.imp.CreateAccessToken(&a)
|
||||
if err != nil {
|
||||
ar.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
return handler(r, &a, &accessToken)
|
||||
}
|
||||
ar.tokenHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
34
api/server/internal/api/routers/authorized/router.go
Normal file
34
api/server/internal/api/routers/authorized/router.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
|
||||
)
|
||||
|
||||
type AuthorizedRouter struct {
|
||||
logger mlogger.Logger
|
||||
db account.DB
|
||||
imp *re.HttpEndpointRouter
|
||||
service mservice.Type
|
||||
}
|
||||
|
||||
func NewRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, db account.DB, enforcer auth.Enforcer, config *middleware.TokenConfig, signature *middleware.Signature) *AuthorizedRouter {
|
||||
ja := jwtauth.New(signature.Algorithm, signature.PrivateKey, signature.PublicKey)
|
||||
router.Use(jwtauth.Verifier(ja))
|
||||
router.Use(jwtauth.Authenticator(ja))
|
||||
l := logger.Named("authorized")
|
||||
ar := AuthorizedRouter{
|
||||
logger: l,
|
||||
db: db,
|
||||
imp: re.NewHttpEndpointRouter(l, apiEndpoint, router, config, signature),
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
return &ar
|
||||
}
|
||||
50
api/server/internal/api/routers/dispatcher.go
Normal file
50
api/server/internal/api/routers/dispatcher.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
rauthorized "github.com/tech/sendico/server/internal/api/routers/authorized"
|
||||
rpublic "github.com/tech/sendico/server/internal/api/routers/public"
|
||||
)
|
||||
|
||||
type Dispatcher struct {
|
||||
logger mlogger.Logger
|
||||
public APIRouter
|
||||
protected ProtectedAPIRouter
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
d.public.InstallHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
d.protected.AccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
|
||||
d := &Dispatcher{
|
||||
logger: logger.Named("api_dispatcher"),
|
||||
}
|
||||
|
||||
d.logger.Debug("Installing endpoints middleware...")
|
||||
endpoint := os.Getenv(config.EndPointEnv)
|
||||
signature := middleware.SignatureConf(config)
|
||||
router.Group(func(r chi.Router) {
|
||||
d.public = rpublic.NewRouter(d.logger, endpoint, db, rtdb, r, &config.Token, &signature)
|
||||
})
|
||||
router.Group(func(r chi.Router) {
|
||||
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)
|
||||
})
|
||||
|
||||
return d
|
||||
}
|
||||
36
api/server/internal/api/routers/endpoint/endpoint.go
Normal file
36
api/server/internal/api/routers/endpoint/endpoint.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
)
|
||||
|
||||
type (
|
||||
RegistratorT = func(chi.Router, string, http.HandlerFunc)
|
||||
ResponderFunc = func(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc
|
||||
)
|
||||
|
||||
type HttpEndpointRouter struct {
|
||||
logger mlogger.Logger
|
||||
apiEndpoint string
|
||||
router chi.Router
|
||||
config middleware.TokenConfig
|
||||
signature middleware.Signature
|
||||
}
|
||||
|
||||
func NewHttpEndpointRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *HttpEndpointRouter {
|
||||
er := HttpEndpointRouter{
|
||||
logger: logger.Named("http"),
|
||||
apiEndpoint: apiEndpoint,
|
||||
router: router,
|
||||
signature: *signature,
|
||||
config: *config,
|
||||
}
|
||||
return &er
|
||||
}
|
||||
50
api/server/internal/api/routers/endpoint/install.go
Normal file
50
api/server/internal/api/routers/endpoint/install.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (er *HttpEndpointRouter) chooseMethod(method api.HTTPMethod) RegistratorT {
|
||||
switch method {
|
||||
case api.Get:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Get(p, h) }
|
||||
case api.Post:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Post(p, h) }
|
||||
case api.Put:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Put(p, h) }
|
||||
case api.Delete:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Delete(p, h) }
|
||||
case api.Patch:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Patch(p, h) }
|
||||
case api.Options:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Options(p, h) }
|
||||
case api.Head:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Head(p, h) }
|
||||
default:
|
||||
}
|
||||
er.logger.Error("Unknown method provided", zap.String("method", api.HTTPMethod2String(method)))
|
||||
panic(fmt.Sprintf("Unknown method provided: %d", method))
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) endpoint(service mservice.Type, handler string) string {
|
||||
return path.Join(er.apiEndpoint, service, handler)
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
ep := er.endpoint(service, endpoint)
|
||||
hm := er.chooseMethod(method)
|
||||
hndlr := func(w http.ResponseWriter, r *http.Request) {
|
||||
res := handler(r)
|
||||
res(w, r)
|
||||
}
|
||||
hm(er.router, ep, hndlr)
|
||||
er.logger.Info("Handler installed", zap.String("endpoint", ep), zap.String("method", api.HTTPMethod2String(method)))
|
||||
}
|
||||
20
api/server/internal/api/routers/endpoint/token.go
Normal file
20
api/server/internal/api/routers/endpoint/token.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
)
|
||||
|
||||
func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse.TokenData, error) {
|
||||
ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey)
|
||||
_, res, err := ja.Encode(emodel.Account2Claims(user, er.config.Expiration.Account))
|
||||
token := sresponse.TokenData{
|
||||
Token: res,
|
||||
Expiration: time.Now().Add(time.Duration(er.config.Expiration.Account) * time.Hour),
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
40
api/server/internal/api/routers/metrics/handler.go
Normal file
40
api/server/internal/api/routers/metrics/handler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/metrics"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type metricsRouter struct {
|
||||
logger mlogger.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (mr *metricsRouter) Finish() {
|
||||
mr.logger.Debug("Stopped")
|
||||
}
|
||||
|
||||
func (mr *metricsRouter) handle(w http.ResponseWriter, r *http.Request) {
|
||||
mr.logger.Debug("Serving metrics request...")
|
||||
mr.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func newMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) *metricsRouter {
|
||||
mr := metricsRouter{
|
||||
logger: logger.Named("metrics"),
|
||||
handler: metrics.Handler(),
|
||||
}
|
||||
|
||||
logger.Debug("Installing Prometheus middleware...")
|
||||
router.Group(func(r chi.Router) {
|
||||
ep := endpoint + "/metrics"
|
||||
r.Get(ep, mr.handle)
|
||||
logger.Info("Prometheus handler installed", zap.String("endpoint", ep))
|
||||
})
|
||||
|
||||
return &mr
|
||||
}
|
||||
14
api/server/internal/api/routers/metrics/router.go
Normal file
14
api/server/internal/api/routers/metrics/router.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type Metrics interface {
|
||||
Finish()
|
||||
}
|
||||
|
||||
func NewMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) (Metrics, error) {
|
||||
return newMetricsRouter(logger, router, endpoint), nil
|
||||
}
|
||||
63
api/server/internal/api/routers/public/login.go
Normal file
63
api/server/internal/api/routers/public/login.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc {
|
||||
// Get the account database entry
|
||||
trimmedLogin := strings.TrimSpace(req.Login)
|
||||
account, err := pr.db.GetByEmail(ctx, strings.ToLower(trimmedLogin))
|
||||
if errors.Is(err, merrors.ErrNoData) || (account == nil) {
|
||||
pr.logger.Debug("User not found while logging in", zap.Error(err), zap.String("login", req.Login))
|
||||
return response.Unauthorized(pr.logger, pr.service, "user not found")
|
||||
}
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to query user with email", zap.Error(err), zap.String("login", req.Login))
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
if account.VerifyToken != "" {
|
||||
return response.Forbidden(pr.logger, pr.service, "account_not_verified", "Account verification required")
|
||||
}
|
||||
|
||||
if !account.MatchPassword(req.Password) {
|
||||
return response.Unauthorized(pr.logger, pr.service, "password does not match")
|
||||
}
|
||||
|
||||
accessToken, err := pr.imp.CreateAccessToken(account)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken)
|
||||
}
|
||||
|
||||
func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {
|
||||
// TODO: add rate check
|
||||
var req srequest.Login
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Info("Failed to decode login request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, mservice.Accounts, err)
|
||||
}
|
||||
req.Login = strings.TrimSpace(req.Login)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
if req.Login == "" {
|
||||
return response.BadRequest(a.logger, mservice.Accounts, "email_missing", "login request has no user name")
|
||||
}
|
||||
if req.Password == "" {
|
||||
return response.BadRequest(a.logger, mservice.Accounts, "password_missing", "login request has no password")
|
||||
}
|
||||
return a.logUserIn(r.Context(), r, &req)
|
||||
}
|
||||
29
api/server/internal/api/routers/public/refresh.go
Normal file
29
api/server/internal/api/routers/public/refresh.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (pr *PublicRouter) refreshAccessToken(r *http.Request) http.HandlerFunc {
|
||||
pr.logger.Debug("Processing access token refresh request")
|
||||
var req srequest.AccessTokenRefresh
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
pr.logger.Info("Failed to decode token rotation request", zap.Error(err))
|
||||
return response.BadPayload(pr.logger, mservice.RefreshTokens, err)
|
||||
}
|
||||
|
||||
account, token, err := pr.validateRefreshToken(r.Context(), r, &req)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to process access token refreshment request", zap.Error(err))
|
||||
return response.Auto(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
return sresponse.Account(pr.logger, account, token)
|
||||
}
|
||||
77
api/server/internal/api/routers/public/respond.go
Normal file
77
api/server/internal/api/routers/public/respond.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func generateRefreshTokenData(length int) (string, error) {
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
|
||||
return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(randomBytes), nil
|
||||
}
|
||||
|
||||
func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) {
|
||||
refreshToken, err := generateRefreshTokenData(er.config.Length)
|
||||
if err != nil {
|
||||
er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &model.RefreshToken{
|
||||
AccountBoundBase: model.AccountBoundBase{
|
||||
AccountRef: account.GetID(),
|
||||
},
|
||||
ClientRefreshToken: model.ClientRefreshToken{
|
||||
SessionIdentifier: *session,
|
||||
RefreshToken: refreshToken,
|
||||
},
|
||||
ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour),
|
||||
IsRevoked: false,
|
||||
UserAgent: r.UserAgent(),
|
||||
IPAddress: r.RemoteAddr,
|
||||
}
|
||||
|
||||
if err = er.rtdb.Create(ctx, token); err != nil {
|
||||
er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
|
||||
zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (pr *PublicRouter) refreshAndRespondLogin(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
session *model.SessionIdentifier,
|
||||
account *model.Account,
|
||||
accessToken *sresponse.TokenData,
|
||||
) http.HandlerFunc {
|
||||
refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account),
|
||||
zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
token := sresponse.TokenData{
|
||||
Token: refreshToken.RefreshToken,
|
||||
Expiration: refreshToken.ExpiresAt,
|
||||
}
|
||||
return sresponse.Login(pr.logger, account, accessToken, &token)
|
||||
}
|
||||
28
api/server/internal/api/routers/public/rotate.go
Normal file
28
api/server/internal/api/routers/public/rotate.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (pr *PublicRouter) rotateRefreshToken(r *http.Request) http.HandlerFunc {
|
||||
pr.logger.Debug("Processing token rotation request...")
|
||||
var req srequest.TokenRefreshRotate
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
pr.logger.Info("Failed to decode token rotation request", zap.Error(err))
|
||||
return response.BadPayload(pr.logger, mservice.RefreshTokens, err)
|
||||
}
|
||||
|
||||
account, token, err := pr.validateRefreshToken(r.Context(), r, &req)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to validate refresh token", zap.Error(err))
|
||||
return response.Auto(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
return pr.refreshAndRespondLogin(r.Context(), r, &req.SessionIdentifier, account, token)
|
||||
}
|
||||
46
api/server/internal/api/routers/public/router.go
Normal file
46
api/server/internal/api/routers/public/router.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
|
||||
)
|
||||
|
||||
type PublicRouter struct {
|
||||
logger mlogger.Logger
|
||||
db account.DB
|
||||
imp *re.HttpEndpointRouter
|
||||
rtdb refreshtokens.DB
|
||||
config middleware.TokenConfig
|
||||
signature middleware.Signature
|
||||
service mservice.Type
|
||||
}
|
||||
|
||||
func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
pr.imp.InstallHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
|
||||
l := logger.Named("public")
|
||||
hr := PublicRouter{
|
||||
logger: l,
|
||||
db: db,
|
||||
rtdb: rtdb,
|
||||
config: *config,
|
||||
signature: *signature,
|
||||
imp: re.NewHttpEndpointRouter(l, apiEndpoint, router, config, signature),
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
hr.InstallHandler(hr.service, "/login", api.Post, hr.login)
|
||||
hr.InstallHandler(hr.service, "/rotate", api.Post, hr.rotateRefreshToken)
|
||||
hr.InstallHandler(hr.service, "/refresh", api.Post, hr.refreshAccessToken)
|
||||
|
||||
return &hr
|
||||
}
|
||||
59
api/server/internal/api/routers/public/validate.go
Normal file
59
api/server/internal/api/routers/public/validate.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func validateToken(token string, rt *model.RefreshToken) string {
|
||||
if rt.AccountRef == nil {
|
||||
return "missing account reference"
|
||||
}
|
||||
if token != rt.RefreshToken {
|
||||
return "tokens do not match"
|
||||
}
|
||||
if rt.ExpiresAt.Before(time.Now()) {
|
||||
return "token expired"
|
||||
}
|
||||
if rt.IsRevoked {
|
||||
return "token has been revoked"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (pr *PublicRouter) validateRefreshToken(ctx context.Context, _ *http.Request, req *srequest.TokenRefreshRotate) (*model.Account, *sresponse.TokenData, error) {
|
||||
rt, err := pr.rtdb.GetByCRT(ctx, req)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
pr.logger.Info("Refresh token not found", zap.String("client_id", req.ClientID), zap.String("device_id", req.DeviceID))
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if reason := validateToken(req.RefreshToken, rt); len(reason) > 0 {
|
||||
pr.logger.Info("Token validation failed", zap.String("reason", reason))
|
||||
return nil, nil, merrors.Unauthorized(reason)
|
||||
}
|
||||
|
||||
var account model.Account
|
||||
if err := pr.db.Get(ctx, *rt.AccountRef, &account); errors.Is(err, merrors.ErrNoData) {
|
||||
pr.logger.Info("User not found while rotating refresh token", zap.Error(err), mzap.ObjRef("account_ref", *rt.AccountRef))
|
||||
return nil, nil, merrors.Unauthorized("user not found")
|
||||
}
|
||||
|
||||
accessToken, err := pr.imp.CreateAccessToken(&account)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &account, &accessToken, nil
|
||||
}
|
||||
15
api/server/internal/api/routers/router.go
Normal file
15
api/server/internal/api/routers/router.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
)
|
||||
|
||||
type APIRouter interface {
|
||||
InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
|
||||
}
|
||||
|
||||
type ProtectedAPIRouter interface {
|
||||
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
|
||||
}
|
||||
Reference in New Issue
Block a user