Added security policies to all endpoints and implemented file upload feature on posts
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
.env
|
.env
|
||||||
|
/uploads
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# API contract (agent reference)
|
||||||
|
|
||||||
|
Base URL: `http://localhost:${PORT}`. Use `Authorization: Bearer <token>` on every endpoint except register/login. JSON uses `Content-Type: application/json`; upload routes use `multipart/form-data`.
|
||||||
|
|
||||||
|
| Method + path | Auth | Input | Result / policy |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `POST /auth/register` | no | JSON: `name`, `username`, `email`, `password` | `201 {id}`; strict fields; see password policy below |
|
||||||
|
| `POST /auth/login` | no | JSON: `email`, `password` | `200 {id,token,expires_at}`; `401` is generic |
|
||||||
|
| `GET /posts` | yes | no body/query | `200` all posts; ownership does not limit viewing |
|
||||||
|
| `GET /posts/me` | yes | no body/query | `200` only posts with `author_id = token.user_id` |
|
||||||
|
| `GET /posts/image/:filename` | yes | no body/query | Authenticated image download |
|
||||||
|
| `POST /posts/create` | yes | multipart fields: `title`, `text`; optional file `image` | `201` post; `author_id` always comes from token |
|
||||||
|
| `PUT /posts/edit?id=<id>` | yes + owner | multipart: optional `title`, `text`, `image`, `remove_image=true\|false` | `200` updated post; upload replaces image; `remove_image=true` clears it; image + remove is invalid |
|
||||||
|
| `DELETE /posts/delete?id=<id>` | yes + owner | query: `id` only | `200`; deletes only matching `id AND author_id` and removes stored image |
|
||||||
|
| `POST /create_relationship?me=<id>&them=<id>` | yes | query: `me`, `them` only | `200`; `me` must equal token user ID |
|
||||||
|
|
||||||
|
## Input rules
|
||||||
|
|
||||||
|
- Any undocumented body or query field returns `400`; do not send `author_id` to create posts.
|
||||||
|
- Registration: `name` 1–100 chars; `username` 1–50, no whitespace; `email` 1–254, trimmed/lowercased, valid format, no whitespace, not disposable; `password` 1–128 and at least 12 chars with lower/upper/number/special, no username/email-local-part, common password, triple repeat, or obvious sequence.
|
||||||
|
- Login accepts only `email` and `password`; email is trimmed/lowercased; password is not trimmed or otherwise transformed (ordinary whitespace is significant).
|
||||||
|
- Passwords, hashes, salts, JWTs, and database details must never be logged or exposed.
|
||||||
|
|
||||||
|
## Uploads
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST http://localhost:3000/posts/create \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-F title='Hello' -F text='Body' -F image=@photo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
To edit text/title, replace the image, or remove it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X PUT "http://localhost:3000/posts/edit?id=12" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-F text='Updated body' -F remove_image=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit `image` and `remove_image` to keep the current image. Stored image URLs require the same Bearer token.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Filing Cabinet API
|
||||||
|
|
||||||
|
Node.js/Express API for accounts, JWT-authenticated actions, relationships, and posts.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run dev # nodemon app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure `.env` (never commit it):
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=3000
|
||||||
|
JWT_SECRET=<random secret, at least 32 characters>
|
||||||
|
JWT_EXPIRES_IN=86400
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
MAX_UPLOAD_SIZE_BYTES=5242880
|
||||||
|
```
|
||||||
|
|
||||||
|
Database and graph-database variables are also required by the relevant routes. See `.env.example`.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
`POST /auth/register` and `POST /auth/login` are public. Every other route requires:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <JWT>
|
||||||
|
```
|
||||||
|
|
||||||
|
Login returns `{ id, token, expires_at }`. The token is HS256-signed and contains `user_id`, `iat`, and `exp`. Never log or expose tokens, passwords, password hashes, or salts.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The compact endpoint contract is in [API.md](API.md). Unknown body/query fields are rejected with `400`; clients must send only documented fields.
|
||||||
|
|
||||||
|
## Security and ownership
|
||||||
|
|
||||||
|
- The API is the authoritative validator; client validation is only UX.
|
||||||
|
- Registration/login validation and password rules are defined in [AUTH_INPUT_POLICY.md](AUTH_INPUT_POLICY.md).
|
||||||
|
- Post creation always uses `author_id` from the verified JWT; clients must not send `author_id`.
|
||||||
|
- Post edit/delete require that the JWT user owns the post.
|
||||||
|
- `/posts` is visible to any authenticated user; `/posts/me` filters by JWT `user_id`.
|
||||||
|
- Relationship creation requires `me` to equal the JWT user ID.
|
||||||
|
- Uploads accept only JPEG, PNG, GIF, and WebP, one file named `image`, up to `MAX_UPLOAD_SIZE_BYTES` (default 5 MiB). Files receive random names and are stored under `UPLOAD_DIR`.
|
||||||
|
- Registration rejects disposable email addresses and weak/reused-pattern passwords. Login failures use the generic `Invalid email or password` response.
|
||||||
|
- Do not expose database errors or stack traces to clients.
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require('express');
|
const express = require("express");
|
||||||
const cors = require('cors');
|
const cors = require("cors");
|
||||||
const app = express();
|
const app = express();
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require("dotenv");
|
||||||
require('dotenv').config();
|
const { authenticateToken } = require("./middleware/authenticate_token");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '')
|
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || "")
|
||||||
.split(',')
|
.split(",")
|
||||||
.map((origin) => origin.trim())
|
.map((origin) => origin.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
@@ -13,28 +14,32 @@ const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '')
|
|||||||
app.use(cors({ origin: allowedOrigins }));
|
app.use(cors({ origin: allowedOrigins }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Data routes
|
|
||||||
const getDataRoute = require('./features/data/GET_data');
|
|
||||||
const postDataRoute = require('./features/data/POST_data');
|
|
||||||
const deleteDataRoute = require('./features/data/DELETE_data');
|
|
||||||
|
|
||||||
app.use('/get_data', getDataRoute);
|
|
||||||
app.use('/post_data', postDataRoute);
|
|
||||||
app.use('/delete_data', deleteDataRoute);
|
|
||||||
|
|
||||||
// People routes
|
// People routes
|
||||||
const CreatePersonRoute = require('./features/people/REGISTER_people');
|
const CreatePersonRoute = require("./features/people/REGISTER_people");
|
||||||
const LoginPersonRoute = require('./features/people/LOGIN_people');
|
const LoginPersonRoute = require("./features/people/LOGIN_people");
|
||||||
|
|
||||||
app.use('/auth/register', CreatePersonRoute);
|
app.use("/auth/register", CreatePersonRoute);
|
||||||
app.use('/auth/login', LoginPersonRoute);
|
app.use("/auth/login", LoginPersonRoute);
|
||||||
|
|
||||||
|
// Every route below this point requires a valid JWT. Registration and login stay public.
|
||||||
|
app.use(authenticateToken);
|
||||||
|
|
||||||
// Relation routes
|
// Relation routes
|
||||||
const CreateRelationRoute = require('./features/relations/CREATE_friendship');
|
const CreateRelationRoute = require("./features/relations/CREATE_friendship");
|
||||||
|
|
||||||
app.use('/create_relationship', CreateRelationRoute)
|
app.use("/create_relationship", CreateRelationRoute);
|
||||||
|
|
||||||
|
// Post routes
|
||||||
|
const CreatePostRoute = require("./features/posts/CREATE_post");
|
||||||
|
const DeletePostRoute = require("./features/posts/DELETE_post");
|
||||||
|
const GetPostsRoute = require("./features/posts/GET_posts");
|
||||||
|
const EditPostRoute = require("./features/posts/EDIT_post");
|
||||||
|
|
||||||
|
app.use("/posts", GetPostsRoute);
|
||||||
|
app.use("/posts/create", CreatePostRoute);
|
||||||
|
app.use("/posts/delete", DeletePostRoute);
|
||||||
|
app.use("/posts/edit", EditPostRoute);
|
||||||
|
|
||||||
app.listen(process.env.PORT, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
console.log(`Server draait op port ${process.env.PORT}`)
|
console.log(`Server draait op port ${process.env.PORT}`);
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const pool = require('../../db');
|
|
||||||
|
|
||||||
|
|
||||||
async function delete_data(req, res) {
|
|
||||||
try {
|
|
||||||
const name = req.query.name;
|
|
||||||
const result = await pool.query('DELETE FROM data WHERE name = $1', [name]);
|
|
||||||
res.status(200).json({
|
|
||||||
message: 'Data Deleted',
|
|
||||||
deleted: result.rows
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('database query failed', {
|
|
||||||
message: err.message,
|
|
||||||
code: err.code,
|
|
||||||
detail: err.detail,
|
|
||||||
hint: err.hint,
|
|
||||||
stack: err.stack,
|
|
||||||
});
|
|
||||||
res.status(500).send('Data has not been deleted, request failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
router.delete('/', delete_data);
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const pool = require('../../db');
|
|
||||||
|
|
||||||
|
|
||||||
async function get_data(req, res) {
|
|
||||||
try {
|
|
||||||
const result = await pool.query('SELECT id, name, created_at FROM data');
|
|
||||||
console.log(result.rows);
|
|
||||||
res.json(result.rows);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('[get_data] database query failed', {
|
|
||||||
message: err.message,
|
|
||||||
code: err.code,
|
|
||||||
detail: err.detail,
|
|
||||||
hint: err.hint,
|
|
||||||
stack: err.stack,
|
|
||||||
});
|
|
||||||
res.status(500).send('No data available, request failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
router.get('/', get_data);
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const pool = require('../../db');
|
|
||||||
|
|
||||||
|
|
||||||
async function post_data(req, res) {
|
|
||||||
try {
|
|
||||||
const { name } = req.body;
|
|
||||||
const { rows } = await pool.query('INSERT INTO data (name) VALUES ($1)', [name]);
|
|
||||||
console.log(rows[0]);
|
|
||||||
res.json(rows[0]);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('database query failed', {
|
|
||||||
message: err.message,
|
|
||||||
code: err.code,
|
|
||||||
detail: err.detail,
|
|
||||||
hint: err.hint,
|
|
||||||
stack: err.stack,
|
|
||||||
});
|
|
||||||
res.status(500).send('Data was not pushed, request failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
router.post('/', post_data);
|
|
||||||
module.exports = router;
|
|
||||||
@@ -4,6 +4,8 @@ const pool = require('../../db');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const { sanitizeLoginInput } = require('../../middleware/sanitize_person_input');
|
const { sanitizeLoginInput } = require('../../middleware/sanitize_person_input');
|
||||||
|
const { strictInput } = require('../../middleware/strict_input');
|
||||||
|
const { signToken } = require('../../middleware/authenticate_token');
|
||||||
|
|
||||||
const scrypt = promisify(crypto.scrypt);
|
const scrypt = promisify(crypto.scrypt);
|
||||||
|
|
||||||
@@ -32,7 +34,8 @@ async function login(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ id: rows[0].id });
|
const { token, expiresAt } = signToken(rows[0].id);
|
||||||
|
res.status(200).json({ id: rows[0].id, token, expires_at: expiresAt });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('login failed', { message: err.message, code: err.code });
|
console.error('login failed', { message: err.message, code: err.code });
|
||||||
@@ -40,5 +43,5 @@ async function login(req, res) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/', sanitizeLoginInput, login);
|
router.post('/', strictInput({ body: ['email', 'password'] }), sanitizeLoginInput, login);
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const pool = require('../../db');
|
|||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const { sanitizePersonInput } = require('../../middleware/sanitize_person_input');
|
const { sanitizePersonInput } = require('../../middleware/sanitize_person_input');
|
||||||
|
const { strictInput } = require('../../middleware/strict_input');
|
||||||
|
|
||||||
const scrypt = promisify(crypto.scrypt);
|
const scrypt = promisify(crypto.scrypt);
|
||||||
|
|
||||||
@@ -84,5 +85,5 @@ async function create_people(req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
router.post('/', sanitizePersonInput, create_people);
|
router.post('/', strictInput({ body: ['name', 'username', 'email', 'password'] }), sanitizePersonInput, create_people);
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = require("../../db");
|
||||||
|
const { strictInput } = require("../../middleware/strict_input");
|
||||||
|
const { authenticateToken } = require("../../middleware/authenticate_token");
|
||||||
|
const {
|
||||||
|
handlePostUpload,
|
||||||
|
imageLinkForFile,
|
||||||
|
removeUploadedFile,
|
||||||
|
} = require("../../middleware/post_upload");
|
||||||
|
|
||||||
|
async function create_post(req, res) {
|
||||||
|
try {
|
||||||
|
const { title, text } = req.body;
|
||||||
|
const author_id = req.user.id;
|
||||||
|
if (!title || !text) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
return res.status(400).send("Title or text was not provided");
|
||||||
|
} else {
|
||||||
|
const imageLink = imageLinkForFile(req.file);
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
"INSERT INTO posts (title, text, author_id, image_link) VALUES ($1, $2, $3, $4) RETURNING id, title, text, author_id, image_link",
|
||||||
|
[title, text, author_id, imageLink],
|
||||||
|
);
|
||||||
|
res.status(201).json(rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("database query failed", {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
res.status(500).send("Data was not pushed, request failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
authenticateToken,
|
||||||
|
handlePostUpload,
|
||||||
|
strictInput({ body: ["title", "text"], cleanupUploadedFile: true }),
|
||||||
|
create_post,
|
||||||
|
);
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = require("../../db");
|
||||||
|
const { strictInput } = require("../../middleware/strict_input");
|
||||||
|
const { authenticateToken } = require("../../middleware/authenticate_token");
|
||||||
|
const { removeImageLink } = require("../../middleware/post_upload");
|
||||||
|
|
||||||
|
async function delete_post(req, res) {
|
||||||
|
try {
|
||||||
|
const id = req.query.id;
|
||||||
|
const result = await pool.query(
|
||||||
|
"DELETE FROM posts WHERE id = $1 AND author_id = $2 RETURNING id, image_link",
|
||||||
|
[id, req.user.id],
|
||||||
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return res.status(404).send("Post not found or does not belong to you");
|
||||||
|
}
|
||||||
|
await removeImageLink(result.rows[0].image_link);
|
||||||
|
res.status(200).json({
|
||||||
|
message: "Post Deleted",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("database query failed", {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
res.status(500).send("Data has not been deleted, request failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/",
|
||||||
|
authenticateToken,
|
||||||
|
strictInput({ query: ["id"] }),
|
||||||
|
delete_post,
|
||||||
|
);
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = require("../../db");
|
||||||
|
const { strictInput } = require("../../middleware/strict_input");
|
||||||
|
const { authenticateToken } = require("../../middleware/authenticate_token");
|
||||||
|
const {
|
||||||
|
handlePostUpload,
|
||||||
|
imageLinkForFile,
|
||||||
|
removeImageLink,
|
||||||
|
removeUploadedFile,
|
||||||
|
} = require("../../middleware/post_upload");
|
||||||
|
|
||||||
|
async function editPost(req, res) {
|
||||||
|
const id = req.query.id;
|
||||||
|
const { title, text, remove_image: removeImageValue } = req.body;
|
||||||
|
const removeImage = removeImageValue === "true";
|
||||||
|
|
||||||
|
if (removeImageValue !== undefined && !["true", "false"].includes(removeImageValue)) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
return res.status(400).send("remove_image must be true or false");
|
||||||
|
}
|
||||||
|
if (req.file && removeImage) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
return res.status(400).send("Choose either an image or remove_image=true");
|
||||||
|
}
|
||||||
|
if (title === "" || text === "") {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
return res.status(400).send("title and text cannot be empty");
|
||||||
|
}
|
||||||
|
if (title === undefined && text === undefined && !req.file && !removeImage) {
|
||||||
|
return res.status(400).send("At least one post field must be changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await pool.query(
|
||||||
|
"SELECT id, image_link FROM posts WHERE id = $1 AND author_id = $2",
|
||||||
|
[id, req.user.id],
|
||||||
|
);
|
||||||
|
if (existing.rowCount === 0) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
return res.status(404).send("Post not found or does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldImageLink = existing.rows[0].image_link;
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
const addField = (field, value) => {
|
||||||
|
fields.push(`${field} = $${values.length + 1}`);
|
||||||
|
values.push(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (title !== undefined) addField("title", title);
|
||||||
|
if (text !== undefined) addField("text", text);
|
||||||
|
if (req.file) addField("image_link", imageLinkForFile(req.file));
|
||||||
|
if (removeImage) addField("image_link", null);
|
||||||
|
values.push(id, req.user.id);
|
||||||
|
|
||||||
|
let updated;
|
||||||
|
try {
|
||||||
|
updated = await pool.query(
|
||||||
|
`UPDATE posts SET ${fields.join(", ")} WHERE id = $${values.length - 1} AND author_id = $${values.length} RETURNING id, title, text, author_id, image_link`,
|
||||||
|
values,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.file || removeImage) {
|
||||||
|
await removeImageLink(oldImageLink).catch((err) => {
|
||||||
|
console.error("old post image could not be removed", { message: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(200).json(updated.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
await removeUploadedFile(req.file);
|
||||||
|
console.error("database query failed", {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
return res.status(500).send("Post was not updated, request failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
"/",
|
||||||
|
authenticateToken,
|
||||||
|
handlePostUpload,
|
||||||
|
strictInput({
|
||||||
|
query: ["id"],
|
||||||
|
body: ["title", "text", "remove_image"],
|
||||||
|
cleanupUploadedFile: true,
|
||||||
|
}),
|
||||||
|
editPost,
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = require("../../db");
|
||||||
|
const { strictInput } = require("../../middleware/strict_input");
|
||||||
|
const { authenticateToken } = require("../../middleware/authenticate_token");
|
||||||
|
const { uploadDirectory } = require("../../middleware/post_upload");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
async function getPosts(req, res, ownOnly) {
|
||||||
|
try {
|
||||||
|
const query = ownOnly
|
||||||
|
? "SELECT id, title, text, author_id, image_link FROM posts WHERE author_id = $1"
|
||||||
|
: "SELECT id, title, text, author_id, image_link FROM posts";
|
||||||
|
const values = ownOnly ? [req.user.id] : [];
|
||||||
|
const { rows } = await pool.query(query, values);
|
||||||
|
res.status(200).json(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("database query failed", {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
res.status(500).send("Posts could not be retrieved, request failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/image/:filename",
|
||||||
|
authenticateToken,
|
||||||
|
strictInput(),
|
||||||
|
(req, res) => {
|
||||||
|
const filename = path.basename(req.params.filename);
|
||||||
|
if (filename !== req.params.filename) return res.status(404).end();
|
||||||
|
res.sendFile(filename, { root: uploadDirectory }, (err) => {
|
||||||
|
if (err && !res.headersSent) res.status(err.statusCode === 404 ? 404 : 500).end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/me",
|
||||||
|
authenticateToken,
|
||||||
|
strictInput(),
|
||||||
|
(req, res) => getPosts(req, res, true),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/",
|
||||||
|
authenticateToken,
|
||||||
|
strictInput(),
|
||||||
|
(req, res) => getPosts(req, res, false),
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
const express = require('express');
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
var driver = require('../../graph_db');
|
var driver = require("../../graph_db");
|
||||||
|
const { strictInput } = require('../../middleware/strict_input');
|
||||||
|
|
||||||
|
|
||||||
async function create_friendship(req, res) {
|
async function create_friendship(req, res) {
|
||||||
try {
|
try {
|
||||||
const me = req.query.me;
|
const me = req.query.me;
|
||||||
const them = req.query.them;
|
const them = req.query.them;
|
||||||
const { records } = await driver.executeQuery('MATCH (m:Person {name: $me}) MATCH (t:Person {name: $them}) CREATE (m)-[:FRIENDS_WITH]-> (t) CREATE (t)-[:FRIENDS_WITH]-> (m)', { me: me, them: them }, { database: process.env.GRAPH_DB_NAME});
|
if (String(req.user.id) !== String(me)) {
|
||||||
res.status(200).send('Relationship has been created');
|
return res.status(403).send("You can only create relationships as yourself");
|
||||||
}
|
}
|
||||||
catch (err) {
|
const { records } = await driver.executeQuery(
|
||||||
console.error('database query failed', {
|
"MATCH (m:Person {db_id: $me}) MATCH (t:Person {db_id: $them}) CREATE (m)-[:FRIENDS_WITH]-> (t) CREATE (t)-[:FRIENDS_WITH]-> (m)",
|
||||||
|
{ me: me, them: them },
|
||||||
|
{ database: process.env.GRAPH_DB_NAME },
|
||||||
|
);
|
||||||
|
res.status(200).send("Relationship has been created");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("database query failed", {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
code: err.code,
|
code: err.code,
|
||||||
detail: err.detail,
|
detail: err.detail,
|
||||||
hint: err.hint,
|
hint: err.hint,
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
});
|
});
|
||||||
res.status(500).send('Relationship was not created, request failed');
|
res.status(500).send("Relationship was not created, request failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
router.post("/", strictInput({ query: ['me', 'them'] }), create_friendship);
|
||||||
router.post('/', create_friendship);
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const DEFAULT_TOKEN_LIFETIME_SECONDS = 60 * 60;
|
||||||
|
|
||||||
|
function base64url(value) {
|
||||||
|
return Buffer.from(value).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJwtSecret() {
|
||||||
|
const secret = process.env.JWT_SECRET;
|
||||||
|
if (!secret || secret.length < 32) {
|
||||||
|
throw new Error('JWT_SECRET must be configured and contain at least 32 characters');
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signToken(userId) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const configuredLifetime = Number.parseInt(process.env.JWT_EXPIRES_IN, 10);
|
||||||
|
const lifetime = Number.isInteger(configuredLifetime) && configuredLifetime > 0
|
||||||
|
? configuredLifetime
|
||||||
|
: DEFAULT_TOKEN_LIFETIME_SECONDS;
|
||||||
|
const payload = {
|
||||||
|
user_id: userId,
|
||||||
|
iat: now,
|
||||||
|
exp: now + lifetime,
|
||||||
|
};
|
||||||
|
const encodedHeader = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||||
|
const encodedPayload = base64url(JSON.stringify(payload));
|
||||||
|
const content = `${encodedHeader}.${encodedPayload}`;
|
||||||
|
const signature = crypto
|
||||||
|
.createHmac('sha256', getJwtSecret())
|
||||||
|
.update(content)
|
||||||
|
.digest('base64url');
|
||||||
|
|
||||||
|
return { token: `${content}.${signature}`, expiresAt: payload.exp };
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticateToken(req, res, next) {
|
||||||
|
const authorization = req.get('authorization') || '';
|
||||||
|
const match = authorization.match(/^Bearer\s+([^\s]+)$/i);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return res.status(401).send('A valid Bearer token is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [encodedHeader, encodedPayload, providedSignature] = match[1].split('.');
|
||||||
|
if (!encodedHeader || !encodedPayload || !providedSignature) {
|
||||||
|
throw new Error('Malformed token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString('utf8'));
|
||||||
|
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));
|
||||||
|
if (header.alg !== 'HS256' || header.typ !== 'JWT') {
|
||||||
|
throw new Error('Unsupported token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedSignature = crypto
|
||||||
|
.createHmac('sha256', getJwtSecret())
|
||||||
|
.update(`${encodedHeader}.${encodedPayload}`)
|
||||||
|
.digest('base64url');
|
||||||
|
const provided = Buffer.from(providedSignature);
|
||||||
|
const expected = Buffer.from(expectedSignature);
|
||||||
|
|
||||||
|
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
|
||||||
|
throw new Error('Invalid signature');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(payload.user_id) || payload.user_id <= 0 ||
|
||||||
|
!Number.isInteger(payload.exp) || payload.exp <= Math.floor(Date.now() / 1000)) {
|
||||||
|
throw new Error('Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = { id: payload.user_id };
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message.startsWith('JWT_SECRET')) {
|
||||||
|
console.error('[auth] JWT configuration error:', err.message);
|
||||||
|
return res.status(500).send('Authentication is not configured');
|
||||||
|
}
|
||||||
|
return res.status(401).send('Invalid or expired token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { authenticateToken, signToken };
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const multer = require("multer");
|
||||||
|
|
||||||
|
const uploadDirectory = path.resolve(
|
||||||
|
process.env.UPLOAD_DIR || path.join(process.cwd(), "uploads"),
|
||||||
|
);
|
||||||
|
const maxFileSize = Number.parseInt(process.env.MAX_UPLOAD_SIZE_BYTES, 10) || 5 * 1024 * 1024;
|
||||||
|
const extensionByMimeType = {
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.mkdirSync(uploadDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: uploadDirectory,
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
callback(null, `${crypto.randomUUID()}${extensionByMimeType[file.mimetype]}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadImage = multer({
|
||||||
|
storage,
|
||||||
|
limits: { fileSize: maxFileSize, files: 1 },
|
||||||
|
fileFilter: (req, file, callback) => {
|
||||||
|
if (!extensionByMimeType[file.mimetype]) {
|
||||||
|
return callback(new Error("Only JPEG, PNG, GIF, and WebP images are allowed"));
|
||||||
|
}
|
||||||
|
callback(null, true);
|
||||||
|
},
|
||||||
|
}).single("image");
|
||||||
|
|
||||||
|
function handlePostUpload(req, res, next) {
|
||||||
|
uploadImage(req, res, (err) => {
|
||||||
|
if (!err) return next();
|
||||||
|
if (err.code === "LIMIT_FILE_SIZE") {
|
||||||
|
return res.status(413).send("Image exceeds the maximum allowed size");
|
||||||
|
}
|
||||||
|
return res.status(400).send(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageLinkForFile(file) {
|
||||||
|
return file ? `/posts/image/${file.filename}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeImageLink(imageLink) {
|
||||||
|
if (!imageLink || !imageLink.startsWith("/posts/image/")) return;
|
||||||
|
const filename = path.basename(imageLink);
|
||||||
|
await fs.promises.unlink(path.join(uploadDirectory, filename)).catch((err) => {
|
||||||
|
if (err.code !== "ENOENT") throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUploadedFile(file) {
|
||||||
|
if (file) await removeImageLink(imageLinkForFile(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handlePostUpload,
|
||||||
|
imageLinkForFile,
|
||||||
|
removeImageLink,
|
||||||
|
removeUploadedFile,
|
||||||
|
uploadDirectory,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function strictInput({ body = [], query = [], cleanupUploadedFile = false } = {}) {
|
||||||
|
const allowedBody = new Set(body);
|
||||||
|
const allowedQuery = new Set(query);
|
||||||
|
|
||||||
|
return function validateInputShape(req, res, next) {
|
||||||
|
const bodyKeys = Object.keys(req.body || {});
|
||||||
|
const queryKeys = Object.keys(req.query || {});
|
||||||
|
const unexpectedBody = bodyKeys.filter((key) => !allowedBody.has(key));
|
||||||
|
const unexpectedQuery = queryKeys.filter((key) => !allowedQuery.has(key));
|
||||||
|
|
||||||
|
if (unexpectedBody.length > 0 || unexpectedQuery.length > 0) {
|
||||||
|
if (cleanupUploadedFile && req.file?.path) {
|
||||||
|
fs.promises.unlink(req.file.path).catch(() => {});
|
||||||
|
}
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Unexpected request field(s)',
|
||||||
|
fields: [...unexpectedBody, ...unexpectedQuery],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { strictInput };
|
||||||
Generated
+135
@@ -13,6 +13,7 @@
|
|||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"fakeout": "^1.0.65",
|
"fakeout": "^1.0.65",
|
||||||
|
"multer": "^2.2.0",
|
||||||
"neo4j-driver": "^6.2.0",
|
"neo4j-driver": "^6.2.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"router": "^2.2.0"
|
"router": "^2.2.0"
|
||||||
@@ -48,6 +49,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-field": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
@@ -178,6 +185,23 @@
|
|||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -241,6 +265,21 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||||
@@ -841,6 +880,68 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/multer": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"append-field": "^1.0.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"type-is": "^1.6.18"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
@@ -1186,6 +1287,20 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -1417,6 +1532,14 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -1508,6 +1631,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undefsafe": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
@@ -1524,6 +1653,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"fakeout": "^1.0.65",
|
"fakeout": "^1.0.65",
|
||||||
|
"multer": "^2.2.0",
|
||||||
"neo4j-driver": "^6.2.0",
|
"neo4j-driver": "^6.2.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"router": "^2.2.0"
|
"router": "^2.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user