From ec30ed1f0b2120a70331351a1f1afeac57285e71 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Mon, 18 Aug 2025 04:18:50 +0800 Subject: [PATCH] Make logging in work --- forged/internal/incoming/web/authn.go | 33 +++++++++++++++++++++++++++++++++ forged/internal/incoming/web/handler.go | 6 ++++-- forged/internal/incoming/web/handlers/special/login.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++ forged/internal/incoming/web/router.go | 4 +++- forged/sql/queries/login.sql | 8 ++++++++ forged/templates/login.tmpl | 4 ++-- diff --git a/forged/internal/incoming/web/authn.go b/forged/internal/incoming/web/authn.go new file mode 100644 index 0000000000000000000000000000000000000000..46263eeef853db870d93d4eed697c1883677ae76 --- /dev/null +++ b/forged/internal/incoming/web/authn.go @@ -0,0 +1,33 @@ +package web + +import ( + "crypto/sha256" + "errors" + "fmt" + "net/http" + + "github.com/jackc/pgx/v5" + "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" +) + +func userResolver(r *http.Request) (string, string, error) { + cookie, err := r.Cookie("session") + if err != nil { + if errors.Is(err, http.ErrNoCookie) { + return "", "", nil + } + return "", "", err + } + + tokenHash := sha256.Sum256([]byte(cookie.Value)) + + session, err := types.Base(r).Queries.GetUserFromSession(r.Context(), tokenHash[:]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return "", "", nil + } + return "", "", err + } + + return fmt.Sprint(session.UserID), session.Username, nil +} diff --git a/forged/internal/incoming/web/handler.go b/forged/internal/incoming/web/handler.go index 63019b4e497a5a9d13e605503db447953971f222..33136379ce288b1f7068fdf2fd303b6890d6d2a4 100644 --- a/forged/internal/incoming/web/handler.go +++ b/forged/internal/incoming/web/handler.go @@ -9,6 +9,7 @@ "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/global" handlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers" repoHandlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers/repo" + specialHandlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers/special" "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/templates" ) @@ -17,7 +18,7 @@ r *Router } func NewHandler(cfg Config, global *global.Global, queries *queries.Queries) *handler { - h := &handler{r: NewRouter().ReverseProxy(cfg.ReverseProxy).Global(global).Queries(queries)} + h := &handler{r: NewRouter().ReverseProxy(cfg.ReverseProxy).Global(global).Queries(queries).UserResolver(userResolver)} staticFS := http.FileServer(http.Dir(cfg.StaticPath)) h.r.ANYHTTP("-/static/*rest", @@ -36,6 +37,7 @@ t := templates.MustParseDir(cfg.TemplatesPath, funcs) renderer := templates.New(t) indexHTTP := handlers.NewIndexHTTP(renderer) + loginHTTP := specialHandlers.NewLoginHTTP(renderer, cfg.CookieExpiry) groupHTTP := handlers.NewGroupHTTP(renderer) repoHTTP := repoHandlers.NewHTTP(renderer) notImpl := handlers.NewNotImplementedHTTP(renderer) @@ -44,7 +46,7 @@ // Index h.r.GET("/", indexHTTP.Index) // Top-level utilities - h.r.ANY("-/login", notImpl.Handle) + h.r.ANY("-/login", loginHTTP.Login) h.r.ANY("-/users", notImpl.Handle) // Group index diff --git a/forged/internal/incoming/web/handlers/special/login.go b/forged/internal/incoming/web/handlers/special/login.go new file mode 100644 index 0000000000000000000000000000000000000000..0287c474df93e12408b64244cb2996ff62351143 --- /dev/null +++ b/forged/internal/incoming/web/handlers/special/login.go @@ -0,0 +1,115 @@ +package handlers + +import ( + "crypto/rand" + "crypto/sha256" + "errors" + "log" + "net/http" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "go.lindenii.runxiyu.org/forge/forged/internal/common/argon2id" + "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" + "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/templates" + "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" + wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" +) + +type LoginHTTP struct { + r templates.Renderer + cookieExpiry int +} + +func NewLoginHTTP(r templates.Renderer, cookieExpiry int) *LoginHTTP { + return &LoginHTTP{ + r: r, + cookieExpiry: cookieExpiry, + } +} + +func (h *LoginHTTP) Login(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) { + renderLoginPage := func(loginError string) bool { + err := h.r.Render(w, "login", struct { + BaseData *types.BaseData + LoginError string + }{ + BaseData: types.Base(r), + LoginError: loginError, + }) + if err != nil { + log.Println("failed to render login page", "error", err) + http.Error(w, "Failed to render login page", http.StatusInternalServerError) + return true + } + return false + } + + if r.Method == http.MethodGet { + renderLoginPage("") + return + } + + username := r.PostFormValue("username") + password := r.PostFormValue("password") + + userCreds, err := types.Base(r).Queries.GetUserCreds(r.Context(), &username) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + renderLoginPage("User not found") + return + } + log.Println("failed to get user credentials", "error", err) + http.Error(w, "Failed to get user credentials", http.StatusInternalServerError) + return + } + + if userCreds.PasswordHash == "" { + renderLoginPage("No password set for this user") + return + } + + passwordMatches, err := argon2id.ComparePasswordAndHash(password, userCreds.PasswordHash) + if err != nil { + log.Println("failed to compare password and hash", "error", err) + http.Error(w, "Failed to verify password", http.StatusInternalServerError) + return + } + + if !passwordMatches { + renderLoginPage("Invalid password") + return + } + + cookieValue := rand.Text() + + now := time.Now() + expiry := now.Add(time.Duration(h.cookieExpiry) * time.Second) + + cookie := &http.Cookie{ + Name: "session", + Value: cookieValue, + SameSite: http.SameSiteLaxMode, + HttpOnly: true, + Secure: false, // TODO + Expires: expiry, + Path: "/", + } //exhaustruct:ignore + + http.SetCookie(w, cookie) + + tokenHash := sha256.Sum256(misc.StringToBytes(cookieValue)) + + err = types.Base(r).Queries.InsertSession(r.Context(), queries.InsertSessionParams{ + UserID: userCreds.ID, + TokenHash: tokenHash[:], + ExpiresAt: pgtype.Timestamptz{ + Time: expiry, + Valid: true, + }, + }) + + http.Redirect(w, r, "/", http.StatusSeeOther) +} diff --git a/forged/internal/incoming/web/router.go b/forged/internal/incoming/web/router.go index c1a0bc0efcec30dff19180f7ebc13bd9e9c8ad5f..07e19a53631d49a307e26c061cf3e1fa124a5901 100644 --- a/forged/internal/incoming/web/router.go +++ b/forged/internal/incoming/web/router.go @@ -152,6 +152,8 @@ DirMode: dirMode, Queries: r.queries, } + req = req.WithContext(wtypes.WithBaseData(req.Context(), bd)) + bd.RefType, bd.RefName, err = GetParamRefTypeName(req) if err != nil { r.err400(w, bd, "Error parsing ref query parameters: "+err.Error()) @@ -202,7 +204,7 @@ bd.GroupPath = strings.Split(g, "/") } // Attach BaseData to request context. - req = req.WithContext(wtypes.WithBaseData(req.Context(), bd)) + // req = req.WithContext(wtypes.WithBaseData(req.Context(), bd)) // Enforce method now. if rt.method != "" && diff --git a/forged/sql/queries/login.sql b/forged/sql/queries/login.sql new file mode 100644 index 0000000000000000000000000000000000000000..ffc402694a25eb413f3def0bfe3c63f224af9ec9 --- /dev/null +++ b/forged/sql/queries/login.sql @@ -0,0 +1,8 @@ +-- name: GetUserCreds :one +SELECT id, COALESCE(password_hash, '') FROM users WHERE username = $1; + +-- name: InsertSession :exec +INSERT INTO sessions (user_id, token_hash, expires_at) VALUES ($1, $2, $3); + +-- name: GetUserFromSession :one +SELECT user_id, COALESCE(username, '') FROM users u JOIN sessions s ON u.id = s.user_id WHERE s.token_hash = $1; diff --git a/forged/templates/login.tmpl b/forged/templates/login.tmpl index 980b8631430dccedf83b995ac6b9a4f0773a6871..09cbb614f7e05fc6888a7d0c18e04c24549ee93e 100644 --- a/forged/templates/login.tmpl +++ b/forged/templates/login.tmpl @@ -7,11 +7,11 @@ {{- template "head_common" . -}} - Login – {{ .global.forge_title -}} + Login – {{ .BaseData.Global.ForgeTitle -}}
- {{- .login_error -}} + {{- .LoginError -}}
-- 2.48.1