Fix compile errors
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
ref
|
ref
|
||||||
node_modules
|
node_modules
|
||||||
target
|
target
|
||||||
|
data
|
||||||
.env
|
.env
|
||||||
**/.env
|
**/.env
|
||||||
|
|||||||
Generated
+13
@@ -12,6 +12,17 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
@@ -191,6 +202,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -1385,6 +1397,7 @@ version = "0.39.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d90b55eff1f0747d9e423972179672e1aacac3d3ccee4c1281147eaa90d6491e"
|
checksum = "d90b55eff1f0747d9e423972179672e1aacac3d3ccee4c1281147eaa90d6491e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bip39",
|
"bip39",
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "macros"
|
|||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
nostr-sdk = "0.39"
|
nostr-sdk = { version = "0.39", features = ["nip04", "nip47", "nip59", "nip98"] }
|
||||||
uuid = { version = "1.7", features = ["v4"] }
|
uuid = { version = "1.7", features = ["v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
tower-http = { version = "0.5", features = ["cors"] }
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
@@ -20,3 +20,4 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
base64 = "0.22"
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ use axum::{
|
|||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::{HeaderMap, Method, StatusCode, Uri},
|
http::{HeaderMap, Method, StatusCode, Uri},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::{delete, get, post, put},
|
routing::{get, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -325,7 +325,7 @@ async fn list_tenant_invoices(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct UpdateTenantBillingRequest {
|
struct UpdateTenantBillingRequest {
|
||||||
tenant_nwc_url: String,
|
tenant_nwc_url: String,
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-4
@@ -1,15 +1,73 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use base64::engine::general_purpose;
|
||||||
|
use base64::Engine;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use nostr_sdk::nostr::key::PublicKey;
|
use nostr_sdk::nostr::key::PublicKey;
|
||||||
use nostr_sdk::nostr::nips::nip98::{self, HttpMethod};
|
use nostr_sdk::nostr::nips::nip98::HttpMethod;
|
||||||
use nostr_sdk::nostr::types::time::Timestamp;
|
use nostr_sdk::nostr::types::time::Timestamp;
|
||||||
use nostr_sdk::nostr::types::url::Url;
|
use nostr_sdk::nostr::types::url::Url;
|
||||||
|
use nostr_sdk::nostr::{Alphabet, Event, Kind, SingleLetterTag, TagKind, TagStandard};
|
||||||
|
use nostr_sdk::JsonUtil;
|
||||||
|
|
||||||
pub fn verify_nip98(auth_header: &str, url: &str, method: &str) -> Result<PublicKey> {
|
pub fn verify_nip98(auth_header: &str, url: &str, method: &str) -> Result<PublicKey> {
|
||||||
let url = Url::parse(url).map_err(|e| anyhow!(e))?;
|
let url = Url::parse(url)?;
|
||||||
let method = HttpMethod::from_str(&method.to_uppercase()).map_err(|e| anyhow!(e))?;
|
let method = HttpMethod::from_str(&method.to_uppercase())?;
|
||||||
let now = Timestamp::now();
|
let now = Timestamp::now();
|
||||||
|
|
||||||
nip98::verify_auth_header(auth_header, &url, method, now, None).map_err(|e| anyhow!(e))
|
let event = decode_auth_event(auth_header)?;
|
||||||
|
|
||||||
|
if event.kind != Kind::HttpAuth {
|
||||||
|
return Err(anyhow!("authorization event kind mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let authorized_url =
|
||||||
|
match event
|
||||||
|
.tags
|
||||||
|
.find_standardized(TagKind::SingleLetter(SingleLetterTag::lowercase(
|
||||||
|
Alphabet::U,
|
||||||
|
))) {
|
||||||
|
Some(TagStandard::AbsoluteURL(url)) => url,
|
||||||
|
_ => return Err(anyhow!("authorization header missing url tag")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let authorized_method = match event.tags.find_standardized(TagKind::Method) {
|
||||||
|
Some(TagStandard::Method(method)) => method,
|
||||||
|
_ => return Err(anyhow!("authorization header missing method tag")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if authorized_url != &url || authorized_method != &method {
|
||||||
|
return Err(anyhow!("authorization does not match request"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let created_at = event.created_at.as_u64();
|
||||||
|
let now_secs = now.as_u64();
|
||||||
|
if created_at > now_secs {
|
||||||
|
if created_at - now_secs > 30 {
|
||||||
|
return Err(anyhow!("authorization timestamp too far in future"));
|
||||||
|
}
|
||||||
|
} else if now_secs - created_at > 60 {
|
||||||
|
return Err(anyhow!("authorization timestamp too old"));
|
||||||
|
}
|
||||||
|
|
||||||
|
event.verify()?;
|
||||||
|
Ok(event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_auth_event(auth_header: &str) -> Result<Event> {
|
||||||
|
if auth_header.trim().is_empty() {
|
||||||
|
return Err(anyhow!("missing authorization header"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (prefix, encoded) = auth_header
|
||||||
|
.split_once(' ')
|
||||||
|
.ok_or_else(|| anyhow!("malformed authorization header"))?;
|
||||||
|
|
||||||
|
if prefix != "Nostr" || encoded.is_empty() {
|
||||||
|
return Err(anyhow!("malformed authorization header"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = general_purpose::STANDARD.decode(encoded)?;
|
||||||
|
let json = String::from_utf8(decoded)?;
|
||||||
|
Ok(Event::from_json(json)?)
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-8
@@ -7,7 +7,8 @@ use crate::models::{Invoice, NewInvoice, NewInvoiceItem, Relay, Tenant};
|
|||||||
use crate::notifications::Nip17Notifier;
|
use crate::notifications::Nip17Notifier;
|
||||||
use crate::repo::Repo;
|
use crate::repo::Repo;
|
||||||
|
|
||||||
use nostr_sdk::nwc::prelude::*;
|
use nostr_sdk::nips::nip47::{self, MakeInvoiceRequest, NostrWalletConnectURI, PayInvoiceRequest};
|
||||||
|
use nostr_sdk::{Client, Filter, Kind, Keys, Timestamp};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BillingService {
|
pub struct BillingService {
|
||||||
@@ -140,20 +141,53 @@ impl BillingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let uri = NostrWalletConnectURI::parse(&self.platform_nwc_url)?;
|
let uri = NostrWalletConnectURI::parse(&self.platform_nwc_url)?;
|
||||||
let nwc = NWC::new(uri);
|
let request = nip47::Request::make_invoice(MakeInvoiceRequest {
|
||||||
let request = MakeInvoiceRequest::new(amount as u64, "Relay hosting");
|
amount: (amount as u64) * 1_000,
|
||||||
let response = nwc.make_invoice(request).await?;
|
description: Some("Relay hosting".to_string()),
|
||||||
Ok(response.invoice)
|
description_hash: None,
|
||||||
|
expiry: None,
|
||||||
|
});
|
||||||
|
let response = self.send_nwc_request(&uri, request).await?;
|
||||||
|
Ok(response.to_make_invoice()?.invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pay_invoice(&self, tenant_nwc_url: &str, invoice: &str) -> Result<()> {
|
async fn pay_invoice(&self, tenant_nwc_url: &str, invoice: &str) -> Result<()> {
|
||||||
let uri = NostrWalletConnectURI::parse(tenant_nwc_url)?;
|
let uri = NostrWalletConnectURI::parse(tenant_nwc_url)?;
|
||||||
let nwc = NWC::new(uri);
|
let request = nip47::Request::pay_invoice(PayInvoiceRequest::new(invoice));
|
||||||
let request = PayInvoiceRequest::new(invoice);
|
self.send_nwc_request(&uri, request).await?.to_pay_invoice()?;
|
||||||
nwc.pay_invoice(request).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_nwc_request(
|
||||||
|
&self,
|
||||||
|
uri: &NostrWalletConnectURI,
|
||||||
|
request: nip47::Request,
|
||||||
|
) -> Result<nip47::Response> {
|
||||||
|
let app_keys = Keys::new(uri.secret.clone());
|
||||||
|
let app_pubkey = app_keys.public_key();
|
||||||
|
let client = Client::new(app_keys);
|
||||||
|
client.add_relay(uri.relay_url.clone()).await?;
|
||||||
|
client.connect().await;
|
||||||
|
|
||||||
|
let started_at = Timestamp::now();
|
||||||
|
let event = request.to_event(uri)?;
|
||||||
|
client.send_event(event).await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::WalletConnectResponse)
|
||||||
|
.author(uri.public_key)
|
||||||
|
.pubkey(app_pubkey)
|
||||||
|
.since(started_at);
|
||||||
|
|
||||||
|
let events = client.fetch_events(filter, Duration::from_secs(10)).await?;
|
||||||
|
let event = events
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|event| event.created_at)
|
||||||
|
.ok_or_else(|| anyhow!("no NWC response"))?;
|
||||||
|
|
||||||
|
Ok(nip47::Response::from_event(uri, &event)?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_invoice_dm(
|
async fn send_invoice_dm(
|
||||||
&self,
|
&self,
|
||||||
tenant: &Tenant,
|
tenant: &Tenant,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -21,6 +22,7 @@ impl Config {
|
|||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
let database_url =
|
let database_url =
|
||||||
env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://data/caravel.db".to_string());
|
env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://data/caravel.db".to_string());
|
||||||
|
let database_url = resolve_database_url(database_url);
|
||||||
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
let port = env::var("PORT")
|
let port = env::var("PORT")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -71,3 +73,20 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_database_url(database_url: String) -> String {
|
||||||
|
const PREFIX: &str = "sqlite://";
|
||||||
|
if !database_url.starts_with(PREFIX) {
|
||||||
|
return database_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = &database_url[PREFIX.len()..];
|
||||||
|
if path.is_empty() || path.starts_with('/') || path == ":memory:" {
|
||||||
|
return database_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let absolute = base.join(path);
|
||||||
|
let normalized = absolute.to_string_lossy();
|
||||||
|
format!("sqlite:///{}", normalized.trim_start_matches('/'))
|
||||||
|
}
|
||||||
|
|||||||
+36
-3
@@ -1,15 +1,48 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, SqlitePool};
|
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, sqlite::SqlitePoolOptions, SqlitePool};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||||
|
|
||||||
pub async fn init_pool(database_url: &str) -> Result<SqlitePool> {
|
pub async fn init_pool(database_url: &str) -> Result<SqlitePool> {
|
||||||
|
ensure_sqlite_directory(database_url)?;
|
||||||
|
let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.connect(database_url)
|
.connect_with(options)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query("PRAGMA journal_mode = WAL;")
|
||||||
|
.execute(&pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
MIGRATOR.run(&pool).await?;
|
MIGRATOR.run(&pool).await?;
|
||||||
|
|
||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_sqlite_directory(database_url: &str) -> Result<()> {
|
||||||
|
const PREFIX: &str = "sqlite://";
|
||||||
|
if !database_url.starts_with(PREFIX) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = &database_url[PREFIX.len()..];
|
||||||
|
if path.is_empty() || path == ":memory:" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = if let Some(stripped) = path.strip_prefix('/') {
|
||||||
|
format!("/{}", stripped)
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = Path::new(&path)
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow!("invalid sqlite path"))?;
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl Nip17Notifier {
|
|||||||
return Err(anyhow!("PLATFORM_SECRET is required for NIP-17 notifications"));
|
return Err(anyhow!("PLATFORM_SECRET is required for NIP-17 notifications"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = Keys::parse(platform_secret)?;
|
let keys = Keys::parse(&platform_secret)?;
|
||||||
let indexer_client = Client::new(keys.clone());
|
let indexer_client = Client::new(keys.clone());
|
||||||
|
|
||||||
for relay in &relays {
|
for relay in &relays {
|
||||||
@@ -71,15 +71,15 @@ impl Nip17Notifier {
|
|||||||
let filter = Filter::new().kind(Kind::Custom(10050)).author(pubkey);
|
let filter = Filter::new().kind(Kind::Custom(10050)).author(pubkey);
|
||||||
let events = self
|
let events = self
|
||||||
.indexer_client
|
.indexer_client
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
|
.fetch_events(filter, Duration::from_secs(5))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut relays = Vec::new();
|
let mut relays = Vec::new();
|
||||||
if let Some(event) = events.into_iter().max_by_key(|event| event.created_at) {
|
if let Some(event) = events.into_iter().max_by_key(|event| event.created_at) {
|
||||||
for tag in event.tags.iter() {
|
for tag in event.tags.iter() {
|
||||||
if let Some(first) = tag.as_vec().get(0) {
|
if let Some(first) = tag.as_slice().get(0) {
|
||||||
if first == "relay" {
|
if first == "relay" {
|
||||||
if let Some(value) = tag.as_vec().get(1) {
|
if let Some(value) = tag.as_slice().get(1) {
|
||||||
relays.push(value.to_string());
|
relays.push(value.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ impl Provisioner {
|
|||||||
return Err(anyhow!("PLATFORM_SECRET is required"));
|
return Err(anyhow!("PLATFORM_SECRET is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let admin_keys = Keys::parse(admin_secret)?;
|
let admin_keys = Keys::parse(&admin_secret)?;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -76,3 +76,20 @@ Super admin dashboard:
|
|||||||
- `/admin/relays` — list relays
|
- `/admin/relays` — list relays
|
||||||
- `/admin/relays/:id` — relay detail
|
- `/admin/relays/:id` — relay detail
|
||||||
- `/admin/relays/:id/edit` — edit relay
|
- `/admin/relays/:id/edit` — edit relay
|
||||||
|
|
||||||
|
## Todos
|
||||||
|
|
||||||
|
- [ ] Marketing page (`/`) with value props, features, and CTA
|
||||||
|
- [ ] Login page (`/login`) via nonboard
|
||||||
|
- [ ] Tenant dashboard auth via NIP-98
|
||||||
|
- [ ] Relays list (`/relays`) with search/filter and add relay CTA
|
||||||
|
- [ ] Relay detail (`/relays/:id`) with edit + deactivate actions
|
||||||
|
- [ ] New relay form (`/relays/new`) with plan selection + invoice flow
|
||||||
|
- [ ] Relay edit form (`/relays/:id/edit`)
|
||||||
|
- [ ] Account page (`/account`) with status, invoices, and recurring billing toggle
|
||||||
|
- [ ] Super admin dashboard auth via `PLATFORM_ADMIN_PUBKEYS`
|
||||||
|
- [ ] Tenants list (`/admin/tenants`)
|
||||||
|
- [ ] Tenant detail (`/admin/tenants/:id`) with status + deactivate actions
|
||||||
|
- [ ] Relays list (`/admin/relays`)
|
||||||
|
- [ ] Relay detail (`/admin/relays/:id`)
|
||||||
|
- [ ] Relay edit form (`/admin/relays/:id/edit`)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "../node_modules/preline/variants.css";
|
@import "../node_modules/preline/variants.css";
|
||||||
@source "../node_modules/preline/dist/*.js";
|
@source "../node_modules/preline/dist/*.js";
|
||||||
|
@import "nonboard/style.css";
|
||||||
|
|
||||||
/* Pointer cursor on buttons */
|
/* Pointer cursor on buttons */
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -21,10 +21,52 @@ export default function Login() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 flex items-center justify-center p-6">
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 w-full max-w-md">
|
<div class="w-full max-w-3xl">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">Log In</h1>
|
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl">
|
||||||
<div ref={container} />
|
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(99,102,241,0.08),_transparent_45%)]" />
|
||||||
|
<div class="relative grid gap-8 p-8 md:grid-cols-[1.2fr_1fr] md:p-10">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="inline-flex items-center gap-2 rounded-full border border-indigo-100 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-700">
|
||||||
|
Secure Nostr Login
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-semibold text-gray-900 md:text-4xl">Welcome back</h1>
|
||||||
|
<p class="mt-3 text-sm leading-6 text-gray-600">
|
||||||
|
Connect your Nostr account to manage relay hosting, billing, and access in one place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-gray-600">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="mt-1 h-2 w-2 rounded-full bg-indigo-500" />
|
||||||
|
<p>Own your identity with cryptographic sign-in.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="mt-1 h-2 w-2 rounded-full bg-indigo-500" />
|
||||||
|
<p>No passwords or email required.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="mt-1 h-2 w-2 rounded-full bg-indigo-500" />
|
||||||
|
<p>Fast access to your relays and invoices.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-6 shadow-sm">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">Log in with Nostr</h2>
|
||||||
|
<p class="mt-2 text-xs text-gray-500">
|
||||||
|
New here? Create an account in seconds using your favorite Nostr signer.
|
||||||
|
</p>
|
||||||
|
<div class="mt-6" ref={container} />
|
||||||
|
<p class="mt-6 text-xs text-gray-500">
|
||||||
|
By continuing, you agree to use NIP-98 authentication for secure access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-6 text-center text-xs text-gray-500">
|
||||||
|
Having trouble? Make sure your signer is unlocked and connected.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user