Files

127 lines
4.3 KiB
Go

package onetimeaccess
import (
"context"
"fmt"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/italypaleale/francis/host/local"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
)
// EmailData is the data rendered in the one-time access email
type EmailData struct {
Code string
LoginLink string
LoginLinkWithCode string
ExpirationString string
}
// EmailSender sends the one-time access email
type EmailSender interface {
SendOneTimeAccessEmail(ctx context.Context, dbConfig *appconfig.AppConfigModel, to email.Address, data EmailData) error
}
type TokenService interface {
GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error)
}
type AuditLogger interface {
Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
}
type UserProvider interface {
GetUser(ctx context.Context, userID string) (model.User, error)
}
// AppConfigResolver loads the current application configuration, so handlers can pass it explicitly to the service methods that need it
type AppConfigResolver interface {
GetConfig(ctx context.Context) (*appconfig.AppConfigModel, error)
}
type Dependencies struct {
DB *gorm.DB
Actors *local.Host
Signer TokenService
AuditLog AuditLogger
UserProvider UserProvider
EmailSender EmailSender
AppConfig AppConfigResolver
}
type Module struct {
service *Service
handler *handler
}
func New(deps Dependencies) (*Module, error) {
// Register the actor that manages a one-time access token
// Each token is its own actor, whose actor ID is the token's value
err := deps.Actors.RegisterActor(TokenActorType, NewTokenActor)
if err != nil {
return nil, fmt.Errorf("error registering the %s actor: %w", TokenActorType, err)
}
service := newService(deps, deps.Actors.Service())
return &Module{
service: service,
handler: newHandler(service, deps.AppConfig),
}, nil
}
// RegisterRoutes mounts the one-time access token endpoints
// auth guards the admin routes and ownAuth the current user's own token, while the rate limiters throttle the public exchange and email endpoints
func (m *Module) RegisterRoutes(api huma.API, auth, ownAuth func(*huma.Operation), exchangeRateLimit, emailRateLimit func(huma.Context, func(huma.Context))) {
httpapi.Register(api, huma.Operation{
OperationID: "create-own-one-time-access-token",
Method: http.MethodPost,
Path: "/api/users/me/one-time-access-token",
Summary: "Create one-time access token for current user",
Tags: []string{"Users"},
DefaultStatus: http.StatusCreated,
}, m.handler.createOwnToken, ownAuth)
httpapi.Register(api, huma.Operation{
OperationID: "create-user-one-time-access-token",
Method: http.MethodPost,
Path: "/api/users/{id}/one-time-access-token",
Summary: "Create one-time access token for user",
Tags: []string{"Users"},
DefaultStatus: http.StatusCreated,
}, m.handler.createTokenForUser, auth)
httpapi.Register(api, huma.Operation{
OperationID: "request-user-one-time-access-email",
Method: http.MethodPost,
Path: "/api/users/{id}/one-time-access-email",
Summary: "Request one-time access email for user",
Tags: []string{"Users"},
DefaultStatus: http.StatusNoContent,
}, m.handler.requestEmailAsAdmin, auth)
httpapi.Register(api, huma.Operation{
OperationID: "exchange-one-time-access-token",
Method: http.MethodPost,
Path: "/api/one-time-access-token/{token}",
Summary: "Exchange one-time access token",
Tags: []string{"Users"},
}, m.handler.exchangeToken, httpapi.WithMiddleware(exchangeRateLimit))
httpapi.Register(api, huma.Operation{
OperationID: "request-one-time-access-email",
Method: http.MethodPost,
Path: "/api/one-time-access-email",
Summary: "Request one-time access email",
Tags: []string{"Users"},
DefaultStatus: http.StatusNoContent,
}, m.handler.requestEmailAsUnauthenticatedUser, httpapi.WithMiddleware(emailRateLimit))
}