Embed React app and add new user authentication interface.
- Integrated a React-based web frontend into the Go server using the `embed` package, serving it from `/`. - Added build and development scripts utilizing Bun for the React app (`package.json`, `README.md`). - Enhanced auth interface to support better user experience and permissions (`App.jsx`, CSS updates). - Refactored `/api/auth/login` to serve React UI, removing hardcoded HTML template. - Implemented `/api/permissions/` with ACL support for user access management.
This commit is contained in:
159
app/web/src/App.jsx
Normal file
159
app/web/src/App.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [status, setStatus] = useState('Ready to authenticate');
|
||||
const [statusType, setStatusType] = useState('info');
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication status on page load
|
||||
checkStatus();
|
||||
}, []);
|
||||
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
if (data.authenticated) {
|
||||
setUser(data.pubkey);
|
||||
updateStatus(`Already authenticated as: ${data.pubkey.slice(0, 16)}...`, 'success');
|
||||
|
||||
// Check permissions if authenticated
|
||||
if (data.pubkey) {
|
||||
const permResponse = await fetch(`/api/permissions/${data.pubkey}`);
|
||||
const permData = await permResponse.json();
|
||||
if (permData && permData.permission) {
|
||||
setUser({...data, permission: permData.permission});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors for status check
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(message, type = 'info') {
|
||||
setStatus(message);
|
||||
setStatusType(type);
|
||||
}
|
||||
|
||||
async function getChallenge() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/challenge');
|
||||
const data = await response.json();
|
||||
return data.challenge;
|
||||
} catch (error) {
|
||||
updateStatus('Failed to get authentication challenge: ' + error.message, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithExtension() {
|
||||
if (!window.nostr) {
|
||||
updateStatus('No Nostr extension found. Please install a NIP-07 compatible extension like nos2x or Alby.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('Connecting to extension...', 'info');
|
||||
|
||||
// Get public key from extension
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
|
||||
// Get challenge from server
|
||||
const challenge = await getChallenge();
|
||||
|
||||
// Create authentication event
|
||||
const authEvent = {
|
||||
kind: 22242,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['relay', window.location.protocol.replace('http', 'ws') + '//' + window.location.host],
|
||||
['challenge', challenge]
|
||||
],
|
||||
content: ''
|
||||
};
|
||||
|
||||
// Sign the event with extension
|
||||
const signedEvent = await window.nostr.signEvent(authEvent);
|
||||
|
||||
// Send to server
|
||||
await authenticate(signedEvent);
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('Extension login failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticate(signedEvent) {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(signedEvent)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setUser(result.pubkey);
|
||||
updateStatus('Successfully authenticated as: ' + result.pubkey.slice(0, 16) + '...', 'success');
|
||||
|
||||
// Check permissions after login
|
||||
const permResponse = await fetch(`/api/permissions/${result.pubkey}`);
|
||||
const permData = await permResponse.json();
|
||||
if (permData && permData.permission) {
|
||||
setUser({pubkey: result.pubkey, permission: permData.permission});
|
||||
}
|
||||
} else {
|
||||
updateStatus('Authentication failed: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
updateStatus('Authentication request failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
setUser(null);
|
||||
updateStatus('Logged out', 'info');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{user?.permission && (
|
||||
<div className="header-panel">
|
||||
<div className="header-content">
|
||||
<img src="/docs/orly.png" alt="Logo" className="header-logo" />
|
||||
<div className="user-info">
|
||||
{user.permission === "admin" ? "Admin Dashboard" : "Subscriber Dashboard"}
|
||||
</div>
|
||||
<button className="logout-button" onClick={logout}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1>Nostr Relay Authentication</h1>
|
||||
<p>Connect to this Nostr relay using your private key or browser extension.</p>
|
||||
|
||||
<div className={`status ${statusType}`}>
|
||||
{status}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<button onClick={loginWithExtension}>Login with Browser Extension (NIP-07)</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="nsec">Or login with private key (nsec):</label>
|
||||
<input type="password" id="nsec" placeholder="nsec1..." />
|
||||
<button onClick={() => updateStatus('Private key login not implemented in this basic interface. Please use a proper Nostr client or extension.', 'error')} style={{marginTop: '10px'}}>Login with Private Key</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<button onClick={logout} className="danger-button">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
11
app/web/src/index.jsx
Normal file
11
app/web/src/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles.css';
|
||||
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
122
app/web/src/styles.css
Normal file
122
app/web/src/styles.css
Normal file
@@ -0,0 +1,122 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
margin-top: 80px; /* Space for the header panel */
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007cba;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #005a87;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.danger-button:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.header-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #f8f9fa;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex-grow: 1;
|
||||
padding-left: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: transparent;
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: rgba(108, 117, 125, 0.1);
|
||||
color: #343a40;
|
||||
}
|
||||
Reference in New Issue
Block a user