From ec30ed1f0b2120a70331351a1f1afeac57285e71 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Mon, 18 Aug 2025 04:18:50 +0800 Subject: Make logging in work --- forged/internal/incoming/web/authn.go | 33 ++++++ forged/internal/incoming/web/handler.go | 6 +- .../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 +- 6 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 forged/internal/incoming/web/authn.go create mode 100644 forged/internal/incoming/web/handlers/special/login.go create mode 100644 forged/sql/queries/login.sql diff --git a/forged/internal/incoming/web/authn.go b/forged/internal/incoming/web/authn.go new file mode 100644 index 0000000..46263ee --- /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 63019b4..3313637 100644 --- a/forged/internal/incoming/web/handler.go +++ b/forged/internal/incoming/web/handler.go @@ -9,6 +9,7 @@ import ( "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 @@ type handler struct { } 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 @@ func NewHandler(cfg Config, global *global.Global, queries *queries.Queries) *ha 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 @@ func NewHandler(cfg Config, global *global.Global, queries *queries.Queries) *ha 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 0000000..0287c47 --- /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 c1a0bc0..07e19a5 100644 --- a/forged/internal/incoming/web/router.go +++ b/forged/internal/incoming/web/router.go @@ -152,6 +152,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 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 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // 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 0000000..ffc4026 --- /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 980b863..09cbb61 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 -}}
-- cgit v1.2.3