Skip to content

Commit f49e8bd

Browse files
authored
Merge pull request #2 from Lawndlwd/feedback
Feedback
2 parents 06d71b8 + a681f50 commit f49e8bd

File tree

12 files changed

+353
-84
lines changed

12 files changed

+353
-84
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# ── Admin credentials ─────────────────────────────────────────────────────────
22
ADMIN_USERNAME=admin
3-
ADMIN_PASSWORD=changeme
3+
ADMIN_PASSWORD=change-this-immediately-123456
44

55
# ── Security ──────────────────────────────────────────────────────────────────
6-
JWT_SECRET=change-this-secret-in-production
6+
JWT_SECRET=generate-a-random-32-character-string-here
77

88
# ── Caddy domain (optional) ───────────────────────────────────────────────────
99
# Leave empty to run on plain HTTP :80

client/src/pages/admin/Settings.tsx

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ export default function Settings() {
5757
const [cfg, setCfg] = useState<Partial<AppConfig> | null>(null);
5858
const [saved, setSaved] = useState(false);
5959
const [saving, setSaving] = useState(false);
60+
const [adminUsername, setAdminUsername] = useState('');
61+
const [currentPassword, setCurrentPassword] = useState('');
6062
const [newPassword, setNewPassword] = useState('');
63+
const [newUsername, setNewUsername] = useState('');
64+
const [changingPassword, setChangingPassword] = useState(false);
6165
const [uploadingAvatars, setUploadingAvatars] = useState(false);
6266
const [avatarUploadMessage, setAvatarUploadMessage] = useState<string | null>(null);
6367
const [availableAvatars, setAvailableAvatars] = useState<string[] | null>(null);
@@ -70,6 +74,12 @@ export default function Settings() {
7074
.then((r) => r.json())
7175
.then(setCfg);
7276

77+
fetch('/api/admin/me', { headers: { Authorization: `Bearer ${token}` } })
78+
.then((r) => r.json())
79+
.then((data) => {
80+
if (data.username) setAdminUsername(data.username);
81+
});
82+
7383
fetch('/api/avatars')
7484
.then((r) => {
7585
if (!r.ok) throw new Error('Failed to load avatars');
@@ -85,19 +95,61 @@ export default function Settings() {
8595
});
8696
}, [token]);
8797

98+
async function changePassword() {
99+
if (!currentPassword) {
100+
alert('Please enter your current password');
101+
return;
102+
}
103+
if (!newPassword && !newUsername) {
104+
alert('Please enter a new password or username to change');
105+
return;
106+
}
107+
108+
setChangingPassword(true);
109+
try {
110+
const body: Record<string, string> = { currentPassword };
111+
if (newPassword) body.newPassword = newPassword;
112+
if (newUsername && newUsername !== adminUsername) body.newUsername = newUsername;
113+
114+
const response = await fetch('/api/admin/change-password', {
115+
method: 'POST',
116+
headers,
117+
body: JSON.stringify(body),
118+
});
119+
120+
if (!response.ok) {
121+
const error = await response.json();
122+
alert(error.error || 'Failed to change password');
123+
return;
124+
}
125+
126+
alert('Password changed successfully! Please log in again.');
127+
// Log out user
128+
localStorage.removeItem('token');
129+
window.location.href = '/admin/login';
130+
} catch (error) {
131+
console.error('Password change failed:', error);
132+
alert('Failed to change password');
133+
} finally {
134+
setChangingPassword(false);
135+
setCurrentPassword('');
136+
setNewPassword('');
137+
setNewUsername('');
138+
}
139+
}
140+
88141
async function save() {
89142
if (!cfg) return;
90143
setSaving(true);
91144
const payload: Record<string, unknown> = { ...cfg };
92-
if (newPassword.trim()) payload.adminPassword = newPassword.trim();
145+
// Password changes now go through separate endpoint (changePassword function)
93146
await fetch('/api/admin/config', {
94147
method: 'PUT',
95148
headers,
96149
body: JSON.stringify(payload),
97150
});
98151
setSaving(false);
99152
setSaved(true);
100-
setNewPassword('');
101153
setTimeout(() => setSaved(false), 2500);
102154
}
103155

@@ -343,24 +395,44 @@ export default function Settings() {
343395
{/* Admin credentials */}
344396
<div className="card">
345397
<h2 className="mb-4">🔐 Admin Credentials</h2>
398+
<p className="text-sm text-muted mb-4">
399+
Current username: <strong style={{ color: 'var(--text)' }}>{adminUsername || '...'}</strong>
400+
</p>
346401

347-
<Input
348-
label="Admin Username"
349-
value={cfg.adminUsername ?? 'admin'}
350-
onChange={(e) => update('adminUsername', e.target.value)}
351-
/>
352-
353-
<Input
354-
label={
355-
<>
356-
New Password <span className="text-muted">(leave blank to keep current)</span>
357-
</>
358-
}
359-
type="password"
360-
value={newPassword}
361-
placeholder="Enter new password"
362-
onChange={(e) => setNewPassword(e.target.value)}
363-
/>
402+
<div className="form-group">
403+
<Input
404+
label="Current Password (required to make changes)"
405+
type="password"
406+
autoComplete="off"
407+
value={currentPassword}
408+
onChange={(e) => setCurrentPassword(e.target.value)}
409+
placeholder="Enter current password"
410+
/>
411+
<Input
412+
label="New Username (leave blank to keep current)"
413+
autoComplete="off"
414+
value={newUsername}
415+
onChange={(e) => setNewUsername(e.target.value)}
416+
placeholder={adminUsername || 'admin'}
417+
/>
418+
<Input
419+
label="New Password (leave blank to keep current)"
420+
type="password"
421+
autoComplete="new-password"
422+
value={newPassword}
423+
onChange={(e) => setNewPassword(e.target.value)}
424+
placeholder="Enter new password"
425+
noMargin
426+
/>
427+
<button
428+
type="button"
429+
onClick={changePassword}
430+
disabled={changingPassword}
431+
className="btn btn-primary mt-3"
432+
>
433+
{changingPassword ? 'Updating...' : 'Update Credentials'}
434+
</button>
435+
</div>
364436

365437
<div className="alert alert-warn mt-4" style={{ fontSize: '0.85rem' }}>
366438
⚠️ After changing credentials, you&apos;ll be logged out and need to log back in.

client/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ export interface AppConfig {
113113
port: number;
114114
appName: string;
115115
appSubtitle: string;
116-
adminUsername: string;
117116
questionTimeSec: number;
118117
defaultBaseScore: number;
119118
speedBonusMax: number;

config.json renamed to data/config.json

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
"port": 3000,
33
"appName": "Scaleway",
44
"appSubtitle": "hellllllooooooo",
5-
"adminUsername": "admin",
6-
"adminPassword": "admin123",
75
"jwtSecret": "change-this-secret-in-production",
86
"questionTimeSec": 20,
9-
"lobbyTimeoutMin": 30,
107
"defaultBaseScore": 300,
118
"speedBonusMax": 200,
129
"speedBonusMin": 10,
@@ -15,17 +12,5 @@
1512
"streakBonusEnabled": true,
1613
"streakMinimum": 2,
1714
"streakBonusBase": 50,
18-
"resultsAutoAdvanceSec": 0,
19-
"speedBonuses": [
20-
100,
21-
85,
22-
70,
23-
65,
24-
50,
25-
35,
26-
25,
27-
15,
28-
5
29-
],
30-
"defaultSpeedBonus": 25
15+
"resultsAutoAdvanceSec": 0
3116
}

docker-compose.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ services:
1010
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
1111
- JWT_SECRET=${JWT_SECRET:-change-this-secret-in-production}
1212
volumes:
13-
- quizz-data:/data
13+
- ./data:/data
1414
# Avatars live on the host — drop image files here and they appear in the picker
1515
- ./avatars:/data/avatars
1616

@@ -31,6 +31,5 @@ services:
3131
- caddy_config:/config
3232

3333
volumes:
34-
quizz-data:
3534
caddy_data:
3635
caddy_config:

0 commit comments

Comments
 (0)