stem-lab-toolkit 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin.html +4 -2
- package/api.js +31 -2
- package/browse.html +101 -3
- package/change-password.html +168 -0
- package/control-panel.html +3 -2
- package/index.html +13 -6
- package/js/admin.js +25 -1
- package/js/games.js +0 -0
- package/js/main.js +0 -0
- package/package.json +1 -1
- package/tool.html +6 -3
- package/users.json +52 -7
- package/version.txt +1 -1
package/admin.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
var n=performance.getEntriesByType('navigation')[0];
|
|
8
8
|
if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}
|
|
9
9
|
if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}
|
|
10
|
+
if(sessionStorage.getItem('mm_force_change')==='true'){window.location.replace('./change-password.html');return;}
|
|
10
11
|
var rank=sessionStorage.getItem('mm_rank');
|
|
11
12
|
if(rank==='admin'){window.location.replace('./control-panel.html');return;}
|
|
12
13
|
if(rank!=='administrator'){window.location.replace('./browse.html');}
|
|
@@ -101,11 +102,12 @@
|
|
|
101
102
|
<tr>
|
|
102
103
|
<th>Username</th>
|
|
103
104
|
<th>Rank</th>
|
|
105
|
+
<th>Force Password Change</th>
|
|
104
106
|
<th>Actions</th>
|
|
105
107
|
</tr>
|
|
106
108
|
</thead>
|
|
107
109
|
<tbody id="users-tbody">
|
|
108
|
-
<tr><td colspan="
|
|
110
|
+
<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:24px;">Loading…</td></tr>
|
|
109
111
|
</tbody>
|
|
110
112
|
</table>
|
|
111
113
|
</div>
|
|
@@ -137,7 +139,7 @@
|
|
|
137
139
|
</div>
|
|
138
140
|
</div>
|
|
139
141
|
|
|
140
|
-
<script src="./js/admin.js?v=1.0.
|
|
142
|
+
<script src="./js/admin.js?v=1.0.3"></script>
|
|
141
143
|
|
|
142
144
|
</body>
|
|
143
145
|
</html>
|
package/api.js
CHANGED
|
@@ -77,7 +77,7 @@ app.post('/api/login', loginLimiter, async (req, res) => {
|
|
|
77
77
|
rank: user.rank,
|
|
78
78
|
expires: Date.now() + 8 * 3600 * 1000,
|
|
79
79
|
});
|
|
80
|
-
res.json({ success: true, rank: user.rank, token });
|
|
80
|
+
res.json({ success: true, rank: user.rank, token, forcePasswordChange: !!user.forcePasswordChange });
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
// ── Games ──────────────────────────────────────────────────────────────────
|
|
@@ -101,7 +101,7 @@ app.post('/api/games', requireAdmin, (req, res) => {
|
|
|
101
101
|
const VALID_RANKS = ['administrator', 'admin', 'user'];
|
|
102
102
|
|
|
103
103
|
app.get('/api/users', requireAdmin, (_req, res) => {
|
|
104
|
-
const users = readUsers().map(u => ({ username: u.username, rank: u.rank }));
|
|
104
|
+
const users = readUsers().map(u => ({ username: u.username, rank: u.rank, forcePasswordChange: !!u.forcePasswordChange }));
|
|
105
105
|
res.json(users);
|
|
106
106
|
});
|
|
107
107
|
|
|
@@ -152,4 +152,33 @@ app.post('/api/users/reset-password', requireAdmin, async (req, res) => {
|
|
|
152
152
|
res.json({ ok: true });
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
app.post('/api/users/change-password', async (req, res) => {
|
|
156
|
+
const { username, currentPassword, newPassword } = req.body || {};
|
|
157
|
+
if (!username || !currentPassword || !newPassword)
|
|
158
|
+
return res.status(400).json({ error: 'All fields required.' });
|
|
159
|
+
if (newPassword.length < 6)
|
|
160
|
+
return res.status(400).json({ error: 'New password must be at least 6 characters.' });
|
|
161
|
+
const users = readUsers();
|
|
162
|
+
const user = users.find(u => u.username === username);
|
|
163
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
164
|
+
const match = await bcrypt.compare(currentPassword, user.password);
|
|
165
|
+
if (!match) return res.status(401).json({ error: 'Current password is incorrect.' });
|
|
166
|
+
user.password = await bcrypt.hash(newPassword, 12);
|
|
167
|
+
user.forcePasswordChange = false;
|
|
168
|
+
writeUsers(users);
|
|
169
|
+
res.json({ ok: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.post('/api/users/set-force-change', requireAdmin, (req, res) => {
|
|
173
|
+
const { username, force } = req.body || {};
|
|
174
|
+
if (!username || typeof force !== 'boolean')
|
|
175
|
+
return res.status(400).json({ error: 'username and force (boolean) required.' });
|
|
176
|
+
const users = readUsers();
|
|
177
|
+
const user = users.find(u => u.username === username);
|
|
178
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
179
|
+
user.forcePasswordChange = force;
|
|
180
|
+
writeUsers(users);
|
|
181
|
+
res.json({ ok: true });
|
|
182
|
+
});
|
|
183
|
+
|
|
155
184
|
app.listen(PORT, '127.0.0.1', () => console.log(`API listening on 127.0.0.1:${PORT}`));
|
package/browse.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<script>(function(){var n=performance.getEntriesByType('navigation')[0];if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}var r=sessionStorage.getItem('mm_rank');if(!r){window.location.replace('./index.html');}})();</script>
|
|
5
|
+
<script>(function(){var n=performance.getEntriesByType('navigation')[0];if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_force_change')==='true'){window.location.replace('./change-password.html');return;}var r=sessionStorage.getItem('mm_rank');if(!r){window.location.replace('./index.html');}})();</script>
|
|
6
6
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
8
|
<title>MM Games</title>
|
|
@@ -21,10 +21,37 @@
|
|
|
21
21
|
<a href="./browse.html" class="active">Browse</a>
|
|
22
22
|
<a href="./admin.html">Admin</a>
|
|
23
23
|
<a href="./index.html" class="nav-calc-btn" title="Calculator"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="8" y1="18" x2="10" y2="18"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="14" y1="18" x2="16" y2="18"/></svg></a>
|
|
24
|
+
<button id="settings-btn" title="Settings" style="display:flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:6px;background:none;border:none;color:var(--muted);cursor:pointer;transition:background .15s,color .15s;" onmouseover="this.style.background='var(--surface)';this.style.color='var(--text)'" onmouseout="this.style.background='none';this.style.color='var(--muted)'"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg></button>
|
|
24
25
|
</nav>
|
|
25
26
|
</div>
|
|
26
27
|
</header>
|
|
27
28
|
|
|
29
|
+
<!-- Change Password Modal -->
|
|
30
|
+
<div id="chpw-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px);">
|
|
31
|
+
<div style="background:#fff;border-radius:16px;padding:32px 28px 28px;width:320px;box-shadow:0 24px 60px rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
32
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
|
|
33
|
+
<div style="font-size:1rem;font-weight:700;">Change Password</div>
|
|
34
|
+
<button id="chpw-close" style="background:none;border:none;font-size:1.3rem;color:var(--muted);cursor:pointer;line-height:1;padding:2px 6px;">×</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div style="display:flex;flex-direction:column;gap:12px;">
|
|
37
|
+
<div>
|
|
38
|
+
<label style="font-size:0.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:4px;">Current Password</label>
|
|
39
|
+
<input id="chpw-current" type="password" autocomplete="current-password" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:0.9rem;outline:none;font-family:inherit;" placeholder="Current password">
|
|
40
|
+
</div>
|
|
41
|
+
<div>
|
|
42
|
+
<label style="font-size:0.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:4px;">New Password</label>
|
|
43
|
+
<input id="chpw-new" type="password" autocomplete="new-password" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:0.9rem;outline:none;font-family:inherit;" placeholder="New password (min 6 chars)">
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<label style="font-size:0.78rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:4px;">Confirm New Password</label>
|
|
47
|
+
<input id="chpw-confirm" type="password" autocomplete="new-password" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:0.9rem;outline:none;font-family:inherit;" placeholder="Confirm new password">
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div id="chpw-msg" style="font-size:0.82rem;min-height:1.2em;margin:10px 0 4px;"></div>
|
|
51
|
+
<button id="chpw-submit" style="width:100%;background:var(--accent);color:#fff;border:none;border-radius:8px;padding:11px;font-size:0.9rem;font-weight:600;cursor:pointer;font-family:inherit;margin-top:4px;transition:background .15s;">Change Password</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
28
55
|
<main style="padding: 32px 0 64px;">
|
|
29
56
|
<div class="container">
|
|
30
57
|
|
|
@@ -60,8 +87,10 @@
|
|
|
60
87
|
</div>
|
|
61
88
|
</main>
|
|
62
89
|
|
|
63
|
-
<script src="./js/games.js?v=1.0.
|
|
64
|
-
<script src="./js/pin-modal.js?v=1.0.
|
|
90
|
+
<script src="./js/games.js?v=1.0.3"></script>
|
|
91
|
+
<script src="./js/pin-modal.js?v=1.0.3c"></script>
|
|
92
|
+
|
|
93
|
+
<a href="https://forms.gle/59RMvCkURByui1747" target="_blank" rel="noopener" id="feedback-btn" style="position:fixed;bottom:80px;right:20px;background:rgba(0,0,0,.06);color:var(--muted);font-size:0.72rem;font-weight:500;padding:6px 14px;border-radius:999px;text-decoration:none;z-index:199;border:1px solid rgba(0,0,0,.07);transition:all .15s;" onmouseover="this.style.background='rgba(0,0,0,.12)';this.style.color='var(--text)'" onmouseout="this.style.background='rgba(0,0,0,.06)';this.style.color='var(--muted)'">Feedback</a>
|
|
65
94
|
|
|
66
95
|
<button id="scroll-top" onclick="window.scrollTo({top:0,behavior:'smooth'})" aria-label="Back to top" style="position:fixed;bottom:28px;right:28px;width:40px;height:40px;border-radius:50%;background:#2563eb;color:#fff;border:none;font-size:18px;cursor:pointer;box-shadow:0 2px 12px rgba(0,0,0,.18);opacity:0;transform:translateY(8px);transition:opacity .25s,transform .25s;pointer-events:none;z-index:200;display:flex;align-items:center;justify-content:center;">↑</button>
|
|
67
96
|
<script>
|
|
@@ -76,5 +105,74 @@
|
|
|
76
105
|
})();
|
|
77
106
|
</script>
|
|
78
107
|
|
|
108
|
+
<script>
|
|
109
|
+
(function () {
|
|
110
|
+
var overlay = document.getElementById('chpw-overlay');
|
|
111
|
+
var settBtn = document.getElementById('settings-btn');
|
|
112
|
+
var closeBtn = document.getElementById('chpw-close');
|
|
113
|
+
var submitBtn = document.getElementById('chpw-submit');
|
|
114
|
+
var msgEl = document.getElementById('chpw-msg');
|
|
115
|
+
var curEl = document.getElementById('chpw-current');
|
|
116
|
+
var newEl = document.getElementById('chpw-new');
|
|
117
|
+
var conEl = document.getElementById('chpw-confirm');
|
|
118
|
+
|
|
119
|
+
function openModal() {
|
|
120
|
+
curEl.value = ''; newEl.value = ''; conEl.value = '';
|
|
121
|
+
msgEl.textContent = ''; msgEl.style.color = '';
|
|
122
|
+
submitBtn.disabled = false;
|
|
123
|
+
overlay.style.display = 'flex';
|
|
124
|
+
setTimeout(function () { curEl.focus(); }, 50);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function closeModal() { overlay.style.display = 'none'; }
|
|
128
|
+
|
|
129
|
+
settBtn.addEventListener('click', openModal);
|
|
130
|
+
closeBtn.addEventListener('click', closeModal);
|
|
131
|
+
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
|
|
132
|
+
document.addEventListener('keydown', function (e) {
|
|
133
|
+
if (e.key === 'Escape' && overlay.style.display === 'flex') closeModal();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
submitBtn.addEventListener('click', doChange);
|
|
137
|
+
[curEl, newEl, conEl].forEach(function (el) {
|
|
138
|
+
el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doChange(); });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
async function doChange() {
|
|
142
|
+
var username = sessionStorage.getItem('mm_username') || '';
|
|
143
|
+
var cur = curEl.value;
|
|
144
|
+
var nw = newEl.value;
|
|
145
|
+
var con = conEl.value;
|
|
146
|
+
if (!username) { msgEl.textContent = 'Session error — please log in again.'; msgEl.style.color = '#b91c1c'; return; }
|
|
147
|
+
if (!cur || !nw || !con) { msgEl.textContent = 'All fields are required.'; msgEl.style.color = '#b91c1c'; return; }
|
|
148
|
+
if (nw !== con) { msgEl.textContent = 'New passwords do not match.'; msgEl.style.color = '#b91c1c'; return; }
|
|
149
|
+
if (nw.length < 6) { msgEl.textContent = 'New password must be at least 6 characters.'; msgEl.style.color = '#b91c1c'; return; }
|
|
150
|
+
submitBtn.disabled = true;
|
|
151
|
+
msgEl.textContent = '';
|
|
152
|
+
try {
|
|
153
|
+
var res = await fetch('./api/users/change-password', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({ username: username, currentPassword: cur, newPassword: nw }),
|
|
157
|
+
});
|
|
158
|
+
var data = await res.json();
|
|
159
|
+
if (res.ok && data.ok) {
|
|
160
|
+
msgEl.textContent = 'Password changed successfully.';
|
|
161
|
+
msgEl.style.color = '#15803d';
|
|
162
|
+
setTimeout(closeModal, 1500);
|
|
163
|
+
} else {
|
|
164
|
+
msgEl.textContent = data.error || 'Failed to change password.';
|
|
165
|
+
msgEl.style.color = '#b91c1c';
|
|
166
|
+
submitBtn.disabled = false;
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
msgEl.textContent = 'Connection error. Try again.';
|
|
170
|
+
msgEl.style.color = '#b91c1c';
|
|
171
|
+
submitBtn.disabled = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
</script>
|
|
176
|
+
|
|
79
177
|
</body>
|
|
80
178
|
</html>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<script>
|
|
6
|
+
(function(){
|
|
7
|
+
if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}
|
|
8
|
+
if(sessionStorage.getItem('mm_force_change')!=='true'){
|
|
9
|
+
var rank=sessionStorage.getItem('mm_rank');
|
|
10
|
+
if(rank==='administrator'){window.location.replace('./admin.html');}
|
|
11
|
+
else if(rank==='admin'){window.location.replace('./control-panel.html');}
|
|
12
|
+
else{window.location.replace('./browse.html');}
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
</script>
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
17
|
+
<title>Change Password — MM Games</title>
|
|
18
|
+
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
19
|
+
<style>
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
min-height: 100dvh;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
background: #f3f4f6;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
28
|
+
padding: 24px;
|
|
29
|
+
}
|
|
30
|
+
.card {
|
|
31
|
+
background: #fff;
|
|
32
|
+
border-radius: 16px;
|
|
33
|
+
padding: 36px 32px 32px;
|
|
34
|
+
width: 100%;
|
|
35
|
+
max-width: 380px;
|
|
36
|
+
box-shadow: 0 4px 24px rgba(0,0,0,.09);
|
|
37
|
+
border: 1px solid #e5e7eb;
|
|
38
|
+
}
|
|
39
|
+
.icon {
|
|
40
|
+
width: 48px; height: 48px;
|
|
41
|
+
background: #fef3c7;
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
display: flex; align-items: center; justify-content: center;
|
|
44
|
+
font-size: 1.4rem;
|
|
45
|
+
margin-bottom: 16px;
|
|
46
|
+
}
|
|
47
|
+
h1 { font-size: 1.15rem; font-weight: 700; color: #111827; margin-bottom: 6px; }
|
|
48
|
+
.sub { font-size: 0.85rem; color: #6b7280; margin-bottom: 28px; line-height: 1.5; }
|
|
49
|
+
.field { margin-bottom: 14px; }
|
|
50
|
+
label {
|
|
51
|
+
display: block;
|
|
52
|
+
font-size: 0.78rem; font-weight: 600;
|
|
53
|
+
color: #6b7280;
|
|
54
|
+
text-transform: uppercase; letter-spacing: .04em;
|
|
55
|
+
margin-bottom: 5px;
|
|
56
|
+
}
|
|
57
|
+
input {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 10px 13px;
|
|
60
|
+
border: 1.5px solid #e5e7eb;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
font-size: 0.9rem;
|
|
63
|
+
outline: none;
|
|
64
|
+
font-family: inherit;
|
|
65
|
+
transition: border-color .15s, box-shadow .15s;
|
|
66
|
+
color: #111827;
|
|
67
|
+
}
|
|
68
|
+
input:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
|
|
69
|
+
.msg { font-size: 0.82rem; min-height: 1.2em; margin: 8px 0 4px; }
|
|
70
|
+
.msg.error { color: #b91c1c; }
|
|
71
|
+
.msg.ok { color: #15803d; }
|
|
72
|
+
.submit {
|
|
73
|
+
width: 100%;
|
|
74
|
+
background: #2563eb; color: #fff;
|
|
75
|
+
border: none; border-radius: 8px;
|
|
76
|
+
padding: 12px; font-size: 0.9rem; font-weight: 600;
|
|
77
|
+
cursor: pointer; font-family: inherit;
|
|
78
|
+
margin-top: 6px; transition: background .15s;
|
|
79
|
+
}
|
|
80
|
+
.submit:hover { background: #1d4ed8; }
|
|
81
|
+
.submit:disabled { opacity: .55; cursor: not-allowed; }
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<div class="card">
|
|
86
|
+
<div class="icon">🔒</div>
|
|
87
|
+
<h1>Password Change Required</h1>
|
|
88
|
+
<p class="sub">Your account requires a password change before you can continue. Please set a new password below.</p>
|
|
89
|
+
|
|
90
|
+
<div class="field">
|
|
91
|
+
<label>Current Password</label>
|
|
92
|
+
<input id="cur" type="password" autocomplete="current-password" placeholder="Current password">
|
|
93
|
+
</div>
|
|
94
|
+
<div class="field">
|
|
95
|
+
<label>New Password</label>
|
|
96
|
+
<input id="nw" type="password" autocomplete="new-password" placeholder="New password (min 6 chars)">
|
|
97
|
+
</div>
|
|
98
|
+
<div class="field">
|
|
99
|
+
<label>Confirm New Password</label>
|
|
100
|
+
<input id="con" type="password" autocomplete="new-password" placeholder="Confirm new password">
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="msg" id="msg"></div>
|
|
104
|
+
<button class="submit" id="submit-btn">Set New Password</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
(function () {
|
|
109
|
+
var curEl = document.getElementById('cur');
|
|
110
|
+
var newEl = document.getElementById('nw');
|
|
111
|
+
var conEl = document.getElementById('con');
|
|
112
|
+
var msgEl = document.getElementById('msg');
|
|
113
|
+
var subBtn = document.getElementById('submit-btn');
|
|
114
|
+
|
|
115
|
+
curEl.focus();
|
|
116
|
+
|
|
117
|
+
[curEl, newEl, conEl].forEach(function (el) {
|
|
118
|
+
el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doChange(); });
|
|
119
|
+
});
|
|
120
|
+
subBtn.addEventListener('click', doChange);
|
|
121
|
+
|
|
122
|
+
async function doChange() {
|
|
123
|
+
var username = sessionStorage.getItem('mm_username') || '';
|
|
124
|
+
var cur = curEl.value;
|
|
125
|
+
var nw = newEl.value;
|
|
126
|
+
var con = conEl.value;
|
|
127
|
+
|
|
128
|
+
msgEl.className = 'msg error';
|
|
129
|
+
if (!username) { msgEl.textContent = 'Session error — please log in again.'; return; }
|
|
130
|
+
if (!cur || !nw || !con) { msgEl.textContent = 'All fields are required.'; return; }
|
|
131
|
+
if (nw !== con) { msgEl.textContent = 'New passwords do not match.'; return; }
|
|
132
|
+
if (nw.length < 6) { msgEl.textContent = 'New password must be at least 6 characters.'; return; }
|
|
133
|
+
|
|
134
|
+
subBtn.disabled = true;
|
|
135
|
+
msgEl.textContent = '';
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
var res = await fetch('./api/users/change-password', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ username: username, currentPassword: cur, newPassword: nw }),
|
|
142
|
+
});
|
|
143
|
+
var data = await res.json();
|
|
144
|
+
if (res.ok && data.ok) {
|
|
145
|
+
sessionStorage.removeItem('mm_force_change');
|
|
146
|
+
msgEl.className = 'msg ok';
|
|
147
|
+
msgEl.textContent = 'Password changed! Redirecting…';
|
|
148
|
+
var rank = sessionStorage.getItem('mm_rank');
|
|
149
|
+
setTimeout(function () {
|
|
150
|
+
if (rank === 'administrator') window.location.href = './admin.html';
|
|
151
|
+
else if (rank === 'admin') window.location.href = './control-panel.html';
|
|
152
|
+
else window.location.href = './browse.html';
|
|
153
|
+
}, 1000);
|
|
154
|
+
} else {
|
|
155
|
+
msgEl.className = 'msg error';
|
|
156
|
+
msgEl.textContent = data.error || 'Failed to change password.';
|
|
157
|
+
subBtn.disabled = false;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
msgEl.className = 'msg error';
|
|
161
|
+
msgEl.textContent = 'Connection error. Try again.';
|
|
162
|
+
subBtn.disabled = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
</script>
|
|
167
|
+
</body>
|
|
168
|
+
</html>
|
package/control-panel.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
var n=performance.getEntriesByType('navigation')[0];
|
|
8
8
|
if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}
|
|
9
9
|
if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}
|
|
10
|
+
if(sessionStorage.getItem('mm_force_change')==='true'){window.location.replace('./change-password.html');return;}
|
|
10
11
|
var rank=sessionStorage.getItem('mm_rank');
|
|
11
12
|
if(rank==='administrator'){window.location.replace('./admin.html');return;}
|
|
12
13
|
if(rank!=='admin'){window.location.replace('./browse.html');}
|
|
@@ -329,7 +330,7 @@ body{background:var(--bg);color:var(--text);font-family:'Courier New',monospace;
|
|
|
329
330
|
<div class="panel-title">⬡ Deployment Pipeline</div>
|
|
330
331
|
<div class="deploy-controls">
|
|
331
332
|
<select class="deploy-select" id="deploy-branch">
|
|
332
|
-
<option>main (v1.0.
|
|
333
|
+
<option>main (v1.0.3)</option>
|
|
333
334
|
<option>release/3.8-hotfix</option>
|
|
334
335
|
<option>staging/beta-features</option>
|
|
335
336
|
</select>
|
|
@@ -712,7 +713,7 @@ function tprint(html){
|
|
|
712
713
|
itermOut.scrollTop=itermOut.scrollHeight;
|
|
713
714
|
}
|
|
714
715
|
|
|
715
|
-
tprint('<span class="t-ok">MM Systems Shell v1.0.
|
|
716
|
+
tprint('<span class="t-ok">MM Systems Shell v1.0.3</span>');
|
|
716
717
|
tprint('<span class="t-muted">Type <span class="t-cmd">help</span> for available commands.</span>');
|
|
717
718
|
tprint('');
|
|
718
719
|
|
package/index.html
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
<div class="calc-logo-icon">∑</div>
|
|
252
252
|
<span class="calc-logo-text">SciCalc Pro</span>
|
|
253
253
|
</div>
|
|
254
|
-
<span class="calc-mode-indicator" id="mode-indicator">DEG</span><span style="font-size:0.6rem;color:rgba(255,255,255,.2);margin-left:8px;">v1.0.
|
|
254
|
+
<span class="calc-mode-indicator" id="mode-indicator">DEG</span><span style="font-size:0.6rem;color:rgba(255,255,255,.2);margin-left:8px;">v1.0.3</span>
|
|
255
255
|
</div>
|
|
256
256
|
|
|
257
257
|
<div class="calc-display">
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
</div>
|
|
322
322
|
</div>
|
|
323
323
|
|
|
324
|
-
<script src="./js/main.js?v=1.0.
|
|
324
|
+
<script src="./js/main.js?v=1.0.3"></script>
|
|
325
325
|
|
|
326
326
|
<!-- Matrix Easter egg -->
|
|
327
327
|
<div id="matrix-overlay" style="display:none;position:fixed;inset:0;z-index:9999;background:#000;overflow:hidden;">
|
|
@@ -449,6 +449,9 @@ function triggerMatrixEgg() {
|
|
|
449
449
|
|
|
450
450
|
btn.addEventListener('click', doLogin);
|
|
451
451
|
|
|
452
|
+
var API_BASE = window.location.hostname === 'unpkg.com' ? 'https://moshelab.com/api/' : './api/';
|
|
453
|
+
var SITE_BASE = window.location.hostname === 'unpkg.com' ? 'https://moshelab.com/' : './';
|
|
454
|
+
|
|
452
455
|
async function doLogin() {
|
|
453
456
|
var now = Date.now();
|
|
454
457
|
if (now < lockedUntil) {
|
|
@@ -462,7 +465,7 @@ function triggerMatrixEgg() {
|
|
|
462
465
|
btn.disabled = true;
|
|
463
466
|
errEl.textContent = '';
|
|
464
467
|
try {
|
|
465
|
-
var res = await fetch('
|
|
468
|
+
var res = await fetch(API_BASE + 'login', {
|
|
466
469
|
method: 'POST',
|
|
467
470
|
headers: { 'Content-Type': 'application/json' },
|
|
468
471
|
body: JSON.stringify({ username: username, password: password }),
|
|
@@ -472,9 +475,13 @@ function triggerMatrixEgg() {
|
|
|
472
475
|
sessionStorage.setItem('mm_auth', 'true');
|
|
473
476
|
sessionStorage.setItem('mm_rank', data.rank);
|
|
474
477
|
sessionStorage.setItem('mm_token', data.token);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
+
sessionStorage.setItem('mm_username', username);
|
|
479
|
+
if (data.forcePasswordChange) {
|
|
480
|
+
sessionStorage.setItem('mm_force_change', 'true');
|
|
481
|
+
window.location.href = SITE_BASE + 'change-password.html';
|
|
482
|
+
} else if (data.rank === 'administrator') window.location.href = SITE_BASE + 'admin.html';
|
|
483
|
+
else if (data.rank === 'admin') window.location.href = SITE_BASE + 'control-panel.html';
|
|
484
|
+
else window.location.href = SITE_BASE + 'browse.html';
|
|
478
485
|
} else {
|
|
479
486
|
failCount++;
|
|
480
487
|
if (failCount >= 5) {
|
package/js/admin.js
CHANGED
|
@@ -154,7 +154,7 @@ const RANK_CLASS = { administrator: 'rank-administrator', admin: 'rank-admin', u
|
|
|
154
154
|
function renderUsers(users) {
|
|
155
155
|
const tbody = document.getElementById('users-tbody');
|
|
156
156
|
if (!users.length) {
|
|
157
|
-
tbody.innerHTML = '<tr><td colspan="
|
|
157
|
+
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:24px;color:var(--muted);">No users.</td></tr>';
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
tbody.innerHTML = users.map(u => `
|
|
@@ -167,6 +167,12 @@ function renderUsers(users) {
|
|
|
167
167
|
<option value="administrator" ${u.rank==='administrator'?'selected':''}>administrator</option>
|
|
168
168
|
</select>
|
|
169
169
|
</td>
|
|
170
|
+
<td style="text-align:center;">
|
|
171
|
+
<label class="toggle">
|
|
172
|
+
<input type="checkbox" data-action="force-change" data-user="${u.username}" ${u.forcePasswordChange ? 'checked' : ''}>
|
|
173
|
+
<span class="toggle-slider"></span>
|
|
174
|
+
</label>
|
|
175
|
+
</td>
|
|
170
176
|
<td style="display:flex;gap:6px;flex-wrap:wrap;padding:6px 8px;">
|
|
171
177
|
<button class="btn btn-sm btn-primary" data-action="update-rank" data-user="${u.username}">Save Rank</button>
|
|
172
178
|
<button class="btn btn-sm btn-ghost" data-action="reset-pass" data-user="${u.username}">Reset Password</button>
|
|
@@ -255,5 +261,23 @@ document.getElementById('add-user-btn').addEventListener('click', async () => {
|
|
|
255
261
|
}
|
|
256
262
|
});
|
|
257
263
|
|
|
264
|
+
document.getElementById('users-tbody').addEventListener('change', async e => {
|
|
265
|
+
const input = e.target.closest('input[data-action="force-change"]');
|
|
266
|
+
if (!input) return;
|
|
267
|
+
const username = input.dataset.user;
|
|
268
|
+
const force = input.checked;
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch('./api/users/set-force-change', {
|
|
271
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, force }),
|
|
272
|
+
});
|
|
273
|
+
const data = await res.json();
|
|
274
|
+
if (!res.ok) throw new Error(data.error);
|
|
275
|
+
showFeedback('users-feedback', `Force password change ${force ? 'enabled' : 'disabled'} for ${username}.`, 'ok');
|
|
276
|
+
} catch (err) {
|
|
277
|
+
input.checked = !force;
|
|
278
|
+
showFeedback('users-feedback', err.message || 'Failed.', 'error');
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
258
282
|
loadGames();
|
|
259
283
|
loadUsers();
|
package/js/games.js
CHANGED
|
File without changes
|
package/js/main.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/tool.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<script>(function(){var n=performance.getEntriesByType('navigation')[0];if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}var r=sessionStorage.getItem('mm_rank');if(!r){window.location.replace('./index.html');}})();</script>
|
|
5
|
+
<script>(function(){var n=performance.getEntriesByType('navigation')[0];if(n&&n.type==='reload'){sessionStorage.clear();window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_auth')!=='true'){window.location.replace('./index.html');return;}if(sessionStorage.getItem('mm_force_change')==='true'){window.location.replace('./change-password.html');return;}var r=sessionStorage.getItem('mm_rank');if(!r){window.location.replace('./index.html');}})();</script>
|
|
6
6
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
8
|
<title>MM Games</title>
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
<div class="game-actions">
|
|
192
192
|
<button class="fullscreen-btn" id="fs-btn">
|
|
193
193
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
194
|
-
<path d="M8 3H5a2 2 0 00-2 2v1.0.
|
|
194
|
+
<path d="M8 3H5a2 2 0 00-2 2v1.0.3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v1.0.3a2 2 0 002 2h3"/>
|
|
195
195
|
</svg>
|
|
196
196
|
Fullscreen
|
|
197
197
|
</button>
|
|
@@ -219,6 +219,9 @@
|
|
|
219
219
|
});
|
|
220
220
|
})();
|
|
221
221
|
</script>
|
|
222
|
-
<script src="./js/pin-modal.js?v=1.0.
|
|
222
|
+
<script src="./js/pin-modal.js?v=1.0.3c"></script>
|
|
223
|
+
|
|
224
|
+
<a href="https://forms.gle/59RMvCkURByui1747" target="_blank" rel="noopener" style="position:fixed;bottom:20px;right:20px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.4);font-size:0.72rem;font-weight:500;padding:6px 14px;border-radius:999px;text-decoration:none;z-index:199;border:1px solid rgba(255,255,255,.1);transition:all .15s;" onmouseover="this.style.background='rgba(255,255,255,.16)';this.style.color='rgba(255,255,255,.8)'" onmouseout="this.style.background='rgba(255,255,255,.08)';this.style.color='rgba(255,255,255,.4)'">Feedback</a>
|
|
225
|
+
|
|
223
226
|
</body>
|
|
224
227
|
</html>
|
package/users.json
CHANGED
|
@@ -1,17 +1,62 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"username": "owner",
|
|
4
|
-
"password": "$2b$12$
|
|
5
|
-
"rank": "administrator"
|
|
4
|
+
"password": "$2b$12$pv3OKADBaxJdCPEni5KHJexzvuSxQZQU0wdZ03OogqGbg1/UbPjc2",
|
|
5
|
+
"rank": "administrator",
|
|
6
|
+
"forcePasswordChange": false
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"username": "Zach",
|
|
10
|
+
"password": "$2b$12$Mmz/l6N5rbwGOzYVoOMHbeVjQtpn.dSFQxoDhM2EJF9PJXesASxDC",
|
|
11
|
+
"rank": "admin",
|
|
12
|
+
"forcePasswordChange": true
|
|
6
13
|
},
|
|
7
14
|
{
|
|
8
15
|
"username": "Anthony",
|
|
9
|
-
"password": "$2b$12$
|
|
10
|
-
"rank": "admin"
|
|
16
|
+
"password": "$2b$12$GOdPip91LXzin0tm405YR.czj4zVDHcKNugvBDASfuyeNs20g0OkS",
|
|
17
|
+
"rank": "admin",
|
|
18
|
+
"forcePasswordChange": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"username": "Viraj",
|
|
22
|
+
"password": "$2b$12$Tq60W3D7ti8l3LOApZP1keKy8aulE9zv.es0dbOxyQ61BF/g1iFJO",
|
|
23
|
+
"rank": "user",
|
|
24
|
+
"forcePasswordChange": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"username": "Blake",
|
|
28
|
+
"password": "$2b$12$5QX1SvmJVDQuP/Ek3IOmVu85Fh5Gp3rgKNUREZGuONIc5zp7quIb.",
|
|
29
|
+
"rank": "admin",
|
|
30
|
+
"forcePasswordChange": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"username": "Niko",
|
|
34
|
+
"password": "$2b$12$orOTbzLeA.Dzw7E.fRuPd.IE52xQahOF1oX57xVlGmUeNSRvB8sLi",
|
|
35
|
+
"rank": "admin",
|
|
36
|
+
"forcePasswordChange": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"username": "Sebastian",
|
|
40
|
+
"password": "$2b$12$gA9e471aAPqqtkxMkiUsou2SWwMEPHF1.rYTEqarwvFWPHiavJxfW",
|
|
41
|
+
"rank": "administrator",
|
|
42
|
+
"forcePasswordChange": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"username": "Logan",
|
|
46
|
+
"password": "$2b$12$ivEJU6D/nOXXg3WP93TuUusjOJaLFc2uEDvWm9IDRzINMFhoxm5cq",
|
|
47
|
+
"rank": "user",
|
|
48
|
+
"forcePasswordChange": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"username": "Spencer",
|
|
52
|
+
"password": "$2b$12$Ru3Vz/.XeOO.xJRD3NW6rOK5LIpXH4nOFGKLf6W36nU5inUM0s27m",
|
|
53
|
+
"rank": "admin",
|
|
54
|
+
"forcePasswordChange": true
|
|
11
55
|
},
|
|
12
56
|
{
|
|
13
|
-
"username": "
|
|
14
|
-
"password": "$2b$12$
|
|
15
|
-
"rank": "
|
|
57
|
+
"username": "Finn",
|
|
58
|
+
"password": "$2b$12$UKA3fZnBItyMSME0y9Jmsu0eN4iXZmqUAY0gVPvB0bonBvWtCo3Gy",
|
|
59
|
+
"rank": "admin",
|
|
60
|
+
"forcePasswordChange": true
|
|
16
61
|
}
|
|
17
62
|
]
|
package/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v1.0.
|
|
1
|
+
v1.0.3
|