From 14a5e01c33abed9123a7285b75951299677acacb Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 3 Jan 2026 16:17:54 -0500 Subject: [PATCH] Add admin reset, polish UI --- docker-compose.yml | 2 +- public/styles.css | 51 +++++++++++++++++++++++++++------------ server.js | 27 ++++++++++++++++++++- views/admin.ejs | 35 ++++++++++++++++++++++++++- views/pad.ejs | 1 + views/partials/header.ejs | 5 +++- views/register.ejs | 3 +++ 7 files changed, 105 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c44995c..f8d7e6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: web: build: . ports: - - "3000:3000" + - "3060:3000" environment: PORT: 3000 DATABASE_URL: postgres://pads:pads@db:5432/pads diff --git a/public/styles.css b/public/styles.css index 397ea1c..0f53f69 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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); } diff --git a/server.js b/server.js index 0c0d189..244e551 100644 --- a/server.js +++ b/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', { diff --git a/views/admin.ejs b/views/admin.ejs index fe54943..b15fd92 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -1,10 +1,11 @@ <%- include('partials/header', { title }) %> -

Pending approvals

+

Admin

<% if (pendingUsers.length === 0) { %>

No pending accounts.

<% } else { %> +

Pending approvals

@@ -29,4 +30,36 @@
<% } %> +

User accounts

+ + + + + + + + + + <% users.forEach((user) => { %> + + + + + + <% }) %> + +
UsernameStatusReset password
<%= user.username %> + <%= user.is_admin ? 'Admin' : 'User' %> + ยท <%= user.is_approved ? 'Approved' : 'Pending' %> + +
+
+ +
+
+ +
+
+
+ <%- include('partials/footer') %> diff --git a/views/pad.ejs b/views/pad.ejs index b3e92d8..0249727 100644 --- a/views/pad.ejs +++ b/views/pad.ejs @@ -2,6 +2,7 @@
+ Back to your pads

Pad

<%= pad.name %>

<%= pad.brand || 'Independent' %>

diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 7b86767..a670926 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -18,7 +18,10 @@