Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 56 additions & 9 deletions public/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -998,19 +998,66 @@
}

document.getElementById('saveProfileBtn').addEventListener('click', async () => {
const socials = {
twitter: document.getElementById('socialTwitter').value.trim(),
instagram: document.getElementById('socialInstagram').value.trim(),
github: document.getElementById('socialGithub').value.trim(),
linkedin: document.getElementById('socialLinkedin').value.trim(),
youtube: document.getElementById('socialYoutube').value.trim(),
tiktok: document.getElementById('socialTiktok').value.trim(),
email: document.getElementById('socialEmail').value.trim()
};

// Auto-prefix http/https for platform links if not empty
const platforms = ['twitter', 'instagram', 'github', 'linkedin', 'youtube', 'tiktok'];
platforms.forEach(p => {
if (socials[p] && !/^https?:\/\//i.test(socials[p])) {
socials[p] = 'https://' + socials[p];
}
});

// Auto-prefix http/https for email if it doesn't look like an email and isn't empty
if (socials.email && !socials.email.includes('@') && !/^https?:\/\//i.test(socials.email)) {
socials.email = 'https://' + socials.email;
}

// Validation patterns
const validationErrors = [];
const patterns = {
twitter: { name: 'Twitter / X', regex: /^https?:\/\/(www\.)?(twitter\.com|x\.com)\/.+/i },
instagram: { name: 'Instagram', regex: /^https?:\/\/(www\.)?instagram\.com\/.+/i },
github: { name: 'GitHub', regex: /^https?:\/\/(www\.)?github\.com\/.+/i },
linkedin: { name: 'LinkedIn', regex: /^https?:\/\/(www\.)?linkedin\.com\/.+/i },
youtube: { name: 'YouTube', regex: /^https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/i },
tiktok: { name: 'TikTok', regex: /^https?:\/\/(www\.)?tiktok\.com\/.+/i }
};

for (const [key, platform] of Object.entries(patterns)) {
if (socials[key]) {
if (!platform.regex.test(socials[key])) {
validationErrors.push(`${platform.name} URL is invalid. It must be a valid link.`);
}
}
}

if (socials.email) {
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(socials.email);
const isUrl = /^https?:\/\/.+/i.test(socials.email);
if (!isEmail && !isUrl) {
validationErrors.push('Email must be a valid email address or a valid URL.');
}
}

if (validationErrors.length > 0) {
showToast(validationErrors[0], 'error');
return;
}

const data = {
name: document.getElementById('inputName').value.trim(),
bio: document.getElementById('inputBio').value.trim(),
avatar: document.getElementById('inputAvatar').value.trim(),
socials: {
twitter: document.getElementById('socialTwitter').value.trim(),
instagram: document.getElementById('socialInstagram').value.trim(),
github: document.getElementById('socialGithub').value.trim(),
linkedin: document.getElementById('socialLinkedin').value.trim(),
youtube: document.getElementById('socialYoutube').value.trim(),
tiktok: document.getElementById('socialTiktok').value.trim(),
email: document.getElementById('socialEmail').value.trim()
}
socials
};
try {
await fetch('/api/profile', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
Expand Down
5 changes: 3 additions & 2 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,11 @@
if (profile.socials) {
Object.entries(profile.socials).forEach(([platform, url]) => {
if (!url || !SOCIAL_ICONS[platform]) return;
const finalUrl = platform === 'email' ? `mailto:${url}` : url;
const isUrl = url.startsWith('http://') || url.startsWith('https://');
const finalUrl = platform === 'email' ? (isUrl ? url : `mailto:${url}`) : url;
const btn = document.createElement('a');
btn.href = finalUrl;
btn.target = platform !== 'email' ? '_blank' : '';
btn.target = (platform !== 'email' || isUrl) ? '_blank' : '';
btn.rel = 'noopener noreferrer';
btn.className = 'social-icon-btn';
btn.title = platform.charAt(0).toUpperCase() + platform.slice(1);
Expand Down
92 changes: 71 additions & 21 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const supabase = require('./db');
const { OAuth2Client } = require('google-auth-library');

Check warning on line 11 in server.js

View workflow job for this annotation

GitHub Actions / Run ESLint

'OAuth2Client' is assigned a value but never used
const rateLimit = require('express-rate-limit');
const app = express();
const PORT = process.env.PORT || 3000;
Expand Down Expand Up @@ -256,7 +256,7 @@
.from('user_links')
.update({ active: shouldBeActive })
.eq('id', link.id);

console.log(`Link "${link.title}" (${link.id}) ${shouldBeActive ? 'activated' : 'deactivated'} by schedule`);
}
}
Expand Down Expand Up @@ -363,7 +363,7 @@
if (!/[0-9]/.test(password)) {
return res.status(400).json({ error: 'Password must contain at least one number.' });
}
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {

Check failure on line 366 in server.js

View workflow job for this annotation

GitHub Actions / Run ESLint

Unnecessary escape character: \/

Check failure on line 366 in server.js

View workflow job for this annotation

GitHub Actions / Run ESLint

Unnecessary escape character: \[
return res.status(400).json({ error: 'Password must contain at least one special character.' });
}

Expand Down Expand Up @@ -418,11 +418,11 @@
const token = generateToken({ id: newUserId, name, username: finalUsername });
setAuthCookie(res, token);

return res.status(201).json({
message: "Registration successful",
name,
username: finalUsername
});
return res.status(201).json({
message: "Registration successful",
name,
username: finalUsername
});
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Server error. Please try again.' });
Expand Down Expand Up @@ -508,7 +508,7 @@

if (credential) {
// Verify Google ID token
const ticket = await googleClient.verifyIdToken({

Check failure on line 511 in server.js

View workflow job for this annotation

GitHub Actions / Run ESLint

'googleClient' is not defined
idToken: credential,
audience: process.env.GOOGLE_CLIENT_ID
});
Expand Down Expand Up @@ -633,11 +633,61 @@
.eq('user_id', req.auth.userId)
.maybeSingle();

const socials = req.body.socials ?? existing?.socials ?? {};

// Validate socials if provided
if (socials && typeof socials === 'object') {
// Auto-prefix http/https for platform links if not empty
const platforms = ['twitter', 'instagram', 'github', 'linkedin', 'youtube', 'tiktok'];
platforms.forEach(p => {
if (socials[p] && typeof socials[p] === 'string') {
socials[p] = socials[p].trim();
if (socials[p] && !/^https?:\/\//i.test(socials[p])) {
socials[p] = 'https://' + socials[p];
}
}
});

// Auto-prefix http/https for email if it doesn't look like an email and isn't empty
if (socials.email && typeof socials.email === 'string') {
socials.email = socials.email.trim();
if (socials.email && !socials.email.includes('@') && !/^https?:\/\//i.test(socials.email)) {
socials.email = 'https://' + socials.email;
}
}

// Validation patterns
const patterns = {
twitter: /^https?:\/\/(www\.)?(twitter\.com|x\.com)\/.+/i,
instagram: /^https?:\/\/(www\.)?instagram\.com\/.+/i,
github: /^https?:\/\/(www\.)?github\.com\/.+/i,
linkedin: /^https?:\/\/(www\.)?linkedin\.com\/.+/i,
youtube: /^https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/i,
tiktok: /^https?:\/\/(www\.)?tiktok\.com\/.+/i
};

for (const [key, regex] of Object.entries(patterns)) {
if (socials[key]) {
if (!regex.test(socials[key])) {
return res.status(400).json({ error: `Invalid URL for ${key}` });
}
}
}

if (socials.email) {
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(socials.email);
const isUrl = /^https?:\/\/.+/i.test(socials.email);
if (!isEmail && !isUrl) {
return res.status(400).json({ error: 'Email must be a valid email address or a valid URL' });
}
}
}

const updates = {
name: req.body.name ?? existing?.name ?? 'Your Name',
bio: req.body.bio ?? existing?.bio ?? '',
avatar: req.body.avatar ?? existing?.avatar ?? '',
socials: req.body.socials ?? existing?.socials ?? {}
socials
};

if (existing) {
Expand Down Expand Up @@ -734,7 +784,7 @@
try {
const categoryId = req.params.id;
const updates = {};

if (req.body?.name !== undefined) updates.name = req.body.name?.toString().trim() || '';
if (req.body?.icon !== undefined) updates.icon = req.body.icon?.toString().trim();
if (req.body?.color !== undefined) updates.color = req.body.color?.toString().trim();
Expand Down Expand Up @@ -855,7 +905,7 @@
app.get('/api/links', requireAuth, async (req, res) => {
// Check if this is for admin panel (needs flat array) or public view (needs grouped)
const grouped = req.query.grouped === 'true';

// Fetch categories
const { data: categories } = await supabase
.from('link_categories')
Expand All @@ -875,11 +925,11 @@
// Map links with schedule status
const mappedLinks = (links || []).map(l => {
let scheduleStatus = 'none';

if (l.is_scheduled) {
const startDate = l.scheduled_start ? new Date(l.scheduled_start) : null;
const endDate = l.scheduled_end ? new Date(l.scheduled_end) : null;

if (startDate && now < startDate) {
scheduleStatus = 'pending';
} else if (endDate && now > endDate) {
Expand Down Expand Up @@ -1030,7 +1080,7 @@
// Bulk update links (enable/disable multiple links)
app.put('/api/links/bulk-update', requireAuth, async (req, res) => {
const { linkIds, active } = req.body;

if (!linkIds || !Array.isArray(linkIds) || linkIds.length === 0) {
return res.status(400).json({ error: 'linkIds array required' });
}
Expand All @@ -1049,10 +1099,10 @@

if (error) throw error;

res.json({
success: true,
res.json({
success: true,
updated: linkIds.length,
active
active
});
} catch (err) {
console.error('Bulk update error:', err);
Expand All @@ -1063,7 +1113,7 @@
// Bulk delete links
app.delete('/api/links/bulk-delete', requireAuth, async (req, res) => {
const { linkIds } = req.body;

if (!linkIds || !Array.isArray(linkIds) || linkIds.length === 0) {
return res.status(400).json({ error: 'linkIds array required' });
}
Expand Down Expand Up @@ -1100,8 +1150,8 @@
}
}

res.json({
success: true,
res.json({
success: true,
deleted: linkIds.length,
undoData: linksToDelete
});
Expand Down Expand Up @@ -1382,14 +1432,14 @@
const now = new Date();
const activeLinks = (links || []).filter(l => {
if (!l.is_scheduled) return true;

const startDate = l.scheduled_start ? new Date(l.scheduled_start) : null;
const endDate = l.scheduled_end ? new Date(l.scheduled_end) : null;

// Check if link is within its scheduled time window
if (startDate && now < startDate) return false; // Not started yet
if (endDate && now > endDate) return false; // Already expired

return true;
});

Expand Down
Loading