Go · 3348 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package api owns shithub's PAT-authenticated HTTP API surface. S08
4 // ships exactly one route: GET /api/v1/user. Later sprints (S22 PRs,
5 // S30 orgs, …) extend the surface here.
6 //
7 // Routes registered here run inside the CSRF-EXEMPT group of the chi
8 // router. Auth is provided by the PATAuth middleware: no PAT → 401;
9 // PAT lacking the required scope → 403; valid scoped PAT → 200.
10 package api
11
12 import (
13 "context"
14 "encoding/json"
15 "errors"
16 "net/http"
17
18 "github.com/go-chi/chi/v5"
19 "github.com/jackc/pgx/v5/pgxpool"
20
21 "github.com/tenseleyFlow/shithub/internal/auth/pat"
22 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
23 "github.com/tenseleyFlow/shithub/internal/web/middleware"
24 )
25
26 // Deps is the wiring the API handlers need. Constructed by the web
27 // package and injected at registration time.
28 type Deps struct {
29 Pool *pgxpool.Pool
30 Debouncer *pat.Debouncer
31 }
32
33 // Handlers is the registered API handler set. Construct with New.
34 type Handlers struct {
35 d Deps
36 q *usersdb.Queries
37 }
38
39 // New constructs the handler set, validating Deps.
40 func New(d Deps) (*Handlers, error) {
41 if d.Pool == nil {
42 return nil, errors.New("api: nil Pool")
43 }
44 if d.Debouncer == nil {
45 d.Debouncer = pat.NewDebouncer(0)
46 }
47 return &Handlers{d: d, q: usersdb.New()}, nil
48 }
49
50 // Mount registers /api/v1/* on r. Caller is responsible for putting r
51 // in a CSRF-exempt group.
52 func (h *Handlers) Mount(r chi.Router) {
53 r.Group(func(r chi.Router) {
54 r.Use(middleware.PATAuthMiddleware(middleware.PATConfig{
55 Pool: h.d.Pool,
56 Debouncer: h.d.Debouncer,
57 }))
58 r.Group(func(r chi.Router) {
59 r.Use(middleware.RequireScope(pat.ScopeUserRead))
60 r.Get("/api/v1/user", h.userMe)
61 })
62 // S24 check-runs / check-suites — RequireScope is per-route
63 // inside the helper since reads need repo:read but writes need
64 // repo:write.
65 h.mountChecks(r)
66 })
67 }
68
69 // userResponse is the public shape of a user record. Mirrors GitHub's
70 // /user response in spirit; we'll grow it organically as features land.
71 type userResponse struct {
72 ID int64 `json:"id"`
73 Username string `json:"username"`
74 Name string `json:"name,omitempty"`
75 Verified bool `json:"email_verified"`
76 CreatedAt string `json:"created_at"`
77 }
78
79 func (h *Handlers) userMe(w http.ResponseWriter, r *http.Request) {
80 auth := middleware.PATAuthFromContext(r.Context())
81 if auth.UserID == 0 {
82 writeAPIError(w, http.StatusUnauthorized, "unauthenticated")
83 return
84 }
85 user, err := h.q.GetUserByID(r.Context(), h.d.Pool, auth.UserID)
86 if err != nil {
87 writeAPIError(w, http.StatusNotFound, "user not found")
88 return
89 }
90 writeJSON(w, http.StatusOK, userResponse{
91 ID: user.ID,
92 Username: user.Username,
93 Name: user.DisplayName,
94 Verified: user.EmailVerified,
95 CreatedAt: user.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
96 })
97 }
98
99 func writeJSON(w http.ResponseWriter, status int, body any) {
100 w.Header().Set("Content-Type", "application/json; charset=utf-8")
101 w.Header().Set("Cache-Control", "no-store")
102 w.WriteHeader(status)
103 _ = json.NewEncoder(w).Encode(body)
104 }
105
106 func writeAPIError(w http.ResponseWriter, status int, msg string) {
107 writeJSON(w, status, map[string]string{"error": msg})
108 }
109
110 // silence unused import on the rare branch where context is not used.
111 var _ = context.Background
112