security fixes

This commit is contained in:
Charmunks 2025-12-07 22:18:23 -05:00
parent 32e5d00861
commit 44089f8a9f
4 changed files with 144 additions and 29 deletions

View file

@ -11,8 +11,9 @@
let hackatimeApiKey = user.hackatime_api_key || '';
let newEmail = '';
let verificationCode = '';
let emailStep = 'input'; // input, verify
let oldEmailCode = '';
let newEmailCode = '';
let emailStep = 'input'; // input, verifyOld, verifyNew
let message = '';
let error = '';
@ -50,7 +51,7 @@
}
}
async function sendEmailCode() {
async function startEmailChange() {
emailMessage = '';
emailError = '';
@ -71,16 +72,16 @@
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: newEmail,
mode: 'update' // Mode doesn't really matter for send logic, but good for logging if added
email: user.email,
mode: 'update'
})
});
const data = await response.json();
if (response.ok) {
emailMessage = 'Verification code sent to ' + newEmail;
emailStep = 'verify';
emailMessage = 'Verification code sent to your current email (' + user.email + ')';
emailStep = 'verifyOld';
} else {
emailError = data.message || 'Failed to send code';
}
@ -90,25 +91,61 @@
}
}
async function verifyEmailChange() {
async function verifyOldEmail() {
emailMessage = '';
emailError = '';
if (!verificationCode) {
if (!oldEmailCode) {
emailError = 'Please enter the verification code';
return;
}
try {
const response = await fetch(`${API_BASE}/users/verify-email-change`, {
const response = await fetch(`${API_BASE}/users/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authorization
'Content-Type': 'application/json'
},
body: JSON.stringify({
newEmail,
verificationCode
email: newEmail,
mode: 'update'
})
});
const data = await response.json();
if (response.ok) {
emailMessage = 'Verification code sent to your new email (' + newEmail + ')';
emailStep = 'verifyNew';
} else {
emailError = data.message || 'Failed to send code to new email';
}
} catch (err) {
emailError = 'An error occurred. Please try again.';
console.error(err);
}
}
async function verifyNewEmailAndUpdate() {
emailMessage = '';
emailError = '';
if (!newEmailCode) {
emailError = 'Please enter the verification code';
return;
}
try {
const response = await fetch(`${API_BASE}/users/update/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
authorization,
email: newEmail,
oldEmailVerificationCode: parseInt(oldEmailCode),
emailVerificationCode: parseInt(newEmailCode)
})
});
@ -117,17 +154,26 @@
if (response.ok) {
emailMessage = 'Email updated successfully';
newEmail = '';
verificationCode = '';
oldEmailCode = '';
newEmailCode = '';
emailStep = 'input';
dispatch('update', data.data);
} else {
emailError = data.message || 'Failed to verify email';
emailError = data.message || 'Failed to update email';
}
} catch (err) {
emailError = 'An error occurred. Please try again.';
console.error(err);
}
}
function cancelEmailChange() {
emailStep = 'input';
oldEmailCode = '';
newEmailCode = '';
emailMessage = '';
emailError = '';
}
</script>
<div class="settings-container">
@ -176,20 +222,36 @@
placeholder="Enter new email"
/>
</div>
<button on:click={sendEmailCode}>Send Verification Code</button>
{:else}
<button on:click={startEmailChange}>Change Email</button>
{:else if emailStep === 'verifyOld'}
<p class="step-info">Step 1 of 2: Verify your current email</p>
<div class="form-group">
<label for="verificationCode">Verification Code</label>
<label for="oldEmailCode">Verification Code (sent to {user.email})</label>
<input
type="text"
id="verificationCode"
bind:value={verificationCode}
id="oldEmailCode"
bind:value={oldEmailCode}
placeholder="Enter 6-digit code"
/>
</div>
<div class="button-group">
<button on:click={verifyEmailChange}>Verify & Update</button>
<button class="secondary" on:click={() => emailStep = 'input'}>Cancel</button>
<button on:click={verifyOldEmail}>Continue</button>
<button class="secondary" on:click={cancelEmailChange}>Cancel</button>
</div>
{:else if emailStep === 'verifyNew'}
<p class="step-info">Step 2 of 2: Verify your new email</p>
<div class="form-group">
<label for="newEmailCode">Verification Code (sent to {newEmail})</label>
<input
type="text"
id="newEmailCode"
bind:value={newEmailCode}
placeholder="Enter 6-digit code"
/>
</div>
<div class="button-group">
<button on:click={verifyNewEmailAndUpdate}>Update Email</button>
<button class="secondary" on:click={cancelEmailChange}>Cancel</button>
</div>
{/if}
@ -302,4 +364,11 @@
display: flex;
align-items: center;
}
.step-info {
font-size: 14px;
color: var(--muted);
margin-bottom: 15px;
font-style: italic;
}
</style>

View file

@ -1,11 +1,13 @@
import express from 'express';
import { updateUser } from '../../utils/user.js';
import { checkEmail } from '../../utils/airtable.js';
import pg from '../../utils/db.js';
const router = express.Router();
router.post('/', async (req, res) => {
try {
const { authorization, email, username, hackatime_api_key } = req.body;
const { authorization, email, oldEmailVerificationCode, emailVerificationCode, username, hackatime_api_key } = req.body;
if (!authorization) {
return res.status(401).json({
@ -13,9 +15,51 @@ router.post('/', async (req, res) => {
message: 'Authorization token is required'
});
}
const user = await pg('users').where('authorization', authorization).first();
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid authorization token'
});
}
const updateData = {};
if (email !== undefined) updateData.email = email;
if (email !== undefined) {
if (!oldEmailVerificationCode) {
return res.status(400).json({
success: false,
message: 'Verification code for current email is required'
});
}
if (!emailVerificationCode) {
return res.status(400).json({
success: false,
message: 'Verification code for new email is required'
});
}
const oldCodeValid = await checkEmail(user.email, oldEmailVerificationCode);
if (!oldCodeValid) {
return res.status(400).json({
success: false,
message: 'Invalid or expired verification code for current email'
});
}
const newCodeValid = await checkEmail(email, emailVerificationCode);
if (!newCodeValid) {
return res.status(400).json({
success: false,
message: 'Invalid or expired verification code for new email'
});
}
updateData.email = email;
}
if (username !== undefined) updateData.username = username;
if (hackatime_api_key !== undefined) updateData.hackatime_api_key = hackatime_api_key;

View file

@ -92,7 +92,6 @@ export const sendEmail = async (email) => {
code: code
});
console.log(`Email verification code created for ${email}`);
return {
success: true,
recordId: record.id,
@ -107,7 +106,8 @@ export const sendEmail = async (email) => {
export const checkEmail = async (email, codeToCheck) => {
try {
const filterFormula = `{email} = "${email}"`;
const sanitizedEmail = email.replace(/["\\]/g, '');
const filterFormula = `{email} = "${sanitizedEmail}"`;
const records = await findRecords('Spaces Emails', filterFormula, [
{ field: 'Time Created', direction: 'desc' }
]);

View file

@ -102,8 +102,9 @@ export const createContainer = async (password, type, authorization) => {
const setupScript = fs.readFileSync(setupScriptPath, "utf8");
const hackatimeApiKey = user.hackatime_api_key || "";
const sanitizedApiKey = hackatimeApiKey.replace(/[^a-zA-Z0-9\-_]/g, '');
const exec = await container.exec({
Cmd: ["bash", "-c", `cat > /tmp/setup.sh << 'EOF'\n${setupScript}\nEOF\nchmod +x /tmp/setup.sh && /tmp/setup.sh '${hackatimeApiKey}' > /app/postinstall.log 2>&1`],
Cmd: ["bash", "-c", `cat > /tmp/setup.sh << 'EOF'\n${setupScript}\nEOF\nchmod +x /tmp/setup.sh && /tmp/setup.sh '${sanitizedApiKey}' > /app/postinstall.log 2>&1`],
AttachStdout: true,
AttachStderr: true,
});
@ -227,8 +228,9 @@ export const startContainer = async (spaceId, authorization) => {
try {
console.log("Setting up Hackatime for code-server container...");
const hackatimeApiKey = user.hackatime_api_key;
const sanitizedApiKey = hackatimeApiKey.replace(/[^a-zA-Z0-9\-_]/g, '');
const exec = await container.exec({
Cmd: ["bash", "-c", `export HACKATIME_API_KEY='${hackatimeApiKey}' && export HACKATIME_API_URL="https://hackatime.hackclub.com/api/hackatime/v1" && export SUCCESS_URL="https://hackatime.hackclub.com//success.txt" && curl -sSL https://hackatime.hackclub.com/hackatime/setup.sh | bash`],
Cmd: ["bash", "-c", `export HACKATIME_API_KEY='${sanitizedApiKey}' && export HACKATIME_API_URL="https://hackatime.hackclub.com/api/hackatime/v1" && export SUCCESS_URL="https://hackatime.hackclub.com//success.txt" && curl -sSL https://hackatime.hackclub.com/hackatime/setup.sh | bash`],
AttachStdout: true,
AttachStderr: true,
});