fix(auth): parse urlencoded form and query fallback for account deletion confirmation

This commit is contained in:
ssbg04 committed 2026-09-17 18:38:08 +08:00
1 parent df60ee9c39
commit b737aede64
3 files changed
+144 -99

No files matched your search

+1
View File
@@ -39,6 +39,7 @@ initSchema();
// Middleware // Middleware
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev')); app.use(morgan('dev'));
app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
+112 -96
View File
@@ -891,98 +891,98 @@ exports.requestAccountDeletion = async (req, res) => {
} }
}; };
const renderDeletionPage = ({ title, contentHtml, isError = false }) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} — Talisay Integrated School RMS</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
color: #0f172a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
.card {
background: #ffffff;
width: 100%;
max-width: 480px;
border-radius: 20px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.03);
border: 1px solid #e2e8f0;
overflow: hidden;
}
.header {
background: ${isError ? 'linear-gradient(135deg, #b91c1c 0%, #991b1b 100%)' : 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'};
color: #ffffff;
padding: 28px 24px;
text-align: center;
}
.header-logo { font-size: 36px; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: 800; letter-spacing: 0.3px; }
.header p { font-size: 13px; opacity: 0.9; margin-top: 4px; }
.body { padding: 32px 28px; }
.danger-badge {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
font-size: 13px;
color: #991b1b;
line-height: 1.6;
}
.btn-danger {
width: 100%;
background: #dc2626;
color: #ffffff;
border: none;
border-radius: 10px;
padding: 14px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.btn-danger:hover { background: #b91c1c; transform: translateY(-1px); }
.footer-text { margin-top: 20px; font-size: 12px; text-align: center; color: #94a3b8; }
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="header-logo">&#128465;</div>
<h1>${title}</h1>
<p>Talisay Integrated School &bull; Record Management System</p>
</div>
<div class="body">
${contentHtml}
<div class="footer-text">This link is single-use and time-limited.</div>
</div>
</div>
</body>
</html>
`;
};
// GET /api/auth/confirm-delete-account-web // GET /api/auth/confirm-delete-account-web
exports.confirmDeleteAccountWebPage = (req, res) => { exports.confirmDeleteAccountWebPage = (req, res) => {
const token = req.query.token; const token = req.query.token;
const renderPage = ({ title, contentHtml, isError = false }) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} — Talisay Integrated School RMS</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
color: #0f172a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
.card {
background: #ffffff;
width: 100%;
max-width: 480px;
border-radius: 20px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.03);
border: 1px solid #e2e8f0;
overflow: hidden;
}
.header {
background: ${isError ? 'linear-gradient(135deg, #b91c1c 0%, #991b1b 100%)' : 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'};
color: #ffffff;
padding: 28px 24px;
text-align: center;
}
.header-logo { font-size: 36px; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: 800; letter-spacing: 0.3px; }
.header p { font-size: 13px; opacity: 0.9; margin-top: 4px; }
.body { padding: 32px 28px; }
.danger-badge {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
font-size: 13px;
color: #991b1b;
line-height: 1.6;
}
.btn-danger {
width: 100%;
background: #dc2626;
color: #ffffff;
border: none;
border-radius: 10px;
padding: 14px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.btn-danger:hover { background: #b91c1c; transform: translateY(-1px); }
.footer-text { margin-top: 20px; font-size: 12px; text-align: center; color: #94a3b8; }
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="header-logo">&#128465;</div>
<h1>${title}</h1>
<p>Talisay Integrated School &bull; Record Management System</p>
</div>
<div class="body">
${contentHtml}
<div class="footer-text">This link is single-use and time-limited.</div>
</div>
</div>
</body>
</html>
`;
};
if (!token) { if (!token) {
return res.status(400).send(renderPage({ return res.status(400).send(renderDeletionPage({
title: 'Invalid Request', title: 'Invalid Request',
isError: true, isError: true,
contentHtml: ` contentHtml: `
@@ -1002,7 +1002,7 @@ exports.confirmDeleteAccountWebPage = (req, res) => {
`).get(token); `).get(token);
if (!record) { if (!record) {
return res.status(404).send(renderPage({ return res.status(404).send(renderDeletionPage({
title: 'Invalid Link', title: 'Invalid Link',
isError: true, isError: true,
contentHtml: ` contentHtml: `
@@ -1014,7 +1014,7 @@ exports.confirmDeleteAccountWebPage = (req, res) => {
} }
if (new Date(record.expires_at) < new Date()) { if (new Date(record.expires_at) < new Date()) {
return res.status(410).send(renderPage({ return res.status(410).send(renderDeletionPage({
title: 'Link Expired', title: 'Link Expired',
isError: true, isError: true,
contentHtml: ` contentHtml: `
@@ -1027,7 +1027,7 @@ exports.confirmDeleteAccountWebPage = (req, res) => {
const fullName = [record.first_name, record.last_name].filter(Boolean).join(' '); const fullName = [record.first_name, record.last_name].filter(Boolean).join(' ');
return res.send(renderPage({ return res.send(renderDeletionPage({
title: 'Confirm Account Deletion', title: 'Confirm Account Deletion',
contentHtml: ` contentHtml: `
<div class="danger-badge"> <div class="danger-badge">
@@ -1036,7 +1036,7 @@ exports.confirmDeleteAccountWebPage = (req, res) => {
Email: <strong>${record.email}</strong><br><br> Email: <strong>${record.email}</strong><br><br>
All your credentials and active sessions will be permanently revoked. This action cannot be reversed. All your credentials and active sessions will be permanently revoked. This action cannot be reversed.
</div> </div>
<form method="POST" action="/api/auth/confirm-delete-account"> <form method="POST" action="/api/auth/confirm-delete-account?token=${encodeURIComponent(token)}">
<input type="hidden" name="token" value="${token}"> <input type="hidden" name="token" value="${token}">
<button type="submit" class="btn-danger">Yes, Permanently Delete My Account</button> <button type="submit" class="btn-danger">Yes, Permanently Delete My Account</button>
</form> </form>
@@ -1044,7 +1044,7 @@ exports.confirmDeleteAccountWebPage = (req, res) => {
})); }));
} catch (error) { } catch (error) {
console.error('confirmDeleteAccountWebPage error:', error); console.error('confirmDeleteAccountWebPage error:', error);
return res.status(500).send(renderPage({ return res.status(500).send(renderDeletionPage({
title: 'System Error', title: 'System Error',
isError: true, isError: true,
contentHtml: `<div class="danger-badge">An error occurred while processing the confirmation request.</div>`, contentHtml: `<div class="danger-badge">An error occurred while processing the confirmation request.</div>`,
@@ -1059,7 +1059,11 @@ exports.confirmDeleteAccount = (req, res) => {
if (!token) { if (!token) {
if (isHtml) { if (isHtml) {
return res.status(400).send('<h3>Invalid Request: Token missing.</h3>'); return res.status(400).send(renderDeletionPage({
title: 'Invalid Request',
isError: true,
contentHtml: `<div class="danger-badge"><strong>Invalid Request:</strong> Verification token is missing.</div>`,
}));
} }
return res.status(400).json({ message: 'Token is required' }); return res.status(400).json({ message: 'Token is required' });
} }
@@ -1074,19 +1078,31 @@ exports.confirmDeleteAccount = (req, res) => {
if (!record) { if (!record) {
const msg = 'Invalid or already used deletion token.'; const msg = 'Invalid or already used deletion token.';
if (isHtml) return res.status(404).send(`<h3>${msg}</h3>`); if (isHtml) return res.status(404).send(renderDeletionPage({
title: 'Link Invalid or Used',
isError: true,
contentHtml: `<div class="danger-badge"><strong>Link Invalid:</strong> ${msg}</div>`,
}));
return res.status(404).json({ message: msg }); return res.status(404).json({ message: msg });
} }
if (new Date(record.expires_at) < new Date()) { if (new Date(record.expires_at) < new Date()) {
const msg = 'Deletion confirmation link has expired.'; const msg = 'Deletion confirmation link has expired.';
if (isHtml) return res.status(410).send(`<h3>${msg}</h3>`); if (isHtml) return res.status(410).send(renderDeletionPage({
title: 'Link Expired',
isError: true,
contentHtml: `<div class="danger-badge"><strong>Expired:</strong> ${msg}</div>`,
}));
return res.status(410).json({ message: msg }); return res.status(410).json({ message: msg });
} }
if (record.is_hidden === 1) { if (record.is_hidden === 1) {
const msg = 'Developer super administrator account cannot be deleted.'; const msg = 'Developer super administrator account cannot be deleted.';
if (isHtml) return res.status(403).send(`<h3>${msg}</h3>`); if (isHtml) return res.status(403).send(renderDeletionPage({
title: 'Action Prohibited',
isError: true,
contentHtml: `<div class="danger-badge"><strong>Action Prohibited:</strong> ${msg}</div>`,
}));
return res.status(403).json({ message: msg }); return res.status(403).json({ message: msg });
} }
+31 -3
View File
@@ -563,7 +563,7 @@ Files backup/restore - mirror mode or zip -->
# STUDENT SCREEN # STUDENT SCREEN
- in windows app, make it modal the add student screen --> - in windows app, make it modal the add student screen -->
# SETTINGS SCREEN <!-- # SETTINGS SCREEN
- add deletion of the account, add at the very bottom of the settings -> sends an email where user needs to click a link to delete the account, add a hidden super admin account and hidden from the frontend only developers can access this the role is admin but hidden to the system only developer can use this account to manage the users - add deletion of the account, add at the very bottom of the settings -> sends an email where user needs to click a link to delete the account, add a hidden super admin account and hidden from the frontend only developers can access this the role is admin but hidden to the system only developer can use this account to manage the users
# ARCHIVE SCREEN # ARCHIVE SCREEN
@@ -573,9 +573,37 @@ Files backup/restore - mirror mode or zip -->
- download success dialog, add notes and its download path location - download success dialog, add notes and its download path location
# STUDENT, DOCUMENT, AND ARCHIVE SCREEN # STUDENT, DOCUMENT, AND ARCHIVE SCREEN
- icon multi select animation, the icon clicked animated to checked icon, remove the checkbox icon next to icon - icon multi select animation, the icon clicked animated to checked icon, remove the checkbox icon next to icon -->
--- # DASHBOARD SCREEN
- in storage analytics, use fewer color, make it simple, and list 3-5 files make it dropdown if more than this list limit
- top bottom fade, show the top fade if it has a component on top, same logic in bottom fade
# STUDENT SCREEN
- in windows app, make it modal the edit student screen
- use card for student table in windows app, make sure hover of doc status working and action button
- add right click (windows app) and hold feature (android app) -> dropdown with action label (view details, edit (active student details tab), add enrollment (active enrollment tab), view documents/archive folder (if not enrolled, shows archive, same functionality of the action button of open document button), change status [graduate, transfer, drop, set inactive] )
- in android app add dropdown to show the doc status
- remove the multi select button on screen header
- make the add studen FAB into icon button right side of filter button, make the color attract attention dont add background and border color
- add fade at top and bottom same from "dashboard screen"
- top bottom fade, show the top fade if it has a component on top, same logic in bottom fade
# ALL SCREENS PAGINATION
- redesign the pagination UI based on this picture F:\SumbrerongBato\tis_rms_server\Pictures\Screenshot 2026-09-17 181328.png
- Desktop: Option 02 (Full width, clear text, page dropdown).
- Mobile: Option 01 (or compact: ‹ 1 2 3 › + Page [ 1 ▾ ]).
# DOCUMENT SCREEN
- all FAB in android move to the screen header title like in dashboard screen
- optimize mini preview/thumbnails (backend & frontend) for fast loading:
- backend: generate & cache resized lightweight thumbnails (~250px WebP/JPEG) + HTTP Cache-Control headers instead of serving raw original files
- frontend: add cacheWidth/cacheHeight image memory decode optimization to eliminate scroll lag
- do not use wolt modal for "upload & OCR modal" and print queue modal after this remove the package of wolt modal sheet
- in print queue, when printing Excel file(s), show dialog asking to automatically convert them to PDF and proceed with printing instead of showing a blocking error dialog
---
# DONT DO # DONT DO
<!-- - documents screen -> add how many files can be uploaded into a document requirement, the current limitation is only 1, so we can dynamically choose number files that can be uploaded into that document, update both frontend and backend, but first give me suggestion where will put the setting of this, ask me first --> <!-- - documents screen -> add how many files can be uploaded into a document requirement, the current limitation is only 1, so we can dynamically choose number files that can be uploaded into that document, update both frontend and backend, but first give me suggestion where will put the setting of this, ask me first -->