rin
Публичный Open-source lightweight git hosting.
device.go · 171 строка · 5.1 KB
Raw
1 package handlers
2
3 import (
4 "strings"
5 "time"
6
7 "github.com/gofiber/fiber/v2"
8
9 "polar/internal/auth"
10 "polar/internal/web/middleware"
11 )
12
13 func (h *Handler) siteURL(c *fiber.Ctx) string {
14 base := strings.TrimRight(h.cfg.Server.BaseURL, "/")
15 if base == "" || strings.Contains(base, "localhost:3000") {
16 if reqBase := c.BaseURL(); reqBase != "" {
17 base = strings.TrimRight(reqBase, "/")
18 }
19 }
20 return base
21 }
22
23 // DeviceCodeAPI starts a device authorization grant (RFC 8628).
24 // POST /api/v1/device/code
25 func (h *Handler) DeviceCodeAPI(c *fiber.Ctx) error {
26 clientID := strings.TrimSpace(c.FormValue("client_id"))
27 if clientID == "" {
28 var payload struct {
29 ClientID string `json:"client_id"`
30 }
31 if err := c.BodyParser(&payload); err == nil {
32 clientID = strings.TrimSpace(payload.ClientID)
33 }
34 }
35 if clientID == "" {
36 clientID = "polar-git"
37 }
38
39 dc, err := h.authSvc.CreateDeviceCode(c.UserContext(), clientID)
40 if err != nil {
41 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "server_error"})
42 }
43
44 base := h.siteURL(c)
45 return c.JSON(fiber.Map{
46 "device_code": dc.Code,
47 "user_code": dc.UserCode,
48 "verification_uri": base + "/login/device",
49 "verification_uri_complete": base + "/login/device?user_code=" + dc.UserCode,
50 "expires_in": int(auth.DeviceCodeTTL.Seconds()),
51 "interval": auth.DeviceCodeInterval,
52 })
53 }
54
55 // DeviceTokenAPI exchanges an approved device code for an access token.
56 // POST /api/v1/device/token
57 func (h *Handler) DeviceTokenAPI(c *fiber.Ctx) error {
58 deviceCode := strings.TrimSpace(c.FormValue("device_code"))
59 if deviceCode == "" {
60 var payload struct {
61 DeviceCode string `json:"device_code"`
62 }
63 if err := c.BodyParser(&payload); err == nil {
64 deviceCode = strings.TrimSpace(payload.DeviceCode)
65 }
66 }
67
68 dc, err := h.authSvc.PollDeviceCode(c.UserContext(), deviceCode)
69 if err != nil {
70 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid_grant"})
71 }
72
73 switch dc.Status {
74 case auth.DeviceStatusPending:
75 return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "authorization_pending"})
76 case auth.DeviceStatusDenied:
77 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "access_denied"})
78 case auth.DeviceStatusApproved:
79 if dc.UserID == nil {
80 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid_grant"})
81 }
82 name := "Device flow"
83 if dc.ClientID != "" {
84 name = "Device flow (" + dc.ClientID + ")"
85 }
86 raw, _, err := h.authSvc.CreateToken(c.UserContext(), *dc.UserID, name)
87 if err != nil {
88 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "server_error"})
89 }
90 h.authSvc.ConsumeDeviceCode(c.UserContext(), dc.Code)
91 username := ""
92 if u, err := h.userSvc.GetByID(c.UserContext(), *dc.UserID); err == nil && u != nil {
93 username = u.Username
94 }
95 return c.JSON(fiber.Map{
96 "access_token": raw,
97 "token_type": "bearer",
98 "username": username,
99 "scope": "git",
100 })
101 default:
102 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expired_token"})
103 }
104 }
105
106 // DeviceVerifyPage renders the device approval page.
107 // GET /login/device
108 func (h *Handler) DeviceVerifyPage(c *fiber.Ctx) error {
109 viewer := middleware.CurrentUser(c)
110 userCode := auth.NormalizeUserCode(c.Query("user_code"))
111
112 data := h.baseData(c, h.tr(c, "device.title"))
113 data["UserCode"] = userCode
114 data["SignedIn"] = viewer != nil
115
116 if userCode != "" && len(userCode) == 9 {
117 dc, err := h.authSvc.GetDeviceCodeByUserCode(c.UserContext(), userCode)
118 switch {
119 case err != nil:
120 data["CodeError"] = h.tr(c, "device.code_unknown")
121 case dc.Status == auth.DeviceStatusApproved:
122 data["CodeApproved"] = true
123 case dc.Status == auth.DeviceStatusDenied:
124 data["CodeDenied"] = true
125 case dc.Status != auth.DeviceStatusPending || time.Now().UTC().After(dc.ExpiresAt):
126 data["CodeError"] = h.tr(c, "device.code_used")
127 default:
128 data["CodePending"] = true
129 if dc.ClientID != "" {
130 data["ClientName"] = dc.ClientID
131 }
132 }
133 }
134
135 return h.render(c, fiber.StatusOK, "device", data)
136 }
137
138 // DeviceVerifySubmit approves or denies a pending grant.
139 // POST /login/device
140 func (h *Handler) DeviceVerifySubmit(c *fiber.Ctx) error {
141 viewer := middleware.CurrentUser(c)
142 if viewer == nil {
143 return c.Redirect("/login?next=/login/device", fiber.StatusSeeOther)
144 }
145
146 userCode := auth.NormalizeUserCode(c.FormValue("user_code"))
147 action := strings.TrimSpace(c.FormValue("action"))
148 redirect := "/login/device?user_code=" + userCode
149
150 if userCode == "" {
151 h.setFlashT(c, "flash.device_code_required")
152 return c.Redirect("/login/device", fiber.StatusSeeOther)
153 }
154
155 status := auth.DeviceStatusApproved
156 if action == "deny" {
157 status = auth.DeviceStatusDenied
158 }
159
160 if err := h.authSvc.SetDeviceCodeStatus(c.UserContext(), userCode, viewer.ID, status); err != nil {
161 h.setFlashT(c, "flash.device_code_invalid")
162 return c.Redirect("/login/device", fiber.StatusSeeOther)
163 }
164
165 if status == auth.DeviceStatusApproved {
166 h.setFlashT(c, "flash.device_approved")
167 } else {
168 h.setFlashT(c, "flash.device_denied")
169 }
170 return c.Redirect(redirect, fiber.StatusSeeOther)
171 }
172