Compare commits

...

2 Commits

5 changed files with 79 additions and 31 deletions

View File

@@ -18,7 +18,7 @@ await dbClient.execute(`
CREATE TABLE users (
id varchar(20) NOT NULL,
name varchar(20) NOT NULL,
pin varchar(16) NOT NULL,
hash varchar(60) NOT NULL,
email varchar(255) NULL,
deleteCode varchar(20) NULL,
PRIMARY KEY (id),

View File

@@ -22,6 +22,7 @@
},
"nodeModulesDir": "none",
"imports": {
"@bcrypt": "jsr:@felix/bcrypt",
"@mysql": "https://deno.land/x/mysql@v2.12.1/mod.ts",
"@nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
"@std/http": "jsr:@std/http@1.0.15",

46
deno.lock generated
View File

@@ -1,17 +1,40 @@
{
"version": "5",
"specifiers": {
"jsr:@denosaurs/plug@^1.1.0": "1.1.0",
"jsr:@felix/bcrypt@*": "1.0.8",
"jsr:@std/cli@^1.0.17": "1.0.17",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/fmt@1": "1.0.7",
"jsr:@std/fmt@^1.0.7": "1.0.7",
"jsr:@std/fs@1": "1.0.23",
"jsr:@std/html@^1.0.3": "1.0.3",
"jsr:@std/http@1.0.15": "1.0.15",
"jsr:@std/internal@^1.0.12": "1.0.13",
"jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@1": "1.0.9",
"jsr:@std/path@^1.0.9": "1.0.9",
"jsr:@std/path@^1.1.4": "1.1.4",
"jsr:@std/streams@^1.0.9": "1.0.9"
},
"jsr": {
"@denosaurs/plug@1.1.0": {
"integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044",
"dependencies": [
"jsr:@std/encoding@1",
"jsr:@std/fmt@1",
"jsr:@std/fs",
"jsr:@std/path@1"
]
},
"@felix/bcrypt@1.0.8": {
"integrity": "59c41160fc027882479c512db5d53792c4d91aadcd49467c85caa2f1679046f2",
"dependencies": [
"jsr:@denosaurs/plug"
]
},
"@std/cli@1.0.17": {
"integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b"
},
@@ -21,6 +44,13 @@
"@std/fmt@1.0.7": {
"integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb"
},
"@std/fs@1.0.23": {
"integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
"dependencies": [
"jsr:@std/internal",
"jsr:@std/path@^1.1.4"
]
},
"@std/html@1.0.3": {
"integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
},
@@ -28,15 +58,18 @@
"integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159",
"dependencies": [
"jsr:@std/cli",
"jsr:@std/encoding",
"jsr:@std/fmt",
"jsr:@std/encoding@^1.0.10",
"jsr:@std/fmt@^1.0.7",
"jsr:@std/html",
"jsr:@std/media-types",
"jsr:@std/net",
"jsr:@std/path",
"jsr:@std/path@^1.0.9",
"jsr:@std/streams"
]
},
"@std/internal@1.0.13": {
"integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0"
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
@@ -46,6 +79,12 @@
"@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
},
"@std/path@1.1.4": {
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/streams@1.0.9": {
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035"
}
@@ -120,6 +159,7 @@
},
"workspace": {
"dependencies": [
"jsr:@felix/bcrypt@*",
"jsr:@std/http@1.0.15"
]
}

53
mod.ts
View File

@@ -1,3 +1,4 @@
import { hash, verify } from '@bcrypt';
import { customAlphabet } from '@nanoid';
import { STATUS_CODE, STATUS_TEXT, StatusCode } from '@std/http/status';
@@ -42,7 +43,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
} 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(() => {
const userMatch = await dbClient.query('SELECT name FROM users WHERE BINARY id = ?', [userId]).catch(() => {
failed = true;
});
if (failed) return buildPage("Couldn't read DB. Please try again.");
@@ -52,7 +53,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
} 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(() => {
const plans = await dbClient.query('SELECT name, folder, data FROM plans WHERE BINARY id = ? AND deleted = 0', [planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB.");
@@ -61,14 +62,14 @@ Deno.serve({ port: config.api.port }, async (req) => {
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(() => {
const userMatch = await dbClient.query('SELECT id FROM users WHERE BINARY 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, lastUpdated FROM plans WHERE ownerId = ? AND deleted = 0 ORDER BY folder ASC,name ASC', [userId])
.query('SELECT id, name, folder, lastUpdated FROM plans WHERE BINARY ownerId = ? AND deleted = 0 ORDER BY folder ASC,name ASC', [userId])
.catch(() => {
failed = true;
});
@@ -81,7 +82,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
return genericResponse(STATUS_CODE.OK, JSON.stringify(plans));
} else if (path.startsWith('/export/')) {
const userId = path.replace('/export/', '');
const userMatch = await dbClient.query('SELECT id FROM users WHERE id = ?', [userId]).catch(() => {
const userMatch = await dbClient.query('SELECT id FROM users WHERE BINARY id = ?', [userId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB.");
@@ -93,7 +94,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
} 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(() => {
const userNameMatches = await dbClient.query('SELECT name FROM users WHERE BINARY name = ?', [body.name]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB.");
@@ -101,12 +102,13 @@ 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 > 16) return genericResponse(STATUS_CODE.BadRequest, `PIN too ${body.pin.length < 4 ? 'short' : 'long'}.`);
if (body.pin.length < 4) 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();
const pinHash = await hash(body.pin);
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,hash,email) values(?,?,?,?)', [id, body.name, pinHash, body.email]).catch(() => {
failed = true;
});
@@ -121,11 +123,12 @@ Deno.serve({ port: config.api.port }, async (req) => {
} 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(() => {
const loginMatch = await dbClient.query('SELECT id, hash, email, deleteCode FROM users WHERE BINARY name = ?', [body.name]).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.');
if (loginMatch.length === 0) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination. Remember name is case sensitive.');
if (!(await verify(body.pin, loginMatch[0].hash))) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination.');
const id = loginMatch[0].id;
const email = loginMatch[0].email;
const hasEmail = email.length > 0;
@@ -152,28 +155,28 @@ Deno.serve({ port: config.api.port }, async (req) => {
case 'PUT':
if (path.startsWith('/undelete/')) {
const planId = path.replace('/undelete/', '');
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => {
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE BINARY 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, lastUpdated = ? WHERE id = ?', [new Date(), planId]).catch(() => {
await dbClient.execute('UPDATE plans SET deleted = 0, lastUpdated = ? WHERE BINARY id = ?', [new Date(), 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(() => {
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE BINARY 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 = ?, lastUpdated = ? WHERE id = ?', [body.data, new Date(), planId]).catch(() => {
await dbClient.execute('UPDATE plans SET data = ?, lastUpdated = ? WHERE BINARY id = ?', [body.data, new Date(), planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB.");
@@ -182,14 +185,14 @@ Deno.serve({ port: config.api.port }, async (req) => {
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(() => {
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE BINARY 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 = ?, lastUpdated = ? WHERE id = ?', [body.planName, new Date(), planId]).catch(() => {
await dbClient.execute('UPDATE plans SET name = ?, lastUpdated = ? WHERE BINARY id = ?', [body.planName, new Date(), planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB.");
@@ -198,14 +201,14 @@ Deno.serve({ port: config.api.port }, async (req) => {
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(() => {
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE BINARY 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 = ?, lastUpdated = ? WHERE id = ?', [body.folder, new Date(), planId]).catch(() => {
await dbClient.execute('UPDATE plans SET folder = ?, lastUpdated = ? WHERE BINARY id = ?', [body.folder, new Date(), planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB.");
@@ -215,12 +218,12 @@ Deno.serve({ port: config.api.port }, async (req) => {
case 'DELETE':
if (path === '/unenroll') {
if (!hasEmail || body.deleteCode.trim() === deleteCode) {
await dbClient.execute('DELETE FROM plans WHERE ownerId = ?', [id]).catch(() => {
await dbClient.execute('DELETE FROM plans WHERE BINARY 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(() => {
await dbClient.execute('DELETE FROM users WHERE BINARY id = ?', [id]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't delete user.");
@@ -228,7 +231,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
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(() => {
await dbClient.execute('UPDATE users SET deleteCode = ? WHERE BINARY id = ?', [newDeleteCode, id]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't set deleteCode.");
@@ -279,21 +282,21 @@ Deno.serve({ port: config.api.port }, async (req) => {
}
} else if (path.startsWith('/delete/')) {
const planId = path.replace('/delete/', '');
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE id = ?', [planId]).catch(() => {
const planMatch = await dbClient.query('SELECT ownerId FROM plans WHERE BINARY 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, lastUpdated = ? WHERE id = ?', [new Date(), planId]).catch(() => {
await dbClient.execute('UPDATE plans SET deleted = 1, lastUpdated = ? WHERE BINARY id = ?', [new Date(), 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(() => {
const planMatch = await dbClient.query('SELECT ownerId, deleted FROM plans WHERE BINARY id = ?', [planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB.");
@@ -301,7 +304,7 @@ Deno.serve({ port: config.api.port }, async (req) => {
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(() => {
await dbClient.execute('DELETE FROM plans WHERE BINARY id = ? AND deleted = 1', [planId]).catch(() => {
failed = true;
});
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't update DB.");

View File

@@ -21,14 +21,18 @@ export default async (userId: string, userName: string) => {
let failed = false;
const plans: Plan[] = await dbClient
.query('SELECT id, name, folder, lastUpdated FROM plans WHERE ownerId = ? AND deleted = 0 GROUP BY folder,name,id ORDER BY folder ASC,name ASC', [userId])
.query('SELECT id, name, folder, lastUpdated FROM plans WHERE BINARY ownerId = ? AND deleted = 0 GROUP BY folder,name,id ORDER BY folder ASC,name ASC', [
userId,
])
.catch((e) => {
failed = true;
});
if (failed) return "Couldn't read DB.";
const deletedPlans: Plan[] = await dbClient
.query('SELECT id, name, folder, lastUpdated FROM plans WHERE ownerId = ? AND deleted = 1 GROUP BY folder,name,id ORDER BY folder ASC,name ASC', [userId])
.query('SELECT id, name, folder, lastUpdated FROM plans WHERE BINARY ownerId = ? AND deleted = 1 GROUP BY folder,name,id ORDER BY folder ASC,name ASC', [
userId,
])
.catch(() => {
failed = true;
});