dev1/backend/server1.js

893 lines
27 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import path from 'path';
import bodyParser from 'body-parser';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { initEncryption, encrypt, decrypt, verifyCanary, SENTINEL } from './shared/crypto/encryption.js';
import pool from './config/mysqlPool.js';
import sqlite3 from 'sqlite3';
import crypto from 'crypto';
import sgMail from '@sendgrid/mail';
import rateLimit from 'express-rate-limit';
import { readFile } from 'fs/promises'; // ← needed for /healthz
const CANARY_SQL = `
CREATE TABLE IF NOT EXISTS encryption_canary (
id TINYINT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, '..');
const env = process.env.NODE_ENV?.trim() || 'development';
const envPath = path.resolve(rootPath, `.env.${env}`);
dotenv.config({ path: envPath, override: false });
const {
JWT_SECRET,
CORS_ALLOWED_ORIGINS,
SERVER1_PORT = 5000
} = process.env;
if (!JWT_SECRET) {
console.error('FATAL: JWT_SECRET missing aborting startup');
process.exit(1);
}
if (!CORS_ALLOWED_ORIGINS) {
console.error('FATAL: CORS_ALLOWED_ORIGINS missing aborting startup');
process.exit(1);
}
// Unwrap / verify DEK and seed canary before serving traffic
try {
await initEncryption(); // <-- wrap in try/catch
const db = pool.raw || pool; // <-- bypass DAO wrapper for canary ops
// quick connectivity check
await db.query('SELECT 1');
// ① ensure table
await db.query(CANARY_SQL);
// ② insert sentinel on first run (ignore if exists)
await db.query(
'INSERT IGNORE INTO encryption_canary (id, value) VALUES (1, ?)',
[encrypt(SENTINEL)]
);
// ③ read back & verify
const [rows] = await db.query(
'SELECT value FROM encryption_canary WHERE id = 1 LIMIT 1'
);
const plaintext = decrypt(rows[0]?.value || '');
if (plaintext !== SENTINEL) {
throw new Error('DEK mismatch with database sentinel');
}
console.log('[ENCRYPT] DEK verified against canary proceeding');
} catch (err) {
console.error('FATAL:', err?.message || err);
process.exit(1);
}
// …the rest of your server: app = express(), middlewares, routes, app.listen()
/* ────────────────────────────────────────────────────────────────
Express app & middleware
---------------------------------------------------------------- */
const app = express();
const PORT = process.env.SERVER1_PORT || 5000;
/* ─── Allowed origins for CORS (comma-separated in env) ──────── */
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
.split(',')
.map(o => o.trim())
.filter(Boolean);
app.disable('x-powered-by');
app.use(bodyParser.json());
app.use(express.json());
app.use(
helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
})
);
function fprPathFromEnv() {
const p = (process.env.DEK_PATH || '').trim();
return p ? path.join(path.dirname(p), 'dek.fpr') : null;
}
// 1) Liveness: process is up and event loop responsive
app.get('/livez', (_req, res) => res.type('text').send('OK'));
// 2) Readiness: crypto + canary are good
app.get('/readyz', async (_req, res) => {
try {
await initEncryption(); // load/unlock DEK
await verifyCanary(pool); // DB + decrypt sentinel
return res.type('text').send('OK');
} catch (e) {
console.error('[READYZ]', e.message);
return res.status(500).type('text').send('FAIL');
}
});
// 3) Health: detailed JSON (you can curl this to “see everything”)
app.get('/healthz', async (_req, res) => {
const out = {
service: process.env.npm_package_name || 'server',
version: process.env.IMG_TAG || null,
uptime_s: Math.floor(process.uptime()),
now: new Date().toISOString(),
checks: {
live: { ok: true }, // if we reached here, process is up
crypto: { ok: false, fp: null },
db: { ok: false, ping_ms: null },
canary: { ok: false }
}
};
// crypto / DEK
try {
await initEncryption();
out.checks.crypto.ok = true;
const p = fprPathFromEnv();
if (p) {
try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); }
catch { /* fp optional */ }
}
} catch (e) {
out.checks.crypto.error = e.message;
}
// DB ping
const t0 = Date.now();
try {
await pool.query('SELECT 1');
out.checks.db.ok = true;
out.checks.db.ping_ms = Date.now() - t0;
} catch (e) {
out.checks.db.error = e.message;
}
// canary
try {
await verifyCanary(pool);
out.checks.canary.ok = true;
} catch (e) {
out.checks.canary.error = e.message;
}
const ready = out.checks.crypto.ok && out.checks.db.ok && out.checks.canary.ok;
return res.status(ready ? 200 : 503).json(out);
});
// Password reset token table (MySQL)
try {
const db = pool.raw || pool;
await db.query(`
CREATE TABLE IF NOT EXISTS password_resets (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at BIGINT NOT NULL,
used_at BIGINT NULL,
created_at BIGINT NOT NULL,
ip VARCHAR(64) NULL,
KEY (email),
KEY (token_hash),
KEY (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
console.log('[AUTH] password_resets table ready');
} catch (e) {
console.error('FATAL creating password_resets table:', e?.message || e);
process.exit(1);
}
// Enable CORS with dynamic origin checking
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.error('Blocked by CORS:', origin);
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: [
'Authorization',
'Content-Type',
'Accept',
'Origin',
'X-Requested-With',
],
credentials: true,
})
);
// Handle preflight requests explicitly
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Content-Type, Accept, Origin, X-Requested-With'
);
res.status(200).end();
});
// Add HTTP headers for security
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
res.setHeader('Content-Security-Policy', "default-src 'self';");
res.removeHeader('X-Powered-By');
next();
});
// Force Content-Type to application/json on all responses
app.use((req, res, next) => {
res.setHeader('Content-Type', 'application/json');
next();
});
const pwBurstLimiter = rateLimit({
windowMs: 30 * 1000, // 1 every 30s
max: 1,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip,
});
const pwDailyLimiter = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // per day
max: 5,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip,
});
async function setPasswordByEmail(email, bcryptHash) {
// Update via join to the profiles email
const sql = `
UPDATE user_auth ua
JOIN user_profile up ON up.id = ua.user_id
SET ua.hashed_password = ?
WHERE up.email = ?
LIMIT 1
`;
const [r] = await (pool.raw || pool).query(sql, [bcryptHash, email]);
return !!r?.affectedRows;
}
// ----- Password reset config (zero-config dev mode) -----
const RESET_CONFIG = {
// accept both spellings just in case
BASE_URL: process.env.APTIVA_API_BASE || process.env.APTIV_API_BASE || 'http://localhost:3000',
FROM: 'no-reply@aptivaai.com', // edit here if you want
TTL_MIN: 60, // edit here if you want
};
// --- SendGrid config (safe + simple) ---
const SENDGRID_KEY = String(
process.env.SUPPORT_SENDGRID_API_KEY ||
process.env.SENDGRID_API_KEY || // optional fallback
process.env.SUPPORT_SENDGRID_API_KEY_dev || // if you exported _dev
''
).trim();
const SENDGRID_ENABLED = SENDGRID_KEY.length > 0;
if (SENDGRID_ENABLED) {
sgMail.setApiKey(SENDGRID_KEY);
console.log('[MAIL] SendGrid enabled');
} else {
console.log('[MAIL] SendGrid disabled — will log reset links only');
}
// Change password (must be logged in)
app.post('/api/auth/password-change', pwBurstLimiter, async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Auth required' });
let userId;
try {
({ id: userId } = jwt.verify(token, JWT_SECRET));
} catch {
return res.status(401).json({ error: 'Invalid or expired token' });
}
const { currentPassword, newPassword } = req.body || {};
if (
typeof currentPassword !== 'string' ||
typeof newPassword !== 'string' ||
newPassword.length < 8
) {
return res.status(400).json({ error: 'New password must be at least 8 characters' });
}
if (newPassword === currentPassword) {
return res.status(400).json({ error: 'New password must be different' });
}
// fetch existing hash
const [rows] = await (pool.raw || pool).query(
'SELECT hashed_password FROM user_auth WHERE user_id = ? LIMIT 1',
[userId]
);
const existing = rows?.[0];
if (!existing) return res.status(404).json({ error: 'Account not found' });
// verify old password
const ok = await bcrypt.compare(currentPassword, existing.hashed_password);
if (!ok) return res.status(403).json({ error: 'Current password is incorrect' });
// write new hash
const newHash = await bcrypt.hash(newPassword, 10);
await (pool.raw || pool).query(
'UPDATE user_auth SET hashed_password = ? WHERE user_id = ? LIMIT 1',
[newHash, userId]
);
return res.status(200).json({ ok: true });
} catch (e) {
console.error('[password-change]', e?.message || e);
return res.status(500).json({ error: 'Server error' });
}
});
/*Password reset request (MySQL)*/
app.post('/api/auth/password-reset/request', pwBurstLimiter, pwDailyLimiter, async (req, res) => {
try {
const email = String(req.body?.email || '').trim().toLowerCase();
// Always respond generically to avoid enumeration
const generic = () => res.status(200).json({ ok: true });
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return generic();
// Check if email exists (generic response regardless)
let exists = false;
try {
const q = `
SELECT ua.user_id
FROM user_auth ua
JOIN user_profile up ON up.id = ua.user_id
WHERE up.email = ?
LIMIT 1
`;
const [rows] = await (pool.raw || pool).query(q, [email]);
exists = !!rows?.length;
} catch { /* ignore */ }
// Only send if (a) we have SendGrid configured AND (b) email exists
if (exists) {
const token = crypto.randomBytes(32).toString('hex');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const now = Date.now();
const expiresAt = now + RESET_CONFIG.TTL_MIN * 60 * 1000;
await (pool.raw || pool).query(
`INSERT INTO password_resets (email, token_hash, expires_at, created_at, ip)
VALUES (?, ?, ?, ?, ?)`,
[email, tokenHash, expiresAt, now, req.ip]
);
const base = RESET_CONFIG.BASE_URL.replace(/\/+$/, '');
const link = `${base}/reset-password/${token}`;
const text =
`We received a request to reset your Aptiva password.
If you requested this, use the link below (valid for ${RESET_CONFIG.TTL_MIN} minutes):
${link}
If you didnt request it, you can ignore this email.`;
if (SENDGRID_ENABLED) {
await sgMail.send({
to: email,
from: RESET_CONFIG.FROM,
subject: 'Reset your Aptiva password',
text,
html: `<pre style="font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap">${text}</pre>`
});
} else {
// Zero-config dev mode: just log the link so you can click it
console.log(`[DEV] Password reset link for ${email}: ${link}`);
}
}
return generic();
} catch (e) {
console.error('[password-reset/request]', e?.message || e);
// Still generic
return res.status(200).json({ ok: true });
}
});
app.post('/api/auth/password-reset/confirm', pwBurstLimiter, async (req, res) => {
try {
const { token, password } = req.body || {};
const t = String(token || '');
const p = String(password || '');
if (!t || p.length < 8) {
return res.status(400).json({ error: 'Invalid request' });
}
const tokenHash = crypto.createHash('sha256').update(t).digest('hex');
const now = Date.now();
const [rows] = await (pool.raw || pool).query(
`SELECT * FROM password_resets
WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?
ORDER BY id DESC LIMIT 1`,
[tokenHash, now]
);
const row = rows?.[0];
if (!row) return res.status(400).json({ error: 'Invalid or expired token' });
const hashed = await bcrypt.hash(p, 10); // matches your registration cost
const ok = await setPasswordByEmail(row.email, hashed);
if (!ok) return res.status(500).json({ error: 'Password update failed' });
await (pool.raw || pool).query(
`UPDATE password_resets SET used_at = ? WHERE id = ? LIMIT 1`,
[now, row.id]
);
return res.status(200).json({ ok: true });
} catch (e) {
console.error('[password-reset/confirm]', e?.message || e);
return res.status(500).json({ error: 'Server error' });
}
});
/* ------------------------------------------------------------------
USER REGISTRATION (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/register
* Body:
* username, password, firstname, lastname, email, zipcode, state, area, career_situation
*/
app.post('/api/register', async (req, res) => {
const {
username, password, firstname, lastname, email,
zipcode, state, area, career_situation, phone_e164, sms_opt_in
} = req.body;
if (!username || !password || !firstname || !lastname || !email || !zipcode || !state || !area) {
return res.status(400).json({ error: 'Missing required fields.' });
}
if (sms_opt_in && !/^\+\d{8,15}$/.test(phone_e164 || '')) {
return res.status(400).json({ error: 'Phone must be +E.164 format.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
const profileQuery = `
INSERT INTO user_profile
(username, firstname, lastname, email, zipcode, state, area, career_situation, phone_e164, sms_opt_in)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const [resultProfile] = await pool.query(profileQuery, [
username, firstname, lastname, email, zipcode, state, area,
career_situation, phone_e164 || null, sms_opt_in ? 1 : 0
]);
const newProfileId = resultProfile.insertId;
const authQuery = `
INSERT INTO user_auth (user_id, username, hashed_password)
VALUES (?, ?, ?)
`;
await pool.query(authQuery, [newProfileId, username, hashedPassword]);
const token = jwt.sign({ id: newProfileId }, JWT_SECRET, { expiresIn: '2h' });
const userPayload = {
username, firstname, lastname, email, zipcode, state, area,
career_situation, phone_e164, sms_opt_in: !!sms_opt_in
};
return res.status(201).json({
message: 'User registered successfully',
profileId: newProfileId,
token,
user: userPayload
});
} catch (err) {
console.error('Error during registration:', err.message);
return res.status(500).json({ error: 'Internal server error' });
}
});
/* ------------------------------------------------------------------
SIGN-IN (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/signin
* Body: { username, password }
* Returns JWT signed with user_profile.id
*/
app.post('/api/signin', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res
.status(400)
.json({ error: 'Both username and password are required' });
}
// SELECT only the columns you actually have:
// 'ua.id' is user_auth's primary key,
// 'ua.user_id' references user_profile.id,
// and we alias user_profile.id as profileId for clarity.
const query = `
SELECT
ua.id AS authId,
ua.user_id AS userProfileId,
ua.hashed_password,
up.firstname,
up.lastname,
up.email,
up.zipcode,
up.state,
up.area,
up.career_situation
FROM user_auth ua
LEFT JOIN user_profile up ON ua.user_id = up.id
WHERE ua.username = ?
`;
try {
const [results] = await pool.query(query, [username]);
if (!results || results.length === 0) {
return res.status(401).json({ error: 'Invalid username or password' });
}
const row = results[0];
// Compare password with bcrypt
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
// Return user info + token
// 'authId' is user_auth's PK, but typically you won't need it on the client
// 'row.userProfileId' is the actual user_profile.id
const [profileRows] = await pool.query(
'SELECT firstname, lastname, email, zipcode, state, area, career_situation \
FROM user_profile WHERE id = ?',
[row.userProfileId]
);
const profile = profileRows[0]; // ← already decrypted
const token = jwt.sign({ id: row.userProfileId }, JWT_SECRET, { expiresIn: '2h' });
res.status(200).json({
message: 'Login successful',
token,
id: row.userProfileId,
user: profile
});
} catch (err) {
console.error('Error querying user_auth:', err.message);
return res
.status(500)
.json({ error: 'Failed to query user authentication data' });
}
});
/* ------------------------------------------------------------------
CHECK USERNAME (MySQL)
------------------------------------------------------------------ */
app.get('/api/check-username/:username', async (req, res) => {
const { username } = req.params;
try {
const [results] = await pool.query(`SELECT username FROM user_auth WHERE username = ?`, [username]);
res.status(200).json({ exists: results.length > 0 });
} catch (err) {
console.error('Error checking username:', err.message);
res.status(500).json({ error: 'Database error' });
}
});
/* ------------------------------------------------------------------
UPSERT USER PROFILE (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/user-profile
* Headers: { Authorization: Bearer <token> }
* Body: { userName, firstName, lastName, email, zipCode, state, area, ... }
*
* If user_profile row exists (id = token.id), update
* else insert
*/
app.post('/api/user-profile', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authorization token is required' });
}
let profileId;
try {
const decoded = jwt.verify(token, JWT_SECRET);
profileId = decoded.id;
} catch (error) {
console.error('JWT verification failed:', error);
return res.status(401).json({ error: 'Invalid or expired token' });
}
const {
userName,
firstName,
lastName,
email,
zipCode,
state,
area,
careerSituation,
interest_inventory_answers,
riasec: riasec_scores,
career_priorities,
career_list,
phone_e164,
sms_opt_in
} = req.body;
try {
const [results] = await pool.query(
`SELECT * FROM user_profile WHERE id = ?`,
[profileId]
);
const existingRow = results.length > 0 ? results[0] : null;
if (
!existingRow &&
(!firstName || !lastName || !email || !zipCode || !state || !area)
) {
return res.status(400).json({
error: 'All fields are required for initial profile creation.',
});
}
const finalAnswers =
interest_inventory_answers !== undefined
? interest_inventory_answers
: existingRow?.interest_inventory_answers || null;
const finalCareerPriorities =
career_priorities !== undefined
? career_priorities
: existingRow?.career_priorities || null;
const finalCareerList =
career_list !== undefined
? career_list
: existingRow?.career_list || null;
const finalUserName =
userName !== undefined ? userName : existingRow?.username || null;
const finalRiasec = riasec_scores
? JSON.stringify(riasec_scores)
: existingRow?.riasec_scores || null;
if (existingRow) {
const updateQuery = `
UPDATE user_profile
SET
username = ?,
firstname = ?,
lastname = ?,
email = ?,
zipcode = ?,
state = ?,
area = ?,
career_situation = ?,
interest_inventory_answers = ?,
riasec_scores = ?,
career_priorities = ?,
career_list = ?
WHERE id = ?
`;
const params = [
finalUserName,
firstName || existingRow.firstname,
lastName || existingRow.lastname,
email || existingRow.email,
zipCode || existingRow.zipcode,
state || existingRow.state,
area || existingRow.area,
careerSituation || existingRow.career_situation,
finalAnswers,
finalRiasec,
finalCareerPriorities,
finalCareerList,
finalCareerList,
phone_e164 ?? existingRow.phone_e164 ?? null,
typeof sms_opt_in === 'boolean' ? (sms_opt_in ? 1 : 0) : existingRow.sms_opt_in ?? 0,
profileId
];
await pool.query(updateQuery, params);
return res
.status(200)
.json({ message: 'User profile updated successfully' });
} else {
const insertQuery = `
INSERT INTO user_profile
(id, username, firstname, lastname, email, zipcode, state, area,
career_situation, interest_inventory_answers, riasec_scores,
career_priorities, career_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
profileId,
finalUserName,
firstName,
lastName,
email,
zipCode,
state,
area,
careerSituation || null,
finalAnswers,
finalRiasec,
finalCareerPriorities,
finalCareerList,
phone_e164 || null,
sms_opt_in ? 1 : 0
];
await pool.query(insertQuery, params);
return res
.status(201)
.json({ message: 'User profile created successfully', id: profileId });
}
} catch (err) {
console.error('Error upserting user profile:', err.message);
return res.status(500).json({ error: 'Internal server error' });
}
});
/* ------------------------------------------------------------------
FETCH USER PROFILE (MySQL)
------------------------------------------------------------------ */
app.get('/api/user-profile', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Authorization token is required' });
let profileId;
try {
const decoded = jwt.verify(token, JWT_SECRET);
profileId = decoded.id;
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
try {
const [results] = await pool.query('SELECT * FROM user_profile WHERE id = ?', [profileId]);
if (!results || results.length === 0) {
return res.status(404).json({ error: 'User profile not found' });
}
res.status(200).json(results[0]);
} catch (err) {
console.error('Error fetching user profile:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/* ------------------------------------------------------------------
SALARY_INFO REMAINS IN SQLITE
------------------------------------------------------------------ */
app.get('/api/areas', (req, res) => {
const { state } = req.query;
if (!state) {
return res.status(400).json({ error: 'State parameter is required' });
}
// Use env when present (Docker), fall back for local dev
const salaryDbPath =
process.env.SALARY_DB_PATH // ← preferred
|| process.env.SALARY_DB // ← legacy
|| '/app/salary_info.db'; // final fallback
const salaryDb = new sqlite3.Database(
salaryDbPath,
sqlite3.OPEN_READONLY,
(err) => {
if (err) {
console.error('DB connect error:', err.message);
return res
.status(500)
.json({ error: 'Failed to connect to database' });
}
}
);
const query =
`SELECT DISTINCT AREA_TITLE
FROM salary_data
WHERE PRIM_STATE = ?`;
salaryDb.all(query, [state], (err, rows) => {
if (err) {
console.error('Query error:', err.message);
return res
.status(500)
.json({ error: 'Failed to fetch areas' });
}
res.json({ areas: rows.map(r => r.AREA_TITLE) });
});
salaryDb.close();
});
/* ------------------------------------------------------------------
PREMIUM UPGRADE ENDPOINT
------------------------------------------------------------------ */
app.post('/api/activate-premium', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Authorization token is required' });
let profileId;
try {
const decoded = jwt.verify(token, JWT_SECRET);
profileId = decoded.id;
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
try {
await pool.query(`
UPDATE user_profile
SET is_premium = 1,
is_pro_premium = 1
WHERE id = ?
`, [profileId]);
res.status(200).json({ message: 'Premium activated successfully' });
} catch (err) {
console.error('Error updating premium status:', err.message);
res.status(500).json({ error: 'Failed to activate premium' });
}
});
/* ------------------------------------------------------------------
START SERVER
------------------------------------------------------------------ */
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});