aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-08-18 04:18:50 +0800
committerRunxi Yu <me@runxiyu.org>2025-08-18 04:38:45 +0800
commitec30ed1f0b2120a70331351a1f1afeac57285e71 (patch)
treee6f4c66e6157d2bd9b35c373b20d350d126743be
parentRename globalData to global (diff)
downloadforge-ec30ed1f0b2120a70331351a1f1afeac57285e71.tar.gz
forge-ec30ed1f0b2120a70331351a1f1afeac57285e71.tar.zst
forge-ec30ed1f0b2120a70331351a1f1afeac57285e71.zip
Make logging in work
-rw-r--r--forged/internal/incoming/web/authn.go33
-rw-r--r--forged/internal/incoming/web/handler.go6
-rw-r--r--forged/internal/incoming/web/handlers/special/login.go115
-rw-r--r--forged/internal/incoming/web/router.go4
-rw-r--r--forged/sql/queries/login.sql8
-rw-r--r--forged/templates/login.tmpl4
6 files changed, 165 insertions, 5 deletions
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 @@
<html lang="en">
<head>
{{- template "head_common" . -}}
- <title>Login &ndash; {{ .global.forge_title -}}</title>
+ <title>Login &ndash; {{ .BaseData.Global.ForgeTitle -}}</title>
</head>
<body class="index">
<main>
- {{- .login_error -}}
+ {{- .LoginError -}}
<div class="padding-wrapper">
<form method="POST" enctype="application/x-www-form-urlencoded">
<table>