diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/backend.iml b/.idea/backend.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/backend.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..e5c2b82
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e066844
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/config/roles.js b/src/config/roles.js
index fb76ba6..22b5ab4 100644
--- a/src/config/roles.js
+++ b/src/config/roles.js
@@ -1,8 +1,15 @@
const roles = ['user', 'admin'];
+const allRights = [
+ 'getUsers', 'manageUsers',
+ 'getActivities', 'manageActivities',
+ 'getProfiles', 'manageProfiles',
+ 'getUnits', 'manageUnits',
+];
+
const roleRights = new Map();
-roleRights.set(roles[0], []);
-roleRights.set(roles[1], ['getUsers', 'manageUsers']);
+roleRights.set(roles[0], allRights);
+roleRights.set(roles[1], allRights);
module.exports = {
roles,
diff --git a/src/controllers/activity.controller.js b/src/controllers/activity.controller.js
new file mode 100644
index 0000000..9753f15
--- /dev/null
+++ b/src/controllers/activity.controller.js
@@ -0,0 +1,43 @@
+const httpStatus = require('http-status');
+const pick = require('../utils/pick');
+const ApiError = require('../utils/ApiError');
+const catchAsync = require('../utils/catchAsync');
+const { activityService } = require('../services');
+
+const createActivity = catchAsync(async (req, res) => {
+ const activity = await activityService.createActivity(req.body);
+ res.status(httpStatus.CREATED).send(activity);
+});
+
+const getActivities = catchAsync(async (req, res) => {
+ const filter = pick(req.query, ['name', 'factor']);
+ const options = pick(req.query, ['sortBy', 'limit', 'page']);
+ const result = await activityService.queryActivities(filter, options);
+ res.send(result);
+});
+
+const getActivity = catchAsync(async (req, res) => {
+ const activity = await activityService.getActivityById(req.params.activityId);
+ if (!activity) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found');
+ }
+ res.send(activity);
+});
+
+const updateActivity = catchAsync(async (req, res) => {
+ const activity = await activityService.updateActivityById(req.params.activityId, req.body);
+ res.send(activity);
+});
+
+const deleteActivity = catchAsync(async (req, res) => {
+ await activityService.deleteActivityById(req.params.activityId);
+ res.status(httpStatus.NO_CONTENT).send();
+});
+
+module.exports = {
+ createActivity,
+ getActivities,
+ getActivity,
+ updateActivity,
+ deleteActivity,
+};
diff --git a/src/controllers/index.js b/src/controllers/index.js
index 653b4e2..345435a 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -1,2 +1,5 @@
module.exports.authController = require('./auth.controller');
module.exports.userController = require('./user.controller');
+module.exports.profileController = require('./profile.controller');
+module.exports.activityController = require('./activity.controller');
+module.exports.unitController = require('./unit.controller');
diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js
new file mode 100644
index 0000000..3992661
--- /dev/null
+++ b/src/controllers/profile.controller.js
@@ -0,0 +1,44 @@
+const httpStatus = require('http-status');
+const pick = require('../utils/pick');
+const ApiError = require('../utils/ApiError');
+const catchAsync = require('../utils/catchAsync');
+const { profileService } = require('../services');
+
+const createProfile = catchAsync(async (req, res) => {
+ req.body.userId = req.user._id;
+ const profile = await profileService.createProfile(req.body);
+ res.status(httpStatus.CREATED).send(profile);
+});
+
+const getProfiles = catchAsync(async (req, res) => {
+ const filter = pick(req.query, ['name', 'role']);
+ const options = pick(req.query, ['sortBy', 'limit', 'page']);
+ const result = await profileService.queryProfiles(filter, options);
+ res.send(result);
+});
+
+const getProfile = catchAsync(async (req, res) => {
+ const profile = await profileService.getProfileById(req.params.profileId);
+ if (!profile) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found');
+ }
+ res.send(profile);
+});
+
+const updateProfile = catchAsync(async (req, res) => {
+ const profile = await profileService.getProfileById(req.params.profileId, req.body);
+ res.send(profile);
+});
+
+const deleteProfile = catchAsync(async (req, res) => {
+ await profileService.deleteProfileById(req.params.profileId);
+ res.status(httpStatus.NO_CONTENT).send();
+});
+
+module.exports = {
+ createProfile,
+ getProfiles,
+ getProfile,
+ updateProfile,
+ deleteProfile,
+};
diff --git a/src/controllers/unit.controller.js b/src/controllers/unit.controller.js
new file mode 100644
index 0000000..62fb1d2
--- /dev/null
+++ b/src/controllers/unit.controller.js
@@ -0,0 +1,43 @@
+const httpStatus = require('http-status');
+const pick = require('../utils/pick');
+const ApiError = require('../utils/ApiError');
+const catchAsync = require('../utils/catchAsync');
+const { unitService } = require('../services');
+
+const createUnit = catchAsync(async (req, res) => {
+ const unit = await unitService.createUnit(req.body);
+ res.status(httpStatus.CREATED).send(unit);
+});
+
+const getUnits = catchAsync(async (req, res) => {
+ const filter = pick(req.query, ['name']);
+ const options = pick(req.query, ['sortBy', 'limit', 'page']);
+ const result = await unitService.queryUnits(filter, options);
+ res.send(result);
+});
+
+const getUnit = catchAsync(async (req, res) => {
+ const unit = await unitService.getUnitById(req.params.unitId);
+ if (!unit) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found');
+ }
+ res.send(unit);
+});
+
+const updateUnit = catchAsync(async (req, res) => {
+ const unit = await unitService.updateUnitById(req.params.unitId, req.body);
+ res.send(unit);
+});
+
+const deleteUnit = catchAsync(async (req, res) => {
+ await unitService.deleteUnitById(req.params.unitId);
+ res.status(httpStatus.NO_CONTENT).send();
+});
+
+module.exports = {
+ createUnit,
+ getUnits,
+ getUnit,
+ updateUnit,
+ deleteUnit,
+};
diff --git a/src/models/activity.model.js b/src/models/activity.model.js
index 57a593c..bb68ed7 100644
--- a/src/models/activity.model.js
+++ b/src/models/activity.model.js
@@ -1,11 +1,13 @@
const mongoose = require('mongoose');
-const { toJSON } = require('./plugins');
+const { toJSON, paginate } = require('./plugins');
const activitySchema = mongoose.Schema(
{
- type: {
+ name: {
type: String,
+ unique: true,
required: true,
+ trim: true,
},
factor: {
type: Number,
@@ -19,6 +21,18 @@ const activitySchema = mongoose.Schema(
// add plugin that converts mongoose to json
activitySchema.plugin(toJSON);
+activitySchema.plugin(paginate);
+
+/**
+ * Check if name is taken
+ * @param {string} name - The activity's name
+ * @param {ObjectId} [excludeActivityId] - The id of the activity to be excluded
+ * @returns {Promise}
+ */
+activitySchema.statics.isNameTaken = async function (name, excludeActivityId) {
+ const activity = await this.findOne({ name, _id: { $ne: excludeActivityId } });
+ return !!activity;
+};
/**
* @typedef Activity
diff --git a/src/models/index.js b/src/models/index.js
index 35fd46b..32f2845 100644
--- a/src/models/index.js
+++ b/src/models/index.js
@@ -1,2 +1,5 @@
module.exports.Token = require('./token.model');
module.exports.User = require('./user.model');
+module.exports.Profile = require('./profile.model');
+module.exports.Activity = require('./activity.model');
+module.exports.Unit = require('./unit.model');
diff --git a/src/models/profile.model.js b/src/models/profile.model.js
index e9b486b..40a5e89 100644
--- a/src/models/profile.model.js
+++ b/src/models/profile.model.js
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
-const { toJSON } = require('./plugins');
+const { toJSON, paginate } = require('./plugins');
const profileSchema = mongoose.Schema(
{
@@ -26,12 +26,12 @@ const profileSchema = mongoose.Schema(
rateOfChange: {
type: Number,
},
- activity: {
+ activityId: {
type: mongoose.SchemaTypes.ObjectId,
ref: 'Activity',
required: true,
},
- user: {
+ userId: {
type: mongoose.SchemaTypes.ObjectId,
ref: 'User',
required: true,
@@ -44,6 +44,19 @@ const profileSchema = mongoose.Schema(
// add plugin that converts mongoose to json
profileSchema.plugin(toJSON);
+profileSchema.plugin(paginate);
+
+
+/**
+ * Check if user has profile
+ * @param {ObjectId} userId - The user's id
+ * @param {ObjectId} [excludeProfileId] - The id of the profile to be excluded
+ * @returns {Promise}
+ */
+profileSchema.statics.hasUser = async function (userId, excludeProfileId) {
+ const user = await this.findOne({ userId, _id: { $ne: excludeProfileId } });
+ return !!user;
+};
/**
* @typedef Profile
diff --git a/src/models/unit.model.js b/src/models/unit.model.js
new file mode 100644
index 0000000..f192de1
--- /dev/null
+++ b/src/models/unit.model.js
@@ -0,0 +1,38 @@
+const mongoose = require('mongoose');
+const { toJSON, paginate } = require('./plugins');
+
+const unitSchema = mongoose.Schema(
+ {
+ name: {
+ type: String,
+ unique: true,
+ required: true,
+ trim: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+// add plugin that converts mongoose to json
+unitSchema.plugin(toJSON);
+unitSchema.plugin(paginate);
+
+/**
+ * Check if name is taken
+ * @param {string} name - The unit's name
+ * @param {ObjectId} [excludeUnitId] - The id of the unit to be excluded
+ * @returns {Promise}
+ */
+unitSchema.statics.isNameTaken = async function (name, excludeUnitId) {
+ const activity = await this.findOne({ name, _id: { $ne: excludeUnitId } });
+ return !!activity;
+};
+
+/**
+ * @typedef Unit
+ */
+const Unit = mongoose.model('Unit', unitSchema);
+
+module.exports = Unit;
diff --git a/src/models/units.model.js b/src/models/units.model.js
deleted file mode 100644
index 1da8e32..0000000
--- a/src/models/units.model.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const mongoose = require('mongoose');
-const { toJSON } = require('./plugins');
-
-const unitSchema = mongoose.Schema(
- {
- type: {
- type: String,
- unique: true,
- required: true,
- trim: true,
- },
- },
- {
- timestamps: true,
- }
-);
-
-// add plugin that converts mongoose to json
-unitSchema.plugin(toJSON);
-
-/**
- * @typedef Unit
- */
-const Unit = mongoose.model('Unit', unitSchema);
-
-module.exports = Unit;
diff --git a/src/models/user.model.js b/src/models/user.model.js
index df87793..d7bdf00 100644
--- a/src/models/user.model.js
+++ b/src/models/user.model.js
@@ -38,7 +38,7 @@ const userSchema = mongoose.Schema(
role: {
type: String,
enum: roles,
- default: 'user',
+ default: 'admin',
},
},
{
diff --git a/src/routes/v1/activity.route.js b/src/routes/v1/activity.route.js
new file mode 100644
index 0000000..03d8011
--- /dev/null
+++ b/src/routes/v1/activity.route.js
@@ -0,0 +1,231 @@
+const express = require('express');
+const auth = require('../../middlewares/auth');
+const validate = require('../../middlewares/validate');
+const activityValidation = require('../../validations/activity.validation');
+const activityController = require('../../controllers/activity.controller');
+
+const router = express.Router();
+
+router
+ .route('/')
+ .post(auth('manageActivities'), validate(activityValidation.createActivity), activityController.createActivity)
+ .get(auth('getActivities'), validate(activityValidation.getActivities), activityController.getActivities);
+
+router
+ .route('/:activityId')
+ .get(auth('getActivities'), validate(activityValidation.getActivity), activityController.getActivity)
+ .patch(auth('manageActivities'), validate(activityValidation.updateActivity), activityController.updateActivity)
+ .delete(auth('manageActivities'), validate(activityValidation.deleteActivity), activityController.deleteActivity);
+
+module.exports = router;
+
+/**
+ * @swagger
+ * tags:
+ * name: Activities
+ * description: Activity management
+ */
+
+/**
+ * @swagger
+ * path:
+ * /activities:
+ * post:
+ * summary: Create a activity
+ * description: Only admins can create activities.
+ * tags: [Activities]
+ * security:
+ * - bearerAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - name
+ * - factor
+ * properties:
+ * name:
+ * type: string
+ * description: must be unique
+ * factor:
+ * type: number
+ * example:
+ * name: g
+ * factor: 1
+ * responses:
+ * "201":
+ * description: Created
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Activity'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ *
+ * get:
+ * summary: Get all activities
+ * description: Only admins can retrieve all activities.
+ * tags: [Activities]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: query
+ * name: name
+ * schema:
+ * type: string
+ * description: Activity name
+ * - in: query
+ * name: factor
+ * schema:
+ * type: number
+ * description: Activity factor
+ * - in: query
+ * name: sortBy
+ * schema:
+ * type: string
+ * description: sort by query in the form of field:desc/asc (ex. name:asc)
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 10
+ * description: Maximum number of activities
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 1
+ * description: Page number
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * results:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/Activity'
+ * page:
+ * type: integer
+ * example: 1
+ * limit:
+ * type: integer
+ * example: 10
+ * totalPages:
+ * type: integer
+ * example: 1
+ * totalResults:
+ * type: integer
+ * example: 1
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ */
+
+/**
+ * @swagger
+ * path:
+ * /activities/{id}:
+ * get:
+ * summary: Get a activity
+ * description: Only admins can fetch activities.
+ * tags: [Users]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Activity id
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Activity'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * patch:
+ * summary: Update a activity
+ * description: Only admins can update activities.
+ * tags: [Activities]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Activity id
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * name:
+ * type: string
+ * description: must be unique
+ * factor:
+ * type: number
+ * example:
+ * name: normal
+ * factor: 1
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Activity'
+ * "400":
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * delete:
+ * summary: Delete a activity
+ * description: Only admins can delete activities.
+ * tags: [Activities]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Activity id
+ * responses:
+ * "200":
+ * description: No content
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ */
diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js
index 1b84c3b..2ca8db9 100644
--- a/src/routes/v1/index.js
+++ b/src/routes/v1/index.js
@@ -1,12 +1,18 @@
const express = require('express');
const authRoute = require('./auth.route');
const userRoute = require('./user.route');
+const profileRoute = require('./profile.route');
+const activityRoute = require('./activity.route');
+const unitRoute = require('./unit.route');
const docsRoute = require('./docs.route');
const router = express.Router();
router.use('/auth', authRoute);
router.use('/users', userRoute);
+router.use('/profiles', profileRoute);
+router.use('/activities', activityRoute);
+router.use('/units', unitRoute);
router.use('/docs', docsRoute);
module.exports = router;
diff --git a/src/routes/v1/profile.route.js b/src/routes/v1/profile.route.js
new file mode 100644
index 0000000..93dadb3
--- /dev/null
+++ b/src/routes/v1/profile.route.js
@@ -0,0 +1,254 @@
+const express = require('express');
+const auth = require('../../middlewares/auth');
+const validate = require('../../middlewares/validate');
+const profileValidation = require('../../validations/profile.validation');
+const profileController = require('../../controllers/profile.controller');
+
+const router = express.Router();
+
+router
+ .route('/')
+ .post(auth(), validate(profileValidation.createProfile), profileController.createProfile)
+ .get(auth('getProfiles'), validate(profileValidation.getProfiles), profileController.getProfiles);
+
+router
+ .route('/:profileId')
+ .get(auth('getProfiles'), validate(profileValidation.getProfile), profileController.getProfile)
+ .patch(auth('manageProfiles'), validate(profileValidation.updateProfile), profileController.updateProfile)
+ .delete(auth('manageProfiles'), validate(profileValidation.deleteProfile), profileController.deleteProfile);
+
+module.exports = router;
+
+/**
+ * @swagger
+ * tags:
+ * name: Profiles
+ * description: Profile management and retrieval
+ */
+
+/**
+ * @swagger
+ * path:
+ * /profiles:
+ * post:
+ * summary: Create a user
+ * description: Only admins can create other users.
+ * tags: [Users]
+ * security:
+ * - bearerAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - name
+ * - email
+ * - password
+ * - role
+ * properties:
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * format: email
+ * description: must be unique
+ * password:
+ * type: string
+ * format: password
+ * minLength: 8
+ * description: At least one number and one letter
+ * role:
+ * type: string
+ * enum: [user, admin]
+ * example:
+ * name: fake name
+ * email: fake@example.com
+ * password: password1
+ * role: user
+ * responses:
+ * "201":
+ * description: Created
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/User'
+ * "400":
+ * $ref: '#/components/responses/DuplicateEmail'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ *
+ * get:
+ * summary: Get all profiles
+ * description: Only admins can retrieve all profiles.
+ * tags: [Profiles]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: query
+ * name: name
+ * schema:
+ * type: string
+ * description: User name
+ * - in: query
+ * name: role
+ * schema:
+ * type: string
+ * description: User role
+ * - in: query
+ * name: sortBy
+ * schema:
+ * type: string
+ * description: sort by query in the form of field:desc/asc (ex. name:asc)
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 10
+ * description: Maximum number of users
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 1
+ * description: Page number
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * results:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/User'
+ * page:
+ * type: integer
+ * example: 1
+ * limit:
+ * type: integer
+ * example: 10
+ * totalPages:
+ * type: integer
+ * example: 1
+ * totalResults:
+ * type: integer
+ * example: 1
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ */
+
+/**
+ * @swagger
+ * path:
+ * /profiles/{id}:
+ * get:
+ * summary: Get a profile
+ * description: Logged in users can fetch only their own profile information. Only admins can fetch other profiles.
+ * tags: [Profiles]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: User id
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/User'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * patch:
+ * summary: Update a profile
+ * description: Logged in users can only update their own information. Only admins can update other profiles.
+ * tags: [Profiles]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: User id
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * format: email
+ * description: must be unique
+ * password:
+ * type: string
+ * format: password
+ * minLength: 8
+ * description: At least one number and one letter
+ * example:
+ * name: fake name
+ * email: fake@example.com
+ * password: password1
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/User'
+ * "400":
+ * $ref: '#/components/responses/DuplicateEmail'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * delete:
+ * summary: Delete a profile
+ * description: Logged in users can delete only themselves. Only admins can delete other users.
+ * tags: [Users]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: User id
+ * responses:
+ * "200":
+ * description: No content
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ */
diff --git a/src/routes/v1/unit.route.js b/src/routes/v1/unit.route.js
new file mode 100644
index 0000000..00ecb94
--- /dev/null
+++ b/src/routes/v1/unit.route.js
@@ -0,0 +1,224 @@
+const express = require('express');
+const auth = require('../../middlewares/auth');
+const validate = require('../../middlewares/validate');
+const unitValidation = require('../../validations/unit.validation');
+const unitController = require('../../controllers/unit.controller');
+
+const router = express.Router();
+
+router
+ .route('/')
+ .post(auth('manageUnits'), validate(unitValidation.createUnit), unitController.createUnit)
+ .get(auth('getUnits'), validate(unitValidation.getUnits), unitController.getUnits);
+
+router
+ .route('/:activityId')
+ .get(auth('getUnits'), validate(unitValidation.getUnit), unitController.getUnit)
+ .patch(auth('manageUnits'), validate(unitValidation.updateUnit), unitController.updateUnit)
+ .delete(auth('manageUnits'), validate(unitValidation.deleteUnit), unitController.deleteUnit);
+
+module.exports = router;
+
+/**
+ * @swagger
+ * tags:
+ * name: Units
+ * description: Unit management
+ */
+
+/**
+ * @swagger
+ * path:
+ * /units:
+ * post:
+ * summary: Create a unit
+ * description: Only admins can create units.
+ * tags: [Units]
+ * security:
+ * - bearerAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - name
+ * properties:
+ * name:
+ * type: string
+ * description: must be unique
+ * example:
+ * name: g
+ * responses:
+ * "201":
+ * description: Created
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Unit'
+ * "400":
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ *
+ * get:
+ * summary: Get all units
+ * description: Only admins can retrieve all units.
+ * tags: [Units]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: query
+ * name: name
+ * schema:
+ * type: string
+ * description: unit name
+ * - in: query
+ * name: name
+ * schema:
+ * type: string
+ * description: Unit name
+ * - in: query
+ * name: sortBy
+ * schema:
+ * type: string
+ * description: sort by query in the form of field:desc/asc (ex. name:asc)
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 10
+ * description: Maximum number of units
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * minimum: 1
+ * default: 1
+ * description: Page number
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * results:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/Unit'
+ * page:
+ * type: integer
+ * example: 1
+ * limit:
+ * type: integer
+ * example: 10
+ * totalPages:
+ * type: integer
+ * example: 1
+ * totalResults:
+ * type: integer
+ * example: 1
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ */
+
+/**
+ * @swagger
+ * path:
+ * /units/{id}:
+ * get:
+ * summary: Get a unit
+ * description: Logged in users and admins can fetch units.
+ * tags: [Units]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Unit id
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Unit'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * patch:
+ * summary: Update a unit
+ * description: Only admins can update units.
+ * tags: [Users]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Unit id
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * name:
+ * type: string
+ * description: must be unique
+ * example:
+ * name: g
+ * responses:
+ * "200":
+ * description: OK
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Unit'
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ *
+ * delete:
+ * summary: Delete a unit
+ * description: Only admins can delete units.
+ * tags: [Users]
+ * security:
+ * - bearerAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: Unit id
+ * responses:
+ * "200":
+ * description: No content
+ * "401":
+ * $ref: '#/components/responses/Unauthorized'
+ * "403":
+ * $ref: '#/components/responses/Forbidden'
+ * "404":
+ * $ref: '#/components/responses/NotFound'
+ */
diff --git a/src/services/activity.service.js b/src/services/activity.service.js
new file mode 100644
index 0000000..75617e3
--- /dev/null
+++ b/src/services/activity.service.js
@@ -0,0 +1,77 @@
+const httpStatus = require('http-status');
+const { Activity } = require('../models');
+const ApiError = require('../utils/ApiError');
+
+/**
+ * Create a activity
+ * @param {Object} activityBody
+ * @returns {Promise}
+ */
+const createActivity = async (activityBody) => {
+ if (await Activity.isNameTaken(activityBody.name)) {
+ throw new ApiError(httpStatus.BAD_REQUEST, 'Name already taken');
+ }
+ const activity = await Activity.create(activityBody);
+ return activity;
+};
+
+/**
+ * Query for activity
+ * @param {Object} filter - Mongo filter
+ * @param {Object} options - Query options
+ * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
+ * @param {number} [options.limit] - Maximum number of results per page (default = 10)
+ * @param {number} [options.page] - Current page (default = 1)
+ * @returns {Promise}
+ */
+const queryActivities = async (filter, options) => {
+ const activities = await Activity.paginate(filter, options);
+ return activities;
+};
+
+/**
+ * Get profile by id
+ * @param {ObjectId} id
+ * @returns {Promise}
+ */
+const getActivityById = async (id) => {
+ return Activity.findById(id);
+};
+
+/**
+ * Update activity by id
+ * @param {ObjectId} activityId
+ * @param {Object} updateBody
+ * @returns {Promise}
+ */
+const updateActivityById = async (activityId, updateBody) => {
+ const activity = await getActivityById(activityId);
+ if (!activity) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found');
+ }
+ Object.assign(activity, updateBody);
+ await activity.save();
+ return activity;
+};
+
+/**
+ * Delete activity by id
+ * @param {ObjectId} activityId
+ * @returns {Promise}
+ */
+const deleteActivityById = async (activityId) => {
+ const activity = await getActivityById(activityId);
+ if (!activity) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Activity not found');
+ }
+ await activity.remove();
+ return activity;
+};
+
+module.exports = {
+ createActivity,
+ queryActivities,
+ getActivityById,
+ updateActivityById,
+ deleteActivityById,
+};
diff --git a/src/services/index.js b/src/services/index.js
index 73547db..c8557d8 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -2,3 +2,6 @@ module.exports.authService = require('./auth.service');
module.exports.emailService = require('./email.service');
module.exports.tokenService = require('./token.service');
module.exports.userService = require('./user.service');
+module.exports.profileService = require('./profile.service');
+module.exports.activityService = require('./activity.service');
+module.exports.unitService = require('./unit.service');
diff --git a/src/services/profile.service.js b/src/services/profile.service.js
new file mode 100644
index 0000000..e73ca5a
--- /dev/null
+++ b/src/services/profile.service.js
@@ -0,0 +1,87 @@
+const httpStatus = require('http-status');
+const { Profile } = require('../models');
+const ApiError = require('../utils/ApiError');
+
+/**
+ * Create a profile
+ * @param {Object} profileBody
+ * @returns {Promise}
+ */
+const createProfile = async (profileBody) => {
+ if (await Profile.hasUser(profileBody.userId)) {
+ throw new ApiError(httpStatus.BAD_REQUEST, 'User already has a profile');
+ }
+ const profile = await Profile.create(profileBody);
+ return profile;
+};
+
+/**
+ * Query for profiles
+ * @param {Object} filter - Mongo filter
+ * @param {Object} options - Query options
+ * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
+ * @param {number} [options.limit] - Maximum number of results per page (default = 10)
+ * @param {number} [options.page] - Current page (default = 1)
+ * @returns {Promise}
+ */
+const queryProfiles = async (filter, options) => {
+ const profiles = await Profile.paginate(filter, options);
+ return profiles;
+};
+
+/**
+ * Get profile by id
+ * @param {ObjectId} id
+ * @returns {Promise}
+ */
+const getProfileById = async (id) => {
+ return Profile.findById(id);
+};
+
+/**
+ * Get profile by user id
+ * @param {ObjectId} id
+ * @returns {Promise}
+ */
+const getProfileByUserId = async (id) => {
+ return Profile.findOne({ user: id });
+};
+
+/**
+ * Update profile by id
+ * @param {ObjectId} profileId
+ * @param {Object} updateBody
+ * @returns {Promise}
+ */
+const updateProfileById = async (profileId, updateBody) => {
+ const profile = await getProfileById(profileId);
+ if (!profile) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found');
+ }
+ Object.assign(profile, updateBody);
+ await profile.save();
+ return profile;
+};
+
+/**
+ * Delete profile by id
+ * @param {ObjectId} profileId
+ * @returns {Promise}
+ */
+const deleteProfileById = async (profileId) => {
+ const profile = await getProfileById(profileId);
+ if (!profile) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found');
+ }
+ await profile.remove();
+ return profile;
+};
+
+module.exports = {
+ createProfile,
+ queryProfiles,
+ getProfileById,
+ getProfileByUserId,
+ updateProfileById,
+ deleteProfileById,
+};
diff --git a/src/services/unit.service.js b/src/services/unit.service.js
new file mode 100644
index 0000000..fcc06bb
--- /dev/null
+++ b/src/services/unit.service.js
@@ -0,0 +1,77 @@
+const httpStatus = require('http-status');
+const { Unit } = require('../models');
+const ApiError = require('../utils/ApiError');
+
+/**
+ * Create a unit
+ * @param {Object} unitBody
+ * @returns {Promise}
+ */
+const createUnit = async (unitBody) => {
+ if (await Unit.isNameTaken(unitBody.name)) {
+ throw new ApiError(httpStatus.BAD_REQUEST, 'Name already taken');
+ }
+ const unit = await Unit.create(unitBody);
+ return unit;
+};
+
+/**
+ * Query for unit
+ * @param {Object} filter - Mongo filter
+ * @param {Object} options - Query options
+ * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
+ * @param {number} [options.limit] - Maximum number of results per page (default = 10)
+ * @param {number} [options.page] - Current page (default = 1)
+ * @returns {Promise}
+ */
+const queryUnits = async (filter, options) => {
+ const units = await Unit.paginate(filter, options);
+ return units;
+};
+
+/**
+ * Get unit by id
+ * @param {ObjectId} id
+ * @returns {Promise}
+ */
+const getUnitById = async (id) => {
+ return Unit.findById(id);
+};
+
+/**
+ * Update unit by id
+ * @param {ObjectId} unitId
+ * @param {Object} updateBody
+ * @returns {Promise}
+ */
+const updateUnitById = async (unitId, updateBody) => {
+ const unit = await getUnitById(unitId);
+ if (!unit) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found');
+ }
+ Object.assign(unit, updateBody);
+ await Unit.save();
+ return unit;
+};
+
+/**
+ * Delete unit by id
+ * @param {ObjectId} unitId
+ * @returns {Promise}
+ */
+const deleteUnitById = async (unitId) => {
+ const unit = await getUnitById(unitId);
+ if (!unit) {
+ throw new ApiError(httpStatus.NOT_FOUND, 'Unit not found');
+ }
+ await Unit.remove();
+ return unit;
+};
+
+module.exports = {
+ createUnit,
+ queryUnits,
+ getUnitById,
+ updateUnitById,
+ deleteUnitById,
+};
diff --git a/src/validations/activity.validation.js b/src/validations/activity.validation.js
new file mode 100644
index 0000000..f8ba6fe
--- /dev/null
+++ b/src/validations/activity.validation.js
@@ -0,0 +1,50 @@
+const Joi = require('joi');
+const { objectId } = require('./custom.validation');
+
+const createActivity = {
+ body: Joi.object().keys({
+ name: Joi.string().required(),
+ factor: Joi.number().required(),
+ }),
+};
+
+const getActivities = {
+ query: Joi.object().keys({
+ name: Joi.string(),
+ factor: Joi.number(),
+ limit: Joi.number().integer(),
+ page: Joi.number().integer(),
+ }),
+};
+
+const getActivity = {
+ params: Joi.object().keys({
+ activityId: Joi.string().custom(objectId),
+ }),
+};
+
+const updateActivity = {
+ params: Joi.object().keys({
+ activityId: Joi.string().custom(objectId),
+ }),
+ body: Joi.object()
+ .keys({
+ name: Joi.string().required(),
+ factor: Joi.number().required(),
+ })
+ .min(1),
+};
+
+const deleteActivity = {
+ params: Joi.object().keys({
+ profileId: Joi.string().custom(objectId),
+ }),
+};
+
+module.exports = {
+ createActivity,
+ getActivities,
+ getActivity,
+ updateActivity,
+ deleteActivity,
+};
diff --git a/src/validations/index.js b/src/validations/index.js
index 16d6baa..327ce5a 100644
--- a/src/validations/index.js
+++ b/src/validations/index.js
@@ -1,2 +1,5 @@
module.exports.authValidation = require('./auth.validation');
module.exports.userValidation = require('./user.validation');
+module.exports.profileValidation = require('./profile.validation');
+module.exports.activityValidation = require('./activity.validation');
+module.exports.unitValidation = require('./unit.validation');
diff --git a/src/validations/profile.validation.js b/src/validations/profile.validation.js
new file mode 100644
index 0000000..2fb10fb
--- /dev/null
+++ b/src/validations/profile.validation.js
@@ -0,0 +1,60 @@
+const Joi = require('joi');
+const { objectId } = require('./custom.validation');
+
+const createProfile = {
+ body: Joi.object().keys({
+ sex: Joi.string().required().valid('male', 'female'),
+ birthday: Joi.date().required(),
+ height: Joi.number().required(),
+ currentWeight: Joi.number().required(),
+ targetWeight: Joi.number(),
+ rateOfChange: Joi.number(),
+ activityId: Joi.string().custom(objectId).required(),
+ }),
+};
+
+const getProfiles = {
+ query: Joi.object().keys({
+ sex: Joi.string(),
+ sortBy: Joi.string(),
+ limit: Joi.number().integer(),
+ page: Joi.number().integer(),
+ }),
+};
+
+const getProfile = {
+ params: Joi.object().keys({
+ profileId: Joi.string().custom(objectId),
+ }),
+};
+
+const updateProfile = {
+ params: Joi.object().keys({
+ profileId: Joi.required().custom(objectId),
+ }),
+ body: Joi.object()
+ .keys({
+ sex: Joi.string().required().valid('male', 'female'),
+ birthday: Joi.date().required(),
+ height: Joi.number().required(),
+ currentWeight: Joi.number().required(),
+ targetWeight: Joi.number(),
+ rateOfChange: Joi.number(),
+ activityId: Joi.string().custom(objectId).required(),
+ })
+ .min(1),
+};
+
+const deleteProfile = {
+ params: Joi.object().keys({
+ profileId: Joi.string().custom(objectId).required(),
+ }),
+};
+
+module.exports = {
+ createProfile,
+ getProfiles,
+ getProfile,
+ updateProfile,
+ deleteProfile,
+};
diff --git a/src/validations/unit.validation.js b/src/validations/unit.validation.js
new file mode 100644
index 0000000..e1f7a42
--- /dev/null
+++ b/src/validations/unit.validation.js
@@ -0,0 +1,47 @@
+const Joi = require('joi');
+const { objectId } = require('./custom.validation');
+
+const createUnit = {
+ body: Joi.object().keys({
+ name: Joi.string().required(),
+ }),
+};
+
+const getUnits = {
+ query: Joi.object().keys({
+ name: Joi.string(),
+ limit: Joi.number().integer(),
+ page: Joi.number().integer(),
+ }),
+};
+
+const getUnit = {
+ params: Joi.object().keys({
+ unitId: Joi.string().custom(objectId),
+ }),
+};
+
+const updateUnit = {
+ params: Joi.object().keys({
+ unitId: Joi.string().custom(objectId),
+ }),
+ body: Joi.object()
+ .keys({
+ name: Joi.string().required(),
+ })
+ .min(1),
+};
+
+const deleteUnit = {
+ params: Joi.object().keys({
+ unitId: Joi.string().custom(objectId),
+ }),
+};
+
+module.exports = {
+ createUnit,
+ getUnits,
+ getUnit,
+ updateUnit,
+ deleteUnit,
+};