import { customAlphabet } from '@nanoid'; import { STATUS_CODE, STATUS_TEXT, StatusCode } from '@std/http/status'; import config from '~config'; import dbClient from 'db/client.ts'; import buildPage from './ssr/buildPage.ts'; import buildLogin from './ssr/buildLogin.ts'; import buildHome from './ssr/buildHome.ts'; // Using custom alphabet to exclude - and _ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const nanoid = customAlphabet(alphabet, 20); const discordHeaders = new Headers(); discordHeaders.append('Content-Type', 'application/json'); discordHeaders.append('Accept', 'application/json'); const zohoHeaders = new Headers(discordHeaders); let zohoAuthExpireDT = new Date().getTime(); const genericResponse = (status: StatusCode, customText = '', customHeaders = new Headers()) => new Response(customText || STATUS_TEXT[status], { status: status, statusText: STATUS_TEXT[status], headers: customHeaders }); Deno.serve({ port: config.api.port }, async (req) => { try { const urlPath = req.url.split('?')[0] ?? ''; let rawPath = (urlPath.split('api')[1] ?? '').trim(); if (rawPath.endsWith('/')) rawPath = rawPath.slice(0, -1); const path = rawPath; console.log(urlPath, path); let failed = false; if (req.method === 'GET') { if (path === '') { const redirectHeaders = new Headers(); redirectHeaders.set('Location', '/api/home'); return genericResponse(STATUS_CODE.PermanentRedirect, undefined, redirectHeaders); } else if (path === '/home') { // SSR "login page" return buildPage(buildLogin); } else if (path.startsWith('/home/')) { // SSR "home page" const userId = path.replace('/home/', ''); const userMatch = await dbClient.query('SELECT name FROM users WHERE id = ?', [userId]).catch(() => { failed = true; }); if (failed) return buildPage("Couldn't read DB. Please try again."); if (!userMatch.length) return buildPage('User ID does not exist. Please click on the page header to go back to the sign in page.'); return buildPage(await buildHome(userId, userMatch[0].name)); } else if (path.startsWith('/read/')) { const planId = path.replace('/read/', ''); const plans = await dbClient.query('SELECT name, folder, data FROM plans WHERE id = ? AND deleted = 0', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!plans.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist, maybe its marked as deleted?'); return genericResponse(STATUS_CODE.OK, JSON.stringify(plans[0])); } else if (path.startsWith('/list/')) { const userId = path.replace('/list/', ''); const userMatch = await dbClient.query('SELECT id FROM users WHERE id = ?', [userId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!userMatch.length) return genericResponse(STATUS_CODE.NotFound, 'User ID does not exist.'); const plans = await dbClient.query('SELECT id, name, folder FROM plans WHERE ownerId = ? AND deleted = 0', [userId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); return genericResponse(STATUS_CODE.OK, JSON.stringify(plans)); } else if (path.startsWith('/export/')) { const userId = path.replace('/list/', ''); const userMatch = await dbClient.query('SELECT id FROM users WHERE id = ?', [userId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!userMatch.length) return genericResponse(STATUS_CODE.NotFound, 'User ID does not exist.'); // WIP: export plans to zip code goes here return genericResponse(STATUS_CODE.NotImplemented, 'WIP'); } } else if (req.method === 'POST' && path === '/enroll') { const body = await req.json(); const userNameMatches = await dbClient.query('SELECT name FROM users WHERE name = ?', [body.name]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (userNameMatches.length === 0) { if (body.name.length < 4 || body.name.length > 20) return genericResponse(STATUS_CODE.BadRequest, `Name too ${body.name.length < 4 ? 'short' : 'long'}.`); if (body.pin.length < 4 || body.pin.length > 20) return genericResponse(STATUS_CODE.BadRequest, `PIN too ${body.pin.length < 4 ? 'short' : 'long'}.`); if (body.email.length > 255) return genericResponse(STATUS_CODE.BadRequest, 'Email too long.'); const id = nanoid(); await dbClient.execute('INSERT INTO users(id,name,pin,email) values(?,?,?,?)', [id, body.name, body.pin, body.email]).catch(() => { failed = true; }); if (failed) { return genericResponse(STATUS_CODE.InternalServerError, "Couldn't write DB."); } else { return genericResponse(STATUS_CODE.OK, JSON.stringify({ id })); } } else { return genericResponse(STATUS_CODE.BadRequest, 'Username Taken.'); } } else { const body = await req.json(); const loginMatch = await dbClient.query('SELECT id, email, deleteCode FROM users WHERE name = ? AND pin = ?', [body.name, body.pin]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (loginMatch.length === 0) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination.'); const id = loginMatch[0].id; const email = loginMatch[0].email; const hasEmail = email.length > 0; const deleteCode = loginMatch[0].deleteCode; switch (req.method) { case 'POST': if (path === '/auth') { return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail })); } else if (path === '/create') { if (body.planName.trim().length > 200) return genericResponse(STATUS_CODE.BadRequest, 'Name too long.'); if (body.folder.trim() && body.folder.trim().length > 200) return genericResponse(STATUS_CODE.BadRequest, 'Folder name too long.'); const newPlanId = nanoid(); await dbClient .execute('INSERT INTO plans(id,ownerId,name,folder,data) values(?,?,?,?,?)', [newPlanId, id, body.planName.trim(), body.folder.trim(), body.data]) .catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't write DB."); return genericResponse(STATUS_CODE.OK, JSON.stringify({ id: newPlanId, name: body.planName.trim() })); } break; case 'PUT': if (path.startsWith('/undelete/')) { const planId = path.replace('/undelete/', ''); const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); await dbClient.execute('UPDATE plans SET deleted = 0 WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan restored.'); } else if (path.startsWith('/update/')) { const planId = path.replace('/update/', ''); const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); await dbClient.execute('UPDATE plans SET data = ? WHERE id = ?', [body.data, planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan updated.'); } else if (path.startsWith('/rename/')) { if (body.planName.trim().length > 200) return genericResponse(STATUS_CODE.BadRequest, 'Name too long.'); const planId = path.replace('/rename/', ''); const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); await dbClient.execute('UPDATE plans SET name = ? WHERE id = ?', [body.planName, planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan renamed.'); } else if (path.startsWith('/move/')) { if (body.folder.trim().length > 200) return genericResponse(STATUS_CODE.BadRequest, 'Folder name too long.'); const planId = path.replace('/move/', ''); const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); await dbClient.execute('UPDATE plans SET folder = ? WHERE id = ?', [body.folder, planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan moved.'); } break; case 'DELETE': if (path === '/unenroll') { if (!hasEmail || body.deleteCode.trim() === deleteCode) { await dbClient.execute('DELETE FROM plans WHERE ownerId = ?', [id]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete plans."); await dbClient.execute('DELETE FROM users WHERE id = ?', [id]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete user."); return genericResponse(STATUS_CODE.OK, 'Deleted user and plans.'); } else if (hasEmail && !deleteCode) { const newDeleteCode = nanoid(); await dbClient.execute('UPDATE users SET deleteCode = ? WHERE id = ?', [newDeleteCode, id]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't set deleteCode."); const nowDT = new Date().getTime(); if (zohoAuthExpireDT < nowDT) { const getNewAuthToken = await fetch( `https://accounts.zoho.com/oauth/v2/token?client_id=${config.email.clientId}&client_secret=${config.email.clientSecret}&grant_type=client_credentials&scope=ZohoMail.messages.CREATE`, { method: 'POST' }, ).catch(() => { failed = true; }); if (failed || !getNewAuthToken) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't get auth token."); const newAuthToken = await getNewAuthToken.json(); zohoHeaders.set('Authorization', `Zoho-oauthtoken ${newAuthToken.access_token}`); zohoAuthExpireDT = nowDT + newAuthToken.expires_in * 1000 - 600000; } const sendEmailReq = await fetch(`https://mail.zoho.com/api/accounts/${config.email.accountId}/messages`, { method: 'POST', headers: zohoHeaders, body: JSON.stringify({ fromAddress: config.email.address, toAddress: email, subject: 'XIVPlan+DB Delete Code', content: `Notice: account deletion is permanent and will delete all plans saved under your account.

Please use the following Delete Code to delete your account:

${newDeleteCode}`, }), }).catch(() => { failed = true; }); if (failed || !sendEmailReq) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't send email."); const sentEmail = await sendEmailReq.json(); if (sentEmail.status.code !== 200) return genericResponse(STATUS_CODE.InternalServerError, 'Failed to send email.'); const maskedEmail = `${email.slice(0, 2)}***@***${email.slice(-5)}`; fetch(config.discordWebhook, { method: 'POST', headers: discordHeaders, body: JSON.stringify({ content: `Delete code email has been sent to \`${maskedEmail}\`.` }), }).catch(() => {}); return genericResponse(STATUS_CODE.PreconditionFailed, `Please resubmit with the confirmation code emailed to "${maskedEmail}".`); } else if (hasEmail && body.deleteCode !== deleteCode) { return genericResponse(STATUS_CODE.BadRequest, 'Invalid delete code.'); } else { return genericResponse(STATUS_CODE.InternalServerError, 'How are you here?'); } } else if (path.startsWith('/delete/')) { const planId = path.replace('/delete/', ''); const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); await dbClient.execute('UPDATE plans SET deleted = 1 WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan deleted.'); } else if (path.startsWith('/perm-delete/')) { const planId = path.replace('/perm-delete/', ''); const planMatch = await dbClient.query('SELECT ownerId, deleted FROM plans WHERE id = ?', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (!planMatch.length) return genericResponse(STATUS_CODE.NotFound, 'Plan ID does not exist.'); if (planMatch[0].ownerId !== id) return genericResponse(STATUS_CODE.Forbidden, "You don't own this plan."); if (!planMatch[0].deleted) return genericResponse(STATUS_CODE.Forbidden, 'Plan must be marked as deleted to perm delete.'); await dbClient.execute('DELETE FROM plans WHERE id = ? AND deleted = 1', [planId]).catch(() => { failed = true; }); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB."); return genericResponse(STATUS_CODE.OK, 'Plan permanently deleted.'); } break; } } return genericResponse(STATUS_CODE.NotImplemented); } catch (e) { console.error(e); return genericResponse(STATUS_CODE.InternalServerError); } });