From a05738c03d9f8e57bb869e189f76b02fa55ee2ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rafael=20L=C3=A1szl=C3=B3?= <rlacko99@gmail.com>
Date: Mon, 4 Jan 2021 21:12:17 +0100
Subject: [PATCH] Notices API

---
 src/index.ts                                  | 14 ---
 src/middlewares/news/responseNews.ts          |  2 +-
 src/middlewares/term/responseTermsList.ts     |  2 +-
 .../user/notice/addNoticeToUser.ts            | 34 +++++++
 src/middlewares/user/notice/deleteNotice.ts   | 23 +++++
 .../user/notice/getUserNoticeNum.ts           | 31 +++++++
 src/middlewares/user/notice/getUserNotices.ts | 34 +++++++
 .../user/notice/markNoticeAsSeen.ts           | 23 +++++
 .../user/notice/responseNoticesList.ts        |  7 ++
 .../user/notice/responseNoticesNum.ts         |  7 ++
 src/middlewares/user/responseUsersList.ts     |  2 +-
 .../user/warning/responseWarningsList.ts      |  2 +-
 src/models/ProfileSchema.ts                   | 11 ++-
 src/routes/notifications.ts                   | 28 ------
 src/routes/setErrorHandler.ts                 | 27 ++++++
 src/routes/setRouters.ts                      |  8 +-
 src/routes/users/notice.ts                    | 92 +++++++++++++++++++
 src/utils/declarations/response.d.ts          | 16 ++--
 18 files changed, 301 insertions(+), 62 deletions(-)
 create mode 100644 src/middlewares/user/notice/addNoticeToUser.ts
 create mode 100644 src/middlewares/user/notice/deleteNotice.ts
 create mode 100644 src/middlewares/user/notice/getUserNoticeNum.ts
 create mode 100644 src/middlewares/user/notice/getUserNotices.ts
 create mode 100644 src/middlewares/user/notice/markNoticeAsSeen.ts
 create mode 100644 src/middlewares/user/notice/responseNoticesList.ts
 create mode 100644 src/middlewares/user/notice/responseNoticesNum.ts
 delete mode 100644 src/routes/notifications.ts
 create mode 100644 src/routes/setErrorHandler.ts
 create mode 100644 src/routes/users/notice.ts

diff --git a/src/index.ts b/src/index.ts
index d8ecbaf4..426b6111 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -58,18 +58,4 @@ app.use((req: Request, res: Response, next: NextFunction) => {
 
 setRouters(app);
 
-app.use((err: any, req: Request, res: Response) => {
-  if (err instanceof ErrorHandler) return handleError(err, res);
-  if (
-    err instanceof mongoose.Error.CastError ||
-    err instanceof mongoose.Error.ValidationError ||
-    err instanceof multer.MulterError
-  )
-    return handleError(new ErrorHandler(400, err.message), res);
-
-  //Flush out the stack to the console
-  console.error(err.stack);
-  res.status(500).send("Houston, we have a problem!");
-});
-
 app.listen(8000, () => console.log(`Example app listening on port 8000!`));
diff --git a/src/middlewares/news/responseNews.ts b/src/middlewares/news/responseNews.ts
index d570a738..2470df93 100644
--- a/src/middlewares/news/responseNews.ts
+++ b/src/middlewares/news/responseNews.ts
@@ -4,7 +4,7 @@ import { NextFunction, Request, Response, response } from "express";
  * Return the found news from res.data.news
  */
 const responseNews = () => (req: Request, res: Response) => {
-  res.json(res.data.news);
+  res.json(res.data.news || []);
 };
 
 export default responseNews;
diff --git a/src/middlewares/term/responseTermsList.ts b/src/middlewares/term/responseTermsList.ts
index a1a91447..4baef97f 100644
--- a/src/middlewares/term/responseTermsList.ts
+++ b/src/middlewares/term/responseTermsList.ts
@@ -4,7 +4,7 @@ import { NextFunction, Request, Response, response } from "express";
  * Return the found Term
  */
 const responseTermsList = () => (req: Request, res: Response) => {
-  return res.json(res.data.terms);
+  return res.json(res.data.terms || []);
 };
 
 export default responseTermsList;
diff --git a/src/middlewares/user/notice/addNoticeToUser.ts b/src/middlewares/user/notice/addNoticeToUser.ts
new file mode 100644
index 00000000..e879afd0
--- /dev/null
+++ b/src/middlewares/user/notice/addNoticeToUser.ts
@@ -0,0 +1,34 @@
+import { Mongoose, ObjectId, Types } from "mongoose";
+import { NextFunction, Request, Response } from "express";
+import Profile, { INotice, IWarning } from "../../../models/ProfileSchema";
+
+import { validateFields } from "../../utils/validateFields";
+
+const addNoticeToUser = (
+  fields: { name: Partial<keyof INotice>; required: boolean }[]
+) => async (req: Request, res: Response, next: NextFunction) => {
+  try {
+    validateFields({ fields, reqBody: req.body });
+
+    const notice: INotice = {
+      text: req.body.text,
+      redirect: req.body.redirect || "",
+      isSeen: false,
+      _id: String(new Types.ObjectId()),
+    };
+
+    await Profile.updateOne(
+      { _id: req.params.userId },
+      { $push: { notices: notice } },
+      { runValidators: true }
+    )
+      .lean()
+      .exec();
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+export default addNoticeToUser;
diff --git a/src/middlewares/user/notice/deleteNotice.ts b/src/middlewares/user/notice/deleteNotice.ts
new file mode 100644
index 00000000..9956c82f
--- /dev/null
+++ b/src/middlewares/user/notice/deleteNotice.ts
@@ -0,0 +1,23 @@
+import { NextFunction, Request, Response } from "express";
+import Profile, { IWarning } from "../../../models/ProfileSchema";
+
+const deleteNotice = () => async (
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  try {
+    await Profile.updateOne(
+      { _id: req.params.userId },
+      { $pull: { notices: { _id: req.params.noticeId } } }
+    )
+      .lean()
+      .exec();
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+export default deleteNotice;
diff --git a/src/middlewares/user/notice/getUserNoticeNum.ts b/src/middlewares/user/notice/getUserNoticeNum.ts
new file mode 100644
index 00000000..e64c8844
--- /dev/null
+++ b/src/middlewares/user/notice/getUserNoticeNum.ts
@@ -0,0 +1,31 @@
+import { NextFunction, Request, Response, request } from "express";
+import Profile, { IProfile } from "../../../models/ProfileSchema";
+
+import { FilterQuery } from "mongoose";
+
+const getUserNoticeNum = () => async (
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  try {
+    const profile = await Profile.findOne(
+      { _id: req.params.userId },
+      {
+        noticesNum: { $size: { $ifNull: ["$notices", []] } },
+      }
+    )
+      .lean()
+      .exec();
+
+    if (!profile) return res.status(404).json({ message: "Profile not found" });
+
+    res.data.number = profile!.noticesNum;
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+export default getUserNoticeNum;
diff --git a/src/middlewares/user/notice/getUserNotices.ts b/src/middlewares/user/notice/getUserNotices.ts
new file mode 100644
index 00000000..f97bfb05
--- /dev/null
+++ b/src/middlewares/user/notice/getUserNotices.ts
@@ -0,0 +1,34 @@
+import { NextFunction, Request, Response, request } from "express";
+
+import { FilterQuery } from "mongoose";
+import Profile from "../../../models/ProfileSchema";
+
+const getUserNotices = () => async (
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  try {
+    const profile = await Profile.findOne(
+      { _id: req.params.userId },
+      {
+        "notices._id": 1,
+        "notices.text": 1,
+        "notices.redirect": 1,
+        "notices.isSeen": 1,
+      }
+    )
+      .lean()
+      .exec();
+
+    if (!profile) return res.status(404).json({ message: "Profile not found" });
+
+    res.data.notices = profile.notices;
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+export default getUserNotices;
diff --git a/src/middlewares/user/notice/markNoticeAsSeen.ts b/src/middlewares/user/notice/markNoticeAsSeen.ts
new file mode 100644
index 00000000..260d28f4
--- /dev/null
+++ b/src/middlewares/user/notice/markNoticeAsSeen.ts
@@ -0,0 +1,23 @@
+import { NextFunction, Request, Response } from "express";
+import Profile, { IWarning } from "../../../models/ProfileSchema";
+
+const markNoticeAsSeen = () => async (
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  try {
+    await Profile.updateOne(
+      { _id: req.params.userId, "notices._id": req.params.noticeId },
+      { $set: { "notices.$.isSeen": true } }
+    )
+      .lean()
+      .exec();
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+export default markNoticeAsSeen;
diff --git a/src/middlewares/user/notice/responseNoticesList.ts b/src/middlewares/user/notice/responseNoticesList.ts
new file mode 100644
index 00000000..1348cb5a
--- /dev/null
+++ b/src/middlewares/user/notice/responseNoticesList.ts
@@ -0,0 +1,7 @@
+import { NextFunction, Request, Response, response } from "express";
+
+const responseNoticesList = () => (req: Request, res: Response) => {
+  res.json(res.data.notices || []);
+};
+
+export default responseNoticesList;
diff --git a/src/middlewares/user/notice/responseNoticesNum.ts b/src/middlewares/user/notice/responseNoticesNum.ts
new file mode 100644
index 00000000..97182bd0
--- /dev/null
+++ b/src/middlewares/user/notice/responseNoticesNum.ts
@@ -0,0 +1,7 @@
+import { NextFunction, Request, Response, response } from "express";
+
+const responseNoticesNum = () => (req: Request, res: Response) => {
+  res.json({ noticesNum: res.data.number || 0 });
+};
+
+export default responseNoticesNum;
diff --git a/src/middlewares/user/responseUsersList.ts b/src/middlewares/user/responseUsersList.ts
index b397c412..d4ba617e 100644
--- a/src/middlewares/user/responseUsersList.ts
+++ b/src/middlewares/user/responseUsersList.ts
@@ -4,7 +4,7 @@ import { NextFunction, Request, Response, response } from "express";
  * Return the found users from res.data.profiles
  */
 const responseUsersList = () => (req: Request, res: Response) => {
-  res.json(res.data.profiles);
+  res.json(res.data.profiles || []);
 };
 
 export default responseUsersList;
diff --git a/src/middlewares/user/warning/responseWarningsList.ts b/src/middlewares/user/warning/responseWarningsList.ts
index 7a511c62..3bd82314 100644
--- a/src/middlewares/user/warning/responseWarningsList.ts
+++ b/src/middlewares/user/warning/responseWarningsList.ts
@@ -1,7 +1,7 @@
 import { NextFunction, Request, Response, response } from "express";
 
 const responseWarningsList = () => (req: Request, res: Response) => {
-  res.json(res.data.warnings);
+  res.json(res.data.warnings || []);
 };
 
 export default responseWarningsList;
diff --git a/src/models/ProfileSchema.ts b/src/models/ProfileSchema.ts
index dc9a24c1..461c88e9 100644
--- a/src/models/ProfileSchema.ts
+++ b/src/models/ProfileSchema.ts
@@ -5,7 +5,7 @@ export enum Role {
   User = "USER",
 }
 
-export interface INotification {
+export interface INotice {
   _id: string;
   text: string;
   redirect?: string;
@@ -31,7 +31,7 @@ export interface IProfile extends Document {
   isStaffMember?: boolean;
   staffMemberText?: string;
   warnings: IWarning[];
-  notifications: INotification[];
+  notices: INotice[];
 }
 
 const ProfileSchema = new Schema({
@@ -54,7 +54,7 @@ const ProfileSchema = new Schema({
   name: { type: String, required: true },
   isStaffMember: { type: Boolean, required: false, default: false },
   staffMemberText: { type: String, required: false },
-  notifications: [
+  notices: [
     {
       text: { type: String, required: true },
       redirect: { type: String, required: false },
@@ -70,4 +70,7 @@ const ProfileSchema = new Schema({
   ],
 });
 
-export default model<IProfile>("Profile", ProfileSchema);
+export default model<IProfile & { noticesNum: number }>(
+  "Profile",
+  ProfileSchema
+);
diff --git a/src/routes/notifications.ts b/src/routes/notifications.ts
deleted file mode 100644
index 98f29e0b..00000000
--- a/src/routes/notifications.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Application } from "express";
-import example from "../middlewares/example";
-
-// getOwnNotificationCount {number}
-// getOwnNotifications {id, text, redirectUrl}
-// deleteOwnNotification (notificationId)
-// addUserNotification (userId) { text, redirectUrl}
-// addTermMembersNotification (termId) { text, redirectUrl}
-// addAllusersNotification { text, redirectUrl}
-
-const notificationsRoute = (prefix: string, app: Application): void => {
-  // Get all Notifications
-  app.get(`${prefix}/`, example());
-  // Get a Notification
-  app.get(`${prefix}/notification/:notificationId`, example());
-  // Delete a Notification
-  app.delete(`${prefix}/notification/:notificationId`, example());
-  // Get own notifications
-  app.get(`${prefix}/me`, example());
-  // Notify all users
-  app.post(`${prefix}/notify/all`, example());
-  // Notify a given user
-  app.post(`${prefix}/notify/user/:userId`, example());
-  // Notify accepted term members
-  app.post(`${prefix}/notify/term/:termId/accepted`, example());
-};
-
-export default notificationsRoute;
diff --git a/src/routes/setErrorHandler.ts b/src/routes/setErrorHandler.ts
new file mode 100644
index 00000000..122ef20f
--- /dev/null
+++ b/src/routes/setErrorHandler.ts
@@ -0,0 +1,27 @@
+import { Application, NextFunction, Request, Response } from "express";
+import { ErrorHandler, handleError } from "../middlewares/utils/ErrorHandler";
+
+import { Error as MongooseError } from "mongoose";
+import complete from "../middlewares/auth/complete";
+import isLoggedIn from "../middlewares/auth/isLoggedIn";
+import login from "../middlewares/auth/login";
+import logout from "../middlewares/auth/logout";
+import multer from "multer";
+
+const setErrorHandler = (app: Application): void => {
+  app.use((err: any, req: Request, res: Response, next: NextFunction) => {
+    if (err instanceof ErrorHandler) return handleError(err, res);
+    if (
+      err instanceof MongooseError.CastError ||
+      err instanceof MongooseError.ValidationError ||
+      err instanceof multer.MulterError
+    )
+      return handleError(new ErrorHandler(400, err.message), res);
+
+    //Flush out the stack to the console
+    console.error(err.stack);
+    res.status(500).send("Houston, we have a problem!");
+  });
+};
+
+export default setErrorHandler;
diff --git a/src/routes/setRouters.ts b/src/routes/setRouters.ts
index bc232c53..e1b11761 100644
--- a/src/routes/setRouters.ts
+++ b/src/routes/setRouters.ts
@@ -2,9 +2,10 @@ import { Application } from "express";
 import authRoute from "./auth";
 import filesRoute from "./files";
 import newsRoute from "./news";
-import notificationsRoute from "./notifications";
+import setErrorHandler from "./setErrorHandler";
 import termsRoute from "./terms";
 import userMembershipRoute from "./users/membership";
+import userNotificeRoute from "./users/notice";
 import userWarningsRoute from "./users/warning";
 import usersRoute from "./users/user";
 
@@ -17,11 +18,12 @@ export const setRouters = (app: Application): void => {
 
   newsRoute(`${prefix}/news`, app);
 
-  notificationsRoute(`${prefix}/notifications`, app);
-
   termsRoute(`${prefix}/terms`, app);
 
   usersRoute(`${prefix}/users`, app);
   userMembershipRoute(`${prefix}/users`, app);
   userWarningsRoute(`${prefix}/users`, app);
+  userNotificeRoute(`${prefix}/users`, app);
+
+  setErrorHandler(app);
 };
diff --git a/src/routes/users/notice.ts b/src/routes/users/notice.ts
new file mode 100644
index 00000000..a3401a4b
--- /dev/null
+++ b/src/routes/users/notice.ts
@@ -0,0 +1,92 @@
+import { Application } from "express";
+import addNoticeToUser from "../../middlewares/user/notice/addNoticeToUser";
+import deleteNotice from "../../middlewares/user/notice/deleteNotice";
+import example from "../../middlewares/example";
+import getUserNoticeNum from "../../middlewares/user/notice/getUserNoticeNum";
+import getUserNotices from "../../middlewares/user/notice/getUserNotices";
+import isRegistered from "../../middlewares/auth/isRegistered";
+import markNoticeAsSeen from "../../middlewares/user/notice/markNoticeAsSeen";
+import noContentResponse from "../../middlewares/utils/noContentResponse";
+import responseNoticesList from "../../middlewares/user/notice/responseNoticesList";
+import responseNoticesNum from "../../middlewares/user/notice/responseNoticesNum";
+import setOwnUserId from "../../middlewares/user/setOwnUserId";
+
+const userNotificeRoute = (prefix: string, app: Application): void => {
+  /**
+   * Get own notifications
+   * Role: NORMAL
+   */
+  app.get(
+    `${prefix}/me/notices`,
+    isRegistered(),
+    setOwnUserId(),
+    getUserNotices(),
+    responseNoticesList()
+  );
+
+  /**
+   * Get own notification number
+   * Role: NORMAL
+   */
+  app.get(
+    `${prefix}/me/notices/number`,
+    isRegistered(),
+    setOwnUserId(),
+    getUserNoticeNum(),
+    responseNoticesNum()
+  );
+
+  /**
+   * Add notification to User
+   * Role: ADMIN
+   */
+  app.post(
+    `${prefix}/user/:userId/notify`,
+    isRegistered(),
+    addNoticeToUser([
+      { name: "text", required: true },
+      { name: "redirect", required: false },
+    ]),
+    noContentResponse()
+  );
+
+  /**
+   * Remove own notification
+   * Role: NORMAL
+   */
+  app.delete(
+    `${prefix}/me/notice/:noticeId`,
+    isRegistered(),
+    setOwnUserId(),
+    deleteNotice(),
+    noContentResponse()
+  );
+
+  /**
+   * Add notification to Term members
+   * Role: ADMIN
+   */
+  // TODO addTermMembersNotification (termId) { text, redirectUrl}
+  app.post(`${prefix}/term/:termId/notify`, example());
+
+  /**
+   * Add notification to all Users
+   * Role: ADMIN
+   */
+  // TODO addAllusersNotification { text, redirectUrl}
+  app.post(`${prefix}/notify`, example());
+
+  /**
+   * Mark Notice as Seen
+   * Role: NORMAL
+   */
+  app.put(
+    `${prefix}/me/notice/:noticeId`,
+    isRegistered(),
+    setOwnUserId(),
+    markNoticeAsSeen(),
+    noContentResponse()
+  );
+};
+
+export default userNotificeRoute;
diff --git a/src/utils/declarations/response.d.ts b/src/utils/declarations/response.d.ts
index c912e8d5..11b1f8f5 100644
--- a/src/utils/declarations/response.d.ts
+++ b/src/utils/declarations/response.d.ts
@@ -1,7 +1,6 @@
 import { INews } from "../../models/NewsSchema";
-import { IProfile } from "../../models/ProfileSchema";
+import { INotice, IProfile, IWarning } from "../../models/ProfileSchema";
 import { IFile } from "../../models/FileSchema";
-import { IWarning } from "../../models/WarningSchema";
 import { IMember, ITerm } from "../../models/TermSchema";
 
 declare global {
@@ -9,21 +8,20 @@ declare global {
     export interface Response {
       data: {
         profile?: Partial<IProfile> | null;
+        profiles?: Partial<IProfile>[] | null;
+        warning?: Partial<IWarning> | null;
         warnings?: Partial<IWarning>[] | null;
         newObjectId?: string | null;
-        value?: string | null;
         term?: Partial<ITerm> | null;
         terms?: Partial<ITerm>[] | null;
         member?: Partial<IMember> | null;
         members?: Partial<IMember>[] | null;
-        memberState?: IMember["state"] | null;
         news?: Partial<INews>[] | null;
         oneNews?: Partial<INews> | null;
-
-        profiles?: Partial<IProfile>[] | null;
-        error?: string | null;
-        files?: Partial<IFile>[] | null;
-        warning?: Partial<IWarning> | null;
+        notice?: Partial<INotice> | null;
+        notices?: Partial<INotice>[] | null;
+        value?: string | null;
+        number?: number | null;
       };
     }
   }
-- 
GitLab