Use email as host login credential; remove separate username field; v2.0.8
Publish Container / docker (push) Failing after 10m44s
Publish Container / docker (push) Failing after 10m44s
- Login form now asks for email + password (no username) - Admin creates hosts with name + email + password; email is always required and welcome email always sends on creation - Duplicate email check replaces duplicate username check - Admin table shows Email (login) column only; username field removed throughout data model, routes, and UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
a12a471c41
commit
684181c34c
5 files changed
+33
-46
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.7",
|
"version": "2.0.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+1
-1
@@ -778,7 +778,7 @@ export async function sendNuggetsHostWelcomeEmail({ to, name, username, password
|
|||||||
</p>
|
</p>
|
||||||
<table style="width:100%;border-collapse:collapse;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;margin-bottom:20px;">
|
<table style="width:100%;border-collapse:collapse;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;margin-bottom:20px;">
|
||||||
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;border-radius:4px;color:#7a7060;width:110px;">Portal URL</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;"><a href="${escapeHtml(portalUrl)}" style="color:#c8a96e;">${escapeHtml(portalUrl)}</a></td></tr>
|
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;border-radius:4px;color:#7a7060;width:110px;">Portal URL</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;"><a href="${escapeHtml(portalUrl)}" style="color:#c8a96e;">${escapeHtml(portalUrl)}</a></td></tr>
|
||||||
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Username</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(username)}</td></tr>
|
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Email</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(username)}</td></tr>
|
||||||
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Password</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(password)}</td></tr>
|
<tr><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;color:#7a7060;">Password</td><td style="padding:8px 12px;background:#faf9f7;border:1px solid #e5e0d5;font-family:monospace;font-weight:700;">${escapeHtml(password)}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p style="margin:0 0 10px;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;">When you're ready to submit an episode, fill in the episode name, the Bible passage, a short description, and attach your audio file. Everything uploads in one step.</p>
|
<p style="margin:0 0 10px;font-family:Arial,sans-serif;font-size:14px;color:#2c2417;">When you're ready to submit an episode, fill in the episode name, the Bible passage, a short description, and attach your audio file. Everything uploads in one step.</p>
|
||||||
|
|||||||
@@ -159,18 +159,18 @@ export function register(app) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/nuggets-upload/login', loginRateLimiter, async (req, res) => {
|
app.post('/api/nuggets-upload/login', loginRateLimiter, async (req, res) => {
|
||||||
const username = typeof req.body?.username === 'string' ? req.body.username.trim() : ''
|
const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : ''
|
||||||
const password = typeof req.body?.password === 'string' ? req.body.password : ''
|
const password = typeof req.body?.password === 'string' ? req.body.password : ''
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!email || !password) {
|
||||||
res.status(400).json({ message: 'Username and password are required.' })
|
res.status(400).json({ message: 'Email and password are required.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await loadUploadHosts()
|
const data = await loadUploadHosts()
|
||||||
const host = data.hosts.find(h => h.username === username && h.active)
|
const host = data.hosts.find(h => h.email === email && h.active)
|
||||||
if (!host || !passwordsMatch(password, host.passwordHash)) {
|
if (!host || !passwordsMatch(password, host.passwordHash)) {
|
||||||
res.status(401).json({ message: 'Invalid username or password.' })
|
res.status(401).json({ message: 'Invalid email or password.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,12 +279,11 @@ export function register(app) {
|
|||||||
|
|
||||||
app.post('/api/admin/nuggets-upload/hosts', requireAdminAuth, async (req, res) => {
|
app.post('/api/admin/nuggets-upload/hosts', requireAdminAuth, async (req, res) => {
|
||||||
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''
|
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''
|
||||||
const username = typeof req.body?.username === 'string' ? req.body.username.trim().toLowerCase() : ''
|
|
||||||
const password = typeof req.body?.password === 'string' ? req.body.password : ''
|
|
||||||
const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : ''
|
const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : ''
|
||||||
|
const password = typeof req.body?.password === 'string' ? req.body.password : ''
|
||||||
|
|
||||||
if (!name || !username || !password) {
|
if (!name || !email || !password) {
|
||||||
res.status(400).json({ message: 'Name, username, and password are required.' })
|
res.status(400).json({ message: 'Name, email, and password are required.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
@@ -293,15 +292,14 @@ export function register(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await loadUploadHosts()
|
const data = await loadUploadHosts()
|
||||||
if (data.hosts.some(h => h.username === username)) {
|
if (data.hosts.some(h => h.email === email)) {
|
||||||
res.status(409).json({ message: 'Username already taken.' })
|
res.status(409).json({ message: 'An account with that email already exists.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = {
|
const host = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
name,
|
name,
|
||||||
username,
|
|
||||||
email,
|
email,
|
||||||
passwordHash: hashPassword(password),
|
passwordHash: hashPassword(password),
|
||||||
active: true,
|
active: true,
|
||||||
@@ -310,19 +308,16 @@ export function register(app) {
|
|||||||
data.hosts.push(host)
|
data.hosts.push(host)
|
||||||
await saveUploadHosts(data)
|
await saveUploadHosts(data)
|
||||||
|
|
||||||
// Send welcome email if an address was provided
|
sendNuggetsHostWelcomeEmail({
|
||||||
if (email) {
|
to: email,
|
||||||
sendNuggetsHostWelcomeEmail({
|
name,
|
||||||
to: email,
|
username: email,
|
||||||
name,
|
password,
|
||||||
username,
|
portalUrl: `${process.env.SITE_URL ?? 'https://versebyversewithnate.us'}/host`,
|
||||||
password,
|
}).catch(err => console.error('[nuggets-host] welcome email error:', err))
|
||||||
portalUrl: `${process.env.SITE_URL ?? 'https://versebyversewithnate.us'}/host`,
|
|
||||||
}).catch(err => console.error('[nuggets-host] welcome email error:', err))
|
|
||||||
}
|
|
||||||
|
|
||||||
const { passwordHash: _ph, ...safeHost } = host
|
const { passwordHash: _ph, ...safeHost } = host
|
||||||
res.json({ ok: true, host: safeHost, emailSent: Boolean(email) })
|
res.json({ ok: true, host: safeHost, emailSent: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/api/admin/nuggets-upload/hosts/:id', requireAdminAuth, async (req, res) => {
|
app.put('/api/admin/nuggets-upload/hosts/:id', requireAdminAuth, async (req, res) => {
|
||||||
|
|||||||
+7
-15
@@ -1100,7 +1100,6 @@ function QrCodeImage({ url, label }: { url: string; label: string }) {
|
|||||||
interface UploadHost {
|
interface UploadHost {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
username: string
|
|
||||||
email: string
|
email: string
|
||||||
active: boolean
|
active: boolean
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@@ -1111,7 +1110,6 @@ function NuggetsUploadPortalAdmin() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [addName, setAddName] = useState('')
|
const [addName, setAddName] = useState('')
|
||||||
const [addUsername, setAddUsername] = useState('')
|
|
||||||
const [addEmail, setAddEmail] = useState('')
|
const [addEmail, setAddEmail] = useState('')
|
||||||
const [addPassword, setAddPassword] = useState('')
|
const [addPassword, setAddPassword] = useState('')
|
||||||
const [addError, setAddError] = useState('')
|
const [addError, setAddError] = useState('')
|
||||||
@@ -1142,12 +1140,12 @@ function NuggetsUploadPortalAdmin() {
|
|||||||
const r = await fetch('/api/admin/nuggets-upload/hosts', {
|
const r = await fetch('/api/admin/nuggets-upload/hosts', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: addName, username: addUsername, email: addEmail, password: addPassword }),
|
body: JSON.stringify({ name: addName, email: addEmail, password: addPassword }),
|
||||||
})
|
})
|
||||||
const d = await r.json()
|
const d = await r.json()
|
||||||
if (!r.ok) { setAddError(d.message ?? 'Failed.'); return }
|
if (!r.ok) { setAddError(d.message ?? 'Failed.'); return }
|
||||||
setAddEmailSent(Boolean(d.emailSent))
|
setAddEmailSent(Boolean(d.emailSent))
|
||||||
setAddName(''); setAddUsername(''); setAddEmail(''); setAddPassword('')
|
setAddName(''); setAddEmail(''); setAddPassword('')
|
||||||
reload()
|
reload()
|
||||||
} finally {
|
} finally {
|
||||||
setAddSaving(false)
|
setAddSaving(false)
|
||||||
@@ -1212,8 +1210,7 @@ function NuggetsUploadPortalAdmin() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: '2px solid var(--color-border, #e5e0d5)' }}>
|
<tr style={{ borderBottom: '2px solid var(--color-border, #e5e0d5)' }}>
|
||||||
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Name</th>
|
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Name</th>
|
||||||
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Username</th>
|
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Email (login)</th>
|
||||||
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Email</th>
|
|
||||||
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Status</th>
|
<th style={{ textAlign: 'left', padding: '6px 8px' }}>Status</th>
|
||||||
<th style={{ padding: '6px 8px' }}></th>
|
<th style={{ padding: '6px 8px' }}></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1222,8 +1219,7 @@ function NuggetsUploadPortalAdmin() {
|
|||||||
{hosts.map(h => (
|
{hosts.map(h => (
|
||||||
<tr key={h.id} style={{ borderBottom: '1px solid var(--color-border, #e5e0d5)' }}>
|
<tr key={h.id} style={{ borderBottom: '1px solid var(--color-border, #e5e0d5)' }}>
|
||||||
<td style={{ padding: '7px 8px' }}>{h.name}</td>
|
<td style={{ padding: '7px 8px' }}>{h.name}</td>
|
||||||
<td style={{ padding: '7px 8px', fontFamily: 'monospace' }}>{h.username}</td>
|
<td style={{ padding: '7px 8px', fontSize: '0.85rem' }}>{h.email}</td>
|
||||||
<td style={{ padding: '7px 8px', fontSize: '0.82rem', color: '#888' }}>{h.email || <em>none</em>}</td>
|
|
||||||
<td style={{ padding: '7px 8px' }}>
|
<td style={{ padding: '7px 8px' }}>
|
||||||
<span style={{ color: h.active ? '#27ae60' : '#888', fontWeight: 600 }}>{h.active ? 'Active' : 'Disabled'}</span>
|
<span style={{ color: h.active ? '#27ae60' : '#888', fontWeight: 600 }}>{h.active ? 'Active' : 'Disabled'}</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -1266,13 +1262,9 @@ function NuggetsUploadPortalAdmin() {
|
|||||||
<label>Display Name</label>
|
<label>Display Name</label>
|
||||||
<input type="text" placeholder="Jane Smith" value={addName} onChange={e => setAddName(e.target.value)} required />
|
<input type="text" placeholder="Jane Smith" value={addName} onChange={e => setAddName(e.target.value)} required />
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field" style={{ flex: '1 1 130px' }}>
|
<div className="admin-field" style={{ flex: '1 1 200px' }}>
|
||||||
<label>Username</label>
|
<label>Email — used to log in</label>
|
||||||
<input type="text" placeholder="janesmith" value={addUsername} onChange={e => setAddUsername(e.target.value.toLowerCase())} required />
|
<input type="email" placeholder="jane@example.com" value={addEmail} onChange={e => setAddEmail(e.target.value)} required />
|
||||||
</div>
|
|
||||||
<div className="admin-field" style={{ flex: '1 1 180px' }}>
|
|
||||||
<label>Email (optional — sends welcome)</label>
|
|
||||||
<input type="email" placeholder="jane@example.com" value={addEmail} onChange={e => setAddEmail(e.target.value)} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field" style={{ flex: '1 1 150px' }}>
|
<div className="admin-field" style={{ flex: '1 1 150px' }}>
|
||||||
<label>Password (min 8 chars)</label>
|
<label>Password (min 8 chars)</label>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function NuggetsHostPortal() {
|
|||||||
const [hostName, setHostName] = useState('')
|
const [hostName, setHostName] = useState('')
|
||||||
const [seafileConfigured, setSeafileConfigured] = useState(true)
|
const [seafileConfigured, setSeafileConfigured] = useState(true)
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [loginError, setLoginError] = useState('')
|
const [loginError, setLoginError] = useState('')
|
||||||
const [loggingIn, setLoggingIn] = useState(false)
|
const [loggingIn, setLoggingIn] = useState(false)
|
||||||
@@ -48,7 +48,7 @@ export function NuggetsHostPortal() {
|
|||||||
const r = await fetch('/api/nuggets-upload/login', {
|
const r = await fetch('/api/nuggets-upload/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
})
|
})
|
||||||
const d = await r.json()
|
const d = await r.json()
|
||||||
if (!r.ok) { setLoginError(d.message ?? 'Login failed.'); return }
|
if (!r.ok) { setLoginError(d.message ?? 'Login failed.'); return }
|
||||||
@@ -63,7 +63,7 @@ export function NuggetsHostPortal() {
|
|||||||
await fetch('/api/nuggets-upload/logout', { method: 'POST' })
|
await fetch('/api/nuggets-upload/logout', { method: 'POST' })
|
||||||
setAuthenticated(false)
|
setAuthenticated(false)
|
||||||
setHostName('')
|
setHostName('')
|
||||||
setUsername('')
|
setEmail('')
|
||||||
setPassword('')
|
setPassword('')
|
||||||
setSubmitted([])
|
setSubmitted([])
|
||||||
resetForm()
|
resetForm()
|
||||||
@@ -166,9 +166,9 @@ export function NuggetsHostPortal() {
|
|||||||
<p style={s.subText}>Sign in with your host credentials to submit an episode.</p>
|
<p style={s.subText}>Sign in with your host credentials to submit an episode.</p>
|
||||||
<form onSubmit={handleLogin} style={s.form}>
|
<form onSubmit={handleLogin} style={s.form}>
|
||||||
<div style={s.field}>
|
<div style={s.field}>
|
||||||
<label style={s.label} htmlFor="hp-u">Username</label>
|
<label style={s.label} htmlFor="hp-u">Email</label>
|
||||||
<input id="hp-u" style={s.input} type="text" autoComplete="username"
|
<input id="hp-u" style={s.input} type="email" autoComplete="email"
|
||||||
value={username} onChange={e => setUsername(e.target.value)} disabled={loggingIn} required />
|
value={email} onChange={e => setEmail(e.target.value)} disabled={loggingIn} required />
|
||||||
</div>
|
</div>
|
||||||
<div style={s.field}>
|
<div style={s.field}>
|
||||||
<label style={s.label} htmlFor="hp-p">Password</label>
|
<label style={s.label} htmlFor="hp-p">Password</label>
|
||||||
|
|||||||
Reference in new issue
Block a user