diff --git a/.bruno/User Endpoints/auth user.bru b/.bruno/User Endpoints/auth user.bru
index f9ae072..373d94e 100644
--- a/.bruno/User Endpoints/auth user.bru
+++ b/.bruno/User Endpoints/auth user.bru
@@ -12,7 +12,7 @@ post {
body:json {
{
- "name": "teest",
+ "name": "test",
"pin": "1234"
}
}
diff --git a/.bruno/User Endpoints/unenroll user.bru b/.bruno/User Endpoints/unenroll user.bru
new file mode 100644
index 0000000..d7bfc94
--- /dev/null
+++ b/.bruno/User Endpoints/unenroll user.bru
@@ -0,0 +1,24 @@
+meta {
+ name: unenroll user
+ type: http
+ seq: 3
+}
+
+delete {
+ url: http://localhost:14014/api/unenroll
+ body: json
+ auth: inherit
+}
+
+body:json {
+ {
+ "name": "test",
+ "pin": "1234",
+ "deleteCode": ""
+ }
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/config.example.ts b/config.example.ts
index f2d31bf..520d91c 100644
--- a/config.example.ts
+++ b/config.example.ts
@@ -11,6 +11,13 @@ export const config = {
password: '',
name: 'xivplan',
},
+ discordWebhook: '',
+ email: {
+ accountId: 'zoho-account-id, from https://mail.zoho.com/api/accounts',
+ address: 'zoho-email@example.com',
+ clientId: 'zoho-self-client-id',
+ clientSecret: 'zoho-self-client-secret',
+ },
};
export default config;
diff --git a/mod.ts b/mod.ts
index c6f544e..7c1e153 100644
--- a/mod.ts
+++ b/mod.ts
@@ -9,6 +9,13 @@ import dbClient from 'db/client.ts';
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 = '') =>
new Response(customText || STATUS_TEXT[status], { status: status, statusText: STATUS_TEXT[status] });
@@ -31,7 +38,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
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 > 20) return genericResponse(STATUS_CODE.BadRequest, `Email too long.`);
+ if (body.email.length > 255) return genericResponse(STATUS_CODE.BadRequest, `Email too long.`);
const id = nanoid();
@@ -59,19 +66,87 @@ Deno.serve({ port: config.api.port }, async (req) => {
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' || path === '/auth/') {
- return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail: email.length > 0 }));
+ return genericResponse(STATUS_CODE.OK, JSON.stringify({ id, hasEmail }));
}
break;
case 'PUT':
break;
case 'DELETE':
if (path === '/unenroll' || '/unenroll/') {
- //
+ if (!hasEmail || body.deleteCode.trim() === deleteCode) {
+ let deleteFailure = false;
+
+ await dbClient.execute('DELETE FROM plans WHERE ownerId = ?', [id]).catch(() => {
+ deleteFailure = true;
+ });
+ if (deleteFailure) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete plans.");
+
+ await dbClient.execute('DELETE FROM users WHERE id = ?', [id]).catch(() => {
+ deleteFailure = true;
+ });
+ if (deleteFailure) 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;
+ });
+ if (updateFailure) 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;
+ });
+ if (fetchFailed || !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(() => {
+ fetchFailed = true;
+ });
+ if (fetchFailed || !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?');
+ }
}
break;
}