package devicelogin import ( "fmt" "net/http" "net/url" "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/appconfig" "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/httpserver" "github.com/pocket-id/pocket-id/backend/internal/utils/cookie" ) type handler struct { service *Service baseURL string appConfig appconfig.AppConfigResolver } func newHandler(service *Service, baseURL string, appConfig appconfig.AppConfigResolver) *handler { return &handler{ service: service, baseURL: baseURL, appConfig: appConfig, } } // createRequest godoc // @Summary Create device login request // @Description Create a short-lived request that can be approved from another authenticated device // @Tags Device Login // @Produce json // @Success 201 {object} requestCreateDto "Created device login request" // @Router /api/device-login/requests [post] func (h *handler) createRequest(c *gin.Context) error { request, deviceToken, err := h.service.Create(c.Request.Context(), c.ClientIP(), c.Request.UserAgent()) if err != nil { return err } verificationURI := h.baseURL + "/device" verificationURIComplete := verificationURI + "?user_code=" + url.QueryEscape(request.Code) cookie.AddDeviceLoginTokenCookie(c, request.ID, deviceToken) c.JSON(http.StatusCreated, requestCreateDto{ ID: request.ID, UserCode: request.Code, VerificationURI: verificationURI, VerificationURIComplete: verificationURIComplete, ExpiresAt: request.ExpiresAt, Interval: PollingInterval, }) return nil } // exchangeRequest godoc // @Summary Exchange device login request // @Description Wait for a device login decision and create a browser session after it has been approved // @Tags Device Login // @Produce json // @Param id path string true "Device login request ID" // @Success 200 {object} dto.UserDto "Approved request exchanged for a user session" // @Success 202 "Authorization pending" // @Router /api/device-login/requests/{id}/exchange [post] func (h *handler) exchangeRequest(c *gin.Context) error { dbConfig, err := h.appConfig.GetConfig(c.Request.Context()) if err != nil { return fmt.Errorf("error loading app configuration: %w", err) } requestID := c.Param("id") deviceToken, _ := c.Cookie(cookie.DeviceLoginTokenCookieName) sessionDuration := dbConfig.SessionDuration.AsDurationMinutes() user, accessToken, status, err := h.service.Exchange(c.Request.Context(), requestID, deviceToken, c.ClientIP(), c.Request.UserAgent(), sessionDuration) if err != nil { if c.Request.Context().Err() != nil { // Context canceled = the client stopped the request // Nothing to do here return c.Request.Context().Err() } return err } if status == RequestStatusPending { // Request is pending, so respond with a 202 c.Status(http.StatusAccepted) return nil } maxAge := int(sessionDuration.Seconds()) cookie.AddAccessTokenCookie(c, maxAge, accessToken) c.JSON(http.StatusOK, dto.UserDto(user)) return nil } // inspectRequest godoc // @Summary Inspect device login request // @Description Retrieve the requesting device details for an authenticated user before approval or denial // @Tags Device Login // @Accept json // @Produce json // @Param request body verificationDto true "Device login code" // @Success 200 {object} verificationInfoDto "Device login request details" // @Router /api/device-login/verification [post] func (h *handler) inspectRequest(c *gin.Context) error { var input verificationDto err := httpserver.BindJSON(c, &input) if err != nil { return err } info, err := h.service.Inspect(c.Request.Context(), input.Code) if err != nil { return err } c.JSON(http.StatusOK, verificationInfoDto(info)) return nil } // decideRequest godoc // @Summary Decide device login request // @Description Approve or deny a device login request; approval requires fresh passkey reauthentication // @Tags Device Login // @Accept json // @Param decision body decisionDto true "Device login decision" // @Success 204 "No Content" // @Router /api/device-login/verification/decision [post] func (h *handler) decideRequest(c *gin.Context) error { var input decisionDto err := httpserver.BindJSON(c, &input) if err != nil { return err } reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName) err = h.service.Decide(c.Request.Context(), input.Code, input.Decision, c.GetString("userID"), reauthenticationToken) if err != nil { return err } c.Status(http.StatusNoContent) return nil }