Switch from email to username, add password change and admin reset
- Login now uses username instead of email - DB migration renames email -> username on existing databases - Users can change their own password from the Stories page - Admin can reset any user's password from the admin panel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
635f56e44b
commit
9afb1dd4c5
@ -14,6 +14,12 @@ export function getUser() {
|
|||||||
} catch { return null }
|
} catch { return null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support tokens that still carry `email` from before the username migration
|
||||||
|
export function getUsername() {
|
||||||
|
const u = getUser()
|
||||||
|
return u?.username ?? u?.email ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
async function req(method, url, body, extraHeaders = {}) {
|
async function req(method, url, body, extraHeaders = {}) {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const isForm = body instanceof FormData
|
const isForm = body instanceof FormData
|
||||||
@ -46,12 +52,13 @@ async function download(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
login: async (email, password) => {
|
login: async (username, password) => {
|
||||||
const data = await req('POST', '/api/auth/login', { email, password })
|
const data = await req('POST', '/api/auth/login', { username, password })
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
return data.user
|
return data.user
|
||||||
},
|
},
|
||||||
logout: clearToken,
|
logout: clearToken,
|
||||||
|
changePassword: (current, newPassword) => req('PUT', '/api/auth/password', { current, newPassword }),
|
||||||
|
|
||||||
getStories: () => req('GET', '/api/stories'),
|
getStories: () => req('GET', '/api/stories'),
|
||||||
getStory: (id) => req('GET', `/api/stories/${id}`),
|
getStory: (id) => req('GET', `/api/stories/${id}`),
|
||||||
@ -73,6 +80,7 @@ export const api = {
|
|||||||
admin: {
|
admin: {
|
||||||
getUsers: (pw) => req('GET', '/api/admin/users', null, { 'x-admin-password': pw }),
|
getUsers: (pw) => req('GET', '/api/admin/users', null, { 'x-admin-password': pw }),
|
||||||
createUser: (pw, data) => req('POST', '/api/admin/users', data, { 'x-admin-password': pw }),
|
createUser: (pw, data) => req('POST', '/api/admin/users', data, { 'x-admin-password': pw }),
|
||||||
|
resetPassword: (pw, id, pass) => req('PUT', `/api/admin/users/${id}/password`, { password: pass }, { 'x-admin-password': pw }),
|
||||||
deleteUser: (pw, id) => req('DELETE', `/api/admin/users/${id}`, null, { 'x-admin-password': pw }),
|
deleteUser: (pw, id) => req('DELETE', `/api/admin/users/${id}`, null, { 'x-admin-password': pw }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
6
frontend/src/lib/pocketbase.js
Normal file
6
frontend/src/lib/pocketbase.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import PocketBase from 'pocketbase'
|
||||||
|
|
||||||
|
const pb = new PocketBase(window.location.origin)
|
||||||
|
pb.autoCancellation(false)
|
||||||
|
|
||||||
|
export default pb
|
||||||
@ -7,17 +7,20 @@ export default function Admin() {
|
|||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([])
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const [form, setForm] = useState({ name: '', email: '', password: '' })
|
const [form, setForm] = useState({ name: '', username: '', password: '' })
|
||||||
const [formError, setFormError] = useState('')
|
const [formError, setFormError] = useState('')
|
||||||
const [formSuccess, setFormSuccess] = useState('')
|
const [formSuccess, setFormSuccess] = useState('')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
|
||||||
|
const [resetId, setResetId] = useState(null)
|
||||||
|
const [resetPw, setResetPw] = useState('')
|
||||||
|
const [resetMsg, setResetMsg] = useState('')
|
||||||
|
|
||||||
async function login(e) {
|
async function login(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const list = await api.admin.getUsers(pw)
|
const list = await api.admin.getUsers(pw)
|
||||||
if (list.error) { setError('Wrong password'); return }
|
|
||||||
setUsers(list)
|
setUsers(list)
|
||||||
setAuthed(true)
|
setAuthed(true)
|
||||||
} catch {
|
} catch {
|
||||||
@ -32,9 +35,8 @@ export default function Admin() {
|
|||||||
setCreating(true)
|
setCreating(true)
|
||||||
try {
|
try {
|
||||||
const user = await api.admin.createUser(pw, form)
|
const user = await api.admin.createUser(pw, form)
|
||||||
if (user.error) { setFormError(user.error); return }
|
|
||||||
setUsers(prev => [user, ...prev])
|
setUsers(prev => [user, ...prev])
|
||||||
setForm({ name: '', email: '', password: '' })
|
setForm({ name: '', username: '', password: '' })
|
||||||
setFormSuccess(`Account created for ${user.name}!`)
|
setFormSuccess(`Account created for ${user.name}!`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFormError(err.message)
|
setFormError(err.message)
|
||||||
@ -43,6 +45,19 @@ export default function Admin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetPassword(e, id) {
|
||||||
|
e.preventDefault()
|
||||||
|
setResetMsg('')
|
||||||
|
try {
|
||||||
|
await api.admin.resetPassword(pw, id, resetPw)
|
||||||
|
setResetMsg('Password updated')
|
||||||
|
setResetPw('')
|
||||||
|
setTimeout(() => { setResetId(null); setResetMsg('') }, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
setResetMsg(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser(id, name) {
|
async function deleteUser(id, name) {
|
||||||
if (!confirm(`Delete ${name}'s account and all their stories?`)) return
|
if (!confirm(`Delete ${name}'s account and all their stories?`)) return
|
||||||
await api.admin.deleteUser(pw, id)
|
await api.admin.deleteUser(pw, id)
|
||||||
@ -87,19 +102,20 @@ export default function Admin() {
|
|||||||
<form onSubmit={createUser} className="login-form">
|
<form onSubmit={createUser} className="login-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Display name"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
||||||
required
|
required
|
||||||
className="input"
|
className="input"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="text"
|
||||||
placeholder="Email"
|
placeholder="Username"
|
||||||
value={form.email}
|
value={form.username}
|
||||||
onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
|
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
|
||||||
required
|
required
|
||||||
className="input"
|
className="input"
|
||||||
|
autoCapitalize="none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -112,7 +128,7 @@ export default function Admin() {
|
|||||||
{formError && <p className="error-msg">{formError}</p>}
|
{formError && <p className="error-msg">{formError}</p>}
|
||||||
{formSuccess && <p className="success-msg">{formSuccess}</p>}
|
{formSuccess && <p className="success-msg">{formSuccess}</p>}
|
||||||
<button type="submit" className="btn btn-primary" disabled={creating}>
|
<button type="submit" className="btn btn-primary" disabled={creating}>
|
||||||
{creating ? 'Creating...' : 'Create Account'}
|
{creating ? 'Creating…' : 'Create Account'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@ -124,18 +140,46 @@ export default function Admin() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="user-list">
|
<ul className="user-list">
|
||||||
{users.map(u => (
|
{users.map(u => (
|
||||||
<li key={u.id} className="user-row">
|
<li key={u.id} className="user-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: '0.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<div>
|
<div>
|
||||||
<strong>{u.name}</strong>
|
<strong>{u.name}</strong>
|
||||||
<span className="user-email">{u.email}</span>
|
<span className="user-email">@{u.username}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost"
|
className="btn btn-ghost"
|
||||||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.6rem', color: 'var(--danger)', borderColor: 'var(--danger)' }}
|
style={{ fontSize: '0.72rem', padding: '0.25rem 0.5rem' }}
|
||||||
|
onClick={() => { setResetId(resetId === u.id ? null : u.id); setResetPw(''); setResetMsg('') }}
|
||||||
|
>
|
||||||
|
{resetId === u.id ? 'Cancel' : 'Reset PW'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: '0.72rem', padding: '0.25rem 0.5rem', color: 'var(--danger)', borderColor: 'var(--danger)' }}
|
||||||
onClick={() => deleteUser(u.id, u.name)}
|
onClick={() => deleteUser(u.id, u.name)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{resetId === u.id && (
|
||||||
|
<form onSubmit={e => resetPassword(e, u.id)} style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="New password"
|
||||||
|
value={resetPw}
|
||||||
|
onChange={e => setResetPw(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
className="input"
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button type="submit" className="btn btn-primary" style={{ flexShrink: 0 }}>Set</button>
|
||||||
|
{resetMsg && <p className={resetMsg === 'Password updated' ? 'success-msg' : 'error-msg'} style={{ width: '100%', margin: 0 }}>{resetMsg}</p>}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState } from 'react'
|
|||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
|
|
||||||
export default function Login({ onLogin }) {
|
export default function Login({ onLogin }) {
|
||||||
const [email, setEmail] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@ -12,10 +12,10 @@ export default function Login({ onLogin }) {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const user = await api.login(email, password)
|
const user = await api.login(username, password)
|
||||||
onLogin(user)
|
onLogin(user)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Wrong email or password')
|
setError(err.message || 'Wrong username or password')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -29,13 +29,14 @@ export default function Login({ onLogin }) {
|
|||||||
<p className="login-sub">Your stories, your world</p>
|
<p className="login-sub">Your stories, your world</p>
|
||||||
<form onSubmit={handleSubmit} className="login-form">
|
<form onSubmit={handleSubmit} className="login-form">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="text"
|
||||||
placeholder="Email"
|
placeholder="Username"
|
||||||
value={email}
|
value={username}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
className="input"
|
className="input"
|
||||||
autoComplete="email"
|
autoComplete="username"
|
||||||
|
autoCapitalize="none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -48,7 +49,7 @@ export default function Login({ onLogin }) {
|
|||||||
/>
|
/>
|
||||||
{error && <p className="error-msg">{error}</p>}
|
{error && <p className="error-msg">{error}</p>}
|
||||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||||
{loading ? 'Opening...' : 'Enter'}
|
{loading ? 'Opening…' : 'Enter'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,10 @@ export default function Stories({ onLogout }) {
|
|||||||
const [goal, setGoal] = useState({ target: 0, count: 0 })
|
const [goal, setGoal] = useState({ target: 0, count: 0 })
|
||||||
const [settingGoal, setSettingGoal] = useState(false)
|
const [settingGoal, setSettingGoal] = useState(false)
|
||||||
const [goalInput, setGoalInput] = useState('')
|
const [goalInput, setGoalInput] = useState('')
|
||||||
|
const [changingPw, setChangingPw] = useState(false)
|
||||||
|
const [pwForm, setPwForm] = useState({ current: '', next: '', confirm: '' })
|
||||||
|
const [pwError, setPwError] = useState('')
|
||||||
|
const [pwSuccess, setPwSuccess] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@ -49,6 +53,21 @@ export default function Stories({ onLogout }) {
|
|||||||
|
|
||||||
function logout() { api.logout(); onLogout() }
|
function logout() { api.logout(); onLogout() }
|
||||||
|
|
||||||
|
async function changePassword(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
setPwError('')
|
||||||
|
if (pwForm.next !== pwForm.confirm) { setPwError('Passwords do not match'); return }
|
||||||
|
if (pwForm.next.length < 6) { setPwError('New password must be at least 6 characters'); return }
|
||||||
|
try {
|
||||||
|
await api.changePassword(pwForm.current, pwForm.next)
|
||||||
|
setPwSuccess(true)
|
||||||
|
setPwForm({ current: '', next: '', confirm: '' })
|
||||||
|
setTimeout(() => { setChangingPw(false); setPwSuccess(false) }, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
setPwError(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveGoal(e) {
|
function saveGoal(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const t = parseInt(goalInput)
|
const t = parseInt(goalInput)
|
||||||
@ -67,10 +86,26 @@ export default function Stories({ onLogout }) {
|
|||||||
<h1 className="page-title">My Stories</h1>
|
<h1 className="page-title">My Stories</h1>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<button onClick={newStory} className="btn btn-primary">+ New Story</button>
|
<button onClick={newStory} className="btn btn-primary">+ New Story</button>
|
||||||
|
<button onClick={() => { setChangingPw(o => !o); setPwError(''); setPwSuccess(false) }} className="btn btn-ghost">Password</button>
|
||||||
<button onClick={logout} className="btn btn-ghost">Sign Out</button>
|
<button onClick={logout} className="btn btn-ghost">Sign Out</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{changingPw && (
|
||||||
|
<form className="goal-form" onSubmit={changePassword} style={{ alignItems: 'flex-start', flexDirection: 'column', maxWidth: 320 }}>
|
||||||
|
<label className="goal-label">Change password</label>
|
||||||
|
<input type="password" className="input" placeholder="Current password" value={pwForm.current} onChange={e => setPwForm(f => ({ ...f, current: e.target.value }))} required autoFocus />
|
||||||
|
<input type="password" className="input" placeholder="New password" value={pwForm.next} onChange={e => setPwForm(f => ({ ...f, next: e.target.value }))} required />
|
||||||
|
<input type="password" className="input" placeholder="Confirm new" value={pwForm.confirm} onChange={e => setPwForm(f => ({ ...f, confirm: e.target.value }))} required />
|
||||||
|
{pwError && <p className="error-msg">{pwError}</p>}
|
||||||
|
{pwSuccess && <p className="success-msg">Password updated!</p>}
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button type="submit" className="btn btn-primary">Update</button>
|
||||||
|
<button type="button" className="btn btn-ghost" onClick={() => setChangingPw(false)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Stats bar */}
|
{/* Stats bar */}
|
||||||
<div className="stats-bar">
|
<div className="stats-bar">
|
||||||
{streak > 0 && (
|
{streak > 0 && (
|
||||||
|
|||||||
@ -15,7 +15,7 @@ db.exec(`
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT UNIQUE NOT NULL COLLATE NOCASE,
|
username TEXT UNIQUE NOT NULL COLLATE NOCASE,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@ -38,4 +38,10 @@ db.exec(`
|
|||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// Migrate: rename email -> username for existing databases
|
||||||
|
const userCols = db.pragma('table_info(users)').map(c => c.name)
|
||||||
|
if (userCols.includes('email') && !userCols.includes('username')) {
|
||||||
|
db.exec('ALTER TABLE users RENAME COLUMN email TO username')
|
||||||
|
}
|
||||||
|
|
||||||
export default db
|
export default db
|
||||||
|
|||||||
@ -8,27 +8,36 @@ router.use(adminAuth)
|
|||||||
|
|
||||||
router.get('/users', (req, res) => {
|
router.get('/users', (req, res) => {
|
||||||
const users = db.prepare(
|
const users = db.prepare(
|
||||||
'SELECT id, name, email, created_at FROM users ORDER BY created_at DESC'
|
'SELECT id, name, username, created_at FROM users ORDER BY created_at DESC'
|
||||||
).all()
|
).all()
|
||||||
res.json(users)
|
res.json(users)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/users', (req, res) => {
|
router.post('/users', (req, res) => {
|
||||||
const { name, email, password } = req.body || {}
|
const { name, username, password } = req.body || {}
|
||||||
if (!name || !email || !password)
|
if (!name || !username || !password)
|
||||||
return res.status(400).json({ error: 'Name, email, and password are all required' })
|
return res.status(400).json({ error: 'Name, username, and password are all required' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = bcrypt.hashSync(password, 10)
|
const hash = bcrypt.hashSync(password, 10)
|
||||||
const { lastInsertRowid } = db.prepare(
|
const { lastInsertRowid } = db.prepare(
|
||||||
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)'
|
'INSERT INTO users (name, username, password) VALUES (?, ?, ?)'
|
||||||
).run(name, email.toLowerCase(), hash)
|
).run(name, username.toLowerCase(), hash)
|
||||||
res.json({ id: lastInsertRowid, name, email })
|
res.json({ id: lastInsertRowid, name, username })
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({ error: 'That email is already in use' })
|
res.status(400).json({ error: 'That username is already taken' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.put('/users/:id/password', (req, res) => {
|
||||||
|
const { password } = req.body || {}
|
||||||
|
if (!password) return res.status(400).json({ error: 'Password required' })
|
||||||
|
if (password.length < 6) return res.status(400).json({ error: 'Password must be at least 6 characters' })
|
||||||
|
|
||||||
|
db.prepare('UPDATE users SET password = ? WHERE id = ?').run(bcrypt.hashSync(password, 10), req.params.id)
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
router.delete('/users/:id', (req, res) => {
|
router.delete('/users/:id', (req, res) => {
|
||||||
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id)
|
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id)
|
||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
|
|||||||
@ -2,25 +2,39 @@ import { Router } from 'express'
|
|||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import db from '../db.js'
|
import db from '../db.js'
|
||||||
import { JWT_SECRET } from '../middleware/auth.js'
|
import { auth, JWT_SECRET } from '../middleware/auth.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post('/login', (req, res) => {
|
router.post('/login', (req, res) => {
|
||||||
const { email, password } = req.body || {}
|
const { username, password } = req.body || {}
|
||||||
if (!email || !password) return res.status(400).json({ error: 'Email and password required' })
|
if (!username || !password) return res.status(400).json({ error: 'Username and password required' })
|
||||||
|
|
||||||
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email)
|
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username)
|
||||||
if (!user || !bcrypt.compareSync(password, user.password)) {
|
if (!user || !bcrypt.compareSync(password, user.password)) {
|
||||||
return res.status(401).json({ error: 'Wrong email or password' })
|
return res.status(401).json({ error: 'Wrong username or password' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, email: user.email, name: user.name },
|
{ id: user.id, username: user.username, name: user.name },
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: '30d' }
|
{ expiresIn: '30d' }
|
||||||
)
|
)
|
||||||
res.json({ token, user: { id: user.id, email: user.email, name: user.name } })
|
res.json({ token, user: { id: user.id, username: user.username, name: user.name } })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.put('/password', auth, (req, res) => {
|
||||||
|
const { current, newPassword } = req.body || {}
|
||||||
|
if (!current || !newPassword) return res.status(400).json({ error: 'Current and new password required' })
|
||||||
|
if (newPassword.length < 6) return res.status(400).json({ error: 'New password must be at least 6 characters' })
|
||||||
|
|
||||||
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id)
|
||||||
|
if (!bcrypt.compareSync(current, user.password)) {
|
||||||
|
return res.status(401).json({ error: 'Current password is incorrect' })
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare('UPDATE users SET password = ? WHERE id = ?').run(bcrypt.hashSync(newPassword, 10), req.user.id)
|
||||||
|
res.json({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user