diff --git a/src/api/routes.js b/src/api/routes.js
index 279b1ef..e2765a3 100644
--- a/src/api/routes.js
+++ b/src/api/routes.js
@@ -15,6 +15,7 @@ function logHttpHeader() {
logPrefixed('@password=admin');
logPrefixed('@email=admin@example.com');
logPrefixed('@userType=admin');
+ logPrefixed('@accessToken=someValueXyzXyz');
logPrefixed('');
logPrefixed('');
}
diff --git a/src/api/routes/adminRoutes.js b/src/api/routes/adminRoutes.js
index 7a1d1e2..4baa5e4 100644
--- a/src/api/routes/adminRoutes.js
+++ b/src/api/routes/adminRoutes.js
@@ -16,6 +16,9 @@ export const adminGetUserIdsRoute = {
path: '/admin/allUserIds',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAdmin],
description: 'Get all user IDs',
async method(req, res) {
@@ -23,9 +26,13 @@ export const adminGetUserIdsRoute = {
res.status(200);
res.write('[\n');
+ let first = true;
const users = DbUser.find().lean().cursor();
for await (const user of users) {
- res.write(JSON.stringify(user._id) + ',\n');
+ res.write(
+ (first ? '' : ',') + JSON.stringify(user._id) + '\n'
+ );
+ first = false;
}
res.write(']');
@@ -38,6 +45,9 @@ export const adminMonitorAllRoute = {
path: '/admin/monitorAllUsers',
methods: {
post: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAdmin],
description: 'Monitor all users',
async method(req, res) {
diff --git a/src/api/routes/alarmRoutes.js b/src/api/routes/alarmRoutes.js
index aeae4ab..b0147ab 100644
--- a/src/api/routes/alarmRoutes.js
+++ b/src/api/routes/alarmRoutes.js
@@ -15,14 +15,20 @@ export const alarmByUserRoute = {
path: '/user/:id/alarm',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Get the alarm for a monitored user, if one is set',
async method(req, res) {
const user = await getUserById(req.params.id);
- res.send(user.alarm);
+ res.send(user.alarm ?? 'null');
}
}),
delete: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Clear the alarm for a monitored user',
async method(req, res) {
@@ -44,23 +50,27 @@ export const alarmListRoute = {
path: '/alarms',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Get a list of all alarms for monitored users',
async method(req, res) {
// execute the query asynchronously and manually construct a response, for scaling reasons
const users = DbUser.find({
- _id: { $in: req.user.monitoredUsers }
+ _id: { $in: req.user.monitoredUsers },
+ alarm: { $exists: true, $ne: null }
})
.lean()
.cursor();
res.status(200);
res.write('{\n');
+ let first = true;
for await (const user of users) {
- if (user.alarm) {
- res.write(
- `"${user._id}": ${JSON.stringify(user.alarm)},\n`
- );
- }
+ res.write(
+ `${first ? '' : ','}"${user._id}": ${JSON.stringify(user.alarm)}\n`
+ );
+ first = false;
}
res.write('}');
res.end();
@@ -76,15 +86,24 @@ export const alarmRoute = {
path: '/alarm/@me',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireUser],
description: "Get the current user's alarm",
async method(req, res) {
- res.send(req.user.alarm);
+ res.send(req.user.alarm ?? 'null');
}
}),
put: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ exampleBody: {
+ reason: 'fall'
+ },
middlewares: [requireUser],
- description: 'Raise an alarm',
+ description: 'Raise an alarm (enum: one of "fall" or "toilet")',
async method(req, res) {
req.user.alarm = await AlarmDto.create(req.body);
await req.user.save();
@@ -92,6 +111,9 @@ export const alarmRoute = {
}
}),
delete: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireUser],
description: 'Clear alarm',
async method(req, res) {
diff --git a/src/api/routes/assignedUserRoutes.js b/src/api/routes/assignedUserRoutes.js
index dac9b13..e870cd8 100644
--- a/src/api/routes/assignedUserRoutes.js
+++ b/src/api/routes/assignedUserRoutes.js
@@ -10,6 +10,9 @@ export const assignedUserRoute = {
path: '/monitor/assignedUsers',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Get assigned users',
async method(req, res) {
@@ -17,7 +20,13 @@ export const assignedUserRoute = {
}
}),
patch: {
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
+ exampleBody: {
+ userId: 'abcdef'
+ },
description: 'Add an assigned user by ID',
async method(req, res) {
if (!req.body.userId) {
@@ -41,6 +50,9 @@ export const assignedUserRoute = {
}
},
delete: {
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Remove an assigned user by ID',
async method(req, res) {
diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js
index 34592ed..f0d0102 100644
--- a/src/api/routes/auth/accountRoutes.js
+++ b/src/api/routes/auth/accountRoutes.js
@@ -3,6 +3,7 @@ import { AuthDto, RegisterDto } from '#dto/index.js';
import { RouteDescription, RouteMethod } from '#api/RouteDescription.js';
import { WhoAmIDto } from '#dto/auth/WhoAmIDto.js';
import { requireAuth } from '#api/middlewares/index.js';
+import { SafeNSoundError } from '#util/error.js';
/**
* @type {RouteDescription}
@@ -59,13 +60,17 @@ export const logoutRoute = {
path: '/auth/logout',
methods: {
post: new RouteMethod({
- description: 'Log out from a device (TODO)',
+ description: 'Log out from a device',
exampleHeaders: {
Authorization: 'Bearer {{accessToken}}'
},
+ middlewares: [requireAuth],
async method(req, res) {
- const data = await AuthDto.create(req.body);
- // const loginResult = await deleteDevice(data, );
+ const deviceIndex = req.user.devices.findIndex(
+ device => device.id === req.device._id
+ );
+ req.user.devices.splice(deviceIndex, 1);
+ await req.user.save();
res.status(204).send();
}
})
@@ -102,6 +107,9 @@ export const whoAmI = {
methods: {
get: new RouteMethod({
description: 'Get current user',
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAuth],
async method(req, res) {
const data = await WhoAmIDto.create({
diff --git a/src/api/routes/auth/adminAccountRoutes.js b/src/api/routes/auth/adminAccountRoutes.js
deleted file mode 100644
index b485002..0000000
--- a/src/api/routes/auth/adminAccountRoutes.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import {
- DbUser,
- deleteUser,
- getUserById,
- loginUser,
- registerUser,
- UserType
-} from '#db/index.js';
-import { AuthDto, RegisterDto } from '#dto/index.js';
-import { requireAdmin, requireRole } from '#api/middlewares/index.js';
-import { RouteDescription, RouteMethod } from '#api/RouteDescription.js';
-
-/**
- * @type {RouteDescription}
- */
-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());
- }
- })
- }
-};
-
-/**
- * @type {RouteDescription}
- */
-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);
- res.send(user);
- }
- }),
- delete: new RouteMethod({
- middlewares: [requireAdmin],
- description: 'Delete a user',
- async method(req, res) {
- await deleteUser(data);
- res.status(204).send();
- }
- })
- }
-};
diff --git a/src/api/routes/auth/deviceRoutes.js b/src/api/routes/auth/deviceRoutes.js
index 551252b..41802b8 100644
--- a/src/api/routes/auth/deviceRoutes.js
+++ b/src/api/routes/auth/deviceRoutes.js
@@ -1,7 +1,11 @@
-import { registerUser } from '#db/index.js';
-import { RegisterDto } from '#dto/index.js';
import { requireAuth } from '#api/middlewares/index.js';
import { RouteMethod } from '#api/RouteDescription.js';
+import { SafeNSoundError } from '#util/error.js';
+import Joi from 'joi';
+
+const deviceUpdateSchema = Joi.object({
+ name: Joi.string().optional().max(100)
+});
/**
* @type {RouteDescription}
@@ -10,12 +14,97 @@ export const getDevicesRoute = {
path: '/auth/devices',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireAuth],
description: 'Get all devices registered to the user',
async method(req, res) {
- const data = await RegisterDto.create(req.body);
- const registerResult = await registerUser(data);
- res.send(registerResult);
+ res.send(req.user.devices);
+ }
+ })
+ }
+};
+
+/**
+ * @type {RouteDescription}
+ */
+export const manageDeviceRoute = {
+ path: '/auth/devices/:id',
+ methods: {
+ get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ middlewares: [requireAuth],
+ description: 'Get user device by ID',
+ async method(req, res) {
+ const device = req.user.devices.find(
+ device => device.id === req.params.id
+ );
+ if (!device) {
+ res.status(404).send(
+ new SafeNSoundError({
+ errCode: 'ENTITY_NOT_FOUND',
+ message: 'Device not found'
+ })
+ );
+ return;
+ }
+ res.send(device);
+ }
+ }),
+ delete: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ middlewares: [requireAuth],
+ description: 'Delete user device by ID',
+ async method(req, res) {
+ const deviceIndex = req.user.devices.findIndex(
+ device => device.id === req.params.id
+ );
+ if (deviceIndex === -1) {
+ res.status(404).send(
+ new SafeNSoundError({
+ errCode: 'ENTITY_NOT_FOUND',
+ message: 'Device not found'
+ })
+ );
+ return;
+ }
+ req.user.devices.splice(deviceIndex, 1);
+ await req.user.save();
+ res.status(204).send();
+ }
+ }),
+ patch: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ exampleBody: {
+ name: 'New Device Name'
+ },
+ middlewares: [requireAuth],
+ description: 'Update user device by ID',
+ async method(req, res) {
+ const device = req.user.devices.find(
+ device => device.id === req.params.id
+ );
+ if (!device) {
+ res.status(404).send(
+ new SafeNSoundError({
+ errCode: 'ENTITY_NOT_FOUND',
+ message: 'Device not found'
+ })
+ );
+ return;
+ }
+ if (req.body.name) {
+ device.name = req.body.name;
+ }
+ await req.user.save();
+ res.send(device);
}
})
}
diff --git a/src/api/routes/auth/index.js b/src/api/routes/auth/index.js
index 2d2cc86..e687911 100644
--- a/src/api/routes/auth/index.js
+++ b/src/api/routes/auth/index.js
@@ -1,3 +1,2 @@
export * from './accountRoutes.js';
export * from './deviceRoutes.js';
-export * from './adminAccountRoutes.js';
diff --git a/src/api/routes/budgetRoutes.js b/src/api/routes/budgetRoutes.js
index bcb9711..e522442 100644
--- a/src/api/routes/budgetRoutes.js
+++ b/src/api/routes/budgetRoutes.js
@@ -3,7 +3,7 @@ import {
requireUser,
requireRole
} from '#api/middlewares/index.js';
-import { UserType } from '#db/schemas/index.js';
+import { DbSpendHistory, UserType } from '#db/schemas/index.js';
import {
RouteDescription,
RouteMethod,
@@ -11,14 +11,25 @@ import {
} from '#api/RouteDescription.js';
import { getUserById } from '#db/dbAccess/index.js';
import { SafeNSoundError } from '#util/error.js';
+import Joi from 'joi';
+
+const budgetModifySchema = new Joi.object({
+ venue: Joi.string().required().max(100),
+ reason: Joi.string().required().max(500),
+ amount: Joi.number().required().min(0),
+ createdAt: Joi.forbidden()
+});
/**
* @type {RouteDescription}
*/
export const getBudgetByUserRoute = {
- path: '/budget/:id',
+ path: '/user/:id/budget',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireMonitor],
description: 'Get the budget for a monitored user',
async method(req, res) {
@@ -33,40 +44,40 @@ export const getBudgetByUserRoute = {
const user = await getUserById(req.params.id);
res.send({ balance: user.balance });
}
- })
- }
-};
-
-/**
- * @type {RouteDescription}
- */
-export const addBudgetByUserRoute = {
- path: '/budget/:id/add',
- methods: {
- get: new RouteMethod({
- description: 'Add budget to a monitored user',
+ }),
+ patch: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ exampleBody: {
+ venue: 'Monitor 123',
+ reason: 'Just short for a coke to deal with diabetes',
+ amount: 0.15
+ },
middlewares: [requireMonitor],
+ description: 'Add budget for a monitored user',
async method(req, res) {
- if (req.user.type !== UserType.ADMIN) {
- if (!req.user.monitoredUsers.includes(req.params.id))
- throw new SafeNSoundError({
- errCode: 'UNAUTHORIZED',
- message:
- "You do not have permission to add budget to this user's account."
- });
- }
-
- const user = await getUserById(req.params.id);
- const amount = parseFloat(req.query.amount);
- if (isNaN(amount) || amount <= 0) {
+ if (
+ req.user.type !== UserType.ADMIN &&
+ !req.user.monitoredUsers.includes(req.params.id)
+ )
throw new SafeNSoundError({
- errCode: 'INVALID_AMOUNT',
- message: 'Invalid amount specified.'
+ errCode: 'UNAUTHORIZED',
+ message:
+ "You do not have permission to update this user's budget."
});
- }
- user.balance += amount;
+ let data = await budgetModifySchema.validateAsync(req.body);
+ const user = await getUserById(req.params.id);
+ user.balance += data.amount;
+ let histEntry = await DbSpendHistory.create({
+ venue: data.venue,
+ reason: data.reason,
+ amount: data.amount
+ });
+ user.spendHistory.push(histEntry._id);
await user.save();
+
res.send({ balance: user.balance });
}
})
@@ -76,14 +87,51 @@ export const addBudgetByUserRoute = {
/**
* @type {RouteDescription}
*/
-export const getBudgetRoute = {
+export const userBudgetRoute = {
path: '/budget/@me',
methods: {
get: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
middlewares: [requireUser],
async method(req, res) {
res.send({ currentBalance: req.user.balance });
}
+ }),
+ patch: new RouteMethod({
+ exampleHeaders: {
+ Authorization: 'Bearer {{accessToken}}'
+ },
+ exampleBody: {
+ venue: 'The Store',
+ reason: 'Bought a coke',
+ amount: 0.85
+ },
+ middlewares: [requireUser],
+ description: 'Spend part of budget',
+ async method(req, res) {
+ let data = await budgetModifySchema.validateAsync(req.body);
+
+ if (data.amount > req.user.balance) {
+ throw new SafeNSoundError({
+ errCode: 'INSUFFICIENT_FUNDS',
+ message:
+ 'You do not have enough funds to complete this transaction.'
+ });
+ }
+
+ req.user.balance -= data.amount;
+ let histEntry = await DbSpendHistory.create({
+ venue: data.venue,
+ reason: data.reason,
+ amount: data.amount
+ });
+ req.user.spendHistory.push(histEntry._id);
+ await req.user.save();
+
+ res.send({ balance: req.user.balance });
+ }
})
}
};
diff --git a/src/db/schemas/sensorHistory.js b/src/db/schemas/sensorHistory.js
index 152f03a..e016079 100644
--- a/src/db/schemas/sensorHistory.js
+++ b/src/db/schemas/sensorHistory.js
@@ -6,11 +6,6 @@ import { model, Schema, ObjectId } from 'mongoose';
*/
export const sensorHistorySchema = new Schema(
{
- createdAt: {
- type: Date,
- default: Date.now,
- immutable: true
- },
sensor: {
type: String,
required: true,
@@ -23,6 +18,10 @@ export const sensorHistorySchema = new Schema(
}
},
{
+ timestamps: {
+ createdAt: true,
+ updatedAt: false
+ },
timeseries: {
timeField: 'createdAt'
}
diff --git a/src/db/schemas/spendHistory.js b/src/db/schemas/spendHistory.js
index b4c3f20..d1d6c2a 100644
--- a/src/db/schemas/spendHistory.js
+++ b/src/db/schemas/spendHistory.js
@@ -11,23 +11,22 @@ export const spendHistorySchema = new Schema(
required: true,
immutable: true
},
- items: {
- type: [String],
+ reason: {
+ type: String,
required: true,
immutable: true
},
- cost: {
+ amount: {
type: Number,
required: true,
immutable: true
- },
- createdAt: {
- type: Date,
- default: Date.now,
- immutable: true
}
},
{
+ timestamps: {
+ createdAt: true,
+ updatedAt: false
+ },
timeseries: {
timeField: 'createdAt'
}
diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 69ebb02..55e2e67 100644
--- a/src/db/schemas/user.js
+++ b/src/db/schemas/user.js
@@ -11,23 +11,26 @@ export const AlarmType = Object.freeze({
TOILET: 'toilet'
});
-export const deviceSchema = new Schema({
- name: {
- type: String,
- required: true,
- trim: true
- },
- createdAt: {
- type: Date,
- default: Date.now,
- immutable: true
+export const deviceSchema = new Schema(
+ {
+ name: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ lastSeen: {
+ type: Date,
+ default: Date.now,
+ required: true
+ }
},
- lastSeen: {
- type: Date,
- default: Date.now,
- required: true
+ {
+ timestamps: {
+ createdAt: true,
+ updatedAt: false
+ }
}
-});
+);
export const alarmSchema = new Schema(
{
@@ -49,66 +52,69 @@ export const alarmSchema = new Schema(
* User schema for MongoDB.
* @type {module:mongoose.Schema}
*/
-export const userSchema = new Schema({
- username: {
- type: String,
- required: true,
- unique: true,
- trim: true
- },
- passwordHash: {
- type: String,
- required: true
- },
- email: {
- type: String,
- required: true,
- unique: true,
- trim: true
- },
- type: {
- type: String,
- enum: Object.values(UserType),
- default: 'user'
- },
- createdAt: {
- type: Date,
- default: Date.now,
- immutable: true
- },
- devices: {
- type: [deviceSchema],
- default: []
- },
- alarm: {
- type: alarmSchema
- },
- monitoredUsers: {
- type: [ObjectId],
- ref: 'users'
- },
- balance: {
- type: Number,
- default: 0
- },
- spendHistory: {
- type: [ObjectId],
- ref: 'spendHistory'
- },
- emergencyContacts: {
- type: String
- },
- medicalInfo: {
- type: String
+export const userSchema = new Schema(
+ {
+ username: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true
+ },
+ passwordHash: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true
+ },
+ type: {
+ type: String,
+ enum: Object.values(UserType),
+ default: 'user'
+ },
+ devices: {
+ type: [deviceSchema],
+ default: []
+ },
+ alarm: {
+ type: alarmSchema
+ },
+ monitoredUsers: {
+ type: [ObjectId],
+ ref: 'users'
+ },
+ balance: {
+ type: Number,
+ default: 0
+ },
+ spendHistory: {
+ type: [ObjectId],
+ ref: 'spendHistory'
+ },
+ emergencyContacts: {
+ type: String
+ },
+ medicalInfo: {
+ type: String
+ },
+ location: {
+ // https://stackoverflow.com/a/27218808
+ // Longtitute, Latitude info
+ type: [Number],
+ index: { type: '2dsphere', sparse: true },
+ count: 2
+ }
},
- location: {
- // https://stackoverflow.com/a/27218808
- // Longtitute, Latitude info
- type: [Number],
- index: { type: '2dsphere', sparse: true },
- count: 2
+ {
+ timestamps: {
+ createdAt: true,
+ updatedAt: false
+ }
}
-});
+);
export const DbUser = model('user', userSchema);
diff --git a/src/dto/auth/RegisterDto.js b/src/dto/auth/RegisterDto.js
index 40f1959..fa319b2 100644
--- a/src/dto/auth/RegisterDto.js
+++ b/src/dto/auth/RegisterDto.js
@@ -22,15 +22,6 @@ export class RegisterDto {
}
}
- try {
- return await RegisterDto.schema.validateAsync(obj);
- } catch (e) {
- console.log(e);
- throw new SafeNSoundError({
- errCode: 'JOI_VALIDATION_ERROR',
- message: e.message,
- validation_details: e.details
- });
- }
+ return await RegisterDto.schema.validateAsync(obj);
}
}
|