Go · 4168 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/secretbox"
21 "github.com/tenseleyFlow/shithub/internal/auth/session"
22 "github.com/tenseleyFlow/shithub/internal/auth/throttle"
23 "github.com/tenseleyFlow/shithub/internal/infra/config"
24 "github.com/tenseleyFlow/shithub/internal/infra/storage"
25 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
26 apih "github.com/tenseleyFlow/shithub/internal/web/handlers/api"
27 authh "github.com/tenseleyFlow/shithub/internal/web/handlers/auth"
28 "github.com/tenseleyFlow/shithub/internal/web/render"
29 )
30
31 // sharedPATDebouncer is used by both the PATAuth middleware (in the API
32 // route group) and any future paths that want to coordinate last-used
33 // updates across handlers in the same process.
34 var sharedPATDebouncer = pat.NewDebouncer(0)
35
36 // buildAPIHandlers wires the PAT-authenticated API surface.
37 func buildAPIHandlers(pool *pgxpool.Pool) (*apih.Handlers, error) {
38 return apih.New(apih.Deps{
39 Pool: pool,
40 Debouncer: sharedPATDebouncer,
41 })
42 }
43
44 // usernameLookup returns the lookup function consumed by middleware.OptionalUser.
45 // It resolves both the username and the user's current session_epoch so the
46 // auth middleware can refuse stale cookies (bumped by "log out everywhere").
47 func usernameLookup(pool *pgxpool.Pool) func(context.Context, int64) (string, int32, error) {
48 q := usersdb.New()
49 return func(ctx context.Context, id int64) (string, int32, error) {
50 u, err := q.GetUserByID(ctx, pool, id)
51 if err != nil {
52 return "", 0, err
53 }
54 return u.Username, u.SessionEpoch, nil
55 }
56 }
57
58 // buildAuthHandlers wires the auth surface from the loaded config and
59 // the bootstrapped DB / session / logger / templates. Selecting the email
60 // backend is config-driven (`auth.email_backend`).
61 func buildAuthHandlers(
62 cfg config.Config,
63 pool *pgxpool.Pool,
64 store session.Store,
65 objectStore storage.ObjectStore,
66 logger *slog.Logger,
67 tmplFS fs.FS,
68 ) (*authh.Handlers, error) {
69 rr, err := render.New(tmplFS, render.Options{Octicons: render.BuiltinOcticons()})
70 if err != nil {
71 return nil, err
72 }
73 sender, err := pickEmailSender(cfg)
74 if err != nil {
75 return nil, err
76 }
77 var box *secretbox.Box
78 if cfg.Auth.TOTPKeyB64 != "" {
79 b, err := secretbox.FromBase64(cfg.Auth.TOTPKeyB64)
80 if err != nil {
81 return nil, err
82 }
83 box = b
84 } else {
85 logger.Warn("auth: no totp_key_b64 configured; 2FA enrollment routes disabled",
86 "hint", "set SHITHUB_TOTP_KEY=$(openssl rand -base64 32) to enable 2FA")
87 }
88 return authh.New(authh.Deps{
89 Logger: logger,
90 Render: rr,
91 Pool: pool,
92 SessionStore: store,
93 Email: sender,
94 Branding: email.Branding{
95 SiteName: cfg.Auth.SiteName,
96 BaseURL: cfg.Auth.BaseURL,
97 From: cfg.Auth.EmailFrom,
98 },
99 Argon2: password.Params{
100 Memory: cfg.Auth.Argon2.MemoryKiB,
101 Time: cfg.Auth.Argon2.Time,
102 Threads: cfg.Auth.Argon2.Threads,
103 SaltLen: 16,
104 KeyLen: 32,
105 },
106 Limiter: throttle.NewLimiter(),
107 RequireEmailVerification: cfg.Auth.RequireEmailVerification,
108 SecretBox: box,
109 Audit: audit.NewRecorder(),
110 ObjectStore: objectStore,
111 })
112 }
113
114 func pickEmailSender(cfg config.Config) (email.Sender, error) {
115 switch cfg.Auth.EmailBackend {
116 case "stdout":
117 return email.NewStdoutSender(os.Stdout), nil
118 case "smtp":
119 return &email.SMTPSender{
120 Addr: cfg.Auth.SMTP.Addr,
121 From: cfg.Auth.EmailFrom,
122 Username: cfg.Auth.SMTP.Username,
123 Password: cfg.Auth.SMTP.Password,
124 }, nil
125 case "postmark":
126 return &email.PostmarkSender{
127 ServerToken: cfg.Auth.Postmark.ServerToken,
128 From: cfg.Auth.EmailFrom,
129 HTTP: &http.Client{Timeout: 10 * time.Second},
130 }, nil
131 default:
132 return nil, errors.New("auth: unknown email_backend")
133 }
134 }
135