package devicelogin import ( "context" "fmt" "time" "github.com/gin-gonic/gin" francishost "github.com/italypaleale/francis/host" "gorm.io/gorm" "github.com/pocket-id/pocket-id/backend/internal/appconfig" "github.com/pocket-id/pocket-id/backend/internal/httpserver" "github.com/pocket-id/pocket-id/backend/internal/model" ) type TokenService interface { GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error) } type ReauthenticationTokenConsumer interface { ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) } type AuditLogger interface { Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool) DeviceStringFromUserAgent(userAgent string) string } type IPLocationResolver interface { GetLocationByIP(ctx context.Context, ipAddress string) (country string, city string, err error) } type Dependencies struct { DB *gorm.DB Actors francishost.Host BaseURL string Signer TokenService Reauth ReauthenticationTokenConsumer AuditLog AuditLogger IPLocator IPLocationResolver AppConfig appconfig.AppConfigResolver } type Module struct { service *Service handler *handler } func New(deps Dependencies) (*Module, error) { service := NewService(deps.Actors.Service(), deps.DB, deps.Signer, deps.Reauth, deps.AuditLog, deps.IPLocator) module := &Module{ service: service, handler: newHandler(service, deps.BaseURL, deps.AppConfig), } // Register the durable request actor before the host starts err := deps.Actors.RegisterActor( requestActorType, newRequestActor, ) if err != nil { return nil, fmt.Errorf("failed to register device login actor: %w", err) } return module, nil } // RegisterRoutes mounts the public exchange and authenticated verification endpoints func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, browserAuth, createRateLimit, exchangeRateLimit, verificationRateLimit gin.HandlerFunc) { apiGroup.POST("/device-login/requests", createRateLimit, httpserver.Handle(m.handler.createRequest)) apiGroup.POST("/device-login/requests/:id/exchange", exchangeRateLimit, httpserver.Handle(m.handler.exchangeRequest)) apiGroup.POST("/device-login/verification", verificationRateLimit, browserAuth, httpserver.Handle(m.handler.inspectRequest)) apiGroup.POST("/device-login/verification/decision", verificationRateLimit, browserAuth, httpserver.Handle(m.handler.decideRequest)) }