Add admin reset, polish UI

This commit is contained in:
chris 2026-01-03 16:17:54 -05:00
parent f5401578bd
commit 14a5e01c33
7 changed files with 105 additions and 19 deletions

View File

@ -2,7 +2,7 @@ services:
web:
build: .
ports:
- "3000:3000"
- "3060:3000"
environment:
PORT: 3000
DATABASE_URL: postgres://pads:pads@db:5432/pads

View File

@ -1,18 +1,20 @@
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Karla:wght@400;600&display=swap');
:root {
--ink: #1f1a17;
--muted: #5a4d45;
--sand: #f2ede7;
--blush: #f7d7c2;
--clay: #c17f59;
--sun: #f0b267;
--ink: #1b1513;
--muted: #5f5a5a;
--sand: #f4efe8;
--blush: #f2b4c4;
--clay: #e58aa1;
--sun: #f3c6a8;
--sky: #b8dbf0;
--mint: #bfe6d1;
}
body {
font-family: 'Karla', sans-serif;
color: var(--ink);
background: radial-gradient(circle at top left, #fff7f1 0%, #f6efe8 40%, #efe5dd 100%);
background: radial-gradient(circle at top left, #f2fbff 0%, #f4f1ea 35%, #f8e5e7 100%);
min-height: 100vh;
}
@ -30,10 +32,29 @@ body {
.navbar-item.brand {
font-size: 1.3rem;
color: var(--clay);
gap: 0.75rem;
}
.brand-logo {
width: 52px;
height: 52px;
border-radius: 0;
}
.back-button {
background: var(--sky);
border-color: var(--sky);
color: var(--ink);
font-weight: 600;
}
.back-button:hover {
background: #c7e3f5;
border-color: #c7e3f5;
}
.pad-hero {
background: linear-gradient(120deg, #ffe9dc 0%, #f9d5c2 50%, #f1c5a7 100%);
background: linear-gradient(120deg, #cbe8f7 0%, #f2c0cf 55%, #f5d3c0 100%);
border-radius: 18px;
margin-bottom: 2rem;
color: var(--ink);
@ -75,7 +96,7 @@ body {
}
.review-card {
border-left: 6px solid var(--sun);
border-left: 6px solid var(--clay);
}
.review-meta {
@ -108,7 +129,7 @@ body {
}
.star-display .star.is-filled {
color: var(--sun);
color: var(--clay);
}
.photo-actions {
@ -201,7 +222,7 @@ body {
.star-rating input:checked ~ label,
.star-rating label:hover,
.star-rating label:hover ~ label {
color: var(--sun);
color: var(--clay);
}
.star-rating input:focus-visible + label {
@ -230,7 +251,7 @@ body {
}
.pending-box {
background: #fff8f1;
background: #fff6f8;
border-left: 6px solid var(--clay);
}
@ -240,13 +261,13 @@ body {
}
.button.is-link {
background-color: var(--sun);
border-color: var(--sun);
background-color: var(--sky);
border-color: var(--sky);
color: var(--ink);
}
.notification.is-warning {
background: #fff1dd;
background: #fde9ee;
color: var(--ink);
}

View File

@ -491,7 +491,12 @@ app.get('/admin', requireAdmin, async (req, res, next) => {
WHERE is_approved = FALSE
ORDER BY created_at ASC`
);
res.render('admin', { title: 'Admin approvals', pendingUsers });
const { rows: users } = await query(
`SELECT id, username, is_admin, is_approved, created_at
FROM users
ORDER BY created_at DESC`
);
res.render('admin', { title: 'Admin', pendingUsers, users });
} catch (err) {
next(err);
}
@ -509,6 +514,26 @@ app.post('/admin/users/:id/approve', requireAdmin, async (req, res, next) => {
}
});
app.post('/admin/users/:id/reset-password', requireAdmin, async (req, res, next) => {
const newPassword = req.body.password?.trim();
if (!newPassword) {
req.session.flash = { type: 'danger', message: 'Password cannot be empty.' };
return res.redirect('/admin');
}
try {
const passwordHash = await bcrypt.hash(newPassword, 12);
await query('UPDATE users SET password_hash = $1 WHERE id = $2', [
passwordHash,
req.params.id,
]);
req.session.flash = { type: 'success', message: 'Password reset.' };
return res.redirect('/admin');
} catch (err) {
return next(err);
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).render('error', {

View File

@ -1,10 +1,11 @@
<%- include('partials/header', { title }) %>
<h1 class="title">Pending approvals</h1>
<h1 class="title">Admin</h1>
<% if (pendingUsers.length === 0) { %>
<p class="has-text-grey">No pending accounts.</p>
<% } else { %>
<h2 class="title is-4">Pending approvals</h2>
<table class="table is-fullwidth is-striped">
<thead>
<tr>
@ -29,4 +30,36 @@
</table>
<% } %>
<h2 class="title is-4 mt-5">User accounts</h2>
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th>Username</th>
<th>Status</th>
<th>Reset password</th>
</tr>
</thead>
<tbody>
<% users.forEach((user) => { %>
<tr>
<td><%= user.username %></td>
<td>
<%= user.is_admin ? 'Admin' : 'User' %>
<span class="has-text-grey">· <%= user.is_approved ? 'Approved' : 'Pending' %></span>
</td>
<td>
<form method="post" action="/admin/users/<%= user.id %>/reset-password" class="field has-addons">
<div class="control">
<input class="input is-small" type="password" name="password" placeholder="New password" required />
</div>
<div class="control">
<button class="button is-small is-warning" type="submit">Reset</button>
</div>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
<%- include('partials/footer') %>

View File

@ -2,6 +2,7 @@
<div class="pad-header">
<div>
<a class="button is-small mb-3 back-button" href="/">Back to your pads</a>
<p class="eyebrow">Pad</p>
<h1 class="title"><%= pad.name %></h1>
<p class="subtitle"><%= pad.brand || 'Independent' %></p>

View File

@ -18,7 +18,10 @@
<body>
<nav class="navbar is-white" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item brand" href="/">Pedal</a>
<a class="navbar-item brand" href="/">
<img class="brand-logo" src="/ios/256.png" alt="Pedal logo" />
<span>Pedal</span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">

View File

@ -3,6 +3,9 @@
<div class="columns is-centered">
<div class="column is-5">
<h1 class="title">Create your account</h1>
<p class="notification is-warning">
New accounts require admin approval before you can add pads, ratings, or photos.
</p>
<form method="post" action="/register" class="box">
<div class="field">
<label class="label" for="username">Username</label>