mm-math 0.0.0
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 +145 -0
- package/ads.txt +880 -0
- package/api.js +179 -0
- package/assets/favicon.png +0 -0
- package/assets/logo.png +0 -0
- package/browse.html +178 -0
- package/change-password.html +168 -0
- package/control-panel.html +885 -0
- package/css/style.css +615 -0
- package/index.html +577 -0
- package/js/admin.js +285 -0
- package/js/games.js +192 -0
- package/js/main.js +191 -0
- package/js/pin-modal.js +13 -0
- package/js/theme.js +20 -0
- package/package.json +36 -0
- package/tool.html +227 -0
- package/version.txt +1 -0
package/api.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const bcrypt = require('bcrypt');
|
|
3
|
+
const rateLimit = require('express-rate-limit');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
const PORT = 3001;
|
|
10
|
+
const USERS_FILE = path.join(__dirname, 'users.json');
|
|
11
|
+
const GAMES_FILE = path.join(__dirname, 'games.json');
|
|
12
|
+
|
|
13
|
+
// In-memory sessions: token -> { username, rank, expires }
|
|
14
|
+
const sessions = new Map();
|
|
15
|
+
|
|
16
|
+
app.set('trust proxy', 1);
|
|
17
|
+
app.use(express.json({ limit: '1mb' }));
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
function readUsers() {
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(USERS_FILE, 'utf8')); }
|
|
22
|
+
catch { return []; }
|
|
23
|
+
}
|
|
24
|
+
function writeUsers(users) {
|
|
25
|
+
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2));
|
|
26
|
+
}
|
|
27
|
+
function readGames() {
|
|
28
|
+
try { return JSON.parse(fs.readFileSync(GAMES_FILE, 'utf8')); }
|
|
29
|
+
catch { return []; }
|
|
30
|
+
}
|
|
31
|
+
function writeGames(games) {
|
|
32
|
+
fs.writeFileSync(GAMES_FILE, JSON.stringify(games, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getSession(req) {
|
|
36
|
+
const auth = (req.headers.authorization || '').replace('Bearer ', '').trim();
|
|
37
|
+
if (!auth) return null;
|
|
38
|
+
const sess = sessions.get(auth);
|
|
39
|
+
if (!sess || sess.expires < Date.now()) { sessions.delete(auth); return null; }
|
|
40
|
+
return sess;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireAdmin(req, res, next) {
|
|
44
|
+
const sess = getSession(req);
|
|
45
|
+
if (!sess || sess.rank !== 'administrator') return res.status(403).json({ error: 'Forbidden.' });
|
|
46
|
+
req.sess = sess;
|
|
47
|
+
next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const loginLimiter = rateLimit({
|
|
51
|
+
windowMs: 60 * 1000,
|
|
52
|
+
max: 5,
|
|
53
|
+
standardHeaders: true,
|
|
54
|
+
legacyHeaders: false,
|
|
55
|
+
handler: (_req, res) =>
|
|
56
|
+
res.status(429).json({ success: false, error: 'Too many attempts. Try again in 60 seconds.' }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── Auth ───────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
app.post('/api/login', loginLimiter, async (req, res) => {
|
|
62
|
+
const { username, password } = req.body || {};
|
|
63
|
+
if (!username || !password) return res.status(400).json({ success: false });
|
|
64
|
+
const users = readUsers();
|
|
65
|
+
const user = users.find(u => u.username === username);
|
|
66
|
+
if (!user) return res.status(401).json({ success: false });
|
|
67
|
+
const match = await bcrypt.compare(password, user.password);
|
|
68
|
+
if (!match) return res.status(401).json({ success: false });
|
|
69
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
70
|
+
sessions.set(token, {
|
|
71
|
+
username: user.username,
|
|
72
|
+
rank: user.rank,
|
|
73
|
+
expires: Date.now() + 8 * 3600 * 1000,
|
|
74
|
+
});
|
|
75
|
+
res.json({ success: true, rank: user.rank, token, forcePasswordChange: !!user.forcePasswordChange });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Games ──────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
app.get('/api/games', (_req, res) => {
|
|
81
|
+
res.json(readGames().filter(g => g.visible));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.get('/api/games/all', requireAdmin, (_req, res) => {
|
|
85
|
+
res.json(readGames());
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
app.post('/api/games', requireAdmin, (req, res) => {
|
|
89
|
+
if (!Array.isArray(req.body.games)) return res.status(400).json({ error: 'Invalid payload.' });
|
|
90
|
+
writeGames(req.body.games);
|
|
91
|
+
res.json({ ok: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── Users ──────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const VALID_RANKS = ['administrator', 'admin', 'user'];
|
|
97
|
+
|
|
98
|
+
app.get('/api/users', requireAdmin, (_req, res) => {
|
|
99
|
+
const users = readUsers().map(u => ({ username: u.username, rank: u.rank, forcePasswordChange: !!u.forcePasswordChange }));
|
|
100
|
+
res.json(users);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
app.post('/api/users/add', requireAdmin, async (req, res) => {
|
|
104
|
+
const { username, password, rank } = req.body || {};
|
|
105
|
+
if (!username || !password || !VALID_RANKS.includes(rank))
|
|
106
|
+
return res.status(400).json({ error: 'username, password and valid rank required.' });
|
|
107
|
+
const users = readUsers();
|
|
108
|
+
if (users.find(u => u.username === username))
|
|
109
|
+
return res.status(409).json({ error: 'Username already exists.' });
|
|
110
|
+
const hashed = await bcrypt.hash(password, 12);
|
|
111
|
+
users.push({ username, password: hashed, rank });
|
|
112
|
+
writeUsers(users);
|
|
113
|
+
res.json({ ok: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
app.post('/api/users/update', requireAdmin, (req, res) => {
|
|
117
|
+
const { username, rank } = req.body || {};
|
|
118
|
+
if (!username || !VALID_RANKS.includes(rank))
|
|
119
|
+
return res.status(400).json({ error: 'username and valid rank required.' });
|
|
120
|
+
const users = readUsers();
|
|
121
|
+
const user = users.find(u => u.username === username);
|
|
122
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
123
|
+
user.rank = rank;
|
|
124
|
+
writeUsers(users);
|
|
125
|
+
res.json({ ok: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
app.post('/api/users/delete', requireAdmin, (req, res) => {
|
|
129
|
+
const { username } = req.body || {};
|
|
130
|
+
if (!username) return res.status(400).json({ error: 'username required.' });
|
|
131
|
+
const users = readUsers();
|
|
132
|
+
const idx = users.findIndex(u => u.username === username);
|
|
133
|
+
if (idx === -1) return res.status(404).json({ error: 'User not found.' });
|
|
134
|
+
users.splice(idx, 1);
|
|
135
|
+
writeUsers(users);
|
|
136
|
+
res.json({ ok: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.post('/api/users/reset-password', requireAdmin, async (req, res) => {
|
|
140
|
+
const { username, password } = req.body || {};
|
|
141
|
+
if (!username || !password) return res.status(400).json({ error: 'username and password required.' });
|
|
142
|
+
const users = readUsers();
|
|
143
|
+
const user = users.find(u => u.username === username);
|
|
144
|
+
if (!user) return res.status(404).json({ error: 'User not found.' });
|
|
145
|
+
user.password = await bcrypt.hash(password, 12);
|
|
146
|
+
writeUsers(users);
|
|
147
|
+
res.json({ ok: true });
|
|
148
|
+
});
|
|
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
|
+
|
|
179
|
+
app.listen(PORT, '127.0.0.1', () => console.log(`API listening on 127.0.0.1:${PORT}`));
|
|
Binary file
|
package/assets/logo.png
ADDED
|
Binary file
|
package/browse.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
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;}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
|
+
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>MM Games</title>
|
|
9
|
+
<link rel="stylesheet" href="./css/style.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<header class="site-header">
|
|
14
|
+
<div class="header-inner">
|
|
15
|
+
<a href="./browse.html" class="logo-link">
|
|
16
|
+
<img src="./assets/logo.png?v=new" alt="" class="logo-img" onerror="this.style.display='none'">
|
|
17
|
+
<span>MM Games</span>
|
|
18
|
+
|
|
19
|
+
</a>
|
|
20
|
+
<nav class="site-nav">
|
|
21
|
+
<a href="./browse.html" class="active">Browse</a>
|
|
22
|
+
<a href="./admin.html">Admin</a>
|
|
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>
|
|
25
|
+
</nav>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
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
|
+
|
|
55
|
+
<main style="padding: 32px 0 64px;">
|
|
56
|
+
<div class="container">
|
|
57
|
+
|
|
58
|
+
<!-- Search + filters -->
|
|
59
|
+
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:16px; margin-bottom:28px;">
|
|
60
|
+
<div class="search-wrap" style="flex:1; min-width:200px;">
|
|
61
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
62
|
+
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
63
|
+
</svg>
|
|
64
|
+
<input class="search-input" id="search" type="search" placeholder="Search games…" autocomplete="off">
|
|
65
|
+
</div>
|
|
66
|
+
<div class="category-filters" id="cat-filters">
|
|
67
|
+
<button class="cat-btn active" data-cat="All">All</button>
|
|
68
|
+
<button class="cat-btn" data-cat="Action">Action</button>
|
|
69
|
+
<button class="cat-btn" data-cat="Adventure">Adventure</button>
|
|
70
|
+
<button class="cat-btn" data-cat="Sports">Sports</button>
|
|
71
|
+
<button class="cat-btn" data-cat="Puzzle">Puzzle</button>
|
|
72
|
+
<button class="cat-btn" data-cat="Strategy">Strategy</button>
|
|
73
|
+
<button class="cat-btn" data-cat="Classic">Classic</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Game of the Day -->
|
|
78
|
+
<div class="section" id="gotd-section"></div>
|
|
79
|
+
|
|
80
|
+
<!-- Game grid -->
|
|
81
|
+
<div class="section">
|
|
82
|
+
<div class="section-title" id="grid-label">All Games</div>
|
|
83
|
+
<div class="game-grid" id="game-grid"></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
</div>
|
|
88
|
+
</main>
|
|
89
|
+
|
|
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>
|
|
94
|
+
|
|
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>
|
|
96
|
+
<script>
|
|
97
|
+
(function(){
|
|
98
|
+
var btn=document.getElementById('scroll-top');
|
|
99
|
+
window.addEventListener('scroll',function(){
|
|
100
|
+
var show=window.scrollY>300;
|
|
101
|
+
btn.style.opacity=show?'1':'0';
|
|
102
|
+
btn.style.transform=show?'translateY(0)':'translateY(8px)';
|
|
103
|
+
btn.style.pointerEvents=show?'auto':'none';
|
|
104
|
+
},{passive:true});
|
|
105
|
+
})();
|
|
106
|
+
</script>
|
|
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
|
+
|
|
177
|
+
</body>
|
|
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>
|