with lightning
+ diff --git a/db/init.sql b/db/init.sql index fe05bb0..860d694 100644 --- a/db/init.sql +++ b/db/init.sql @@ -1,8 +1,8 @@ CREATE TABLE users( id SERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - ln_pubkey TEXT, - nostr_pubkey TEXT, + ln_pubkey TEXT UNIQUE, + nostr_pubkey TEXT UNIQUE, msats BIGINT NOT NULL DEFAULT 0 ); diff --git a/public/css/_tw-input.css b/public/css/_tw-input.css index 1684a50..a9ba781 100644 --- a/public/css/_tw-input.css +++ b/public/css/_tw-input.css @@ -29,14 +29,14 @@ @apply pb-1; } - a, + a:not(.no-link), button[hx-get], button[hx-post] { text-decoration: underline; transition: background-color 150ms ease-in, color 150ms ease-in; } - a:hover, + a:not(.no-link):hover, button[hx-get]:hover, button[hx-post]:hover { background-color: var(--color); @@ -71,9 +71,11 @@ } .login, .signup { + text-decoration: none !important; + transition: none !important; + padding: 0.25em 1em !important; width: fit-content; margin: 0 auto; - padding: 0.25em 1em; border-radius: 5px; font-weight: bold; } diff --git a/server/auth/lnauth.go b/server/auth/lnauth.go index 43f190c..0ac167a 100644 --- a/server/auth/lnauth.go +++ b/server/auth/lnauth.go @@ -8,58 +8,74 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcutil/bech32" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "git.ekzyis.com/ekzyis/delphi.market/env" ) -type LNAuth struct { +type LnAuth struct { K1 string LNURL string } -type LNAuthResponse struct { +type LnAuthCallback struct { K1 string `query:"k1"` Sig string `query:"sig"` Key string `query:"key"` + Tag string `query:"tag"` } -func NewLNAuth() (*LNAuth, error) { - k1 := make([]byte, 32) - _, err := rand.Read(k1) - if err != nil { +func NewLnAuth(tag string) (*LnAuth, error) { + var ( + k1 = make([]byte, 32) + k1hex string + url []byte + bech32Url []byte + lnurl string + err error + ) + + if _, err := rand.Read(k1); err != nil { return nil, fmt.Errorf("rand.Read error: %w", err) } - k1hex := hex.EncodeToString(k1) - url := []byte(fmt.Sprintf("https://%s/api/login/callback?tag=login&k1=%s&action=login", env.PublicURL, k1hex)) - conv, err := bech32.ConvertBits(url, 8, 5, true) - if err != nil { + + k1hex = hex.EncodeToString(k1) + url = []byte(fmt.Sprintf("https://%s/api/lnauth/callback?tag=%s&k1=%s&action=login", env.PublicURL, tag, k1hex)) + + if bech32Url, err = bech32.ConvertBits(url, 8, 5, true); err != nil { return nil, fmt.Errorf("bech32.ConvertBits error: %w", err) } - lnurl, err := bech32.Encode("lnurl", conv) - if err != nil { + + if lnurl, err = bech32.Encode("lnurl", bech32Url); err != nil { return nil, fmt.Errorf("bech32.Encode error: %w", err) } - return &LNAuth{k1hex, lnurl}, nil + + return &LnAuth{k1hex, lnurl}, nil } -func VerifyLNAuth(r *LNAuthResponse) (bool, error) { - var k1Bytes, sigBytes, keyBytes []byte - k1Bytes, err := hex.DecodeString(r.K1) - if err != nil { +func VerifyLNAuth(r *LnAuthCallback) (bool, error) { + var ( + k1Bytes, sigBytes, keyBytes []byte + key *secp256k1.PublicKey + err error + ) + + if k1Bytes, err = hex.DecodeString(r.K1); err != nil { return false, fmt.Errorf("k1 decode error: %w", err) } - sigBytes, err = hex.DecodeString(r.Sig) - if err != nil { + + if sigBytes, err = hex.DecodeString(r.Sig); err != nil { return false, fmt.Errorf("sig decode error: %w", err) } - keyBytes, err = hex.DecodeString(r.Key) - if err != nil { + + if keyBytes, err = hex.DecodeString(r.Key); err != nil { return false, fmt.Errorf("key decode error: %w", err) } - key, err := btcec.ParsePubKey(keyBytes) - if err != nil { + + if key, err = btcec.ParsePubKey(keyBytes); err != nil { return false, fmt.Errorf("key parse error: %w", err) } + ecdsaKey := ecdsa.PublicKey{Curve: btcec.S256(), X: key.X(), Y: key.Y()} return ecdsa.VerifyASN1(&ecdsaKey, k1Bytes, sigBytes), nil } diff --git a/server/router/context/context.go b/server/router/context/context.go index 4bf1d6d..445beda 100644 --- a/server/router/context/context.go +++ b/server/router/context/context.go @@ -25,6 +25,7 @@ var ( EnvContextKey RenderContextKey = "env" SessionContextKey RenderContextKey = "session" CommitContextKey RenderContextKey = "commit" + ReqPathContextKey RenderContextKey = "reqPath" ) func RenderContext(sc Context, c echo.Context) context.Context { @@ -32,5 +33,6 @@ func RenderContext(sc Context, c echo.Context) context.Context { ctx = context.WithValue(ctx, EnvContextKey, sc.Environment) ctx = context.WithValue(ctx, SessionContextKey, c.Get("session")) ctx = context.WithValue(ctx, CommitContextKey, sc.CommitShortSha) + ctx = context.WithValue(ctx, ReqPathContextKey, c.Request().URL.Path) return ctx } diff --git a/server/router/handler/signup.go b/server/router/handler/signup.go index a980000..e950419 100644 --- a/server/router/handler/signup.go +++ b/server/router/handler/signup.go @@ -1,6 +1,12 @@ package handler import ( + "database/sql" + "net/http" + "time" + + "git.ekzyis.com/ekzyis/delphi.market/lib" + "git.ekzyis.com/ekzyis/delphi.market/server/auth" "git.ekzyis.com/ekzyis/delphi.market/server/router/context" "git.ekzyis.com/ekzyis/delphi.market/server/router/pages" "github.com/labstack/echo/v4" @@ -8,6 +14,112 @@ import ( func HandleSignup(sc context.Context) echo.HandlerFunc { return func(c echo.Context) error { + + if c.Param("method") == "lightning" { + return LnAuthSignup(sc, c) + } + return pages.Signup().Render(context.RenderContext(sc, c), c.Response().Writer) } } + +func LnAuthSignup(sc context.Context, c echo.Context) error { + var ( + db = sc.Db + ctx = c.Request().Context() + lnAuth *auth.LnAuth + sessionId string + // sessions expire in 30 days. TODO: refresh sessions + expires = time.Now().Add(60 * 60 * 24 * 30 * time.Second) + qr string + err error + ) + + if lnAuth, err = auth.NewLnAuth("signup"); err != nil { + return err + } + + if err = db.QueryRowContext( + ctx, + "INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id", + lnAuth.K1, lnAuth.LNURL).Scan(&sessionId); err != nil { + return err + } + + if qr, err = lib.ToQR(lnAuth.LNURL); err != nil { + return err + } + + c.SetCookie(&http.Cookie{ + Name: "session", + HttpOnly: true, + Path: "/", + Value: sessionId, + Secure: true, + Expires: expires, + }) + + return pages.LnAuthSignup(qr, lnAuth.LNURL).Render(context.RenderContext(sc, c), c.Response().Writer) +} + +func HandleLnAuthCallback(sc context.Context) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + db = sc.Db + tx *sql.Tx + ctx = c.Request().Context() + query auth.LnAuthCallback + sessionId string + userId int + ok bool + err error + ) + + if err = c.Bind(&query); err != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + + if tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}); err != nil { + return err + } + + err = tx.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1 LIMIT 1", query.K1).Scan(&sessionId) + if err == sql.ErrNoRows { + return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"}) + } else if err != nil { + return err + } + + ok, err = auth.VerifyLNAuth(&query) + if err != nil { + return err + } else if !ok { + return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"}) + } + + if query.Tag == "signup" { + if err = tx.QueryRow("INSERT INTO users(ln_pubkey) VALUES ($1) RETURNING id").Scan(&userId); err != nil { + return err + } + } else if query.Tag == "login" { + err = tx.QueryRow("SELECT id FROM users WHERE ln_pubkey = $1", query.Key).Scan(&userId) + if err == sql.ErrNoRows { + return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "user not found"}) + } else if err != nil { + return err + } + } else { + return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "tag must be signup or login"}) + } + + if _, err = tx.Exec("INSERT INTO sessions(user_id, session_id) VALUES($1, $2)", userId, sessionId); err != nil { + return err + } + + if _, err = tx.Exec("DELETE FROM lnauth WHERE k1 = $1", query.K1); err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]string{"status": "OK"}) + } +} diff --git a/server/router/pages/lnAuthSignup.templ b/server/router/pages/lnAuthSignup.templ new file mode 100644 index 0000000..77c5281 --- /dev/null +++ b/server/router/pages/lnAuthSignup.templ @@ -0,0 +1,32 @@ +package pages + +import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" + +templ LnAuthSignup(qr string, lnurl string) { + + @components.Head() +
+ @components.Nav() +with lightning
+