stem-lab-toolkit 1.0.2 → 1.0.4
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 +33 -9
- package/browse.html +101 -3
- package/change-password.html +168 -0
- package/control-panel.html +3 -2
- package/index.html +14 -6
- package/js/admin.js +34 -8
- package/js/games.js +3 -1
- 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=
|
|
142
|
+
<script src="./js/admin.js?v=0.0.1"></script>
|
|
141
143
|
|
|
142
144
|
</body>
|
|
143
145
|
</html>
|
package/api.js
CHANGED
|
@@ -13,14 +13,9 @@ const GAMES_FILE = path.join(__dirname, 'games.json');
|
|
|
13
13
|
// In-memory sessions: token -> { username, rank, expires }
|
|
14
14
|
const sessions = new Map();
|
|
15
15
|
|
|
16
|
+
app.set('trust proxy', 1);
|
|
16
17
|
app.use(express.json({ limit: '1mb' }));
|
|
17
|
-
|
|
18
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
19
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
20
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
21
|
-
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
22
|
-
next();
|
|
23
|
-
});
|
|
18
|
+
|
|
24
19
|
|
|
25
20
|
function readUsers() {
|
|
26
21
|
try { return JSON.parse(fs.readFileSync(USERS_FILE, 'utf8')); }
|
|
@@ -77,7 +72,7 @@ app.post('/api/login', loginLimiter, async (req, res) => {
|
|
|
77
72
|
rank: user.rank,
|
|
78
73
|
expires: Date.now() + 8 * 3600 * 1000,
|
|
79
74
|
});
|
|
80
|
-
res.json({ success: true, rank: user.rank, token });
|
|
75
|
+
res.json({ success: true, rank: user.rank, token, forcePasswordChange: !!user.forcePasswordChange });
|
|
81
76
|
});
|
|
82
77
|
|
|
83
78
|
// ── Games ──────────────────────────────────────────────────────────────────
|
|
@@ -101,7 +96,7 @@ app.post('/api/games', requireAdmin, (req, res) => {
|
|
|
101
96
|
const VALID_RANKS = ['administrator', 'admin', 'user'];
|
|
102
97
|
|
|
103
98
|
app.get('/api/users', requireAdmin, (_req, res) => {
|
|
104
|
-
const users = readUsers().map(u => ({ username: u.username, rank: u.rank }));
|
|
99
|
+
const users = readUsers().map(u => ({ username: u.username, rank: u.rank, forcePasswordChange: !!u.forcePasswordChange }));
|
|
105
100
|
res.json(users);
|
|
106
101
|
});
|
|
107
102
|
|
|
@@ -152,4 +147,33 @@ app.post('/api/users/reset-password', requireAdmin, async (req, res) => {
|
|
|
152
147
|
res.json({ ok: true });
|
|
153
148
|
});
|
|
154
149
|
|
|
150
|
+
app.post('/api/users/change-password', async (req, res) => {
|
|
151
|
+
const { username, currentPassword, newPassword } = req.body || {};
|
|
152
|
+
if (!username || !currentPassword || !newPassword)
|
|
153
|
+
return res.status(400).json({ error: 'All fields required.' });
|
|
154
|
+
if (newPassword.length < 6)
|
|
155
|
+
return res.status(400).json({ error: 'New password must be at least 6 characters.' });
|
|
156
|
+
const users = readUsers();
|
|
157
|
+
const user = users.find(u => u.username === username);
|
|
158
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
159
|
+
const match = await bcrypt.compare(currentPassword, user.password);
|
|
160
|
+
if (!match) return res.status(401).json({ error: 'Current password is incorrect.' });
|
|
161
|
+
user.password = await bcrypt.hash(newPassword, 12);
|
|
162
|
+
user.forcePasswordChange = false;
|
|
163
|
+
writeUsers(users);
|
|
164
|
+
res.json({ ok: true });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.post('/api/users/set-force-change', requireAdmin, (req, res) => {
|
|
168
|
+
const { username, force } = req.body || {};
|
|
169
|
+
if (!username || typeof force !== 'boolean')
|
|
170
|
+
return res.status(400).json({ error: 'username and force (boolean) required.' });
|
|
171
|
+
const users = readUsers();
|
|
172
|
+
const user = users.find(u => u.username === username);
|
|
173
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
174
|
+
user.forcePasswordChange = force;
|
|
175
|
+
writeUsers(users);
|
|
176
|
+
res.json({ ok: true });
|
|
177
|
+
});
|
|
178
|
+
|
|
155
179
|
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=
|
|
64
|
-
<script src="./js/pin-modal.js?v=
|
|
90
|
+
<script src="./js/games.js?v=0.0.1"></script>
|
|
91
|
+
<script src="./js/pin-modal.js?v=0.0.1c"></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 (
|
|
333
|
+
<option>main (v0.0.1)</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
|
|
716
|
+
tprint('<span class="t-ok">MM Systems Shell v0.0.1</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;">
|
|
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;">v0.0.1</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=
|
|
324
|
+
<script src="./js/main.js?v=0.0.1"></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,10 @@ function triggerMatrixEgg() {
|
|
|
449
449
|
|
|
450
450
|
btn.addEventListener('click', doLogin);
|
|
451
451
|
|
|
452
|
+
var isCDN = window.location.hostname === 'unpkg.com' || window.location.hostname === 'cdn.jsdelivr.net';
|
|
453
|
+
var API_BASE = isCDN ? 'https://calc.moshelab.com' : '';
|
|
454
|
+
var SITE_BASE = isCDN ? 'https://moshelab.com/' : './';
|
|
455
|
+
|
|
452
456
|
async function doLogin() {
|
|
453
457
|
var now = Date.now();
|
|
454
458
|
if (now < lockedUntil) {
|
|
@@ -462,7 +466,7 @@ function triggerMatrixEgg() {
|
|
|
462
466
|
btn.disabled = true;
|
|
463
467
|
errEl.textContent = '';
|
|
464
468
|
try {
|
|
465
|
-
var res = await fetch('
|
|
469
|
+
var res = await fetch(API_BASE + '/api/login', {
|
|
466
470
|
method: 'POST',
|
|
467
471
|
headers: { 'Content-Type': 'application/json' },
|
|
468
472
|
body: JSON.stringify({ username: username, password: password }),
|
|
@@ -472,9 +476,13 @@ function triggerMatrixEgg() {
|
|
|
472
476
|
sessionStorage.setItem('mm_auth', 'true');
|
|
473
477
|
sessionStorage.setItem('mm_rank', data.rank);
|
|
474
478
|
sessionStorage.setItem('mm_token', data.token);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
479
|
+
sessionStorage.setItem('mm_username', username);
|
|
480
|
+
if (data.forcePasswordChange) {
|
|
481
|
+
sessionStorage.setItem('mm_force_change', 'true');
|
|
482
|
+
window.location.href = SITE_BASE + 'change-password.html';
|
|
483
|
+
} else if (data.rank === 'administrator') window.location.href = SITE_BASE + 'admin.html';
|
|
484
|
+
else if (data.rank === 'admin') window.location.href = SITE_BASE + 'control-panel.html';
|
|
485
|
+
else window.location.href = SITE_BASE + 'browse.html';
|
|
478
486
|
} else {
|
|
479
487
|
failCount++;
|
|
480
488
|
if (failCount >= 5) {
|
package/js/admin.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const API_BASE = (window.location.hostname === 'unpkg.com' || window.location.hostname === 'cdn.jsdelivr.net') ? 'https://calc.moshelab.com' : '';
|
|
2
|
+
|
|
1
3
|
let games = [];
|
|
2
4
|
|
|
3
5
|
function getToken() {
|
|
@@ -10,7 +12,7 @@ function authHeaders() {
|
|
|
10
12
|
|
|
11
13
|
async function loadGames() {
|
|
12
14
|
try {
|
|
13
|
-
const res = await fetch(
|
|
15
|
+
const res = await fetch(`${API_BASE}/api/games/all`, { headers: authHeaders() });
|
|
14
16
|
if (!res.ok) throw new Error();
|
|
15
17
|
games = await res.json();
|
|
16
18
|
} catch {
|
|
@@ -25,7 +27,7 @@ async function loadGames() {
|
|
|
25
27
|
|
|
26
28
|
async function saveGames() {
|
|
27
29
|
try {
|
|
28
|
-
const res = await fetch(
|
|
30
|
+
const res = await fetch(`${API_BASE}/api/games`, {
|
|
29
31
|
method: 'POST',
|
|
30
32
|
headers: authHeaders(),
|
|
31
33
|
body: JSON.stringify({ games }),
|
|
@@ -139,7 +141,7 @@ document.getElementById('table-body').addEventListener('change', async e => {
|
|
|
139
141
|
|
|
140
142
|
async function loadUsers() {
|
|
141
143
|
try {
|
|
142
|
-
const res = await fetch(
|
|
144
|
+
const res = await fetch(`${API_BASE}/api/users`, { headers: authHeaders() });
|
|
143
145
|
if (!res.ok) throw new Error();
|
|
144
146
|
const users = await res.json();
|
|
145
147
|
renderUsers(users);
|
|
@@ -154,7 +156,7 @@ const RANK_CLASS = { administrator: 'rank-administrator', admin: 'rank-admin', u
|
|
|
154
156
|
function renderUsers(users) {
|
|
155
157
|
const tbody = document.getElementById('users-tbody');
|
|
156
158
|
if (!users.length) {
|
|
157
|
-
tbody.innerHTML = '<tr><td colspan="
|
|
159
|
+
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:24px;color:var(--muted);">No users.</td></tr>';
|
|
158
160
|
return;
|
|
159
161
|
}
|
|
160
162
|
tbody.innerHTML = users.map(u => `
|
|
@@ -167,6 +169,12 @@ function renderUsers(users) {
|
|
|
167
169
|
<option value="administrator" ${u.rank==='administrator'?'selected':''}>administrator</option>
|
|
168
170
|
</select>
|
|
169
171
|
</td>
|
|
172
|
+
<td style="text-align:center;">
|
|
173
|
+
<label class="toggle">
|
|
174
|
+
<input type="checkbox" data-action="force-change" data-user="${u.username}" ${u.forcePasswordChange ? 'checked' : ''}>
|
|
175
|
+
<span class="toggle-slider"></span>
|
|
176
|
+
</label>
|
|
177
|
+
</td>
|
|
170
178
|
<td style="display:flex;gap:6px;flex-wrap:wrap;padding:6px 8px;">
|
|
171
179
|
<button class="btn btn-sm btn-primary" data-action="update-rank" data-user="${u.username}">Save Rank</button>
|
|
172
180
|
<button class="btn btn-sm btn-ghost" data-action="reset-pass" data-user="${u.username}">Reset Password</button>
|
|
@@ -187,7 +195,7 @@ document.getElementById('users-tbody').addEventListener('click', async e => {
|
|
|
187
195
|
const sel = row.querySelector('select.rank-select');
|
|
188
196
|
const rank = sel.value;
|
|
189
197
|
try {
|
|
190
|
-
const res = await fetch(
|
|
198
|
+
const res = await fetch(`${API_BASE}/api/users/update`, {
|
|
191
199
|
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, rank }),
|
|
192
200
|
});
|
|
193
201
|
const data = await res.json();
|
|
@@ -203,7 +211,7 @@ document.getElementById('users-tbody').addEventListener('click', async e => {
|
|
|
203
211
|
const newPass = prompt(`New password for "${username}":`);
|
|
204
212
|
if (!newPass) return;
|
|
205
213
|
try {
|
|
206
|
-
const res = await fetch(
|
|
214
|
+
const res = await fetch(`${API_BASE}/api/users/reset-password`, {
|
|
207
215
|
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, password: newPass }),
|
|
208
216
|
});
|
|
209
217
|
const data = await res.json();
|
|
@@ -217,7 +225,7 @@ document.getElementById('users-tbody').addEventListener('click', async e => {
|
|
|
217
225
|
if (action === 'del-user') {
|
|
218
226
|
if (!confirm(`Delete user "${username}"?`)) return;
|
|
219
227
|
try {
|
|
220
|
-
const res = await fetch(
|
|
228
|
+
const res = await fetch(`${API_BASE}/api/users/delete`, {
|
|
221
229
|
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username }),
|
|
222
230
|
});
|
|
223
231
|
const data = await res.json();
|
|
@@ -241,7 +249,7 @@ document.getElementById('add-user-btn').addEventListener('click', async () => {
|
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
try {
|
|
244
|
-
const res = await fetch(
|
|
252
|
+
const res = await fetch(`${API_BASE}/api/users/add`, {
|
|
245
253
|
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, password, rank }),
|
|
246
254
|
});
|
|
247
255
|
const data = await res.json();
|
|
@@ -255,5 +263,23 @@ document.getElementById('add-user-btn').addEventListener('click', async () => {
|
|
|
255
263
|
}
|
|
256
264
|
});
|
|
257
265
|
|
|
266
|
+
document.getElementById('users-tbody').addEventListener('change', async e => {
|
|
267
|
+
const input = e.target.closest('input[data-action="force-change"]');
|
|
268
|
+
if (!input) return;
|
|
269
|
+
const username = input.dataset.user;
|
|
270
|
+
const force = input.checked;
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch(`${API_BASE}/api/users/set-force-change`, {
|
|
273
|
+
method: 'POST', headers: authHeaders(), body: JSON.stringify({ username, force }),
|
|
274
|
+
});
|
|
275
|
+
const data = await res.json();
|
|
276
|
+
if (!res.ok) throw new Error(data.error);
|
|
277
|
+
showFeedback('users-feedback', `Force password change ${force ? 'enabled' : 'disabled'} for ${username}.`, 'ok');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
input.checked = !force;
|
|
280
|
+
showFeedback('users-feedback', err.message || 'Failed.', 'error');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
258
284
|
loadGames();
|
|
259
285
|
loadUsers();
|
package/js/games.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
const API_BASE = (window.location.hostname === 'unpkg.com' || window.location.hostname === 'cdn.jsdelivr.net') ? 'https://calc.moshelab.com' : '';
|
|
2
|
+
|
|
1
3
|
let allGames = [];
|
|
2
4
|
let activeCategory = 'All';
|
|
3
5
|
let searchQuery = '';
|
|
4
6
|
|
|
5
7
|
async function loadGames() {
|
|
6
8
|
try {
|
|
7
|
-
const res = await fetch(
|
|
9
|
+
const res = await fetch(`${API_BASE}/api/games`);
|
|
8
10
|
if (!res.ok) throw new Error();
|
|
9
11
|
allGames = await res.json();
|
|
10
12
|
} catch {
|
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
|
|
194
|
+
<path d="M8 3H5a2 2 0 00-2 2v0.0.1m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v0.0.1a2 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=
|
|
222
|
+
<script src="./js/pin-modal.js?v=0.0.1c"></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
|
-
|
|
1
|
+
v0.0.1
|