From 4b80f4b83b646f9dc0074602bb19f321b7f8bbb3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9EBAS8243=E2=80=9C?= <gerd.embruch@uni-hamburg.de>
Date: Sat, 10 Aug 2024 13:20:10 +0200
Subject: [PATCH] added routes for user handling

---
 __tests__/manualREST/users.rest |  34 ++++++++++
 controllers/User.js             | 110 ++++++++++++++++++++++++++++++--
 routes/users.js                 |  40 +++++++++++-
 utils/handleDB.js               |  16 +++++
 validationSchemes/User.js       |  14 ++++
 5 files changed, 207 insertions(+), 7 deletions(-)

diff --git a/__tests__/manualREST/users.rest b/__tests__/manualREST/users.rest
index a7f44ca..f644c77 100644
--- a/__tests__/manualREST/users.rest
+++ b/__tests__/manualREST/users.rest
@@ -15,6 +15,8 @@
 @token = {{login.response.body.accessToken}}
 @token = {{refreshJWT.response.body.accessToken}}
 
+@userId = {{login.response.body.document.id}}
+
 #################
 # HANDLE SIGNUP
 #################
@@ -79,6 +81,38 @@ Accept: application/json
 # @name logout
 delete {{host}}/auth
 
+#################
+# HANDLE PROFILE
+#################
+### get one
+# @name getOne
+GET {{host}}/users/{{userId}}
+Authorization: Bearer {{token}}
+Accept: application/json
+
+### get multiple
+# @name getMultiple
+GET {{host}}/users
+Authorization: Bearer {{token}}
+Accept: application/json
+
+### UPDATE
+PATCH {{host}}/users/{{userId}}
+content-type: application/json
+Authorization: Bearer {{token}}
+
+{
+  "name": "{{name}}",
+  "username": "{{username}}",
+  "email": "{{email}}"
+}
+
+
+### DELETE
+DELETE {{host}}/users/{{userId}}
+content-type: application/json
+Authorization: Bearer {{token}}
+
 #################
 # HANDLE CHANGES
 #################
diff --git a/controllers/User.js b/controllers/User.js
index ac1df7c..10cc72b 100644
--- a/controllers/User.js
+++ b/controllers/User.js
@@ -1,5 +1,5 @@
 import User from "../models/User.js";
-import { createRecord, findOneRecord } from "../utils/handleDB.js";
+import { createRecord, deleteOneRecord, findOneRecord, findRecordByID, findRecords } from "../utils/handleDB.js";
 import { prefillDocumentObject, hideConfidentialFields } from '../utils/handleSchemes.js';
 
 /** *******************************************************
@@ -12,9 +12,6 @@ export const createUser = async (req, res, next) => {
     if (isArtilleryAgent) {
       req.body.verified = true;
     }
-    // console.log("🚀 ~ createUser ~ isArtilleryAgent:", isArtilleryAgent);
-    // return res.status(200).json({ message: 'tmp abort', isArtilleryAgent });
-
     // create user object
     const newRecord = await createRecord(User, prefillDocumentObject(User, req.body));
     // remember document but remove confidential info
@@ -32,6 +29,82 @@ export const createUser = async (req, res, next) => {
 };
 
 
+/** *******************************************************
+ * GET ONE
+ */
+export const getUser = (req, res, next) => {
+  return res.json(req.requestedDocument);
+};
+
+/** *******************************************************
+ * GET MULTIPLE
+ */
+export const getUsers = async (req, res, next) => {
+  try {
+    const users = await findRecords(User, {});
+    return res.json(users);
+  } catch (error) {
+    next(error);
+  }
+};
+
+
+/** *******************************************************
+ * UPDATE ONE
+ */
+export const updateUser = async (req, res, next) => {
+
+  // check if user is allowed to change data
+  // if not self editing 
+  if (global.currentUserId !== req.requestedDocument.id) {
+    // check for current users role
+    if (!(global.currentUserRole >= 2)) return res.status(403).json({ message: 'Access forbidden' });
+  }
+
+  // filter req data by schema field names
+  const newData = prefillDocumentObject(User, req.body);
+  // drop password if empty to prevent setting empty
+  if (!newData.password) delete newData.password;
+  // merge new data into document
+  Object.assign(req.requestedDocument, newData);
+  // try saving
+  try {
+    const updatedUser = await req.requestedDocument.save({ new: true });
+    const document = hideConfidentialFields(User, updatedUser);
+    // return msg incl. document
+    res.json({ msg: 'User successfully updated', document });
+  } catch (error) {
+    next(error);
+  }
+};
+
+/** *******************************************************
+ * DELETE ONE
+ */
+export const deleteUser = async (req, res, next) => {
+  // check if user is allowed to change data
+  // if not self editing 
+  if (global.currentUserId !== req.params.id) {
+    // check for current users role
+    if (!(global.currentUserRole >= 2)) return res.status(403).json({ message: 'Access forbidden' });
+  }
+
+  try {
+    // delete document
+    console.log("🚀 ~ deleteUser ~ req.params.id:", req.params.id);
+    const document = await deleteOneRecord(User, req.params.id);
+    // return msg incl. document
+    return res.status(200).json({ msg: 'User successfully deleted', document });
+  } catch (error) {
+    console.error(error);
+    next(error);
+  }
+};
+
+
+/** *******************************************************
+ ####################### FUNCTIONS #######################
+ ******************************************************* */
 
 /** *******************************************************
  * FIND USER BY MAIL
@@ -52,3 +125,32 @@ export const prefetchUserByEmail = async (req, res, next) => {
     next(error);
   }
 };
+
+
+/**
+ * get a document based on ID from request param 
+ * save this to req.requestedDocument for further computing
+ *
+ * @param   req   request data
+ * @param   res   response, sended to the user
+ * @param   next  simply tell the code to go on with next function
+ *
+ */
+export const prefetchUser = async (req, res, next) => {
+
+  try {
+    if (!req.params.id) return res.status(400).json({ msg: 'No ID given' });
+    // search for matching document
+    // FIX Mongoose .populate() für createdBy & updatedBy (updatedBy wird bei Update überschrieben und ist nicht länger populated)
+    const user = await findRecordByID(User, req.params.id);
+    // if no matching document was found
+    if (!user._id) return res.status(404).json({ message: 'Cannot find user' });
+    // remember document in res
+    req.requestedDocument = user;
+    // going on with the rest of the code
+    next();
+  } catch (error) {
+    // on error
+    next(error);
+  }
+};
\ No newline at end of file
diff --git a/routes/users.js b/routes/users.js
index bda9262..39ac37a 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -1,8 +1,9 @@
 import { Router } from "express";
-import { createUser } from '../controllers/User.js';
-import { sendVerificationEmail } from '../controllers/Auth.js';
-import { createUserSchema } from "../validationSchemes/User.js";
+import { createUser, deleteUser, getUser, getUsers, prefetchUser, updateUser } from '../controllers/User.js';
+import { gateKeeper, sendVerificationEmail } from '../controllers/Auth.js';
+import { createUserSchema, updateUserSchema } from "../validationSchemes/User.js";
 import { validate } from "../utils/handleValidations.js";
+import { verifyAccessToken } from "../utils/handleTokens.js";
 
 
 const router = Router();
@@ -20,5 +21,38 @@ const router = Router();
  */
 router.post('/', validate(createUserSchema), createUser, sendVerificationEmail);
 
+/**
+ * GET ONE
+ * @header  {authorization}  Bearer       [required] access token
+ * 
+ * @prop    {string}          id          [required] id of the user to fetch
+ * 
+ */
+router.get('/:id', verifyAccessToken, prefetchUser, getUser);
+
+/**
+ * GET MULTIPLE
+ * @header  {authorization}  Bearer       [required] access token
+ * 
+ */
+router.get('/', verifyAccessToken, gateKeeper, getUsers);
+
+
+/**
+ * UPDATE ONE
+ * @header  {authorization}  Bearer       [required] access token
+ * 
+ * @prop    {string}          id          [required] id of the user to fetch  
+ */
+router.patch('/:id', verifyAccessToken, validate(updateUserSchema), prefetchUser, updateUser);
+
+
+/**
+ * DELETE ONE
+ * @header  {authorization}  Bearer       [required] access token
+ * 
+ * @prop    {string}          id          [required] id of the user to fetch 
+ */
+router.delete('/:id', verifyAccessToken, prefetchUser, deleteUser);
 
 export default router;
\ No newline at end of file
diff --git a/utils/handleDB.js b/utils/handleDB.js
index 6455dd2..12df065 100644
--- a/utils/handleDB.js
+++ b/utils/handleDB.js
@@ -142,6 +142,22 @@ export const updateOneRecord = async (newData) => {
   }
 };
 
+/**
+ * delete one record
+ *
+ * @param   {mongoose model}      model         [required] model to search the record in
+ * @param   {mongoose document}   record        [required] a mongoose document
+ *
+ * @return  {mongoose document}             deleted document
+ */
+export const deleteOneRecord = async (model, id) => {
+  try {
+    return await model.deleteOne({ _id: id });
+  } catch (error) {
+    throw error;
+  }
+};
+
 /**
  * Find a document by id and update it
  *
diff --git a/validationSchemes/User.js b/validationSchemes/User.js
index c482930..9b5499c 100644
--- a/validationSchemes/User.js
+++ b/validationSchemes/User.js
@@ -14,3 +14,17 @@ export const createUserSchema = z.object({
   message: "Passwords don't match",
   path: ["confirmPassword"],
 });
+
+// UPDATE
+export const updateUserSchema = z.object({
+  name: z.string().min(1),
+  username: z.string().min(1),
+  email: z.string().email(),
+  password: z.string().refine((val) => val && isStrongPassword(val), {
+    message: 'This field must be min 6 characters long and contain uppercase, lowercase, number, specialchar.',
+  }).nullish().or(z.literal('')),
+  confirmPassword: z.string().nullish().or(z.literal('')),
+}).refine((data) => data.password === data.confirmPassword, {
+  message: "Passwords don't match",
+  path: ["confirmPassword"],
+});
-- 
GitLab