From 52abe6f3a8cf0c2c8209e4b1892688c778a79640 Mon Sep 17 00:00:00 2001 From: Ean Milligan Date: Thu, 9 Apr 2026 16:24:26 -0400 Subject: [PATCH] add delete and undelete endpoints --- .bruno/Plan Endpoints/delete plan.bru | 23 +++++++ .bruno/Plan Endpoints/folder.bru | 8 +++ .bruno/Plan Endpoints/undelete plan.bru | 23 +++++++ .bruno/User Endpoints/auth user.bru | 2 +- .bruno/User Endpoints/enroll user.bru | 2 +- README.md | 8 +-- mod.ts | 80 ++++++++++++++++--------- 7 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 .bruno/Plan Endpoints/delete plan.bru create mode 100644 .bruno/Plan Endpoints/folder.bru create mode 100644 .bruno/Plan Endpoints/undelete plan.bru diff --git a/.bruno/Plan Endpoints/delete plan.bru b/.bruno/Plan Endpoints/delete plan.bru new file mode 100644 index 0000000..3f624bc --- /dev/null +++ b/.bruno/Plan Endpoints/delete plan.bru @@ -0,0 +1,23 @@ +meta { + name: delete plan + type: http + seq: 1 +} + +delete { + url: http://localhost:14014/api/delete/[planId] + body: json + auth: inherit +} + +body:json { + { + "name": "test", + "pin": "1234" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/Plan Endpoints/folder.bru b/.bruno/Plan Endpoints/folder.bru new file mode 100644 index 0000000..ff6ad8b --- /dev/null +++ b/.bruno/Plan Endpoints/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Plan Endpoints + seq: 2 +} + +auth { + mode: inherit +} diff --git a/.bruno/Plan Endpoints/undelete plan.bru b/.bruno/Plan Endpoints/undelete plan.bru new file mode 100644 index 0000000..34e8cdc --- /dev/null +++ b/.bruno/Plan Endpoints/undelete plan.bru @@ -0,0 +1,23 @@ +meta { + name: undelete plan + type: http + seq: 2 +} + +put { + url: http://localhost:14014/api/undelete/[planId] + body: json + auth: inherit +} + +body:json { + { + "name": "test", + "pin": "1234" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/User Endpoints/auth user.bru b/.bruno/User Endpoints/auth user.bru index 373d94e..0a46da0 100644 --- a/.bruno/User Endpoints/auth user.bru +++ b/.bruno/User Endpoints/auth user.bru @@ -1,7 +1,7 @@ meta { name: auth user type: http - seq: 2 + seq: 1 } post { diff --git a/.bruno/User Endpoints/enroll user.bru b/.bruno/User Endpoints/enroll user.bru index cee808c..d7a1fcf 100644 --- a/.bruno/User Endpoints/enroll user.bru +++ b/.bruno/User Endpoints/enroll user.bru @@ -1,7 +1,7 @@ meta { name: enroll user type: http - seq: 1 + seq: 2 } post { diff --git a/README.md b/README.md index 4f44915..bae7486 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ The API will be a combination API and basic SSR. ### Routes - /api - SSR Page: shows login form - - /api/[userId] - SSR Page: provides import plan button to upload an existing xivplan url to the db, shows all user's plans (have section for deleted plans), each plan should have the following buttons: [Open], [Share], [Rename*] [Delete*] Buttons with an \* will prompt for PIN to confirm. + - /api/[userId] - SSR Page: provides import plan button to upload an existing xivplan url to the db, shows all user's plans (have section for deleted plans), each plan should have the following buttons: [Open], [Share], [Rename*] [Delete*] Buttons with a \* will prompt for PIN to confirm. - /api/read/[planId] - API Page: **GET** returns name and data as JSON object - /api/[userId]/list - API Page: **GET** returns non-deleted plans, specifically the name, id, and folder of each plan as a JSON array of objects - /api/[userId]/export - API Page: **GET** returns zip of .xivplan files - /api/[userId]/create - API Page: **POST** to save new plan to DB, requires name, **PIN**, and data (optionally folder), api will generate a nanoid for the PK - - /api/[userId]/update/[planId] - API Page: **PUT** to overwrite plan while keeping same name and id, requires **PIN** and data - - /api/[userId]/undelete/[planId] - API Page: **PUT** to unmark plan as deleted, requires **PIN** - - /api/[userId]/delete/[planId] - API Page: **DELETE** to mark plan as deleted, requires **PIN** + - /api/update/[planId] - API Page: **PUT** to overwrite plan while keeping same name and id, requires **PIN** and data + - /api/undelete/[planId] - API Page: **PUT** to unmark plan as deleted, requires **PIN** + - /api/delete/[planId] - API Page: **DELETE** to mark plan as deleted, requires **PIN** - /api/auth - API Page: **POST** to check if you are who you say you are, requires username and **PIN**, returns userId and boolean of if email was set - /api/enroll - API Page: **POST** to create new user, requires username and **PIN** (optionally email), returns userId - /api/unenroll - API Page: **DELETE** to get rid of user and all of their plans, requires username and **PIN** (and deletion-confirmation-code if email present) diff --git a/mod.ts b/mod.ts index 9680de8..6c075a8 100644 --- a/mod.ts +++ b/mod.ts @@ -21,19 +21,21 @@ const genericResponse = (status: StatusCode, customText = '') => Deno.serve({ port: config.api.port }, async (req) => { const urlPath = req.url.split('?')[0] ?? ''; - const path = (urlPath.split('api')[1] ?? '').toLowerCase().trim(); + 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') { // handle all gets - } else if (req.method === 'POST' && (path === '/enroll' || path === '/enroll/')) { + } else if (req.method === 'POST' && path === '/enroll') { const body = await req.json(); - let readFailure = false; const userNameMatches = await dbClient.query('SELECT name FROM users WHERE name = ?', [body.name]).catch(() => { - readFailure = true; + failed = true; }); - if (readFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); + 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'}.`); @@ -42,12 +44,11 @@ Deno.serve({ port: config.api.port }, async (req) => { const id = nanoid(); - let writeFailure = false; await dbClient.execute('INSERT INTO users(id,name,pin,email) values(?,?,?,?)', [id, body.name, body.pin, body.email]).catch(() => { - writeFailure = true; + failed = true; }); - if (writeFailure) { + if (failed) { return genericResponse(STATUS_CODE.InternalServerError, "Couldn't write DB."); } else { return genericResponse(STATUS_CODE.OK, JSON.stringify({ id })); @@ -58,11 +59,10 @@ Deno.serve({ port: config.api.port }, async (req) => { } else { const body = await req.json(); - let readFailure = false; const loginMatch = await dbClient.query('SELECT id, email, deleteCode FROM users WHERE name = ? AND pin = ?', [body.name, body.pin]).catch(() => { - readFailure = true; + failed = true; }); - if (readFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); + 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; @@ -71,47 +71,57 @@ Deno.serve({ port: config.api.port }, async (req) => { switch (req.method) { case 'POST': - if (path === '/auth' || path === '/auth/') { + if (path === '/auth') { return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail })); } 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 deleted.'); + } break; case 'DELETE': - if (path === '/unenroll' || '/unenroll/') { + if (path === '/unenroll') { if (!hasEmail || body.deleteCode.trim() === deleteCode) { - let deleteFailure = false; - await dbClient.execute('DELETE FROM plans WHERE ownerId = ?', [id]).catch(() => { - deleteFailure = true; + failed = true; }); - if (deleteFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete plans."); + if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete plans."); await dbClient.execute('DELETE FROM users WHERE id = ?', [id]).catch(() => { - deleteFailure = true; + failed = true; }); - if (deleteFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete user."); + if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete user."); return genericResponse(STATUS_CODE.OK, 'Deleted user and plans.'); } else if (hasEmail && !deleteCode) { - let updateFailure = false; - const newDeleteCode = nanoid(); await dbClient.execute('UPDATE users SET deleteCode = ? WHERE id = ?', [newDeleteCode, id]).catch(() => { - updateFailure = true; + failed = true; }); - if (updateFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't set deleteCode."); + if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't set deleteCode."); const nowDT = new Date().getTime(); - let fetchFailed = false; 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(() => { - fetchFailed = true; + failed = true; }); - if (fetchFailed || !getNewAuthToken) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't get auth token."); + 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}`); @@ -128,9 +138,9 @@ Deno.serve({ port: config.api.port }, async (req) => { 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(() => { - fetchFailed = true; + failed = true; }); - if (fetchFailed || !sendEmailReq) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't send email."); + 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.'); @@ -147,6 +157,20 @@ Deno.serve({ port: config.api.port }, async (req) => { } 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.'); } break; }