package handlers import ( "strings" "time" "github.com/gofiber/fiber/v2" "polar/internal/auth" "polar/internal/web/middleware" ) func (h *Handler) siteURL(c *fiber.Ctx) string { base := strings.TrimRight(h.cfg.Server.BaseURL, "/") if base == "" || strings.Contains(base, "localhost:3000") { if reqBase := c.BaseURL(); reqBase != "" { base = strings.TrimRight(reqBase, "/") } } return base } // DeviceCodeAPI starts a device authorization grant (RFC 8628). // POST /api/v1/device/code func (h *Handler) DeviceCodeAPI(c *fiber.Ctx) error { clientID := strings.TrimSpace(c.FormValue("client_id")) if clientID == "" { var payload struct { ClientID string `json:"client_id"` } if err := c.BodyParser(&payload); err == nil { clientID = strings.TrimSpace(payload.ClientID) } } if clientID == "" { clientID = "polar-git" } dc, err := h.authSvc.CreateDeviceCode(c.UserContext(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "server_error"}) } base := h.siteURL(c) return c.JSON(fiber.Map{ "device_code": dc.Code, "user_code": dc.UserCode, "verification_uri": base + "/login/device", "verification_uri_complete": base + "/login/device?user_code=" + dc.UserCode, "expires_in": int(auth.DeviceCodeTTL.Seconds()), "interval": auth.DeviceCodeInterval, }) } // DeviceTokenAPI exchanges an approved device code for an access token. // POST /api/v1/device/token func (h *Handler) DeviceTokenAPI(c *fiber.Ctx) error { deviceCode := strings.TrimSpace(c.FormValue("device_code")) if deviceCode == "" { var payload struct { DeviceCode string `json:"device_code"` } if err := c.BodyParser(&payload); err == nil { deviceCode = strings.TrimSpace(payload.DeviceCode) } } dc, err := h.authSvc.PollDeviceCode(c.UserContext(), deviceCode) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid_grant"}) } switch dc.Status { case auth.DeviceStatusPending: return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "authorization_pending"}) case auth.DeviceStatusDenied: return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "access_denied"}) case auth.DeviceStatusApproved: if dc.UserID == nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid_grant"}) } name := "Device flow" if dc.ClientID != "" { name = "Device flow (" + dc.ClientID + ")" } raw, _, err := h.authSvc.CreateToken(c.UserContext(), *dc.UserID, name) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "server_error"}) } h.authSvc.ConsumeDeviceCode(c.UserContext(), dc.Code) username := "" if u, err := h.userSvc.GetByID(c.UserContext(), *dc.UserID); err == nil && u != nil { username = u.Username } return c.JSON(fiber.Map{ "access_token": raw, "token_type": "bearer", "username": username, "scope": "git", }) default: return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expired_token"}) } } // DeviceVerifyPage renders the device approval page. // GET /login/device func (h *Handler) DeviceVerifyPage(c *fiber.Ctx) error { viewer := middleware.CurrentUser(c) userCode := auth.NormalizeUserCode(c.Query("user_code")) data := h.baseData(c, h.tr(c, "device.title")) data["UserCode"] = userCode data["SignedIn"] = viewer != nil if userCode != "" && len(userCode) == 9 { dc, err := h.authSvc.GetDeviceCodeByUserCode(c.UserContext(), userCode) switch { case err != nil: data["CodeError"] = h.tr(c, "device.code_unknown") case dc.Status == auth.DeviceStatusApproved: data["CodeApproved"] = true case dc.Status == auth.DeviceStatusDenied: data["CodeDenied"] = true case dc.Status != auth.DeviceStatusPending || time.Now().UTC().After(dc.ExpiresAt): data["CodeError"] = h.tr(c, "device.code_used") default: data["CodePending"] = true if dc.ClientID != "" { data["ClientName"] = dc.ClientID } } } return h.render(c, fiber.StatusOK, "device", data) } // DeviceVerifySubmit approves or denies a pending grant. // POST /login/device func (h *Handler) DeviceVerifySubmit(c *fiber.Ctx) error { viewer := middleware.CurrentUser(c) if viewer == nil { return c.Redirect("/login?next=/login/device", fiber.StatusSeeOther) } userCode := auth.NormalizeUserCode(c.FormValue("user_code")) action := strings.TrimSpace(c.FormValue("action")) redirect := "/login/device?user_code=" + userCode if userCode == "" { h.setFlashT(c, "flash.device_code_required") return c.Redirect("/login/device", fiber.StatusSeeOther) } status := auth.DeviceStatusApproved if action == "deny" { status = auth.DeviceStatusDenied } if err := h.authSvc.SetDeviceCodeStatus(c.UserContext(), userCode, viewer.ID, status); err != nil { h.setFlashT(c, "flash.device_code_invalid") return c.Redirect("/login/device", fiber.StatusSeeOther) } if status == auth.DeviceStatusApproved { h.setFlashT(c, "flash.device_approved") } else { h.setFlashT(c, "flash.device_denied") } return c.Redirect(redirect, fiber.StatusSeeOther) }