Go · 4967 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package web
4
5 import (
6 "context"
7 "errors"
8 "io/fs"
9 "log/slog"
10 "net/http"
11 "os"
12 "time"
13
14 "github.com/jackc/pgx/v5/pgxpool"
15
16 "github.com/tenseleyFlow/shithub/internal/auth/audit"
17 "github.com/tenseleyFlow/shithub/internal/auth/email"
18 "github.com/tenseleyFlow/shithub/internal/auth/password"
19 "github.com/tenseleyFlow/shithub/internal/auth/pat"
20 "github.com/tenseleyFlow/shithub/internal/auth/runnerjwt"
21 "github.com/tenseleyFlow/shithub/internal/auth/secretbox"
22 "github.com/tenseleyFlow/shithub/internal/auth/session"
23 "github.com/tenseleyFlow/shithub/internal/auth/throttle"
24 "github.com/tenseleyFlow/shithub/internal/infra/config"
25 "github.com/tenseleyFlow/shithub/internal/infra/storage"
26 "github.com/tenseleyFlow/shithub/internal/ratelimit"
27 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
28 apih "github.com/tenseleyFlow/shithub/internal/web/handlers/api"
29 authh "github.com/tenseleyFlow/shithub/internal/web/handlers/auth"
30 "github.com/tenseleyFlow/shithub/internal/web/middleware"
31 "github.com/tenseleyFlow/shithub/internal/web/render"
32 )
33
34 // sharedPATDebouncer is used by both the PATAuth middleware (in the API
35 // route group) and any future paths that want to coordinate last-used
36 // updates across handlers in the same process.
37 var sharedPATDebouncer = pat.NewDebouncer(0)
38
39 // buildAPIHandlers wires the PAT-authenticated API surface.
40 func buildAPIHandlers(
41 pool *pgxpool.Pool,
42 objectStore storage.ObjectStore,
43 runnerJWT *runnerjwt.Signer,
44 secretBox *secretbox.Box,
45 rateLimiter *ratelimit.Limiter,
46 logger *slog.Logger,
47 ) (*apih.Handlers, error) {
48 return apih.New(apih.Deps{
49 Pool: pool,
50 Debouncer: sharedPATDebouncer,
51 Logger: logger,
52 ObjectStore: objectStore,
53 RunnerJWT: runnerJWT,
54 SecretBox: secretBox,
55 RateLimiter: rateLimiter,
56 })
57 }
58
59 // usernameLookup returns the lookup function consumed by middleware.OptionalUser.
60 // It resolves the username, the user's current session_epoch (so the auth
61 // middleware can refuse stale cookies bumped by "log out everywhere"), and
62 // the suspended state (so policy.UserActor can deny writes by suspended
63 // accounts at the web layer — the audit found this gap, S00-S25 audit C1).
64 func usernameLookup(pool *pgxpool.Pool) middleware.UserLookup {
65 q := usersdb.New()
66 return func(ctx context.Context, id int64) (middleware.UserLookupResult, error) {
67 u, err := q.GetUserByID(ctx, pool, id)
68 if err != nil {
69 return middleware.UserLookupResult{}, err
70 }
71 return middleware.UserLookupResult{
72 Username: u.Username,
73 SessionEpoch: u.SessionEpoch,
74 IsSuspended: u.SuspendedAt.Valid,
75 IsSiteAdmin: u.IsSiteAdmin,
76 }, nil
77 }
78 }
79
80 // buildAuthHandlers wires the auth surface from the loaded config and
81 // the bootstrapped DB / session / logger / templates. Selecting the email
82 // backend is config-driven (`auth.email_backend`).
83 func buildAuthHandlers(
84 cfg config.Config,
85 pool *pgxpool.Pool,
86 store session.Store,
87 objectStore storage.ObjectStore,
88 logger *slog.Logger,
89 tmplFS fs.FS,
90 ) (*authh.Handlers, error) {
91 rr, err := render.New(tmplFS, render.Options{Octicons: render.BuiltinOcticons()})
92 if err != nil {
93 return nil, err
94 }
95 sender, err := pickEmailSender(cfg)
96 if err != nil {
97 return nil, err
98 }
99 var box *secretbox.Box
100 if cfg.Auth.TOTPKeyB64 != "" {
101 b, err := secretbox.FromBase64(cfg.Auth.TOTPKeyB64)
102 if err != nil {
103 return nil, err
104 }
105 box = b
106 } else {
107 logger.Warn("auth: no totp_key_b64 configured; 2FA enrollment routes disabled",
108 "hint", "set SHITHUB_TOTP_KEY=$(openssl rand -base64 32) to enable 2FA")
109 }
110 return authh.New(authh.Deps{
111 Logger: logger,
112 Render: rr,
113 Pool: pool,
114 SessionStore: store,
115 Email: sender,
116 Branding: email.Branding{
117 SiteName: cfg.Auth.SiteName,
118 BaseURL: cfg.Auth.BaseURL,
119 From: cfg.Auth.EmailFrom,
120 },
121 Argon2: password.Params{
122 Memory: cfg.Auth.Argon2.MemoryKiB,
123 Time: cfg.Auth.Argon2.Time,
124 Threads: cfg.Auth.Argon2.Threads,
125 SaltLen: 16,
126 KeyLen: 32,
127 },
128 Limiter: throttle.NewLimiter(),
129 RateLimiter: ratelimit.New(pool),
130 RequireEmailVerification: cfg.Auth.RequireEmailVerification,
131 SecretBox: box,
132 Audit: audit.NewRecorder(),
133 ObjectStore: objectStore,
134 })
135 }
136
137 func pickEmailSender(cfg config.Config) (email.Sender, error) {
138 switch cfg.Auth.EmailBackend {
139 case "stdout":
140 return email.NewStdoutSender(os.Stdout), nil
141 case "smtp":
142 return &email.SMTPSender{
143 Addr: cfg.Auth.SMTP.Addr,
144 From: cfg.Auth.EmailFrom,
145 Username: cfg.Auth.SMTP.Username,
146 Password: cfg.Auth.SMTP.Password,
147 }, nil
148 case "postmark":
149 return &email.PostmarkSender{
150 ServerToken: cfg.Auth.Postmark.ServerToken,
151 From: cfg.Auth.EmailFrom,
152 HTTP: &http.Client{Timeout: 10 * time.Second},
153 }, nil
154 default:
155 return nil, errors.New("auth: unknown email_backend")
156 }
157 }
158