Implement LNURL-auth
This commit is contained in:
parent
8b841aaa09
commit
2653e816bb
4
db/db.go
4
db/db.go
@ -13,7 +13,7 @@ type DB struct {
|
||||
}
|
||||
|
||||
var (
|
||||
initSqlPath = "./db/init.sql"
|
||||
schemaPath = "./db/schema.sql"
|
||||
)
|
||||
|
||||
func New(dbUrl string) (*DB, error) {
|
||||
@ -42,7 +42,7 @@ func (db *DB) Reset(dbName string) error {
|
||||
if err = db.Clear(dbName); err != nil {
|
||||
return err
|
||||
}
|
||||
if f, err = ioutil.ReadFile(initSqlPath); err != nil {
|
||||
if f, err = ioutil.ReadFile(schemaPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = db.Exec(string(f)); err != nil {
|
||||
|
80
db/init.sql
80
db/init.sql
@ -1,79 +1 @@
|
||||
CREATE TABLE users(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ln_pubkey TEXT,
|
||||
nostr_pubkey TEXT,
|
||||
msats BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE sessions(
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
session_id VARCHAR(48)
|
||||
);
|
||||
|
||||
CREATE TABLE lnauth(
|
||||
k1 VARCHAR(64) PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
lnurl TEXT NOT NULL,
|
||||
session_id VARCHAR(48) NOT NULL DEFAULT encode(gen_random_uuid()::text::bytea, 'base64')
|
||||
);
|
||||
|
||||
CREATE TABLE invoices(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
msats BIGINT NOT NULL,
|
||||
msats_received BIGINT,
|
||||
preimage TEXT NOT NULL UNIQUE,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
bolt11 TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
confirmed_at TIMESTAMP WITH TIME ZONE,
|
||||
held_since TIMESTAMP WITH TIME ZONE,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE markets(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT NOT NULL,
|
||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
settled_at TIMESTAMP WITH TIME ZONE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
invoice_id INTEGER NOT NULL UNIQUE REFERENCES invoices(id)
|
||||
);
|
||||
|
||||
CREATE TABLE shares(
|
||||
id SERIAL PRIMARY KEY,
|
||||
market_id INTEGER NOT NULL REFERENCES markets(id),
|
||||
description TEXT NOT NULL,
|
||||
win BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
|
||||
|
||||
CREATE TABLE orders(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
share_id INTEGER NOT NULL REFERENCES shares(id),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
side ORDER_SIDE NOT NULL,
|
||||
quantity BIGINT NOT NULL,
|
||||
price BIGINT NOT NULL,
|
||||
invoice_id INTEGER REFERENCES invoices(id),
|
||||
order_id INTEGER REFERENCES orders(id)
|
||||
);
|
||||
|
||||
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
|
||||
ALTER TABLE orders ADD CONSTRAINT order_quantity CHECK(quantity > 0);
|
||||
|
||||
CREATE TABLE withdrawals(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
canceled_at TIMESTAMP WITH TIME ZONE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
bolt11 TEXT NOT NULL UNIQUE,
|
||||
paid_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE DATABASE "delphi_test";
|
||||
|
79
db/schema.sql
Normal file
79
db/schema.sql
Normal file
@ -0,0 +1,79 @@
|
||||
CREATE TABLE users(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ln_pubkey TEXT UNIQUE,
|
||||
nostr_pubkey TEXT UNIQUE,
|
||||
msats BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE sessions(
|
||||
id VARCHAR(48) PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE lnauth(
|
||||
k1 VARCHAR(64) PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
lnurl TEXT NOT NULL,
|
||||
session_id VARCHAR(48) NOT NULL DEFAULT encode(gen_random_uuid()::text::bytea, 'base64')
|
||||
);
|
||||
|
||||
CREATE TABLE invoices(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
msats BIGINT NOT NULL,
|
||||
msats_received BIGINT,
|
||||
preimage TEXT NOT NULL UNIQUE,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
bolt11 TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
confirmed_at TIMESTAMP WITH TIME ZONE,
|
||||
held_since TIMESTAMP WITH TIME ZONE,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE markets(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT NOT NULL,
|
||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
settled_at TIMESTAMP WITH TIME ZONE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
invoice_id INTEGER NOT NULL UNIQUE REFERENCES invoices(id)
|
||||
);
|
||||
|
||||
CREATE TABLE shares(
|
||||
id SERIAL PRIMARY KEY,
|
||||
market_id INTEGER NOT NULL REFERENCES markets(id),
|
||||
description TEXT NOT NULL,
|
||||
win BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
|
||||
|
||||
CREATE TABLE orders(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
share_id INTEGER NOT NULL REFERENCES shares(id),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
side ORDER_SIDE NOT NULL,
|
||||
quantity BIGINT NOT NULL,
|
||||
price BIGINT NOT NULL,
|
||||
invoice_id INTEGER REFERENCES invoices(id),
|
||||
order_id INTEGER REFERENCES orders(id)
|
||||
);
|
||||
|
||||
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
|
||||
ALTER TABLE orders ADD CONSTRAINT order_quantity CHECK(quantity > 0);
|
||||
|
||||
CREATE TABLE withdrawals(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
canceled_at TIMESTAMP WITH TIME ZONE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
bolt11 TEXT NOT NULL UNIQUE,
|
||||
paid_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
@ -13,6 +13,7 @@ services:
|
||||
volumes:
|
||||
- delphi:/var/lib/postgresql/data
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
- ./postgresql.conf:/var/lib/postgresql/data/postgresql.conf # for some reason this can't be mounted on first run
|
||||
|
||||
volumes:
|
||||
|
@ -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);
|
||||
@ -70,10 +70,12 @@
|
||||
@apply my-3
|
||||
}
|
||||
|
||||
.login {
|
||||
.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;
|
||||
}
|
||||
|
@ -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 {
|
||||
K1 string `query:"k1"`
|
||||
Sig string `query:"sig"`
|
||||
Key string `query:"key"`
|
||||
type LnAuthCallback struct {
|
||||
K1 string `query:"k1"`
|
||||
Sig string `query:"sig"`
|
||||
Key string `query:"key"`
|
||||
Action string `query:"action"`
|
||||
}
|
||||
|
||||
func NewLNAuth() (*LNAuth, error) {
|
||||
k1 := make([]byte, 32)
|
||||
_, err := rand.Read(k1)
|
||||
if err != nil {
|
||||
func NewLnAuth(action 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=login&k1=%s&action=%s", env.PublicURL, k1hex, action))
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
191
server/router/handler/auth.go
Normal file
191
server/router/handler/auth.go
Normal file
@ -0,0 +1,191 @@
|
||||
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"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func HandleAuth(sc context.Context, action string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
if c.Param("method") == "lightning" {
|
||||
return LnAuth(sc, c, action)
|
||||
}
|
||||
|
||||
return pages.Auth(mapAction(action)).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||
}
|
||||
}
|
||||
|
||||
func LnAuth(sc context.Context, c echo.Context, action string) 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(action); 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.LnAuth(qr, lnAuth.LNURL, mapAction(action)).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
|
||||
pqErr *pq.Error
|
||||
)
|
||||
|
||||
bail := func(code int, reason string) error {
|
||||
c.JSON(code, map[string]string{"status": "ERROR", "reason": reason})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.Bind(&query); err != nil {
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
} else if query.K1 == "" || query.Sig == "" || query.Key == "" || query.Action == "" {
|
||||
return bail(http.StatusBadRequest, "bad query")
|
||||
}
|
||||
|
||||
if tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}); err != nil {
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = tx.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1 LIMIT 1", query.K1).Scan(&sessionId)
|
||||
if err == sql.ErrNoRows {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusNotFound, "session not found")
|
||||
} else if err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
ok, err = auth.VerifyLNAuth(&query)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
} else if !ok {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusBadRequest, "bad signature")
|
||||
}
|
||||
|
||||
if query.Action == "register" {
|
||||
err = tx.QueryRow("INSERT INTO users(ln_pubkey) VALUES ($1) RETURNING id", query.Key).Scan(&userId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
pqErr, ok = err.(*pq.Error)
|
||||
if ok && pqErr.Code == "23505" {
|
||||
return bail(http.StatusBadRequest, "user already exists")
|
||||
}
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else if query.Action == "login" {
|
||||
err = tx.QueryRow("SELECT id FROM users WHERE ln_pubkey = $1", query.Key).Scan(&userId)
|
||||
if err == sql.ErrNoRows {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusNotFound, "user not found")
|
||||
} else if err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else {
|
||||
return bail(http.StatusBadRequest, "bad action")
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("INSERT INTO sessions(id, user_id) VALUES($1, $2)", sessionId, userId); err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("DELETE FROM lnauth WHERE k1 = $1", query.K1); err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return bail(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSessionCheck(sc context.Context) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
db = sc.Db
|
||||
ctx = c.Request().Context()
|
||||
cookie *http.Cookie
|
||||
userId int
|
||||
err error
|
||||
)
|
||||
|
||||
if cookie, err = c.Cookie("session"); err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, "no session cookie")
|
||||
}
|
||||
|
||||
if err = db.QueryRowContext(ctx,
|
||||
"SELECT user_id FROM sessions WHERE id = $1", cookie.Value).
|
||||
Scan(&userId); err != nil {
|
||||
return c.JSON(http.StatusNotFound, "session not found")
|
||||
}
|
||||
|
||||
c.Response().Header().Set("HX-Location", "/")
|
||||
// htmx requires a 200 response to follow redirects
|
||||
// see https://github.com/bigskysoftware/htmx/issues/2052
|
||||
return c.HTML(http.StatusOK, "/")
|
||||
}
|
||||
}
|
||||
|
||||
func mapAction(action string) string {
|
||||
// LNURL spec uses "register" but we want to show "signup" to the user
|
||||
// see https://github.com/lnurl/luds/blob/luds/04.md
|
||||
switch action {
|
||||
case "register":
|
||||
return "signup"
|
||||
default:
|
||||
return action
|
||||
}
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"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/handler"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/test"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -16,12 +20,12 @@ func init() {
|
||||
test.Init(&db)
|
||||
}
|
||||
|
||||
func TestLnAuth(t *testing.T) {
|
||||
func TestLnAuthSignup(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
sc = context.Context{Db: db}
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
sc context.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
cookies []*http.Cookie
|
||||
@ -29,29 +33,263 @@ func TestLnAuth(t *testing.T) {
|
||||
dbSessionId string
|
||||
err error
|
||||
)
|
||||
|
||||
e, req, rec = test.HTTPMocks("GET", "/signup/lightning", nil)
|
||||
c = e.NewContext(req, rec)
|
||||
c.SetParamNames("method")
|
||||
c.SetParamValues("lightning")
|
||||
|
||||
err = handler.HandleAuth(sc, "register")(c)
|
||||
assert.NoErrorf(err, "handler returned error")
|
||||
|
||||
// Set-Cookie header present
|
||||
cookies = rec.Result().Cookies()
|
||||
assert.Equalf(1, len(cookies), "wrong number of Set-Cookie headers")
|
||||
assert.Equalf("session", cookies[0].Name, "wrong cookie name")
|
||||
|
||||
// new challenge inserted which matches cookie value
|
||||
sessionId = cookies[0].Value
|
||||
err = db.QueryRow("SELECT session_id FROM lnauth WHERE session_id = $1", sessionId).Scan(&dbSessionId)
|
||||
assert.NoError(err)
|
||||
assert.Equalf(sessionId, dbSessionId, "wrong session id")
|
||||
}
|
||||
|
||||
func TestLnAuthSignupCallbackUserNotExists(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
sc context.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
lnAuth *auth.LnAuth
|
||||
sk *secp256k1.PrivateKey
|
||||
pk *secp256k1.PublicKey
|
||||
sig string
|
||||
key string
|
||||
sessionId string
|
||||
userId int
|
||||
count int
|
||||
err error
|
||||
)
|
||||
|
||||
lnAuth, err = auth.NewLnAuth("register")
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
err = db.QueryRow(
|
||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||
lnAuth.K1, lnAuth.LNURL).Scan(&sessionId)
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
sk, pk, err = test.GenerateKeyPair()
|
||||
assert.NoErrorf(err, "error generating keypair")
|
||||
|
||||
sig, err = test.Sign(sk, lnAuth.K1)
|
||||
assert.NoErrorf(err, "error signing k1")
|
||||
|
||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
||||
|
||||
sc = context.Context{Db: db}
|
||||
e, req, rec = test.HTTPMocks("GET", "/login", nil)
|
||||
e, req, rec = test.HTTPMocks("GET",
|
||||
fmt.Sprintf("/api/login?tag=login&k1=%s&key=%s&sig=%s&action=%s", lnAuth.K1, key, sig, "register"), nil)
|
||||
c = e.NewContext(req, rec)
|
||||
|
||||
err = handler.HandleLogin(sc)(c)
|
||||
err = handler.HandleLnAuthCallback(sc)(c)
|
||||
assert.NoErrorf(err, "handler returned error")
|
||||
|
||||
// user created
|
||||
err = db.QueryRow("SELECT id FROM users WHERE ln_pubkey = $1", key).Scan(&userId)
|
||||
assert.NoErrorf(err, "error fetching user")
|
||||
|
||||
// session created
|
||||
err = db.QueryRow("SELECT COUNT(1) FROM sessions WHERE id = $1 AND user_id = $2", sessionId, userId).Scan(&count)
|
||||
assert.NoErrorf(err, "error fetching session")
|
||||
assert.Equalf(1, count, "invalid session count")
|
||||
|
||||
// challenge deleted
|
||||
err = db.QueryRow("SELECT COUNT(1) FROM lnauth WHERE k1 = $1", lnAuth.K1).Scan(&count)
|
||||
assert.NoErrorf(err, "error fetching challenge")
|
||||
assert.Equalf(count, 0, "challenge not deleted")
|
||||
}
|
||||
|
||||
func TestLnAuthSignupCallbackUserExists(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
sc context.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
lnAuth *auth.LnAuth
|
||||
sk *secp256k1.PrivateKey
|
||||
pk *secp256k1.PublicKey
|
||||
sig string
|
||||
key string
|
||||
sessionId string
|
||||
err error
|
||||
)
|
||||
|
||||
lnAuth, err = auth.NewLnAuth("register")
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
err = db.QueryRow(
|
||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||
lnAuth.K1, lnAuth.LNURL).Scan(&sessionId)
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
sk, pk, err = test.GenerateKeyPair()
|
||||
assert.NoErrorf(err, "error generating keypair")
|
||||
|
||||
sig, err = test.Sign(sk, lnAuth.K1)
|
||||
assert.NoErrorf(err, "error signing k1")
|
||||
|
||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
||||
|
||||
// create user such that signup must fail
|
||||
_, err = db.Exec("INSERT INTO users(ln_pubkey) VALUES($1) RETURNING id", key)
|
||||
assert.NoErrorf(err, "error creating user")
|
||||
|
||||
sc = context.Context{Db: db}
|
||||
e, req, rec = test.HTTPMocks("GET",
|
||||
fmt.Sprintf("/api/login?tag=login&k1=%s&key=%s&sig=%s&action=%s", lnAuth.K1, key, sig, "register"), nil)
|
||||
c = e.NewContext(req, rec)
|
||||
|
||||
// must throw error because user already exists
|
||||
err = handler.HandleLnAuthCallback(sc)(c)
|
||||
assert.ErrorContains(err, "user already exists", "user check failed")
|
||||
}
|
||||
|
||||
func TestLnAuthLogin(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
sc = context.Context{Db: db}
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
cookies []*http.Cookie
|
||||
sessionId string
|
||||
dbSessionId string
|
||||
err error
|
||||
)
|
||||
|
||||
e, req, rec = test.HTTPMocks("GET", "/login/lightning", nil)
|
||||
c = e.NewContext(req, rec)
|
||||
c.SetParamNames("method")
|
||||
c.SetParamValues("lightning")
|
||||
|
||||
err = handler.HandleAuth(sc, "login")(c)
|
||||
assert.NoErrorf(err, "handler returned error")
|
||||
|
||||
// Set-Cookie header present
|
||||
cookies = rec.Result().Cookies()
|
||||
assert.Equalf(len(cookies), 1, "wrong number of Set-Cookie headers")
|
||||
assert.Equalf(cookies[0].Name, "session", "wrong cookie name")
|
||||
assert.Equalf("session", cookies[0].Name, "wrong cookie name")
|
||||
|
||||
// new challenge inserted
|
||||
// new challenge inserted which matches cookie value
|
||||
sessionId = cookies[0].Value
|
||||
err = db.QueryRow("SELECT session_id FROM lnauth WHERE session_id = $1", sessionId).Scan(&dbSessionId)
|
||||
if !assert.NoError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
// inserted challenge matches cookie value
|
||||
assert.NoError(err)
|
||||
assert.Equalf(sessionId, dbSessionId, "wrong session id")
|
||||
}
|
||||
|
||||
func TestLnAuthCallback(t *testing.T) {
|
||||
t.Skip()
|
||||
func TestLnAuthLoginCallbackUserNotExists(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
sc context.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
lnAuth *auth.LnAuth
|
||||
sk *secp256k1.PrivateKey
|
||||
pk *secp256k1.PublicKey
|
||||
sig string
|
||||
key string
|
||||
sessionId string
|
||||
err error
|
||||
)
|
||||
|
||||
lnAuth, err = auth.NewLnAuth("login")
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
err = db.QueryRow(
|
||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||
lnAuth.K1, lnAuth.LNURL).Scan(&sessionId)
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
sk, pk, err = test.GenerateKeyPair()
|
||||
assert.NoErrorf(err, "error generating keypair")
|
||||
|
||||
sig, err = test.Sign(sk, lnAuth.K1)
|
||||
assert.NoErrorf(err, "error signing k1")
|
||||
|
||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
||||
|
||||
sc = context.Context{Db: db}
|
||||
e, req, rec = test.HTTPMocks("GET",
|
||||
fmt.Sprintf("/api/login?tag=login&k1=%s&key=%s&sig=%s&action=%s", lnAuth.K1, key, sig, "login"), nil)
|
||||
c = e.NewContext(req, rec)
|
||||
|
||||
// must throw error because user does not exist
|
||||
err = handler.HandleLnAuthCallback(sc)(c)
|
||||
assert.ErrorContains(err, "user not found", "user check failed")
|
||||
}
|
||||
|
||||
func TestLnAuthLoginCallbackUserExists(t *testing.T) {
|
||||
var (
|
||||
assert = assert.New(t)
|
||||
e *echo.Echo
|
||||
c echo.Context
|
||||
sc context.Context
|
||||
req *http.Request
|
||||
rec *httptest.ResponseRecorder
|
||||
lnAuth *auth.LnAuth
|
||||
sk *secp256k1.PrivateKey
|
||||
pk *secp256k1.PublicKey
|
||||
sig string
|
||||
key string
|
||||
sessionId string
|
||||
userId int
|
||||
count int
|
||||
err error
|
||||
)
|
||||
|
||||
lnAuth, err = auth.NewLnAuth("login")
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
err = db.QueryRow(
|
||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||
lnAuth.K1, lnAuth.LNURL).Scan(&sessionId)
|
||||
assert.NoErrorf(err, "error creating challenge")
|
||||
|
||||
sk, pk, err = test.GenerateKeyPair()
|
||||
assert.NoErrorf(err, "error generating keypair")
|
||||
|
||||
sig, err = test.Sign(sk, lnAuth.K1)
|
||||
assert.NoErrorf(err, "error signing k1")
|
||||
|
||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
||||
|
||||
// create user such that login does not fail
|
||||
err = db.QueryRow("INSERT INTO users(ln_pubkey) VALUES($1) RETURNING id", key).Scan(&userId)
|
||||
assert.NoErrorf(err, "error creating user")
|
||||
|
||||
sc = context.Context{Db: db}
|
||||
e, req, rec = test.HTTPMocks("GET",
|
||||
fmt.Sprintf("/api/login?tag=login&k1=%s&key=%s&sig=%s&action=%s", lnAuth.K1, key, sig, "login"), nil)
|
||||
c = e.NewContext(req, rec)
|
||||
|
||||
err = handler.HandleLnAuthCallback(sc)(c)
|
||||
assert.NoErrorf(err, "handler returned error")
|
||||
|
||||
// session created
|
||||
err = db.QueryRow("SELECT COUNT(1) FROM sessions WHERE id = $1 AND user_id = $2", sessionId, userId).Scan(&count)
|
||||
assert.NoErrorf(err, "error fetching session")
|
||||
assert.Equalf(1, count, "invalid session count")
|
||||
|
||||
// challenge deleted
|
||||
err = db.QueryRow("SELECT COUNT(1) FROM lnauth WHERE k1 = $1", lnAuth.K1).Scan(&count)
|
||||
assert.NoErrorf(err, "error fetching challenge")
|
||||
assert.Equalf(count, 0, "challenge not deleted")
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleLogin(sc context.Context) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return pages.Login().Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||
}
|
||||
}
|
66
server/router/pages/auth.templ
Normal file
66
server/router/pages/auth.templ
Normal file
@ -0,0 +1,66 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||
)
|
||||
|
||||
templ Auth(action string) {
|
||||
<html>
|
||||
@components.Head()
|
||||
<body class="container">
|
||||
@components.Nav()
|
||||
<div id="content" class="flex flex-col text-center">
|
||||
@components.Figlet("random", action)
|
||||
<div
|
||||
class="flex flex-col mb-3 text-center"
|
||||
hx-target="#content"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<button
|
||||
class="flex signup lightning my-3 items-center"
|
||||
hx-get={ string(templ.SafeURL(fmt.Sprintf("/%s/lightning", action))) }
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="var(--black)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19 10.1907L8.48754 21L12.6726 12.7423H5L14.6157 3L11.5267 10.2835L19 10.1907Z"></path>
|
||||
</svg>
|
||||
{ action } with lightning
|
||||
</button>
|
||||
<button class="flex signup nostr my-3 items-center">
|
||||
<svg
|
||||
class="me-1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 875 875"
|
||||
fill="var(--white)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z"
|
||||
></path>
|
||||
</svg>{ action } with nostr
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col mb-3 text-center">
|
||||
if action == "signup" {
|
||||
<small>
|
||||
<a class="text-muted" href="/login">not your first time?</a>
|
||||
</small>
|
||||
} else {
|
||||
<small><a class="text-muted" href="/signup">first time?</a></small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@components.Footer()
|
||||
</body>
|
||||
</html>
|
||||
}
|
@ -13,7 +13,7 @@ templ Nav() {
|
||||
if ctx.Value(c.SessionContextKey) != nil {
|
||||
<button hx-get="/user">user</button>
|
||||
} else {
|
||||
<button hx-get="/login">login</button>
|
||||
<button hx-get="/signup">signup</button>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
|
33
server/router/pages/lnAuth.templ
Normal file
33
server/router/pages/lnAuth.templ
Normal file
@ -0,0 +1,33 @@
|
||||
package pages
|
||||
|
||||
import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||
|
||||
templ LnAuth(qr string, lnurl string, action string) {
|
||||
<html>
|
||||
@components.Head()
|
||||
<body class="container">
|
||||
@components.Nav()
|
||||
<div id="content" class="flex flex-col text-center">
|
||||
@components.Figlet("random", action)
|
||||
<small><code>with lightning</code></small>
|
||||
<div
|
||||
class="flex flex-col my-3 text-center"
|
||||
hx-target="#content"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<a
|
||||
class="mx-auto no-link"
|
||||
href={ templ.SafeURL("lightning:" + lnurl) }
|
||||
>
|
||||
<img src={ "data:image/jpeg;base64," + qr }/>
|
||||
</a>
|
||||
<small class="mx-auto w-[256px] my-1 break-words">{ lnurl }</small>
|
||||
</div>
|
||||
<div hx-get="/session" hx-trigger="every 1s" hx-swap="none"></div>
|
||||
</div>
|
||||
@components.Footer()
|
||||
</body>
|
||||
</html>
|
||||
}
|
@ -9,12 +9,44 @@ templ Login() {
|
||||
@components.Nav()
|
||||
<div id="content" class="flex flex-col text-center">
|
||||
@components.Figlet("random", "login")
|
||||
<div class="flex flex-col mb-3 text-center">
|
||||
<button class="login lightning my-3">login with lightning</button>
|
||||
<button class="login nostr my-3">login with nostr</button>
|
||||
<div
|
||||
class="flex flex-col mb-3 text-center"
|
||||
hx-target="#content"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<button class="flex login lightning my-3 items-center">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="var(--black)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19 10.1907L8.48754 21L12.6726 12.7423H5L14.6157 3L11.5267 10.2835L19 10.1907Z"></path>
|
||||
</svg>
|
||||
login with lightning
|
||||
</button>
|
||||
<button class="flex login nostr my-3 items-center">
|
||||
<svg
|
||||
class="me-1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 875 875"
|
||||
fill="var(--white)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z"
|
||||
></path>
|
||||
</svg>
|
||||
login with nostr
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col mb-3 text-center">
|
||||
<small><a class="text-muted" href="/signup">new here?</a></small>
|
||||
<small><a class="text-muted" href="/signup">first time?</a></small>
|
||||
</div>
|
||||
</div>
|
||||
@components.Footer()
|
||||
|
@ -14,5 +14,11 @@ func Init(e *echo.Echo, sc Context) {
|
||||
|
||||
e.GET("/", handler.HandleIndex(sc))
|
||||
e.GET("/about", handler.HandleAbout(sc))
|
||||
e.GET("/login", handler.HandleLogin(sc))
|
||||
|
||||
e.GET("/login", handler.HandleAuth(sc, "login"))
|
||||
e.GET("/login/:method", handler.HandleAuth(sc, "login"))
|
||||
e.GET("/signup", handler.HandleAuth(sc, "register"))
|
||||
e.GET("/signup/:method", handler.HandleAuth(sc, "register"))
|
||||
e.GET("/api/lnauth/callback", handler.HandleLnAuthCallback(sc))
|
||||
e.GET("/session", handler.HandleSessionCheck(sc))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user