diff --git a/assets/icons/blocked.svg b/assets/icons/blocked.svg
new file mode 100755
index 0000000..7d6d1e4
--- /dev/null
+++ b/assets/icons/blocked.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/js/api.js b/assets/js/api.js
index 11edb1d..0145318 100644
--- a/assets/js/api.js
+++ b/assets/js/api.js
@@ -42,7 +42,7 @@ export async function post(path, data, { query } = {}) {
return body;
}
- throw new Error(body.message);
+ throw new Error(body.statusMessage);
}
export async function patch(path, data, { query } = {}) {
diff --git a/components/posts/post.vue b/components/posts/post.vue
new file mode 100644
index 0000000..ac40b2e
--- /dev/null
+++ b/components/posts/post.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/knexfile.js b/knexfile.js
old mode 100755
new mode 100644
index dcef71c..4d28793
--- a/knexfile.js
+++ b/knexfile.js
@@ -1,6 +1,6 @@
-const config = require('config');
+import config from 'config';
-module.exports = {
+export default {
client: 'pg',
connection: config.database,
};
diff --git a/migrations/20230513004141_init.js b/migrations/20230513004141_init.js
index 624d43a..3d3fdc4 100644
--- a/migrations/20230513004141_init.js
+++ b/migrations/20230513004141_init.js
@@ -1,4 +1,16 @@
-exports.up = async function(knex) {
+import fs from 'fs';
+
+export async function up(knex) {
+ const nanoidFn = await fs.promises.readFile('./migrations/nanoid.sql', 'utf8'); // from https://github.com/viascom/nanoid-postgres
+
+ await knex.raw(nanoidFn);
+
+ await knex.raw(`
+ CREATE FUNCTION shack_id(length smallint DEFAULT 8) RETURNS TEXT AS $$
+ SELECT nanoid(length, '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
+ $$ LANGUAGE SQL STABLE;
+ `);
+
await knex.schema.createTable('users', (table) => {
table.increments('id');
@@ -63,7 +75,9 @@ exports.up = async function(knex) {
});
await knex.schema.createTable('posts', (table) => {
- table.increments('id');
+ table.text('id', 8)
+ .primary()
+ .defaultTo(knex.raw('shack_id()'));
table.text('title')
.notNullable();
@@ -86,12 +100,39 @@ exports.up = async function(knex) {
.defaultTo(knex.fn.now());
});
- await knex.raw(`ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR url IS NOT NULL)`);
-};
+ await knex.raw('ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR url IS NOT NULL)');
-exports.down = async function(knex) {
+ await knex.schema.createTable('comments', (table) => {
+ table.text('id', 8)
+ .primary()
+ .defaultTo(knex.raw('shack_id()'));
+
+ table.text('post_id', 8)
+ .notNullable()
+ .references('id')
+ .inTable('posts');
+
+ table.integer('user_id')
+ .notNullable()
+ .references('id')
+ .inTable('users');
+
+ table.text('body');
+
+ table.datetime('created_at')
+ .notNullable()
+ .defaultTo(knex.fn.now());
+ });
+}
+
+export async function down(knex) {
+ await knex.schema.dropTable('comments');
await knex.schema.dropTable('posts');
await knex.schema.dropTable('shelves_settings');
await knex.schema.dropTable('shelves');
await knex.schema.dropTable('users');
-};
+
+ await knex.raw(`
+ DROP FUNCTION IF EXISTS shack_id;
+ `);
+}
diff --git a/migrations/nanoid.sql b/migrations/nanoid.sql
new file mode 100644
index 0000000..63e6bcc
--- /dev/null
+++ b/migrations/nanoid.sql
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Viascom Ltd liab. Co
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+CREATE OR REPLACE FUNCTION nanoid(
+ size int DEFAULT 21,
+ alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+)
+ RETURNS text
+ LANGUAGE plpgsql
+ volatile
+AS
+$$
+DECLARE
+ idBuilder text := '';
+ counter int := 0;
+ bytes bytea;
+ alphabetIndex int;
+ alphabetArray text[];
+ alphabetLength int;
+ mask int;
+ step int;
+BEGIN
+ alphabetArray := regexp_split_to_array(alphabet, '');
+ alphabetLength := array_length(alphabetArray, 1);
+ mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
+ step := cast(ceil(1.6 * mask * size / alphabetLength) AS int);
+
+ while true
+ loop
+ bytes := gen_random_bytes(step);
+ while counter < step
+ loop
+ alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
+ if alphabetIndex <= alphabetLength then
+ idBuilder := idBuilder || alphabetArray[alphabetIndex];
+ if length(idBuilder) = size then
+ return idBuilder;
+ end if;
+ end if;
+ counter := counter + 1;
+ end loop;
+
+ counter := 0;
+ end loop;
+END
+$$;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 747a43b..74cf541 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"config": "^3.3.9",
"connect-redis": "^7.1.0",
"cross-env": "^7.0.3",
+ "date-fns": "^2.30.0",
"error-stack-parser": "^2.1.4",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
@@ -36,6 +37,7 @@
"pg": "^8.11.0",
"pinia": "^2.1.3",
"redis": "^4.6.6",
+ "short-uuid": "^4.2.2",
"sirv": "^2.0.2",
"vite": "^4.0.3",
"vite-plugin-ssr": "^0.4.126",
@@ -2623,6 +2625,11 @@
"node": ">=4"
}
},
+ "node_modules/any-base": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
+ "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -3381,6 +3388,21 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -6747,6 +6769,26 @@
"node": ">=8"
}
},
+ "node_modules/short-uuid": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/short-uuid/-/short-uuid-4.2.2.tgz",
+ "integrity": "sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==",
+ "dependencies": {
+ "any-base": "^1.1.0",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/short-uuid/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -9570,6 +9612,11 @@
"color-convert": "^1.9.0"
}
},
+ "any-base": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
+ "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
+ },
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -10135,6 +10182,14 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
+ "date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "requires": {
+ "@babel/runtime": "^7.21.0"
+ }
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -12534,6 +12589,22 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
+ "short-uuid": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/short-uuid/-/short-uuid-4.2.2.tgz",
+ "integrity": "sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==",
+ "requires": {
+ "any-base": "^1.1.0",
+ "uuid": "^8.3.2"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+ }
+ }
+ },
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
diff --git a/package.json b/package.json
index e1267ad..ecaee00 100644
--- a/package.json
+++ b/package.json
@@ -20,9 +20,9 @@
"build": "vite build",
"server": "node --experimental-specifier-resolution=node ./src/web/server",
"server:prod": "cross-env NODE_ENV=production node ./src/web/server",
- "migrate-make": "knex-migrate generate",
- "migrate": "knex-migrate up",
- "rollback": "knex-migrate down"
+ "migrate-make": "knex migrate:make",
+ "migrate": "knex migrate:latest",
+ "rollback": "knex migrate:rollback"
},
"dependencies": {
"@babel/cli": "^7.21.5",
@@ -39,6 +39,7 @@
"config": "^3.3.9",
"connect-redis": "^7.1.0",
"cross-env": "^7.0.3",
+ "date-fns": "^2.30.0",
"error-stack-parser": "^2.1.4",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
@@ -53,6 +54,7 @@
"pg": "^8.11.0",
"pinia": "^2.1.3",
"redis": "^4.6.6",
+ "short-uuid": "^4.2.2",
"sirv": "^2.0.2",
"vite": "^4.0.3",
"vite-plugin-ssr": "^0.4.126",
diff --git a/pages/account/login.page.vue b/pages/account/login.page.vue
index d547aa7..fd494e2 100644
--- a/pages/account/login.page.vue
+++ b/pages/account/login.page.vue
@@ -33,21 +33,24 @@
-
diff --git a/src/posts.js b/src/posts.js
new file mode 100644
index 0000000..adf69fe
--- /dev/null
+++ b/src/posts.js
@@ -0,0 +1,66 @@
+// import knex from './knex';
+import { verifyPrivilege } from './privileges';
+import knex from './knex';
+import { HttpError } from './errors';
+
+import { fetchShelf, curateDatabaseShelf } from './shelves';
+import { curateDatabaseUser } from './users';
+
+function curatePost(post) {
+ const curatedPost = {
+ id: post.id,
+ title: post.title,
+ body: post.body,
+ url: post.url,
+ shelfId: post.shelf_id,
+ createdAt: post.created_at,
+ shelf: curateDatabaseShelf(post.shelf),
+ user: curateDatabaseUser(post.user),
+ };
+
+ return curatedPost;
+}
+
+async function fetchShelfPosts(shelfId, limit = 100) {
+ const shelf = await fetchShelf(shelfId);
+
+ const posts = await knex('posts')
+ .select('posts.*', knex.raw('row_to_json(users) as user'), knex.raw('row_to_json(shelves) as shelf'))
+ .leftJoin('users', 'users.id', 'posts.user_id')
+ .leftJoin('shelves', 'shelves.id', 'posts.shelf_id')
+ .where('shelf_id', shelf.id)
+ .orderBy('created_at', 'desc')
+ .limit(limit);
+
+ return posts.map((post) => curatePost(post));
+}
+
+async function createPost(post, shelfId, user) {
+ await verifyPrivilege('createPost', user);
+
+ const shelf = await fetchShelf(shelfId);
+
+ if (!shelf) {
+ throw new HttpError({
+ statusMessage: 'The target shelf does not exist',
+ statusCode: 404,
+ });
+ }
+
+ const postId = await knex('posts')
+ .insert({
+ title: post.title,
+ body: post.body,
+ url: post.url,
+ shelf_id: shelf.id,
+ user_id: user.id,
+ })
+ .returning('id');
+
+ return postId;
+}
+
+export {
+ createPost,
+ fetchShelfPosts,
+};
diff --git a/src/privileges.js b/src/privileges.js
new file mode 100644
index 0000000..724eaee
--- /dev/null
+++ b/src/privileges.js
@@ -0,0 +1,18 @@
+import { HttpError } from './errors';
+
+function verifyPrivilege(privilege, user, context) {
+ if (!user) {
+ throw new HttpError({
+ statusMessage: 'You are not authenticated',
+ statusCode: 401,
+ });
+ }
+
+ console.log('verify privilege', privilege, user, context);
+
+ return true;
+}
+
+export {
+ verifyPrivilege,
+};
diff --git a/src/shelves.js b/src/shelves.js
index 15e3824..f3cd893 100644
--- a/src/shelves.js
+++ b/src/shelves.js
@@ -51,4 +51,5 @@ export {
fetchShelf,
fetchShelves,
createShelf,
+ curateDatabaseShelf,
};
diff --git a/src/users.js b/src/users.js
index e15f408..7b5179a 100644
--- a/src/users.js
+++ b/src/users.js
@@ -143,6 +143,7 @@ async function createUser(credentials, context) {
}
export {
+ curateDatabaseUser,
createUser,
login,
};
diff --git a/src/web/error.js b/src/web/error.js
index 0359e78..367a58e 100755
--- a/src/web/error.js
+++ b/src/web/error.js
@@ -12,7 +12,7 @@ export default function errorHandler(error, req, res, _next) {
if (error.statusCode) {
res.status(error.statusCode).send({
statusCode: error.statusCode,
- message: error.statusMessage,
+ statusMessage: error.statusMessage,
});
return;
diff --git a/src/web/posts.js b/src/web/posts.js
new file mode 100644
index 0000000..5d3268d
--- /dev/null
+++ b/src/web/posts.js
@@ -0,0 +1,11 @@
+import { createPost } from '../posts';
+
+async function createPostApi(req, res) {
+ const post = await createPost(req.body, req.params.shelfId, req.user);
+
+ res.send(post);
+}
+
+export {
+ createPostApi as createPost,
+};
diff --git a/src/web/server.js b/src/web/server.js
index 91591ed..abfca39 100644
--- a/src/web/server.js
+++ b/src/web/server.js
@@ -25,6 +25,8 @@ import {
import { createShelf } from './shelves';
+import { createPost } from './posts';
+
const logger = initLogger();
async function startServer() {
@@ -54,13 +56,19 @@ async function startServer() {
app.use(viteDevMiddleware);
}
+ // SESSIONS
router.get('/api/session', fetchUser);
router.post('/api/session', login);
router.delete('/api/session', logout);
+ // USERS
+ router.post('/api/users', createUser);
+
+ // SHELVES
router.post('/api/shelves', createShelf);
- router.post('/api/users', createUser);
+ // POSTS
+ router.post('/api/shelves/:shelfId/posts', createPost);
router.get('*', defaultHandler);
router.use(errorHandler);