Files
pocket-id/backend/internal/devicelogin/handler.go
T

135 lines
4.1 KiB
Go

package devicelogin
import (
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
)
type handler struct {
service *Service
baseURL string
appConfig AppConfigProvider
}
func newHandler(service *Service, baseURL string, appConfig AppConfigProvider) *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) {
request, deviceToken, err := h.service.Create(c.Request.Context(), c.ClientIP(), c.Request.UserAgent())
if err != nil {
_ = c.Error(err)
return
}
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,
})
}
// exchangeRequest godoc
// @Summary Exchange device login request
// @Description Poll a device login request 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) {
requestID := c.Param("id")
deviceToken, _ := c.Cookie(cookie.DeviceLoginTokenCookieName)
user, accessToken, status, err := h.service.Exchange(c.Request.Context(), requestID, deviceToken, c.ClientIP(), c.Request.UserAgent())
if err != nil {
_ = c.Error(err)
return
}
if status == RequestStatusPending {
c.Status(http.StatusAccepted)
return
}
var userDto dto.UserDto
if err = dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
}
maxAge := int(h.appConfig.GetDbConfig().SessionDuration.AsDurationMinutes().Seconds())
cookie.AddAccessTokenCookie(c, maxAge, accessToken)
c.JSON(http.StatusOK, userDto)
}
// 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) {
var input verificationDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
}
info, err := h.service.Inspect(c.Request.Context(), input.Code)
if err != nil {
_ = c.Error(err)
return
}
c.JSON(http.StatusOK, verificationInfoDto(info))
}
// 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) {
var input decisionDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
}
reauthenticationToken, _ := c.Cookie(cookie.ReauthenticationTokenCookieName)
if err := h.service.Decide(c.Request.Context(), input.Code, input.Decision, c.GetString("userID"), reauthenticationToken); err != nil {
_ = c.Error(err)
return
}
c.Status(http.StatusNoContent)
}