diff --git a/endpoints.http b/endpoints.http
index 93a4b53..b9a1238 100644
--- a/endpoints.http
+++ b/endpoints.http
@@ -1,28 +1,88 @@
-@baseUri=http://localhost:3000
-@username={{$randomInt}}
-@email={{$randomInt}}@google.com
-GET {{baseUri}} HTTP/1.1
+GET {{baseUrl}}/budget/:id/add HTTP/1.1
-POST {{baseUri}}/auth/register HTTP/1.1
-Content-Type: application/json
+###
+GET {{baseUrl}}/admin/users HTTP/1.1
-{"username":"{{username}}","password":"password","email":"{{email}}","type":"monitor"}
###
+GET {{baseUrl}}/admin/user/:id HTTP/1.1
-POST {{baseUri}}/auth/login HTTP/1.1
+###
+DELETE {{baseUrl}}/admin/user/:id HTTP/1.1
+
+###
+GET {{baseUrl}}/alarm/:id HTTP/1.1
+
+###
+PUT {{baseUrl}}/alarm/:id HTTP/1.1
+
+###
+GET {{baseUrl}}/alarms HTTP/1.1
+
+###
+GET {{baseUrl}}/alarm/@me HTTP/1.1
+
+###
+PUT {{baseUrl}}/alarm/@me HTTP/1.1
+
+###
+DELETE {{baseUrl}}/alarm/@me HTTP/1.1
+
+###
+# Delete account
+DELETE {{baseUrl}}/auth/delete HTTP/1.1
Content-Type: application/json
-{"username":"{{username}}","password":"password"}
+{
+ "username": "{{username}}",
+ "email": "{{email}}",
+ "password": "{{email}}"
+}
+
+
###
+GET {{baseUrl}}/budget/:id HTTP/1.1
-POST {{baseUri}}/auth/logout HTTP/1.1
+###
+GET {{baseUrl}}/budget/@me HTTP/1.1
+
+###
+GET {{baseUrl}}/auth/devices HTTP/1.1
+
+###
+GET {{baseUrl}}/ HTTP/1.1
+
+###
+# Log in as a user
+POST {{baseUrl}}/auth/login HTTP/1.1
Content-Type: application/json
-{"username":"{{username}}","password":"password"}
+{
+ "username": "{{username}}",
+ "email": "{{email}}",
+ "password": "{{email}}"
+}
+
+
###
+# Log out from a device (TODO)
+POST {{baseUrl}}/auth/logout HTTP/1.1
+Authorization: Bearer {{accessToken}}
-DELETE {{baseUri}}/auth/delete
+###
+# Create a new user
+POST {{baseUrl}}/auth/register HTTP/1.1
Content-Type: application/json
-{"username":"{{username}}","password":"password"}
+{
+ "username": "{{username}}",
+ "email": "{{email}}",
+ "password": "{{email}}",
+ "type": "{{userType}}"
+}
+
+
+###
+GET {{baseUrl}}/status HTTP/1.1
+
+###
diff --git a/endpoints.old.http b/endpoints.old.http
new file mode 100644
index 0000000..93a4b53
--- /dev/null
+++ b/endpoints.old.http
@@ -0,0 +1,28 @@
+@baseUri=http://localhost:3000
+@username={{$randomInt}}
+@email={{$randomInt}}@google.com
+
+GET {{baseUri}} HTTP/1.1
+
+POST {{baseUri}}/auth/register HTTP/1.1
+Content-Type: application/json
+
+{"username":"{{username}}","password":"password","email":"{{email}}","type":"monitor"}
+###
+
+POST {{baseUri}}/auth/login HTTP/1.1
+Content-Type: application/json
+
+{"username":"{{username}}","password":"password"}
+###
+
+POST {{baseUri}}/auth/logout HTTP/1.1
+Content-Type: application/json
+
+{"username":"{{username}}","password":"password"}
+###
+
+DELETE {{baseUri}}/auth/delete
+Content-Type: application/json
+
+{"username":"{{username}}","password":"password"}
diff --git a/package.json b/package.json
index 09cbaff..1f3b217 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
"prepare": "husky install",
"test": "node --test",
"test:watch": "node --test --watch",
- "coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
+ "coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info",
+ "build:http": "node src/api/routes.js | grep '^%' --line-buffered | sed -u 's/^% //g' > endpoints.http"
},
"repository": {
"type": "git",
diff --git a/plan.md b/plan.md
index cbedbd5..f1ab28b 100644
--- a/plan.md
+++ b/plan.md
@@ -1,15 +1,15 @@
# Assignment requirements
-- [ ] Express API with at least 17 endpoints
-- [ ] Testing: both integration and unit tests
-- [ ] Authentication & Authorization: using JWT tokens or a similar technology
-- [ ] Error handling
+- [x] Express API with at least 17 endpoints
+- [x] Testing: both integration and unit tests
+- [x] Authentication & Authorization: using JWT tokens or a similar technology
+- [x] Error handling
- [ ] MongoDB CRUD operations and Mongoose modeling (with Joi validation)
- [ ] Complete data and input validation (including ObjectId validation)
-- [ ] Middleware functions
+- [x] Middleware functions
- [ ] A detailed deployment step-by-step plan: the application must be deployed on a cloud server and publicly accessible
-- [ ] REST Client API calls provided for all API endpoints
-- [ ] Comprehensive API documentation
+- [x] REST Client API calls provided for all API endpoints
+- [x] Comprehensive API documentation
# Feature plan
@@ -17,13 +17,14 @@
- [x] Registration
- [ ] Validation based on type
- [x] Login
+ - [-] Logout
- [x] Delete
- [ ] Password reset
- [ ] User profile management
- [ ] Device management
- [ ] Organisation (who's coming? announcement of events, ...)
- [ ] Budgeting with tracking
- - [ ] Get current budget
+ - [x] Get current budget
- [ ] Review spending history
- [ ] Request additional budget in case of emergency
- [x] Emergency alarm
@@ -37,7 +38,8 @@
- [ ] Sugar intake limitations
- [x] Location tracking
- [ ] Endpoints
-- [ ] Sensor tracking (sugar level, heart rate, temperature, ...)
+- [x] Sensor tracking (sugar level, heart rate, temperature, ...)
+ - [ ] Endpoints
- [ ] Day planning (meetups, departure, ...)
- [ ] Location of activities
- [ ] Deadlines
diff --git a/src/api/RouteDescription.js b/src/api/RouteDescription.js
index 6ca426b..290fc82 100644
--- a/src/api/RouteDescription.js
+++ b/src/api/RouteDescription.js
@@ -56,10 +56,12 @@ export class RouteMethod {
* @type {Promise}
*/
method;
-
description;
-
exampleBody;
+ /*
+ * @type {Map<string, string>}
+ */
+ exampleHeaders = [];
/**
* @param data {RouteMethod}
@@ -68,7 +70,5 @@ export class RouteMethod {
for (const key of Object.keys(data)) {
this[key] = data[key];
}
-
- if (!Array.isArray(this.middlewares)) this.middlewares = [];
}
}
diff --git a/src/api/routes.js b/src/api/routes.js
index e808d50..b91e7ba 100644
--- a/src/api/routes.js
+++ b/src/api/routes.js
@@ -1,13 +1,26 @@
import * as routes from './routes/index.js';
+import * as url from 'node:url';
+import express from 'express';
-function logHttpEntry(method, route, exampleBody, description) {
- if (description) console.log('%', '#', description);
+function logHttpEntry(method, route, routeMethod) {
+ if (routeMethod.description) console.log('%', '#', routeMethod.description);
console.log('%', method, '{{baseUrl}}' + route, 'HTTP/1.1');
- if (exampleBody) {
+ if (routeMethod.exampleHeaders) {
+ for (var key of Object.keys(routeMethod.exampleHeaders)) {
+ console.log('%', `${key}: ${routeMethod.exampleHeaders[key]}`);
+ }
+ }
+ if (routeMethod.exampleBody) {
console.log('%', 'Content-Type: application/json');
console.log('% ');
- console.log('%', JSON.stringify(exampleBody, null, 4));
+ console.log(
+ '%',
+ JSON.stringify(routeMethod.exampleBody, null, 4).replaceAll(
+ '\n',
+ '\n% '
+ )
+ );
console.log('% ');
}
console.log('% ');
@@ -24,12 +37,15 @@ export function registerRoutes(app) {
* @type {RouteDescription}
*/
const route = routes[routeName];
+ if (route === undefined) return;
Object.keys(route.methods).forEach(routeMethodName => {
/**
* @type {RouteMethod}
*/
const routeMethod = route.methods[routeMethodName];
+ if (routeMethod === undefined) return;
+
console.log(
'Registering',
routeMethodName.toUpperCase(),
@@ -38,7 +54,7 @@ export function registerRoutes(app) {
logHttpEntry(
routeMethodName.toUpperCase(),
route.path,
- routeMethod.exampleBody
+ routeMethod
);
app[routeMethodName](route.path, [
...routeMethod.middlewares,
@@ -50,3 +66,12 @@ export function registerRoutes(app) {
console.log(`Registered ${routeCount} routes.`);
}
+
+if (import.meta.url.startsWith('file:')) {
+ const modulePath = url.fileURLToPath(import.meta.url);
+ if (process.argv[1] === modulePath) {
+ const app = express();
+ registerRoutes(app);
+ process.exit(1);
+ }
+}
diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js
index 76452e3..a2181d1 100644
--- a/src/api/routes/auth/accountRoutes.js
+++ b/src/api/routes/auth/accountRoutes.js
@@ -9,6 +9,13 @@ export const registerRoute = {
path: '/auth/register',
methods: {
post: new RouteMethod({
+ description: 'Create a new user',
+ exampleBody: {
+ username: '{{username}}',
+ email: '{{email}}',
+ password: '{{email}}',
+ type: '{{userType}}'
+ },
async method(req, res) {
const data = await RegisterDto.create(req.body);
await registerUser(data);
@@ -25,6 +32,12 @@ export const loginRoute = {
path: '/auth/login',
methods: {
post: new RouteMethod({
+ description: 'Log in as a user',
+ exampleBody: {
+ username: '{{username}}',
+ email: '{{email}}',
+ password: '{{email}}'
+ },
async method(req, res) {
const data = await AuthDto.create(req.body);
const loginResult = await loginUser(
@@ -44,6 +57,10 @@ export const logoutRoute = {
path: '/auth/logout',
methods: {
post: new RouteMethod({
+ description: 'Log out from a device (TODO)',
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
async method(req, res) {
const data = await AuthDto.create(req.body);
// const loginResult = await deleteDevice(data, );
@@ -60,6 +77,12 @@ export const deleteRoute = {
path: '/auth/delete',
methods: {
delete: new RouteMethod({
+ description: 'Delete account',
+ exampleBody: {
+ username: '{{username}}',
+ email: '{{email}}',
+ password: '{{email}}'
+ },
async method(req, res) {
const data = await AuthDto.create(req.body);
await deleteUser(data);
diff --git a/src/api/routes/auth/adminAccountRoutes.js b/src/api/routes/auth/adminAccountRoutes.js
index eb9b270..2153945 100644
--- a/src/api/routes/auth/adminAccountRoutes.js
+++ b/src/api/routes/auth/adminAccountRoutes.js
@@ -10,6 +10,10 @@ export const adminGetUsersRoute = {
path: '/admin/users',
methods: {
get: new RouteMethod({
+ description: 'Get all users (raw)',
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAdmin],
async method(req, res) {
res.send(DbUser.find({}).exec());
@@ -25,6 +29,10 @@ export const adminUserRoute = {
path: '/admin/user/:id',
methods: {
get: new RouteMethod({
+ description: 'Get a user (raw)',
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAdmin],
async method(req, res) {
const user = await getUserById(req.params.id);
diff --git a/src/api/routes/budgetRoutes.js b/src/api/routes/budgetRoutes.js
index a808f58..d7ebde4 100644
--- a/src/api/routes/budgetRoutes.js
+++ b/src/api/routes/budgetRoutes.js
@@ -4,23 +4,31 @@ import {
validateAuth
} from '#api/middlewares/index.js';
import { UserType } from '#db/schemas/index.js';
-import { RouteDescription, RouteMethod } from '#api/RouteDescription.js';
-import {getUserById} from "#db/dbAccess/index.js";
+import {
+ RouteDescription,
+ RouteMethod,
+ RouteMethodList
+} from '#api/RouteDescription.js';
+import { getUserById } from '#db/dbAccess/index.js';
/**
* @type {RouteDescription}
*/
export const getBudgetByUserRoute = {
path: '/budget/:id',
- methods: {
+ methods: new RouteMethodList({
get: new RouteMethod({
middlewares: [requireMonitor],
async method(req, res) {
- if (!req.)
- const user = await getUserById(req.);
+ if (req.user.type !== UserType.ADMIN) {
+ if (!req.user.monitoredUsers.includes(req.params.id))
+ throw new Error('meow');
+ }
+ //if (!req.)
+ // const user = await getUserById(req.);
}
})
- }
+ })
};
/**
@@ -44,7 +52,9 @@ export const getBudgetRoute = {
methods: {
get: new RouteMethod({
middlewares: [requireUser],
- async method(req, res) {}
+ async method(req, res) {
+ res.send({ currentBalance: req.user.balance });
+ }
})
}
};
diff --git a/src/api/routes/indexRoute.js b/src/api/routes/indexRoute.js
index b6fe28e..0c15b73 100644
--- a/src/api/routes/indexRoute.js
+++ b/src/api/routes/indexRoute.js
@@ -7,8 +7,11 @@ export const indexRoute = {
path: '/',
methods: {
get: new RouteMethod({
+ description: 'Get the index page (empty)',
method(req, res) {
- res.send('What art thou doing here???');
+ res.send(
+ "Welcome to SafeNSound! If you're confused, please visit the app instead!"
+ );
}
})
}
diff --git a/src/api/routes/statusRoute.js b/src/api/routes/statusRoute.js
index 1dbca45..2c111a8 100644
--- a/src/api/routes/statusRoute.js
+++ b/src/api/routes/statusRoute.js
@@ -8,6 +8,7 @@ export const statusRoute = {
path: '/status',
methods: {
get: new RouteMethod({
+ description: 'Get the server status',
async method(req, res) {
const status = {
status: 'ok',
diff --git a/test.http b/test.http
deleted file mode 100644
index 98b1ebd..0000000
--- a/test.http
+++ /dev/null
@@ -1,58 +0,0 @@
-
-GET {{baseUrl}}/budget/:id/add HTTP/1.1
-
-###
-GET {{baseUrl}}/admin/users HTTP/1.1
-
-###
-GET {{baseUrl}}/admin/user/:id HTTP/1.1
-
-###
-DELETE {{baseUrl}}/admin/user/:id HTTP/1.1
-
-###
-GET {{baseUrl}}/alarm/:id HTTP/1.1
-
-###
-PUT {{baseUrl}}/alarm/:id HTTP/1.1
-
-###
-GET {{baseUrl}}/alarms HTTP/1.1
-
-###
-GET {{baseUrl}}/alarm/@me HTTP/1.1
-
-###
-PUT {{baseUrl}}/alarm/@me HTTP/1.1
-
-###
-DELETE {{baseUrl}}/alarm/@me HTTP/1.1
-
-###
-DELETE {{baseUrl}}/auth/delete HTTP/1.1
-
-###
-GET {{baseUrl}}/budget/:id HTTP/1.1
-
-###
-GET {{baseUrl}}/budget/@me HTTP/1.1
-
-###
-GET {{baseUrl}}/auth/devices HTTP/1.1
-
-###
-GET {{baseUrl}}/ HTTP/1.1
-
-###
-POST {{baseUrl}}/auth/login HTTP/1.1
-
-###
-POST {{baseUrl}}/auth/logout HTTP/1.1
-
-###
-POST {{baseUrl}}/auth/register HTTP/1.1
-
-###
-GET {{baseUrl}}/status HTTP/1.1
-
-###
|