Add admin reset, polish UI
This commit is contained in:
parent
f5401578bd
commit
14a5e01c33
@ -2,7 +2,7 @@ services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3060:3000"
|
||||
environment:
|
||||
PORT: 3000
|
||||
DATABASE_URL: postgres://pads:pads@db:5432/pads
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
27
server.js
27
server.js
@ -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', {
|
||||
|
||||
@ -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') %>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user