summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/routes.js1
-rw-r--r--src/api/routes/adminRoutes.js12
-rw-r--r--src/api/routes/alarmRoutes.js40
-rw-r--r--src/api/routes/assignedUserRoutes.js12
-rw-r--r--src/api/routes/auth/accountRoutes.js14
-rw-r--r--src/api/routes/auth/adminAccountRoutes.js58
-rw-r--r--src/api/routes/auth/deviceRoutes.js99
-rw-r--r--src/api/routes/auth/index.js1
-rw-r--r--src/api/routes/budgetRoutes.js110
-rw-r--r--src/db/schemas/sensorHistory.js9
-rw-r--r--src/db/schemas/spendHistory.js15
-rw-r--r--src/db/schemas/user.js152
-rw-r--r--src/dto/auth/RegisterDto.js11
13 files changed, 330 insertions, 204 deletions
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); } }