11 Commits

Author SHA1 Message Date
Michilis
da1d1b0d53 Merge pull request #4 from Michilis/dev
Move terms.txt to public/ to fix Terms page loading
2025-11-10 21:22:50 -03:00
Noderunners
0ab66479d9 Move terms.txt to public/ to fix Terms page loading 2025-11-11 01:21:52 +01:00
Michilis
1129fb9341 Merge pull request #3 from Michilis/dev
fix(lnbits): normalize invoice response (fallback to bolt11) and upda…
2025-11-10 16:51:25 -03:00
Noderunners
4afe3f6d3e fix(lnbits): normalize invoice response (fallback to bolt11) and update payment status check; chore: add .gitignore 2025-11-10 20:47:58 +01:00
Michilis
21bbd0fab1 Merge pull request #2 from Michilis/dev
Fix API calls to use npub instead of identifier
2025-11-06 17:17:48 -03:00
Michilis
bb3b5604a1 Fix API calls to use npub instead of identifier
- Updated getUserInfo to send npub field instead of identifier
- Updated whitelistUser to send npub field instead of identifier
- Resolves 422 Unprocessable Content error from backend API
2025-07-01 19:18:29 +00:00
Michilis
9faf7b5cef Update README.md 2025-02-10 05:04:59 +01:00
Michilis
c984601352 Update README.md 2025-02-10 05:04:42 +01:00
Michilis
8cdca7bb8c Update README.md 2025-02-10 04:50:11 +01:00
Michilis
cbf2172fe4 Merge pull request #1 from Michilis/V0.02-ref-login
V0.02 ref login
2025-02-10 04:47:27 +01:00
Michilis
a22040b705 Add files via upload 2025-02-10 04:40:31 +01:00
7 changed files with 166 additions and 55 deletions

64
.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
# Dependencies
node_modules/
# Production builds
dist/
dist-ssr/
build/
# Vite and tooling caches
.vite/
.esbuild/
.rollup.cache/
.parcel-cache/
.turbo/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment files
.env
.env.local
.env.*.local
.env.production.local
.env.development.local
.env.test.local
!.env.example
# Coverage
coverage/
# TypeScript build info
*.tsbuildinfo
# Editor directories and files
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*~
# OS metadata
.DS_Store
Thumbs.db
# Misc secrets/keys (if present)
*.pem
*.key
*.crt
*.p12
*.pfx
# Package tarballs
*.tgz

View File

@@ -1,12 +1,14 @@
# Noderunners Relay # Noderunners Relay
A high-performance Nostr relay built by Bitcoiners, for Bitcoiners. This project provides a web interface for managing access to the Noderunners relay service. A high-performance Nostr relay built by Bitcoiners, for Bitcoiners. This project provides a web interface for managing access to the Noderunners relay service.
```
wss://relay.noderunners.network
```
![Noderunners Relay](https://cdn.azzamo.net/5cc03420a18166ef7a20b1e6b7dad240ad7d634824649643c80d74a924062258.png) ![Noderunners Relay](https://cdn.azzamo.net/5cc03420a18166ef7a20b1e6b7dad240ad7d634824649643c80d74a924062258.png)
## Features ## Features
- 🚀 Lightning-fast relay performance with strfry v1.0.3
- ⚡ Lightning Network integration for payments - ⚡ Lightning Network integration for payments
- 🔒 Secure authentication with Nostr - 🔒 Secure authentication with Nostr
- 💻 Modern, responsive web interface - 💻 Modern, responsive web interface
@@ -116,20 +118,6 @@ You can combine iframe mode with URL-based authentication:
<iframe src="https://your-relay-domain.com?iframe=1&npub=npub1..." width="100%" height="600px"></iframe> <iframe src="https://your-relay-domain.com?iframe=1&npub=npub1..." width="100%" height="600px"></iframe>
``` ```
## Supported NIPs
The relay supports the following Nostr Implementation Possibilities (NIPs):
- NIP-01: Basic protocol flow description
- NIP-02: Contact List and Petnames
- NIP-04: Encrypted Direct Messages
- NIP-09: Event Deletion
- NIP-11: Relay Information Document
- NIP-22: Event `created_at` Limits
- NIP-28: Public Chat
- NIP-40: Expiration Timestamp
- NIP-70: Relay Payment Info
- NIP-77: Lightning Network Relay Payment
## API Services ## API Services
### LNbits Integration ### LNbits Integration
@@ -156,10 +144,6 @@ The relay supports the following Nostr Implementation Possibilities (NIPs):
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Support
For support, join our [Telegram group](https://t.me/noderunners) or visit [our website](https://noderunners.network).
## Acknowledgments ## Acknowledgments
- Built by the Noderunners community - Built by the Noderunners community

View File

@@ -45,4 +45,5 @@ We reserve the right to terminate or suspend access to our service immediately,
## 8. Changes to Terms ## 8. Changes to Terms
We reserve the right to modify these terms at any time. We will notify users of any changes by updating the date at the top of this page. We reserve the right to modify these terms at any time. We will notify users of any changes by updating the date at the top of this page.

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, useSearchParams, useNavigate } from 'react-router-dom';
import { Home } from './pages/Home'; import { Home } from './pages/Home';
import { Login } from './pages/Login'; import { Login } from './pages/Login';
import { Dashboard } from './pages/Dashboard'; import { Dashboard } from './pages/Dashboard';
@@ -7,20 +7,43 @@ import { Payment } from './pages/Payment';
import { ThankYou } from './pages/ThankYou'; import { ThankYou } from './pages/ThankYou';
import { Terms } from './pages/Terms'; import { Terms } from './pages/Terms';
import { Layout } from './components/Layout'; import { Layout } from './components/Layout';
import { useStore } from './store/useStore';
// Wrapper component to handle auto-login logic
function AutoLoginHandler({ children }: { children: React.ReactNode }) {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { user, setUser } = useStore();
useEffect(() => {
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
const isIframe = searchParams.get('iframe') === '1';
// Auto-login if pubkey/npub is in URL and user isn't already logged in
if (urlPubkey && !user) {
setUser({ pubkey: urlPubkey, isWhitelisted: false });
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
}
}, [searchParams, user, setUser, navigate]);
return <>{children}</>;
}
function App() { function App() {
return ( return (
<Router> <Router>
<Routes> <AutoLoginHandler>
<Route path="/" element={<Layout />}> <Routes>
<Route index element={<Home />} /> <Route path="/" element={<Layout />}>
<Route path="login" element={<Login />} /> <Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} /> <Route path="login" element={<Login />} />
<Route path="payment" element={<Payment />} /> <Route path="dashboard" element={<Dashboard />} />
<Route path="thank-you" element={<ThankYou />} /> <Route path="payment" element={<Payment />} />
<Route path="terms" element={<Terms />} /> <Route path="thank-you" element={<ThankYou />} />
</Route> <Route path="terms" element={<Terms />} />
</Routes> </Route>
</Routes>
</AutoLoginHandler>
</Router> </Router>
); );
} }

View File

@@ -10,12 +10,21 @@ export function Login() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [pubkeyInput, setPubkeyInput] = useState(''); const [pubkeyInput, setPubkeyInput] = useState('');
const isIframe = searchParams.get('iframe') === '1'; const isIframe = searchParams.get('iframe') === '1';
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
useEffect(() => { useEffect(() => {
// Handle URL-based login
if (urlPubkey && !user) {
setUser({ pubkey: urlPubkey, isWhitelisted: false });
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
return;
}
// Regular user redirect
if (user) { if (user) {
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard'); navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
} }
}, [user, navigate, isIframe]); }, [user, navigate, isIframe, urlPubkey, setUser]);
const handleExtensionLogin = async () => { const handleExtensionLogin = async () => {
setIsLoading(true); setIsLoading(true);
@@ -73,6 +82,15 @@ export function Login() {
} }
}; };
// If we're processing URL-based login, show loading state
if (urlPubkey && !user) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
</div>
);
}
return ( return (
<div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8"> <div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8">
<h1 className="text-2xl font-bold mb-6 text-center">Connect with Nostr</h1> <h1 className="text-2xl font-bold mb-6 text-center">Connect with Nostr</h1>

View File

@@ -11,7 +11,7 @@ export const apiService = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
identifier: pubkey, npub: pubkey,
}), }),
}); });
@@ -48,7 +48,7 @@ export const apiService = {
'X-Api-Key': apiKey, 'X-Api-Key': apiKey,
}, },
body: JSON.stringify({ body: JSON.stringify({
identifier: pubkey, npub: pubkey,
}), }),
}); });

View File

@@ -8,6 +8,7 @@ const api = axios.create({
headers: { headers: {
'X-Api-Key': API_KEY, 'X-Api-Key': API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json',
}, },
}); });
@@ -62,16 +63,26 @@ export const lnbitsService = {
extra = {}, extra = {},
}: CreateInvoiceParams): Promise<Invoice> { }: CreateInvoiceParams): Promise<Invoice> {
try { try {
const response = await api.post('/api/v1/payments', { // Build a V1-safe payload: only include known/supported fields when defined
const payload: Record<string, any> = {
out: false, out: false,
amount, amount,
memo, memo,
unit, };
webhook, if (unit) payload.unit = unit;
internal, if (webhook) payload.webhook = webhook;
extra, if (typeof internal === 'boolean') payload.internal = internal;
}); if (extra && Object.keys(extra).length > 0) payload.extra = extra;
return response.data;
const response = await api.post('/api/v1/payments', payload);
const data = response.data ?? {};
// Normalize response to ensure payment_request is always populated for the UI
const normalized: Invoice = {
...data,
payment_request: data.payment_request || data.bolt11,
bolt11: data.bolt11 || data.payment_request,
};
return normalized;
} catch (error) { } catch (error) {
console.error('Error creating invoice:', error); console.error('Error creating invoice:', error);
throw error; throw error;
@@ -82,10 +93,13 @@ export const lnbitsService = {
async checkPayment(paymentHash: string): Promise<PaymentStatus> { async checkPayment(paymentHash: string): Promise<PaymentStatus> {
try { try {
const response = await api.get(`/api/v1/payments/${paymentHash}`); const response = await api.get(`/api/v1/payments/${paymentHash}`);
const data = response.data || {};
// Normalize potential V1 shapes
const paid: boolean = (data.paid === true) || (data.status === 'paid') || (data.settled === true);
return { return {
paid: response.data.paid, paid,
preimage: response.data.preimage, preimage: data.preimage,
details: response.data.details, details: data.details,
}; };
} catch (error) { } catch (error) {
console.error('Error checking payment:', error); console.error('Error checking payment:', error);
@@ -150,17 +164,24 @@ export const lnbitsService = {
// Long poll payment status // Long poll payment status
async longPollPayment(paymentHash: string, timeout = 60000): Promise<boolean> { async longPollPayment(paymentHash: string, timeout = 60000): Promise<boolean> {
try { // Fallback to authenticated polling against V1 status endpoint to ensure compatibility
const response = await api.get(`/public/v1/payment/${paymentHash}`, { const start = Date.now();
timeout, const pollIntervalMs = 2000;
}); while (Date.now() - start < timeout) {
return response.data.paid; try {
} catch (error) { const status = await this.checkPayment(paymentHash);
if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') { if (status.paid) return true;
return false; // Timeout reached } catch (error) {
// If transient error, keep polling until timeout
if (axios.isAxiosError(error) && (error.response?.status ?? 0) >= 500) {
// continue
} else {
console.error('Error polling payment:', error);
throw error;
}
} }
console.error('Error polling payment:', error); await new Promise((r) => setTimeout(r, pollIntervalMs));
throw error;
} }
return false;
}, },
}; };