add delete and undelete endpoints
This commit is contained in:
23
.bruno/Plan Endpoints/delete plan.bru
Normal file
23
.bruno/Plan Endpoints/delete plan.bru
Normal file
@@ -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
|
||||||
|
}
|
||||||
8
.bruno/Plan Endpoints/folder.bru
Normal file
8
.bruno/Plan Endpoints/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Plan Endpoints
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
23
.bruno/Plan Endpoints/undelete plan.bru
Normal file
23
.bruno/Plan Endpoints/undelete plan.bru
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: auth user
|
name: auth user
|
||||||
type: http
|
type: http
|
||||||
seq: 2
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: enroll user
|
name: enroll user
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ The API will be a combination API and basic SSR.
|
|||||||
### Routes
|
### Routes
|
||||||
|
|
||||||
- /api - SSR Page: shows login form
|
- /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/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]/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]/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]/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/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/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/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/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/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)
|
- /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)
|
||||||
|
|||||||
80
mod.ts
80
mod.ts
@@ -21,19 +21,21 @@ const genericResponse = (status: StatusCode, customText = '') =>
|
|||||||
|
|
||||||
Deno.serve({ port: config.api.port }, async (req) => {
|
Deno.serve({ port: config.api.port }, async (req) => {
|
||||||
const urlPath = req.url.split('?')[0] ?? '';
|
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);
|
console.log(urlPath, path);
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
// handle all gets
|
// handle all gets
|
||||||
} else if (req.method === 'POST' && (path === '/enroll' || path === '/enroll/')) {
|
} else if (req.method === 'POST' && path === '/enroll') {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
|
||||||
let readFailure = false;
|
|
||||||
const userNameMatches = await dbClient.query('SELECT name FROM users WHERE name = ?', [body.name]).catch(() => {
|
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 (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.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();
|
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(() => {
|
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.");
|
return genericResponse(STATUS_CODE.InternalServerError, "Couldn't write DB.");
|
||||||
} else {
|
} else {
|
||||||
return genericResponse(STATUS_CODE.OK, JSON.stringify({ id }));
|
return genericResponse(STATUS_CODE.OK, JSON.stringify({ id }));
|
||||||
@@ -58,11 +59,10 @@ Deno.serve({ port: config.api.port }, async (req) => {
|
|||||||
} else {
|
} else {
|
||||||
const body = await req.json();
|
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(() => {
|
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.');
|
if (loginMatch.length === 0) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination.');
|
||||||
const id = loginMatch[0].id;
|
const id = loginMatch[0].id;
|
||||||
const email = loginMatch[0].email;
|
const email = loginMatch[0].email;
|
||||||
@@ -71,47 +71,57 @@ Deno.serve({ port: config.api.port }, async (req) => {
|
|||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'POST':
|
case 'POST':
|
||||||
if (path === '/auth' || path === '/auth/') {
|
if (path === '/auth') {
|
||||||
return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail }));
|
return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail }));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'PUT':
|
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;
|
break;
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
if (path === '/unenroll' || '/unenroll/') {
|
if (path === '/unenroll') {
|
||||||
if (!hasEmail || body.deleteCode.trim() === deleteCode) {
|
if (!hasEmail || body.deleteCode.trim() === deleteCode) {
|
||||||
let deleteFailure = false;
|
|
||||||
|
|
||||||
await dbClient.execute('DELETE FROM plans WHERE ownerId = ?', [id]).catch(() => {
|
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(() => {
|
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.');
|
return genericResponse(STATUS_CODE.OK, 'Deleted user and plans.');
|
||||||
} else if (hasEmail && !deleteCode) {
|
} else if (hasEmail && !deleteCode) {
|
||||||
let updateFailure = false;
|
|
||||||
|
|
||||||
const newDeleteCode = nanoid();
|
const newDeleteCode = nanoid();
|
||||||
await dbClient.execute('UPDATE users SET deleteCode = ? WHERE id = ?', [newDeleteCode, id]).catch(() => {
|
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();
|
const nowDT = new Date().getTime();
|
||||||
let fetchFailed = false;
|
|
||||||
if (zohoAuthExpireDT < nowDT) {
|
if (zohoAuthExpireDT < nowDT) {
|
||||||
const getNewAuthToken = await fetch(
|
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`,
|
`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' },
|
{ method: 'POST' },
|
||||||
).catch(() => {
|
).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();
|
const newAuthToken = await getNewAuthToken.json();
|
||||||
zohoHeaders.set('Authorization', `Zoho-oauthtoken ${newAuthToken.access_token}`);
|
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.<br/><br/>Please use the following Delete Code to delete your account:<br/><br/>${newDeleteCode}`,
|
content: `Notice: account deletion is permanent and will delete all plans saved under your account.<br/><br/>Please use the following Delete Code to delete your account:<br/><br/>${newDeleteCode}`,
|
||||||
}),
|
}),
|
||||||
}).catch(() => {
|
}).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();
|
const sentEmail = await sendEmailReq.json();
|
||||||
if (sentEmail.status.code !== 200) return genericResponse(STATUS_CODE.InternalServerError, 'Failed to send email.');
|
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 {
|
} else {
|
||||||
return genericResponse(STATUS_CODE.InternalServerError, 'How are you here?');
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user