From e74aae94423950e7dcb376628b5530a3b3e21aae Mon Sep 17 00:00:00 2001 From: sopy Date: Sat, 27 May 2023 21:03:34 +0300 Subject: [PATCH 001/118] updated setup.sql Signed-off-by: sopy --- src/sql/setup.sql | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sql/setup.sql b/src/sql/setup.sql index 64978a5..b82b7b3 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -1,13 +1,14 @@ create table if not exists users ( - id bigint auto_increment + id bigint auto_increment primary key, - name varchar(255) not null, - email varchar(255) not null, - role int default 0 not null, - pwdHash varchar(255) default '' not null, - googleId varchar(255) null, - githubId varchar(255) null + name varchar(255) not null, + email varchar(255) not null, + role int default 0 not null, + pwdHash varchar(255) default '' not null, + googleId varchar(255) null, + githubId varchar(255) null, + createdAt timestamp default current_timestamp() not null ); create table if not exists followers From 0e6793e3c2b179e0bc9e9a820fef0f7ca8154253 Mon Sep 17 00:00:00 2001 From: sopy Date: Sat, 27 May 2023 21:07:41 +0300 Subject: [PATCH 002/118] added metrics.sql Signed-off-by: sopy --- src/sql/metrics.sql | 261 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/sql/metrics.sql diff --git a/src/sql/metrics.sql b/src/sql/metrics.sql new file mode 100644 index 0000000..44dc541 --- /dev/null +++ b/src/sql/metrics.sql @@ -0,0 +1,261 @@ +select +# select date today + current_date as dateToday, + +# get users + (select + count(*) + from users) as users, + +# get users created in the last 7 days + (select + count(*) + from users + where createdAt >= CURRENT_TIMESTAMP - interval 7 day) as usersChange, + +# get active users in the last 7 days + (select + count(distinct userId) + from sessions) as usersActive, + +# get count of users that created a roadmap in the last 7 days + (select + count(distinct ownerId) + from roadmaps + where createdAt >= CURRENT_TIMESTAMP - interval 7 day) as creators, + +# roadmaps created + (select + count(*) + from roadmaps) as roadmapCount, + +# roadmaps created in the last 7 days + (select + count(*) + from roadmaps + where createdAt >= CURRENT_TIMESTAMP - interval 7 day) as roadmapsNew, + +# roadmap views + (select + count(*) + from roadmapViews + where full = 1) as roadmapsTotalViews, + +# roadmap views authenticated users + (select + count(*) + from roadmapViews + where userId != -1 and full = 1) as roadmapsTotalViewsAuth, + +# roadmap views anon users + (select (roadmapsTotalViews - roadmapsTotalViewsAuth)) + as roadmapsTotalViewsAnon, + + +# roadmap shows + (select + count(*) + from roadmapViews) as roadmapsTotalShows, + +# roadmap shows authenticated users + (select + count(*) + from roadmapViews where userId != -1) as roadmapsTotalShowsAuth, + +# roadmap shows by anonymous users + (select (roadmapsTotalshows - roadmapsTotalShowsAuth)) + as roadmapTotalShowsAnon, + +# top roadmap data + tr1.name as topRoadmap1Name, + tr1.shows as topRoadmap1Shows, + tr1.views as topRoadmap1Views, + tr1.likes as topRoadmap1Likes, + tr2.name as topRoadmap2Name, + tr2.shows as topRoadmap2Shows, + tr2.views as topRoadmap2Views, + tr2.likes as topRoadmap2Likes, + tr3.name as topRoadmap3Name, + tr3.shows as topRoadmap3Shows, + tr3.views as topRoadmap3Views, + tr3.likes as topRoadmap3Likes, + tr4.name as topRoadmap4Name, + tr4.shows as topRoadmap4Shows, + tr4.views as topRoadmap4Views, + tr4.likes as topRoadmap4Likes, + tr5.name as topRoadmap5Name, + tr5.shows as topRoadmap5Shows, + tr5.views as topRoadmap5Views, + tr5.likes as topRoadmap5Likes, + tr6.name as topRoadmap6Name, + tr6.shows as topRoadmap6Shows, + tr6.views as topRoadmap6Views, + tr6.likes as topRoadmap6Likes, + tr7.name as topRoadmap7Name, + tr7.shows as topRoadmap7Shows, + tr7.views as topRoadmap7Views, + tr7.likes as topRoadmap7Likes, + tr8.name as topRoadmap8Name, + tr8.shows as topRoadmap8Shows, + tr8.views as topRoadmap8Views, + tr8.likes as topRoadmap8Likes, + tr9.name as topRoadmap9Name, + tr9.shows as topRoadmap9Shows, + tr9.views as topRoadmap9Views, + tr9.likes as topRoadmap9Likes, + tr10.name as topRoadmap10Likes, + tr10.shows as topRoadmap10Shows, + tr10.views as topRoadmap10Views, + tr10.likes as topRoadmap10Likes +from + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1) tr1 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 1) tr2 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 2) tr3 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 3) tr4 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 4) tr5 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 5) tr6 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 6) tr7 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 7) tr8 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 8) tr9 +join + (select + name, + count(distinct rV.id) as shows, + count(distinct rVF.id) as views, + count(distinct rL.id) as likes + from + roadmaps r + left join roadmapViews rV on r.id = rV.roadmapId + left join roadmapViews rVF on r.id = rVF.roadmapId and rVF.full = 1 + left join roadmapLikes rL on r.id = rL.roadmapId + GROUP BY + name + order by views desc + limit 1 offset 9) tr10 + + From 9bfb51bf3954f7c3be21cbcba2f52bef53dd6837 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 02:57:28 +0300 Subject: [PATCH 003/118] new file structure --- src/{routes => }/constants/Paths.ts | 0 src/controllers/authController.ts | 0 src/middleware/session.ts | 17 ---------- src/routes/{api => }/AuthRouter.ts | 8 ++--- src/routes/{api => }/ExploreRouter.ts | 4 +-- src/routes/{api => }/RoadmapsRouter.ts | 18 +++++----- src/routes/{api => }/UsersRouter.ts | 10 +++--- src/routes/constants/FullPaths.ts | 34 ------------------- src/routes/{api/api.ts => entry.ts} | 10 +++--- .../RoadmapIssues.ts | 14 ++++---- .../RoadmapsGet.ts | 2 +- .../RoadmapsTabsInfo.ts | 8 ++--- .../RoadmapsUpdate.ts | 6 ++-- .../issuesRoutes}/CommentsRouter.ts | 10 +++--- .../issuesRoutes}/IssuesUpdate.ts | 8 ++--- .../{api/Users => usersRoutes}/UsersGet.ts | 10 +++--- .../{api/Users => usersRoutes}/UsersUpdate.ts | 6 ++-- src/server.ts | 4 +-- src/validators/validateSession.ts | 20 +++++++++++ 19 files changed, 79 insertions(+), 110 deletions(-) rename src/{routes => }/constants/Paths.ts (100%) create mode 100644 src/controllers/authController.ts rename src/routes/{api => }/AuthRouter.ts (98%) rename src/routes/{api => }/ExploreRouter.ts (94%) rename src/routes/{api => }/RoadmapsRouter.ts (93%) rename src/routes/{api => }/UsersRouter.ts (80%) delete mode 100644 src/routes/constants/FullPaths.ts rename src/routes/{api/api.ts => entry.ts} (60%) rename src/routes/{api/Roadmaps => roadmapsRoutes}/RoadmapIssues.ts (92%) rename src/routes/{api/Roadmaps => roadmapsRoutes}/RoadmapsGet.ts (99%) rename src/routes/{api/Roadmaps => roadmapsRoutes}/RoadmapsTabsInfo.ts (95%) rename src/routes/{api/Roadmaps => roadmapsRoutes}/RoadmapsUpdate.ts (98%) rename src/routes/{api/Roadmaps/Issues => roadmapsRoutes/issuesRoutes}/CommentsRouter.ts (98%) rename src/routes/{api/Roadmaps/Issues => roadmapsRoutes/issuesRoutes}/IssuesUpdate.ts (97%) rename src/routes/{api/Users => usersRoutes}/UsersGet.ts (97%) rename src/routes/{api/Users => usersRoutes}/UsersUpdate.ts (98%) create mode 100644 src/validators/validateSession.ts diff --git a/src/routes/constants/Paths.ts b/src/constants/Paths.ts similarity index 100% rename from src/routes/constants/Paths.ts rename to src/constants/Paths.ts diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/middleware/session.ts b/src/middleware/session.ts index bf5bfeb..ee95b4c 100644 --- a/src/middleware/session.ts +++ b/src/middleware/session.ts @@ -104,20 +104,3 @@ export async function sessionMiddleware( next(); } - -export function requireSessionMiddleware( - req: RequestWithSession, - res: Response, - next: NextFunction, -): void { - // if session isn't set, return forbidden - if (!req.session) { - res - .status(HttpStatusCodes.UNAUTHORIZED) - .json({ error: 'Token not found, please login' }); - return; - } - - // call next() - next(); -} diff --git a/src/routes/api/AuthRouter.ts b/src/routes/AuthRouter.ts similarity index 98% rename from src/routes/api/AuthRouter.ts rename to src/routes/AuthRouter.ts index 77b3920..5aef25a 100644 --- a/src/routes/api/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -1,5 +1,5 @@ import { Response, Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; @@ -12,9 +12,9 @@ import logger from 'jet-logger'; import { UserInfo } from '@src/models/UserInfo'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import { checkEmail } from '@src/util/EmailUtil'; +import validateSession from '@src/validators/validateSession'; const AuthRouter = Router(); @@ -233,7 +233,7 @@ AuthRouter.post(Paths.Auth.Register, async (req, res) => { }); }); -AuthRouter.use(Paths.Auth.ChangePassword, requireSessionMiddleware); +AuthRouter.use(Paths.Auth.ChangePassword, validateSession); AuthRouter.post( Paths.Auth.ChangePassword, async (req: RequestWithSession, res) => { @@ -565,7 +565,7 @@ AuthRouter.get(Paths.Auth.GithubCallback, async (req, res) => { } }); -AuthRouter.delete(Paths.Auth.Logout, requireSessionMiddleware); +AuthRouter.delete(Paths.Auth.Logout, validateSession); AuthRouter.delete(Paths.Auth.Logout, async (req: RequestWithSession, res) => { // get session const token = req.session?.token; diff --git a/src/routes/api/ExploreRouter.ts b/src/routes/ExploreRouter.ts similarity index 94% rename from src/routes/api/ExploreRouter.ts rename to src/routes/ExploreRouter.ts index c2bc5b7..760d9fd 100644 --- a/src/routes/api/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { ExploreDB } from '@src/util/ExploreDB'; import { RoadmapMini } from '@src/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; import { RequestWithSession } from '@src/middleware/session'; -import { addView } from '@src/routes/api/Roadmaps/RoadmapsGet'; +import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; const ExploreRouter = Router(); diff --git a/src/routes/api/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts similarity index 93% rename from src/routes/api/RoadmapsRouter.ts rename to src/routes/RoadmapsRouter.ts index 3f10815..846697d 100644 --- a/src/routes/api/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -1,23 +1,23 @@ -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { Router } from 'express'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { IRoadmap, Roadmap } from '@src/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; -import GetRouter from '@src/routes/api/Roadmaps/RoadmapsGet'; -import UpdateRouter from '@src/routes/api/Roadmaps/RoadmapsUpdate'; +import GetRouter from '@src/routes/roadmapsRoutes/RoadmapsGet'; +import UpdateRouter from '@src/routes/roadmapsRoutes/RoadmapsUpdate'; import * as console from 'console'; -import RoadmapIssues from '@src/routes/api/Roadmaps/RoadmapIssues'; -import RoadmapTabsInfo from '@src/routes/api/Roadmaps/RoadmapsTabsInfo'; +import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; +import RoadmapTabsInfo from '@src/routes/roadmapsRoutes/RoadmapsTabsInfo'; import envVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; +import validateSession from "@src/validators/validateSession"; const RoadmapsRouter = Router(); -RoadmapsRouter.post(Paths.Roadmaps.Create, requireSessionMiddleware); +RoadmapsRouter.post(Paths.Roadmaps.Create, validateSession); RoadmapsRouter.post( Paths.Roadmaps.Create, async (req: RequestWithSession, res) => { @@ -88,7 +88,7 @@ RoadmapsRouter.use(Paths.Roadmaps.Get.Base, GetRouter); RoadmapsRouter.use(Paths.Roadmaps.Update.Base, UpdateRouter); -RoadmapsRouter.delete(Paths.Roadmaps.Delete, requireSessionMiddleware); +RoadmapsRouter.delete(Paths.Roadmaps.Delete, validateSession); RoadmapsRouter.delete( Paths.Roadmaps.Delete, async (req: RequestWithSession, res) => { @@ -144,7 +144,7 @@ RoadmapsRouter.use(Paths.Roadmaps.TabsInfo.Base, RoadmapTabsInfo); /* ! like roadmaps */ -RoadmapsRouter.all(Paths.Roadmaps.Like, requireSessionMiddleware); +RoadmapsRouter.all(Paths.Roadmaps.Like, validateSession); RoadmapsRouter.get( Paths.Roadmaps.Like, diff --git a/src/routes/api/UsersRouter.ts b/src/routes/UsersRouter.ts similarity index 80% rename from src/routes/api/UsersRouter.ts rename to src/routes/UsersRouter.ts index ded55cc..4f6e9bb 100644 --- a/src/routes/api/UsersRouter.ts +++ b/src/routes/UsersRouter.ts @@ -1,13 +1,13 @@ import { Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; -import UsersGet from '@src/routes/api/Users/UsersGet'; +import Paths from '@src/constants/Paths'; +import UsersGet from '@src/routes/usersRoutes/UsersGet'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import DatabaseDriver from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import UsersUpdate from '@src/routes/api/Users/UsersUpdate'; +import UsersUpdate from '@src/routes/usersRoutes/UsersUpdate'; +import validateSession from "@src/validators/validateSession"; const UsersRouter = Router(); @@ -18,7 +18,7 @@ UsersRouter.use(Paths.Users.Get.Base, UsersGet); UsersRouter.use(Paths.Users.Update.Base, UsersUpdate); // Delete route - delete user - requires session -UsersRouter.delete(Paths.Users.Delete, requireSessionMiddleware); +UsersRouter.delete(Paths.Users.Delete, validateSession); UsersRouter.delete(Paths.Users.Delete, async (req: RequestWithSession, res) => { // get database const db = new DatabaseDriver(); diff --git a/src/routes/constants/FullPaths.ts b/src/routes/constants/FullPaths.ts deleted file mode 100644 index 1155cc9..0000000 --- a/src/routes/constants/FullPaths.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Convert paths to full paths. - */ - -import Paths, { TPaths } from './Paths'; - -interface IPathObj { - Base: string; - [key: string]: string | IPathObj; -} - -/** - * The recursive function. - */ -function getFullPaths(parent: IPathObj, baseUrl: string): IPathObj { - const url = baseUrl + parent.Base, - keys = Object.keys(parent), - retVal: IPathObj = { Base: url }; - // Iterate keys - for (const key of keys) { - const pval = parent[key]; - if (key !== 'Base' && typeof pval === 'string') { - retVal[key] = url + pval; - } else if (typeof pval === 'object') { - retVal[key] = getFullPaths(pval, url); - } - } - // Return - return retVal; -} - -// **** Export default **** // - -export default getFullPaths(Paths, '') as TPaths; diff --git a/src/routes/api/api.ts b/src/routes/entry.ts similarity index 60% rename from src/routes/api/api.ts rename to src/routes/entry.ts index d165fc2..a096772 100644 --- a/src/routes/api/api.ts +++ b/src/routes/entry.ts @@ -1,9 +1,9 @@ import { Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; -import AuthRouter from '@src/routes/api/AuthRouter'; -import RoadmapsRouter from '@src/routes/api/RoadmapsRouter'; -import UsersRouter from '@src/routes/api/UsersRouter'; -import ExploreRouter from '@src/routes/api/ExploreRouter'; +import Paths from '@src/constants/Paths'; +import AuthRouter from '@src/routes/AuthRouter'; +import RoadmapsRouter from '@src/routes/RoadmapsRouter'; +import UsersRouter from '@src/routes/UsersRouter'; +import ExploreRouter from '@src/routes/ExploreRouter'; const BaseRouter = Router(); diff --git a/src/routes/api/Roadmaps/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts similarity index 92% rename from src/routes/api/Roadmaps/RoadmapIssues.ts rename to src/routes/roadmapsRoutes/RoadmapIssues.ts index 11b0321..3384910 100644 --- a/src/routes/api/Roadmaps/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -1,19 +1,19 @@ import { Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import { IIssue, Issue } from '@src/models/Issue'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; -import IssuesUpdate from '@src/routes/api/Roadmaps/Issues/IssuesUpdate'; -import Comments from '@src/routes/api/Roadmaps/Issues/CommentsRouter'; +import IssuesUpdate from '@src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate'; +import Comments from '@src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter'; +import validateSession from "@src/validators/validateSession"; const RoadmapIssues = Router({ mergeParams: true }); -RoadmapIssues.post(Paths.Roadmaps.Issues.Create, requireSessionMiddleware); +RoadmapIssues.post(Paths.Roadmaps.Issues.Create, validateSession); RoadmapIssues.post( Paths.Roadmaps.Issues.Create, async (req: RequestWithSession, res) => { @@ -158,11 +158,11 @@ RoadmapIssues.get(Paths.Roadmaps.Issues.GetAll, async (req, res) => { return res.status(HttpStatusCodes.OK).json({ issues: result }); }); -RoadmapIssues.post(Paths.Roadmaps.Issues.Update.Base, requireSessionMiddleware); +RoadmapIssues.post(Paths.Roadmaps.Issues.Update.Base, validateSession); RoadmapIssues.use(Paths.Roadmaps.Issues.Update.Base, IssuesUpdate); // Delete Issue -RoadmapIssues.delete(Paths.Roadmaps.Issues.Delete, requireSessionMiddleware); +RoadmapIssues.delete(Paths.Roadmaps.Issues.Delete, validateSession); RoadmapIssues.delete( Paths.Roadmaps.Issues.Delete, async (req: RequestWithSession, res) => { diff --git a/src/routes/api/Roadmaps/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts similarity index 99% rename from src/routes/api/Roadmaps/RoadmapsGet.ts rename to src/routes/roadmapsRoutes/RoadmapsGet.ts index ac31022..ff76a8a 100644 --- a/src/routes/api/Roadmaps/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -1,5 +1,5 @@ import { Response, Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; diff --git a/src/routes/api/Roadmaps/RoadmapsTabsInfo.ts b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts similarity index 95% rename from src/routes/api/Roadmaps/RoadmapsTabsInfo.ts rename to src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts index b4ebf1a..96d7fea 100644 --- a/src/routes/api/Roadmaps/RoadmapsTabsInfo.ts +++ b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts @@ -1,18 +1,18 @@ import { Router } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import { ITabInfo, TabInfo } from '@src/models/TabInfo'; import Database from '@src/util/DatabaseDriver'; import * as console from 'console'; import { IRoadmap } from '@src/models/Roadmap'; +import validateSession from "@src/validators/validateSession"; const RoadmapTabsInfo = Router({ mergeParams: true }); -RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Create, requireSessionMiddleware); +RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Create, validateSession); RoadmapTabsInfo.post( Paths.Roadmaps.TabsInfo.Create, async (req: RequestWithSession, res) => { @@ -101,7 +101,7 @@ RoadmapTabsInfo.get(Paths.Roadmaps.TabsInfo.Get, async (req, res) => { return res.status(HttpStatusCodes.OK).json({ tabInfo: result }); }); -RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, requireSessionMiddleware); +RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, validateSession); RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, async (req: RequestWithSession, res) => { diff --git a/src/routes/api/Roadmaps/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts similarity index 98% rename from src/routes/api/Roadmaps/RoadmapsUpdate.ts rename to src/routes/roadmapsRoutes/RoadmapsUpdate.ts index a879ab6..aca7157 100644 --- a/src/routes/api/Roadmaps/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -1,14 +1,14 @@ import { Response, Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; import { Tag } from '@src/models/Tags'; import User from '@src/models/User'; +import validateSession from "@src/validators/validateSession"; const RoadmapsUpdate = Router({ mergeParams: true }); @@ -58,7 +58,7 @@ async function isRoadmapValid( return { id: BigInt(id), roadmap }; } -RoadmapsUpdate.post('*', requireSessionMiddleware); +RoadmapsUpdate.post('*', validateSession); RoadmapsUpdate.post( Paths.Roadmaps.Update.Title, diff --git a/src/routes/api/Roadmaps/Issues/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts similarity index 98% rename from src/routes/api/Roadmaps/Issues/CommentsRouter.ts rename to src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts index b96a88e..20503b3 100644 --- a/src/routes/api/Roadmaps/Issues/CommentsRouter.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts @@ -1,15 +1,15 @@ import { Request, Response, Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import { Roadmap } from '@src/models/Roadmap'; import { Issue } from '@src/models/Issue'; import User from '@src/models/User'; import Database from '@src/util/DatabaseDriver'; import { Comment } from '@src/models/Comment'; +import validateSession from "@src/validators/validateSession"; const CommentsRouter = Router({ mergeParams: true }); @@ -108,7 +108,7 @@ async function checkUser( CommentsRouter.post( Paths.Roadmaps.Issues.Comments.Create, - requireSessionMiddleware, + validateSession, ); CommentsRouter.post( Paths.Roadmaps.Issues.Comments.Create, @@ -188,7 +188,7 @@ CommentsRouter.get(Paths.Roadmaps.Issues.Comments.Get, async (req, res) => { CommentsRouter.use( Paths.Roadmaps.Issues.Comments.Update, - requireSessionMiddleware, + validateSession, ); CommentsRouter.post( Paths.Roadmaps.Issues.Comments.Update, @@ -266,7 +266,7 @@ CommentsRouter.post( CommentsRouter.use( Paths.Roadmaps.Issues.Comments.Delete, - requireSessionMiddleware, + validateSession, ); CommentsRouter.delete( Paths.Roadmaps.Issues.Comments.Delete, diff --git a/src/routes/api/Roadmaps/Issues/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts similarity index 97% rename from src/routes/api/Roadmaps/Issues/IssuesUpdate.ts rename to src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts index 27bed05..254f268 100644 --- a/src/routes/api/Roadmaps/Issues/IssuesUpdate.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts @@ -1,13 +1,13 @@ import { Response, Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import Database from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import { Issue } from '@src/models/Issue'; import { Roadmap } from '@src/models/Roadmap'; +import validateSession from "@src/validators/validateSession"; const IssuesUpdate = Router({ mergeParams: true }); @@ -253,14 +253,14 @@ IssuesUpdate.post( }, ); -IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, requireSessionMiddleware); +IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, validateSession); IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, (req, res) => statusChangeIssue(req, res, true), ); IssuesUpdate.delete( Paths.Roadmaps.Issues.Update.Status, - requireSessionMiddleware, + validateSession, ); IssuesUpdate.delete(Paths.Roadmaps.Issues.Update.Status, (req, res) => statusChangeIssue(req, res, false), diff --git a/src/routes/api/Users/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts similarity index 97% rename from src/routes/api/Users/UsersGet.ts rename to src/routes/usersRoutes/UsersGet.ts index d5a2339..80900f2 100644 --- a/src/routes/api/Users/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -1,8 +1,7 @@ import { Router } from 'express'; -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; @@ -11,7 +10,8 @@ import { IUserInfo } from '@src/models/UserInfo'; import { Roadmap, RoadmapMini } from '@src/models/Roadmap'; import { Issue } from '@src/models/Issue'; import { Follower } from '@src/models/Follower'; -import { addView } from '@src/routes/api/Roadmaps/RoadmapsGet'; +import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; +import validateSession from "@src/validators/validateSession"; // ! What would I do without StackOverflow? // ! https://stackoverflow.com/a/60848873 @@ -372,7 +372,7 @@ UsersGet.get( }, ); -UsersGet.get(Paths.Users.Get.Follow, requireSessionMiddleware); +UsersGet.get(Paths.Users.Get.Follow, validateSession); UsersGet.get(Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { // get the target userDisplay id const userId = BigInt(req.params.userId || -1); @@ -432,7 +432,7 @@ UsersGet.get(Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { .json({ error: 'Failed to follow' }); }); -UsersGet.delete(Paths.Users.Get.Follow, requireSessionMiddleware); +UsersGet.delete(Paths.Users.Get.Follow, validateSession); UsersGet.delete( Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { diff --git a/src/routes/api/Users/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts similarity index 98% rename from src/routes/api/Users/UsersUpdate.ts rename to src/routes/usersRoutes/UsersUpdate.ts index a6b9e30..caeab5d 100644 --- a/src/routes/api/Users/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -1,8 +1,7 @@ -import Paths from '@src/routes/constants/Paths'; +import Paths from '@src/constants/Paths'; import { Router } from 'express'; import { RequestWithSession, - requireSessionMiddleware, } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import axios from 'axios'; @@ -11,10 +10,11 @@ import { checkEmail } from '@src/util/EmailUtil'; import { comparePassword } from '@src/util/LoginUtil'; import User from '@src/models/User'; import { UserInfo } from '@src/models/UserInfo'; +import validateSession from "@src/validators/validateSession"; const UsersUpdate = Router({ mergeParams: true }); -UsersUpdate.post('*', requireSessionMiddleware); +UsersUpdate.post('*', validateSession); UsersUpdate.post( Paths.Users.Update.ProfilePicture, async (req: RequestWithSession, res) => { diff --git a/src/server.ts b/src/server.ts index 7b671b9..2228f87 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,8 +12,8 @@ import { sessionMiddleware } from '@src/middleware/session'; import 'express-async-errors'; -import BaseRouter from '@src/routes/api/api'; -import Paths from '@src/routes/constants/Paths'; +import BaseRouter from '@src/routes/entry'; +import Paths from '@src/constants/Paths'; import EnvVars from '@src/constants/EnvVars'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; diff --git a/src/validators/validateSession.ts b/src/validators/validateSession.ts new file mode 100644 index 0000000..651a7fc --- /dev/null +++ b/src/validators/validateSession.ts @@ -0,0 +1,20 @@ +import { NextFunction, Response } from 'express'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; +import { RequestWithSession } from '@src/middleware/session'; + +export default function ( + req: RequestWithSession, + res: Response, + next: NextFunction, +): void { + // if session isn't set, return forbidden + if (!req.session) { + res + .status(HttpStatusCodes.UNAUTHORIZED) + .json({ error: 'Token not found, please login' }); + return; + } + + // call next() + next(); +} \ No newline at end of file From 138aa5989bfef0fb81f0440ef083748fdc2a4bb7 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 04:04:24 +0300 Subject: [PATCH 004/118] Markdown FIles Update --- README.md | 108 ++++++++++++++-------- docs/available-scripts.md | 36 ++++++++ env.example/development.env | 37 ++++++++ env.example/production.env | 37 ++++++++ env.example/test.env | 27 ++++++ package-lock.json | 176 +++++++++++++++--------------------- package.json | 4 +- 7 files changed, 280 insertions(+), 145 deletions(-) create mode 100644 docs/available-scripts.md create mode 100644 env.example/development.env create mode 100644 env.example/production.env create mode 100644 env.example/test.env diff --git a/README.md b/README.md index 0a58f1f..1d3a123 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Navigo Learn API [![CodeFactor](https://www.codefactor.io/repository/github/navigolearn/api/badge/master)](https://www.codefactor.io/repository/github/navigolearn/api/overview/master) +# Navigo Learn API + +![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg) ## ! This is a work in progress ! @@ -13,45 +15,73 @@ REST api for NavigoLearning. This project is built with Node.js, Express, and MariaDB. +# Getting Started + +## Prerequisites + +- [Node.js](https://nodejs.org/en/) - v16 or higher +- [MariaDB](https://mariadb.org/) - v10.6 or higher +- [Git](https://git-scm.com/) - v2.32 or higher + +## Installation + +1. Clone the repo + ```sh + git clone git@github.com:NavigoLearn/API.git + ``` + +2. Install NPM packages + ```sh + npm install + ``` + +3. Create a MariaDB database + ```sh + CREATE DATABASE navigo_learn; + USE navigo_learn; + CREATE USER 'navigo_learn'@'localhost' IDENTIFIED BY 'password'; + GRANT ALL PRIVILEGES ON navigo_learn.* TO 'navigo_learn'@'localhost'; + ``` + +4. Rename the env.example folder to env and fill in the values for + development.env +5. Run tests to make sure everything works + ```sh + npm test + ``` +5. Run the server + ```sh + npm run dev + ``` + ## Documentation Documentation for the api can be found [here](docs/paths/README.md). -## Available Scripts - -### `npm run dev` - -Run the server in development mode. - -### `npm test` - -Run all unit-tests with hot-reloading. - -### `npm test -- --testFile="name of test file" (i.e. --testFile=Users).` - -Run a single unit-test. - -### `npm run test:no-reloading` - -Run all unit-tests without hot-reloading. - -### `npm run lint` - -Check for linting errors. - -### `npm run build` - -Build the project for production. - -### `npm start` - -Run the production build (Must be built first). - -### `npm start -- --env="name of env file" (default is production).` - -Run production build with a different env file. - -## Additional Notes - -- If `npm run dev` gives you issues with bcrypt on MacOS you may need to - run: `npm rebuild bcrypt --build-from-source`. +## Structure of the Project + +The project is split into 4 main folders: + +- `src` - Contains all the source code for the project. +- `spec` - Contains all the unit-tests for the project. +- `docs` - Contains all the documentation for the project. +- `env` - Contains all the environment files for the project. (rename the + env.example folder to env to use it) + +### `src` + +The `src` folder is split into multiple main folders: + +- `constants` - Contains constants used in the project. (HTTP status codes, env + variables, etc.) +- `controllers` - Contains the controllers of the project. +- `middleware` - Contains middleware used in the project. (session, etc.) +- `models` - Contains the data models for the project. (Roadmap, User, etc.) +- `routes` - Contains the routers pointing to controllers. (auth, users, etc.) +- `sql` - Contains sql files used in the project. (create tables, metrics, etc.) +- `utils` - Contains utility functions used in the project. (databaseDriver, + etc.) +- `validators` - Contains the validators used in the project. (user, roadmap, + etc.) +- `index.ts` - The entry point. +- `server.ts` - The server. diff --git a/docs/available-scripts.md b/docs/available-scripts.md new file mode 100644 index 0000000..7c9dc11 --- /dev/null +++ b/docs/available-scripts.md @@ -0,0 +1,36 @@ +### `npm run dev` + +Run the server in development mode. + +### `npm test` + +Run all unit-tests with hot-reloading. + +### `npm test -- --testFile="name of test file" (i.e. --testFile=Users).` + +Run a single unit-test. + +### `npm run test:no-reloading` + +Run all unit-tests without hot-reloading. + +### `npm run lint` + +Check for linting errors. + +### `npm run build` + +Build the project for production. + +### `npm start` + +Run the production build (Must be built first). + +### `npm start -- --env="name of env file" (default is production).` + +Run production build with a different env file. + +## Additional Notes + +- If `npm run dev` gives you issues with bcrypt on MacOS you may need to + run: `npm rebuild bcrypt --build-from-source`. \ No newline at end of file diff --git a/env.example/development.env b/env.example/development.env new file mode 100644 index 0000000..76c4dfa --- /dev/null +++ b/env.example/development.env @@ -0,0 +1,37 @@ +## Environment ## +NODE_ENV=development + +## Server ## +PORT=3001 +HOST=localhost + +## Setup jet-logger ## +JET_LOGGER_MODE=CONSOLE +JET_LOGGER_FILEPATH=jet-logger.log +JET_LOGGER_TIMESTAMP=TRUE +JET_LOGGER_FORMAT=LINE + +## Authentication ## +COOKIE_DOMAIN=localhost +COOKIE_PATH=/ +SECURE_COOKIE=false +JWT_SECRET=xxxxxxxxxxxxxx +COOKIE_SECRET=xxxxxxxxxxxxxx +# expires in 3 days +COOKIE_EXP=259200000 + +## Database Authentication +MARIADB_HOST=localhost +MARIADB_USER=xxxxxxx +MARIADB_PASSWORD=xxxxxx +MARIADB_DATABASE=xxxxxxx + +## Google Authentication +GOOGLE_CLIENT_ID=xxxxxxxxx +GOOGLE_CLIENT_SECRET=xxxxxxxxx +GOOGLE_REDIRECT_URI=xxxxxxxxxxx + +## GITHUB Authentication +GITHUB_CLIENT_ID=xxxxxxxxxxx +GITHUB_CLIENT_SECRET=xxxxxxxxxxxx +GITHUB_REDIRECT_URI=xxxxxxxxxxxxxx \ No newline at end of file diff --git a/env.example/production.env b/env.example/production.env new file mode 100644 index 0000000..9951d66 --- /dev/null +++ b/env.example/production.env @@ -0,0 +1,37 @@ +## Environment ## +NODE_ENV=production + +## Server ## +PORT=3001 +HOST=localhost + +## Setup jet-logger ## +JET_LOGGER_MODE=CONSOLE +JET_LOGGER_FILEPATH=jet-logger.log +JET_LOGGER_TIMESTAMP=TRUE +JET_LOGGER_FORMAT=LINE + +## Authentication ## +COOKIE_DOMAIN=localhost +COOKIE_PATH=/ +SECURE_COOKIE=false +JWT_SECRET=xxxxxxxxxxxxxx +COOKIE_SECRET=xxxxxxxxxxxxxx +# expires in 3 days +COOKIE_EXP=259200000 + +## Database Authentication +MARIADB_HOST=localhost +MARIADB_USER=xxxxxxx +MARIADB_PASSWORD=xxxxxx +MARIADB_DATABASE=xxxxxxx + +## Google Authentication +GOOGLE_CLIENT_ID=xxxxxxxxx +GOOGLE_CLIENT_SECRET=xxxxxxxxx +GOOGLE_REDIRECT_URI=xxxxxxxxxxx + +## GITHUB Authentication +GITHUB_CLIENT_ID=xxxxxxxxxxx +GITHUB_CLIENT_SECRET=xxxxxxxxxxxx +GITHUB_REDIRECT_URI=xxxxxxxxxxxxxx \ No newline at end of file diff --git a/env.example/test.env b/env.example/test.env new file mode 100644 index 0000000..5ac4435 --- /dev/null +++ b/env.example/test.env @@ -0,0 +1,27 @@ +## Environment ## +NODE_ENV=test + +## Server ## +PORT=3001 +HOST=localhost + +## Setup jet-logger ## +JET_LOGGER_MODE=CONSOLE +JET_LOGGER_FILEPATH=jet-logger.log +JET_LOGGER_TIMESTAMP=TRUE +JET_LOGGER_FORMAT=LINE + +## Authentication ## +COOKIE_DOMAIN=localhost +COOKIE_PATH=/ +SECURE_COOKIE=false +JWT_SECRET=xxxxxxxxxxxxxx +COOKIE_SECRET=xxxxxxxxxxxxxx +# expires in 3 days +COOKIE_EXP=259200000 + +## Database Authentication +MARIADB_HOST=localhost +MARIADB_USER=xxxxxxx +MARIADB_PASSWORD=xxxxxx +MARIADB_DATABASE=xxxxxxx \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2bdefb2..0f0868e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,17 +45,26 @@ "find": "^0.3.0", "fs-extra": "^11.1.1", "jasmine": "^4.6.0", - "nodemon": "^2.0.22", + "nodemon": "^3.0.1", "prettier": "^2.8.8", "supertest": "^6.3.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.0.4" + "typescript": "^5.1.6" }, "engines": { "node": ">=16" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1547,9 +1556,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2614,9 +2623,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -2887,9 +2896,9 @@ } }, "node_modules/nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -2897,8 +2906,8 @@ "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" @@ -2907,7 +2916,7 @@ "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" }, "funding": { "type": "opencollective", @@ -2932,15 +2941,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3031,17 +3031,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -3410,9 +3410,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3539,24 +3539,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "dependencies": { - "semver": "~7.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/slash": { @@ -3943,15 +3934,15 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/typical": { @@ -4065,15 +4056,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrapjs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", @@ -4127,6 +4109,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -5279,9 +5267,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -6082,9 +6070,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -6289,9 +6277,9 @@ } }, "nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", "dev": true, "requires": { "chokidar": "^3.5.2", @@ -6299,8 +6287,8 @@ "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" @@ -6321,12 +6309,6 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6395,17 +6377,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -6629,9 +6611,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" }, @@ -6740,20 +6722,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "requires": { - "semver": "~7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } + "semver": "^7.5.3" } }, "slash": { @@ -7031,9 +7005,9 @@ } }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==" + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" }, "typical": { "version": "4.0.0", @@ -7122,12 +7096,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wordwrapjs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", diff --git a/package.json b/package.json index 6eb1d67..1a7e654 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,11 @@ "find": "^0.3.0", "fs-extra": "^11.1.1", "jasmine": "^4.6.0", - "nodemon": "^2.0.22", + "nodemon": "^3.0.1", "prettier": "^2.8.8", "supertest": "^6.3.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.0.4" + "typescript": "^5.1.6" } } From 1c101e59fc38911181c51f1b6d031ef8b1c4c2e1 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 04:17:09 +0300 Subject: [PATCH 005/118] Updated README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1d3a123..e2a15ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ REST api for NavigoLearning. This project is built with Node.js, Express, and MariaDB. +## Documentation + +Documentation for the api can be found [here](docs/paths/README.md). + # Getting Started ## Prerequisites @@ -54,10 +58,6 @@ MariaDB. npm run dev ``` -## Documentation - -Documentation for the api can be found [here](docs/paths/README.md). - ## Structure of the Project The project is split into 4 main folders: From 420e21030fb82195e0b09f09e8da83222c8ba2b6 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 04:18:22 +0300 Subject: [PATCH 006/118] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2a15ee..1da5908 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Documentation for the api can be found [here](docs/paths/README.md). CREATE DATABASE navigo_learn; USE navigo_learn; CREATE USER 'navigo_learn'@'localhost' IDENTIFIED BY 'password'; - GRANT ALL PRIVILEGES ON navigo_learn.* TO 'navigo_learn'@'localhost'; + GRANT ALL PRIVILEGES ON navigo_learn.* TO 'navigo_learn'@'localhost'; ``` 4. Rename the env.example folder to env and fill in the values for From f478737c207c8bb9b5af197bd5273daa51ae0216 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 04:19:55 +0300 Subject: [PATCH 007/118] Updated README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1da5908..0d143c6 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,13 @@ Documentation for the api can be found [here](docs/paths/README.md). 4. Rename the env.example folder to env and fill in the values for development.env + 5. Run tests to make sure everything works ```sh npm test ``` -5. Run the server + +6. Run the server ```sh npm run dev ``` From 31f11037c3e38e273eef95336b9684a60fb88c73 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 05:51:33 +0300 Subject: [PATCH 008/118] WIP Auth refactoring progress and tsconfig updates Added new validators/validateBody.ts to implement body validation middleware for required fields in express routes. Included the new middleware in AuthRouter.ts where I refactored login and register routes. Refactor includes extracting the logic to handle sign up and log in to authController.ts to separate the route handling from logic. Updated TSconfig.json to target ES2020 instead of ES6 to take advantage of updated features. Moved session creation logic to sessionManager.ts from AuthRouter.ts to centralize session handling. I also updated UserInfo.ts model, to allow optional parameter in constructor for simpler instantiation. The idea is to keep things DRY and to modularize functionality for better maintainability. --- src/controllers/authController.ts | 158 ++++++++++++++++++++++ src/models/UserInfo.ts | 12 +- src/routes/AuthRouter.ts | 211 ++---------------------------- src/util/sessionManager.ts | 59 +++++++++ src/validators/validateBody.ts | 35 +++++ tsconfig.json | 2 +- 6 files changed, 269 insertions(+), 208 deletions(-) create mode 100644 src/util/sessionManager.ts create mode 100644 src/validators/validateBody.ts diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index e69de29..5e1fe4b 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -0,0 +1,158 @@ +import { RequestWithBody } from '@src/validators/validateBody'; +import { Response } from 'express'; +import DatabaseDriver from '@src/util/DatabaseDriver'; +import User from '@src/models/User'; +import { HttpStatusCode } from 'axios'; +import { comparePassword, saltPassword } from '@src/util/LoginUtil'; +import { createSaveSession } from '@src/util/sessionManager'; +import { UserInfo } from '@src/models/UserInfo'; +import { checkEmail } from '@src/util/EmailUtil'; + +/* + * Interfaces + */ +interface GoogleUserData { + id: string; + email: string; + name: string; + picture: string; +} + +interface GitHubUserData { + login: string; + id: number; + name: string; + email: string; + avatar_url: string; + bio: string; + blog: string; +} + +/* + * Helpers + */ +function _invalidLogin(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + error: 'Invalid email or password', + success: false, + }); +} + +function _invalidBody(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + error: 'Invalid request body', + success: false, + }); +} + +function _conflict(res: Response): void { + res.status(HttpStatusCode.Conflict).json({ + error: 'Email already in use', + success: false, + }); +} + +function _serverError(res: Response): void { + res.status(HttpStatusCode.InternalServerError).json({ + error: 'Internal server error', + success: false, + }); +} + +async function insertUser( + db: DatabaseDriver, + email: string, + name: string, + pwdHash: string, + userInfo?: UserInfo, +): Promise { + const userId = await db.insert('users', { + email, + name, + pwdHash, + }); + + if (await insertUserInfo(db, userId, userInfo)) { + return userId; + } else { + return -1n; + } +} + +async function insertUserInfo( + db: DatabaseDriver, + userId: bigint, + userInfo?: UserInfo, +): Promise { + if (!userInfo) userInfo = new UserInfo(userId); + userInfo.userId = userId; + return (await db.insert('userInfo', userInfo)) >= 0; +} + +/* + * Controllers + */ +export async function authLogin( + req: RequestWithBody, + res: Response, +): Promise { + const { email, password } = req.body; + + if (typeof password !== 'string') return _invalidLogin(res); + + // get database + const db = new DatabaseDriver(); + + // check if user exists + const user = await db.getWhere('users', 'email', email); + if (!user) return _invalidLogin(res); + + // check if password is correct + const isCorrect = comparePassword(password, user.pwdHash || ''); + if (!isCorrect) return _invalidLogin(res); + + // check userInfo table for user + const userInfo = await db.getWhere('userInfo', 'userId', user.id); + if (!userInfo) + if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) + return _serverError(res); + + // create session and save it + if (await createSaveSession(res, user.id)) + return res + .status(HttpStatusCode.Ok) + .json({ message: 'Login successful', success: true }); +} + +export async function authRegister( + req: RequestWithBody, + res: Response, +): Promise { + const { email, password, name } = req.body; + + if ( + typeof password !== 'string' || + typeof email !== 'string' || + typeof name !== 'string' + ) + return _invalidBody(res); + + // get database + const db = new DatabaseDriver(); + + // check if user exists + const user = await db.getWhere('users', 'email', email); + if (!!user) return _conflict(res); + + if (!checkEmail(email) || password.length < 8) return _invalidBody(res); + + // create user + const userId = await insertUser(db, email, name, saltPassword(password)); + if (userId === -1n) return _serverError(res); + + // create session and save it + if (await createSaveSession(res, BigInt(userId))) + return res + .status(HttpStatusCode.Ok) + .json({ message: 'Registration successful', success: true }); +} diff --git a/src/models/UserInfo.ts b/src/models/UserInfo.ts index 189fbb2..4618195 100644 --- a/src/models/UserInfo.ts +++ b/src/models/UserInfo.ts @@ -25,12 +25,12 @@ export class UserInfo implements IUserInfo { public constructor( userId: bigint, - profilePictureUrl: string, - bio: string, - quote: string, - blogUrl: string, - websiteUrl: string, - githubUrl: string, + profilePictureUrl = '', + bio = '', + quote = '', + blogUrl = '', + websiteUrl = '', + githubUrl = '', id = BigInt(-1), // id last cause usually set by db ) { this.userId = userId; diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 5aef25a..646c177 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -10,95 +10,14 @@ import { NodeEnvs } from '@src/constants/misc'; import axios, { AxiosError } from 'axios'; import logger from 'jet-logger'; import { UserInfo } from '@src/models/UserInfo'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import { checkEmail } from '@src/util/EmailUtil'; import validateSession from '@src/validators/validateSession'; +import { authLogin, authRegister } from '@src/controllers/authController'; +import validateBody from '@src/validators/validateBody'; const AuthRouter = Router(); -interface GoogleUserData { - id: string; - email: string; - name: string; - picture: string; -} - -interface GitHubUserData { - login: string; - id: number; - name: string; - email: string; - avatar_url: string; - bio: string; - blog: string; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getInfoFromRequest(req: any): { email: string; password: string } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const email = req?.body?.email as string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const password = req?.body?.password as string; - return { email, password }; -} - -async function createSession(user: User): Promise { - const token = randomBytes(32).toString('hex'); - - // get database - const db = new DatabaseDriver(); - - // check if token is already in use - statistically unlikely but possible - const session = await db.getWhere('sessions', 'token', token); - if (!!session) { - return createSession(user); - } - - // save session - const sessionId = await db.insert('sessions', { - token, - userId: user.id, - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days - }); - - // check if session was saved - if (sessionId < 0) { - return ''; - } - - return token; -} - -async function saveSession( - res: Response, - user: User, - register = false, -): Promise { - // get session token - const token = await createSession(user); - - // check if session was created - if (!token) { - res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Internal server error', - }); - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - res.cookie('token', token, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days - httpOnly: false, - secure: EnvVars.NodeEnv === NodeEnvs.Production, - sameSite: 'strict', - }); - - res.status(register ? HttpStatusCodes.CREATED : HttpStatusCodes.OK).json({ - message: `${register ? 'Registe' : 'Login'} successful`, - }); - } -} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore function handleExternalAuthError(error, res: Response): void { @@ -106,7 +25,7 @@ function handleExternalAuthError(error, res: Response): void { if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(error); if (error instanceof AxiosError) { res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Couldn\'t get access token from external service', + error: "Couldn't get access token from external service", }); } else { res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ @@ -115,123 +34,13 @@ function handleExternalAuthError(error, res: Response): void { } } -AuthRouter.post(Paths.Auth.Login, async (req, res) => { - // check if data is valid - const { email, password } = getInfoFromRequest(req); - // if not, return error - if (!email || !password) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Email and password are required', - }); - } - - // get database - const db = new DatabaseDriver(); +AuthRouter.post(Paths.Auth.Login, validateBody('email', 'password'), authLogin); - // check if user exists - const user = await db.getWhere('users', 'email', email); - // if not, return error - if (!user) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Invalid email or password', - }); - } - - // check if the password is correct - const isPasswordCorrect = comparePassword(password, user.pwdHash || ''); - // if not, return error - if (!isPasswordCorrect) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Invalid email or password', - }); - } - - // check if user has userInfo - const userInfo = db.getWhere('userInfo', 'userId', user.id); - - if (!userInfo) { - // create userInfo - const row = await db.insert('userInfo', { - userId: user.id, - name: user.name, - email: user.email, - bio: '', - website: '', - profilePicture: '', - }); - - if (row < 0) - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Internal server error', - }); - } - - // save session - return await saveSession(res, user); -}); - -AuthRouter.post(Paths.Auth.Register, async (req, res) => { - // check if data is valid - const { email, password } = getInfoFromRequest(req); - // if not, return error - if (!email || !password) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Email and password are required', - }); - } - - // check if email is valid - // https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1 - if (!checkEmail(email)) - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Invalid Email', - }); - - // get database - const db = new DatabaseDriver(); - - // check if user exists - const user = await db.getWhere('users', 'email', email); - // if yes, return error - if (!!user) { - return res.status(HttpStatusCodes.CONFLICT).json({ - error: 'User with this Email already exists', - }); - } - - // parse email for username - const username = email.split('@')[0]; - const saltedPassword = saltPassword(password); - - // create user - const newUser = new User(username, email, UserRoles.Standard, saltedPassword); - - // save user - const result = await db.insert('users', newUser); - newUser.id = result; - - // check result - if (result >= 0) { - // create userInfo - const userInfo = new UserInfo(newUser.id, '', '', '', '', '', ''); - - // save userInfo - const userInfoResult = await db.insert('userInfo', userInfo); - - // check if userInfo was created - if (userInfoResult < 0) - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Something went wrong', - }); - - // save session - return await saveSession(res, newUser, true); - } - - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Something went wrong', - }); -}); +AuthRouter.post( + Paths.Auth.Register, + validateBody('email', 'password', 'name'), + authRegister, +); AuthRouter.use(Paths.Auth.ChangePassword, validateSession); AuthRouter.post( diff --git a/src/util/sessionManager.ts b/src/util/sessionManager.ts new file mode 100644 index 0000000..c2673dc --- /dev/null +++ b/src/util/sessionManager.ts @@ -0,0 +1,59 @@ +import { randomBytes } from 'crypto'; +import DatabaseDriver from '@src/util/DatabaseDriver'; +import { Response } from 'express'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; +import EnvVars from '@src/constants/EnvVars'; +import { NodeEnvs } from '@src/constants/misc'; + +export async function createSession(userId: bigint): Promise { + const token = randomBytes(32).toString('hex'); + + // get database + const db = new DatabaseDriver(); + + // check if token is already in use - statistically unlikely but possible + const session = await db.getWhere('sessions', 'token', token); + if (!!session) { + return await createSession(userId); + } + + // save session + const sessionId = await db.insert('sessions', { + token, + userId, + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days + }); + + // check if session was saved + if (sessionId < 0) { + return ''; + } + + return token; +} + +export function saveSession(res: Response, token: string): boolean { + // check if session was created + if (!token) { + res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + error: 'Internal server error', + }); + return false; + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + res.cookie('token', token, { + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days + httpOnly: false, + secure: EnvVars.NodeEnv === NodeEnvs.Production, + sameSite: 'strict', + }); + return true; + } +} + +export async function createSaveSession( + res: Response, + userId: bigint, +): Promise { + return saveSession(res, await createSession(userId)); +} diff --git a/src/validators/validateBody.ts b/src/validators/validateBody.ts new file mode 100644 index 0000000..db337e7 --- /dev/null +++ b/src/validators/validateBody.ts @@ -0,0 +1,35 @@ +import { RequestWithSession } from '@src/middleware/session'; +import { NextFunction, Response } from 'express'; +import { HttpStatusCode } from 'axios'; + +interface IBody { + [key: string]: unknown; +} + +export interface RequestWithBody extends RequestWithSession { + body: IBody; +} + +export default function ValidateBody( + ...requiredFields: string[] +): (req: RequestWithBody, res: Response, next: NextFunction) => unknown { + return (req: RequestWithBody, res: Response, next: NextFunction): unknown => { + const body = req.body; + + if (!body) { + return res + .status(HttpStatusCode.BadRequest) + .json({ error: 'Missing request body' }); + } + + for (const item of requiredFields) { + if (!body[item]) { + return res + .status(HttpStatusCode.BadRequest) + .json({ error: `Missing required field: ${item}` }); + } + } + + return next(); + }; +} diff --git a/tsconfig.json b/tsconfig.json index 225aeb2..4e47447 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es6", + "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ From 22e1e53ef20aed33765ec82a5b3722bfc0a4a54a Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 10:00:09 +0300 Subject: [PATCH 009/118] Update README with branch specific build badges The README file was updated to include build status badges specifically for production, master, and refactor branches. This change will help to quickly understand the build status of each of the main branches directly from the README and thus, promises better maintainability for the main branches of the repository. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d143c6..dbaa2aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Navigo Learn API -![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg) +| Build Status | Badge | +|:------------:|:--------------------------------------------------------------------------------------------------------:| +| Production | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=prod) | +| Master | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=master) | +| Refactor | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=Refactor) | ## ! This is a work in progress ! From bfc4cfd0ce72f97444098c8fd385590c09940c10 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 10:05:50 +0300 Subject: [PATCH 010/118] merged readme.md from refactor branch --- README.md | 114 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 0a58f1f..dbaa2aa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# Navigo Learn API [![CodeFactor](https://www.codefactor.io/repository/github/navigolearn/api/badge/master)](https://www.codefactor.io/repository/github/navigolearn/api/overview/master) +# Navigo Learn API + +| Build Status | Badge | +|:------------:|:--------------------------------------------------------------------------------------------------------:| +| Production | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=prod) | +| Master | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=master) | +| Refactor | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=Refactor) | ## ! This is a work in progress ! @@ -17,41 +23,71 @@ MariaDB. Documentation for the api can be found [here](docs/paths/README.md). -## Available Scripts - -### `npm run dev` - -Run the server in development mode. - -### `npm test` - -Run all unit-tests with hot-reloading. - -### `npm test -- --testFile="name of test file" (i.e. --testFile=Users).` - -Run a single unit-test. - -### `npm run test:no-reloading` - -Run all unit-tests without hot-reloading. - -### `npm run lint` - -Check for linting errors. - -### `npm run build` - -Build the project for production. - -### `npm start` - -Run the production build (Must be built first). - -### `npm start -- --env="name of env file" (default is production).` - -Run production build with a different env file. - -## Additional Notes - -- If `npm run dev` gives you issues with bcrypt on MacOS you may need to - run: `npm rebuild bcrypt --build-from-source`. +# Getting Started + +## Prerequisites + +- [Node.js](https://nodejs.org/en/) - v16 or higher +- [MariaDB](https://mariadb.org/) - v10.6 or higher +- [Git](https://git-scm.com/) - v2.32 or higher + +## Installation + +1. Clone the repo + ```sh + git clone git@github.com:NavigoLearn/API.git + ``` + +2. Install NPM packages + ```sh + npm install + ``` + +3. Create a MariaDB database + ```sh + CREATE DATABASE navigo_learn; + USE navigo_learn; + CREATE USER 'navigo_learn'@'localhost' IDENTIFIED BY 'password'; + GRANT ALL PRIVILEGES ON navigo_learn.* TO 'navigo_learn'@'localhost'; + ``` + +4. Rename the env.example folder to env and fill in the values for + development.env + +5. Run tests to make sure everything works + ```sh + npm test + ``` + +6. Run the server + ```sh + npm run dev + ``` + +## Structure of the Project + +The project is split into 4 main folders: + +- `src` - Contains all the source code for the project. +- `spec` - Contains all the unit-tests for the project. +- `docs` - Contains all the documentation for the project. +- `env` - Contains all the environment files for the project. (rename the + env.example folder to env to use it) + +### `src` + +The `src` folder is split into multiple main folders: + +- `constants` - Contains constants used in the project. (HTTP status codes, env + variables, etc.) +- `controllers` - Contains the controllers of the project. +- `middleware` - Contains middleware used in the project. (session, etc.) +- `models` - Contains the data models for the project. (Roadmap, User, etc.) +- `routes` - Contains the routers pointing to controllers. (auth, users, etc.) +- `sql` - Contains sql files used in the project. (create tables, metrics, etc.) +- `utils` - Contains utility functions used in the project. (databaseDriver, + etc.) +- `validators` - Contains the validators used in the project. (user, roadmap, + etc.) +- `index.ts` - The entry point. +- `server.ts` - The server. From 196b182f7229a62610cc7a8c928f8b952fac2787 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 14:56:41 +0300 Subject: [PATCH 011/118] Added password change and Google login functionalities to the authentication controller. The changes include adding authChangePassword, authForgotPassword, and authGoogleCallback functions to the authController. The implementation of Google login is done using Google OAuth2 authorization and user info endpoint responses. We also made changes to our AuthRouter to include routes for changing the password and the forgot password functionality. --- src/controllers/authController.ts | 185 +++++++++++++++++++++++- src/routes/AuthRouter.ts | 224 +++--------------------------- 2 files changed, 198 insertions(+), 211 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 5e1fe4b..86deb94 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -7,6 +7,9 @@ import { comparePassword, saltPassword } from '@src/util/LoginUtil'; import { createSaveSession } from '@src/util/sessionManager'; import { UserInfo } from '@src/models/UserInfo'; import { checkEmail } from '@src/util/EmailUtil'; +import EnvVars from '@src/constants/EnvVars'; +import axios from 'axios/index'; +import logger from 'jet-logger'; /* * Interfaces @@ -89,6 +92,46 @@ async function insertUserInfo( return (await db.insert('userInfo', userInfo)) >= 0; } +async function updateUser( + db: DatabaseDriver, + userId: bigint, + user: User, + userInfo?: UserInfo, +): Promise { + if (userInfo) if (!(await updateUserInfo(db, userId, userInfo))) return false; + + return await db.update('users', userId, user); +} + +async function updateUserInfo( + db: DatabaseDriver, + userId: bigint, + userInfo: UserInfo, +): Promise { + return await db.update('userInfo', userId, userInfo); +} + +async function getUserInfo( + db: DatabaseDriver, + userId: bigint, +): Promise { + return await db.get('userInfo', userId); +} + +async function getUser( + db: DatabaseDriver, + userId: bigint, +): Promise { + return await db.get('users', userId); +} + +async function getUserByEmail( + db: DatabaseDriver, + email: string, +): Promise { + return await db.getWhere('users', 'email', email); +} + /* * Controllers */ @@ -98,13 +141,14 @@ export async function authLogin( ): Promise { const { email, password } = req.body; - if (typeof password !== 'string') return _invalidLogin(res); + if (typeof email !== 'string' || typeof password !== 'string') + return _invalidLogin(res); // get database const db = new DatabaseDriver(); // check if user exists - const user = await db.getWhere('users', 'email', email); + const user = await getUserByEmail(db, email); if (!user) return _invalidLogin(res); // check if password is correct @@ -112,7 +156,7 @@ export async function authLogin( if (!isCorrect) return _invalidLogin(res); // check userInfo table for user - const userInfo = await db.getWhere('userInfo', 'userId', user.id); + const userInfo = await getUserInfo(db, user.id); if (!userInfo) if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) return _serverError(res); @@ -141,7 +185,7 @@ export async function authRegister( const db = new DatabaseDriver(); // check if user exists - const user = await db.getWhere('users', 'email', email); + const user = await getUserByEmail(db, email); if (!!user) return _conflict(res); if (!checkEmail(email) || password.length < 8) return _invalidBody(res); @@ -156,3 +200,136 @@ export async function authRegister( .status(HttpStatusCode.Ok) .json({ message: 'Registration successful', success: true }); } + +export async function authChangePassword( + req: RequestWithBody, + res: Response, +): Promise { + const { password, newPassword } = req.body; + + if (typeof password !== 'string' || typeof newPassword !== 'string') + return _invalidBody(res); + + // get database + const db = new DatabaseDriver(); + + // check if user is logged in + if (!req.session?.userId) return _serverError(res); + const userId = req.session.userId; + + // check if user exists + const user = await getUser(db, userId); + if (!user) return _serverError(res); + + // check if password is correct + const isCorrect = comparePassword(password, user.pwdHash || ''); + if (!isCorrect) return _invalidLogin(res); + + user.pwdHash = saltPassword(newPassword); + + // update password in ussr + const result = await updateUser(db, userId, user); + + if (result) + return res + .status(HttpStatusCode.Ok) + .json({ message: 'Password changed', success: true }); + else return _serverError(res); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function authForgotPassword( + req: RequestWithBody, + res: Response, +): Promise { + // TODO: implement + return res + .status(HttpStatusCode.NotImplemented) + .json({ error: 'Not implemented', success: false }); +} + +export function authGoogle(_: never, res: Response): unknown { + return res.redirect( + 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + + EnvVars.Google.ClientID + + '&redirect_uri=' + + EnvVars.Google.RedirectUri + + '&response_type=code&scope=email%20profile', + ); +} + +export async function authGoogleCallback( + req: RequestWithBody, + res: Response, +): Promise { + const code = req.query.code; + + if (typeof code !== 'string') return _invalidBody(res); + + try { + // get access token + let response = await axios.post('https://oauth2.googleapis.com/token', { + client_id: EnvVars.Google.ClientID, + client_secret: EnvVars.Google.ClientSecret, + redirect_uri: EnvVars.Google.RedirectUri, + grant_type: 'authorization_code', + code, + }); + + // check if response is valid + if (response.status !== 200) return _serverError(res); + if (!response.data) return _serverError(res); + + // get access token from response + const data = response.data as { access_token?: string }; + const accessToken = data.access_token; + if (!accessToken) return _serverError(res); + + // get user info + response = await axios.get( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + const userData = response?.data as GoogleUserData; + if (!userData) return _serverError(res); + + // get database + const db = new DatabaseDriver(); + + // check if user exists + let user = await getUserByEmail(db, userData.email); + + if (!user) { + // create user + user = new User(userData.name, userData.email, 0, ''); + user.googleId = userData.id; + user.id = await insertUser(db, userData.email, userData.name, ''); + } else { + user.googleId = userData.id; + + // update user + if (!(await updateUser(db, user.id, user))) return _serverError(res); + + // check userInfo table for user + const userInfo = await getUserInfo(db, user.id); + if (!userInfo) + if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) + return _serverError(res); + } + // check if user was created + if (user.id === -1n) return _serverError(res); + + // create session and save it + if (await createSaveSession(res, BigInt(user.id))) + return res + .status(HttpStatusCode.Ok) + .json({ message: 'Registration successful', success: true }); + } catch (e) { + logger.err(e, true); + return _serverError(res); + } +} diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 646c177..514b4f7 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -1,39 +1,26 @@ -import { Response, Router } from 'express'; +import { Router } from 'express'; import Paths from '@src/constants/Paths'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import { comparePassword, saltPassword } from '@src/util/LoginUtil'; -import User, { UserRoles } from '@src/models/User'; -import { randomBytes } from 'crypto'; +import User from '@src/models/User'; import EnvVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; -import axios, { AxiosError } from 'axios'; -import logger from 'jet-logger'; +import axios from 'axios'; import { UserInfo } from '@src/models/UserInfo'; import { RequestWithSession } from '@src/middleware/session'; -import { checkEmail } from '@src/util/EmailUtil'; import validateSession from '@src/validators/validateSession'; -import { authLogin, authRegister } from '@src/controllers/authController'; +import { + authChangePassword, + authForgotPassword, + authGoogle, + authGoogleCallback, + authLogin, + authRegister, +} from '@src/controllers/authController'; import validateBody from '@src/validators/validateBody'; const AuthRouter = Router(); -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -function handleExternalAuthError(error, res: Response): void { - if (!(error instanceof Error)) return; - if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(error); - if (error instanceof AxiosError) { - res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: "Couldn't get access token from external service", - }); - } else { - res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: error.message || 'Internal server error', - }); - } -} - AuthRouter.post(Paths.Auth.Login, validateBody('email', 'password'), authLogin); AuthRouter.post( @@ -42,195 +29,18 @@ AuthRouter.post( authRegister, ); -AuthRouter.use(Paths.Auth.ChangePassword, validateSession); AuthRouter.post( Paths.Auth.ChangePassword, - async (req: RequestWithSession, res) => { - // check if data is valid - const { password } = getInfoFromRequest(req); - - // get new password from body - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { newPassword } = req?.body || {}; - - // if not, return error - if (!password || !newPassword || !req.session?.userId) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'A valid session, the old and the new password are required', - }); - } - - // get database - const db = new DatabaseDriver(); - - // check if user exists - const user = await db.get('users', req.session.userId); - - // if not, return error - if (!user) { - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Invalid user', - }); - } - - // check if the password is correct - const isPasswordCorrect = comparePassword(password, user.pwdHash || ''); - - // if not, return error - if password is empty, allow change - if (!isPasswordCorrect && user.pwdHash !== '') { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Incorrect password', - }); - } - - // change password - const saltedPassword = saltPassword(newPassword as string); - - // update user - const result = await db.update('users', req.session.userId, { - pwdHash: saltedPassword, - }); - - // check result - if (result) { - return res.status(HttpStatusCodes.OK).json({ - message: 'Password changed successfully', - }); - } else { - return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ - error: 'Something went wrong', - }); - } - }, + validateSession, + validateBody('password', 'newPassword'), + authChangePassword, ); -AuthRouter.post(Paths.Auth.ForgotPassword, async (req, res) => { - // check if data is valid - const { email } = getInfoFromRequest(req); - - // if not, return error - if (!email) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'Email is required', - }); - } - - // get database - const db = new DatabaseDriver(); - - // check if user exists - const user = await db.getWhere('users', 'email', email); - - // if not, return error - if (!user) { - return res.status(HttpStatusCodes.BAD_REQUEST).json({ - error: 'User with this Email does not exist', - }); - } - - // TODO: send email with reset code - return res.status(HttpStatusCodes.NOT_IMPLEMENTED).json({ - error: 'Not implemented', - }); -}); - -AuthRouter.get(Paths.Auth.GoogleLogin, (req, res) => { - res.redirect( - 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + - EnvVars.Google.ClientID + - '&redirect_uri=' + - EnvVars.Google.RedirectUri + - '&response_type=code&scope=email%20profile', - ); -}); +AuthRouter.post(Paths.Auth.ForgotPassword, authForgotPassword); -AuthRouter.get(Paths.Auth.GoogleCallback, async (req, res) => { - const code = req.query.code as string; +AuthRouter.get(Paths.Auth.GoogleLogin, authGoogle); +AuthRouter.get(Paths.Auth.GoogleCallback, authGoogleCallback); - if (!code) { - return res.status(HttpStatusCodes.FORBIDDEN).json({ - error: 'Error while logging in with Google', - }); - } - - try { - // get access token - let response = await axios.post('https://oauth2.googleapis.com/token', { - client_id: EnvVars.Google.ClientID, - client_secret: EnvVars.Google.ClientSecret, - redirect_uri: EnvVars.Google.RedirectUri, - grant_type: 'authorization_code', - code, - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const accessToken = response?.data?.access_token as string; - - // if no token, return error - if (!accessToken) throw new AxiosError('No access token received'); - - // get user data - response = await axios.get( - 'https://www.googleapis.com/oauth2/v2/userinfo', - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - const userData = response?.data as GoogleUserData; - - // if no userDisplay data, return error - if (!userData) throw new Error('No userDisplay data received'); - - // get database - const db = new DatabaseDriver(); - - // check if userDisplay exists - let user = await db.getWhere('users', 'email', userData.email); - - // if a userDisplay doesn't exist, create a new userDisplay - if (!user) { - const userId = await db.insert('users', { - email: userData.email, - name: userData.name, - googleId: userData.id, - }); - - user = await db.get('users', userId); - - if (!user) throw new Error('User not found'); - } else if (!user.googleId) { - // if a userDisplay exists but doesn't have a Google id, add it - await db.update('users', user.id, { - googleId: userData.id, - }); - } - - //check if userDisplay has userInfo - const userInfo = await db.getWhere('userInfo', 'userId', user.id); - - // if not, create userInfo - if (!userInfo) { - const userInfoId = await db.insert('userInfo', { - userId: user.id, - profilePictureUrl: '', - bio: '', - quote: '', - blogUrl: '', - websiteUrl: '', - githubUrl: '', - }); - - // check if userInfo was created - if (userInfoId < 0) throw new Error('Could not create userInfo'); - } - - // save session - return await saveSession(res, user); - } catch (e) { - handleExternalAuthError(e, res); - } -}); AuthRouter.get(Paths.Auth.GithubLogin, (req, res) => { res.redirect( 'https://github.com/login/oauth/authorize?client_id=' + From 62f726ccf61b7789b5d244a5f3478eac84703484 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 15:43:08 +0300 Subject: [PATCH 012/118] Refactor AuthRouter and related files for clean architecture AuthRouter.ts has been refactored to use functions from 'authController.ts' instead of having all the logic within the AuthRouter. The main changes include: - GitHub authentication and callback functions have been moved from AuthRouter to new functions in authController: authGitHub and authGitHubCallback respectively. - Logout function has been simplified and moved to deleteClearSession in sessionManager.ts and authLogout in authController.ts. - Duplicate error validation code moved to a handleNotOkay helper function in authController. These changes enhance readability and maintainability by enforcing separation of concerns between route handling and the application's logic. --- src/controllers/authController.ts | 201 +++++++++++++++++++++++++++++- src/routes/AuthRouter.ts | 197 +---------------------------- src/util/sessionManager.ts | 44 +++++++ 3 files changed, 247 insertions(+), 195 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 86deb94..f53fd2e 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -4,12 +4,16 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import User from '@src/models/User'; import { HttpStatusCode } from 'axios'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; -import { createSaveSession } from '@src/util/sessionManager'; +import { + createSaveSession, + deleteClearSession, +} from '@src/util/sessionManager'; import { UserInfo } from '@src/models/UserInfo'; import { checkEmail } from '@src/util/EmailUtil'; import EnvVars from '@src/constants/EnvVars'; import axios from 'axios/index'; import logger from 'jet-logger'; +import { RequestWithSession } from '@src/middleware/session'; /* * Interfaces @@ -132,6 +136,24 @@ async function getUserByEmail( return await db.getWhere('users', 'email', email); } +function _handleNotOkay(res: Response, error: number): unknown { + logger.err(error, true); + + if (error >= HttpStatusCode.InternalServerError) + return res.status(HttpStatusCode.BadGateway).json({ + error: 'Remote resource error', + success: false, + }); + + if (error === HttpStatusCode.Unauthorized) + return res.status(HttpStatusCode.Unauthorized).json({ + error: 'Unauthorized', + success: false, + }); + + return _serverError(res); +} + /* * Controllers */ @@ -239,7 +261,7 @@ export async function authChangePassword( // eslint-disable-next-line @typescript-eslint/require-await export async function authForgotPassword( - req: RequestWithBody, + _: unknown, res: Response, ): Promise { // TODO: implement @@ -248,7 +270,7 @@ export async function authForgotPassword( .json({ error: 'Not implemented', success: false }); } -export function authGoogle(_: never, res: Response): unknown { +export function authGoogle(_: unknown, res: Response): unknown { return res.redirect( 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + EnvVars.Google.ClientID + @@ -277,7 +299,7 @@ export async function authGoogleCallback( }); // check if response is valid - if (response.status !== 200) return _serverError(res); + if (response.status !== 200) return _handleNotOkay(res, response.status); if (!response.data) return _serverError(res); // get access token from response @@ -333,3 +355,174 @@ export async function authGoogleCallback( return _serverError(res); } } + +export function authGitHub(_: unknown, res: Response): unknown { + return res.redirect( + 'https://github.com/login/oauth/authorize?client_id=' + + EnvVars.GitHub.ClientID + + '&redirect_uri=' + + EnvVars.GitHub.RedirectUri + + '&scope=user:email', + ); +} + +export async function authGitHubCallback( + req: RequestWithBody, + res: Response, +): Promise { + const code = req.query.code; + + if (typeof code !== 'string') return _invalidBody(res); + + try { + // get access token + let response = await axios.post( + 'https://github.com/login/oauth/access_token', + { + client_id: EnvVars.GitHub.ClientID, + client_secret: EnvVars.GitHub.ClientSecret, + code: code, + redirect_uri: EnvVars.GitHub.RedirectUri, + }, + { + headers: { + Accept: 'application/json', + }, + }, + ); + + // check if response is valid + if (response.status !== 200) return _handleNotOkay(res, response.status); + if (!response.data) return _serverError(res); + + // get access token from response + const data = response.data as { access_token?: string }; + const accessToken = data.access_token; + if (!accessToken) return _serverError(res); + + // get user info from github + response = await axios.get('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }); + + // check if response is valid + if (response.status !== 200) return _handleNotOkay(res, response.status); + if (!response.data) return _serverError(res); + + // get user data + const userData = response.data as GitHubUserData; + if (!userData) return _serverError(res); + + // if email is not public, get it from github + if (userData.email == '') { + response = await axios.get('https://api.github.com/user/emails', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + 'X-OAuth-Scopes': 'userDisplay:email', + }, + }); + + const emails = response.data as { + email: string; + primary: boolean; + verified: boolean; + }[]; + + // check if response is valid + if (response.status !== 200) return _handleNotOkay(res, response.status); + + // get primary email + userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; + } + + // check if email is valid + if (userData.email == '') return _serverError(res); + + // get database + const db = new DatabaseDriver(); + + // check if user exists + let user = await getUserByEmail(db, userData.email); + + if (!user) { + // create user + user = new User(userData.login, userData.email, 0, ''); + user.githubId = userData.id.toString(); + user.id = await insertUser( + db, + userData.email, + userData.name || userData.login, + '', + new UserInfo( + -1n, + userData.avatar_url, + userData.bio, + '', + userData.blog, + '', + `https://github.com/${userData.login}`, + ), + ); + } else { + user.githubId = userData.id.toString(); + await updateUser(db, user.id, user); + + // get user info + const userInfo = await getUserInfo(db, user.id); + if (!userInfo) { + // create user info + if ( + !(await insertUserInfo( + db, + user.id, + new UserInfo( + user.id, + userData.avatar_url, + userData.bio, + '', + userData.blog, + '', + `https://github.com/${userData.login}`, + ), + )) + ) + return _serverError(res); + } else { + if (userInfo.bio == '') userInfo.bio = userData.bio; + if (userInfo.profilePictureUrl == '') + userInfo.profilePictureUrl = userData.avatar_url; + if (userInfo.blogUrl == '') userInfo.blogUrl = userData.blog; + if (userInfo.githubUrl == '') + userInfo.githubUrl = `https://github.com/${userData.login}`; + + // update user info + if (!(await updateUserInfo(db, user.id, userInfo))) + return _serverError(res); + } + } + } catch (e) { + logger.err(e, true); + return _serverError(res); + } +} + +export async function authLogout( + req: RequestWithSession, + res: Response, +): Promise { + if (!req.session) return _serverError(res); + + // delete session + if (!(await deleteClearSession(res, req.session.token))) + return _serverError(res); + + // return success + return res + .status(HttpStatusCode.Ok) + .json({ message: 'Logout successful', success: true }); +} diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 514b4f7..f50b1bb 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -1,20 +1,15 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import DatabaseDriver from '@src/util/DatabaseDriver'; -import User from '@src/models/User'; -import EnvVars from '@src/constants/EnvVars'; -import { NodeEnvs } from '@src/constants/misc'; -import axios from 'axios'; -import { UserInfo } from '@src/models/UserInfo'; -import { RequestWithSession } from '@src/middleware/session'; import validateSession from '@src/validators/validateSession'; import { authChangePassword, authForgotPassword, + authGitHub, + authGitHubCallback, authGoogle, authGoogleCallback, authLogin, + authLogout, authRegister, } from '@src/controllers/authController'; import validateBody from '@src/validators/validateBody'; @@ -41,189 +36,9 @@ AuthRouter.post(Paths.Auth.ForgotPassword, authForgotPassword); AuthRouter.get(Paths.Auth.GoogleLogin, authGoogle); AuthRouter.get(Paths.Auth.GoogleCallback, authGoogleCallback); -AuthRouter.get(Paths.Auth.GithubLogin, (req, res) => { - res.redirect( - 'https://github.com/login/oauth/authorize?client_id=' + - EnvVars.GitHub.ClientID + - '&redirect_uri=' + - EnvVars.GitHub.RedirectUri + - '&scope=user:email', - ); -}); +AuthRouter.get(Paths.Auth.GithubLogin, authGitHub); +AuthRouter.get(Paths.Auth.GithubCallback, authGitHubCallback); -AuthRouter.get(Paths.Auth.GithubCallback, async (req, res) => { - const code = req.query.code as string; - - if (!code) { - return res.status(HttpStatusCodes.FORBIDDEN).json({ - error: 'Error while logging in with GitHub', - }); - } - - try { - const response = await axios.post( - 'https://github.com/login/oauth/access_token', - { - client_id: EnvVars.GitHub.ClientID, - client_secret: EnvVars.GitHub.ClientSecret, - code: code, - redirect_uri: EnvVars.GitHub.RedirectUri, - }, - { - headers: { - Accept: 'application/json', - }, - }, - ); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const accessToken = response?.data?.access_token as string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const response1 = await axios.get('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', - }, - }); - - // get userDisplay email - const response2 = await axios.get('https://api.github.com/user/emails', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', - 'X-GitHub-Api-Version': '2022-11-28', - 'X-OAuth-Scopes': 'userDisplay:email', - }, - }); - - // array of {email:string, primary:boolean, verified:boolean} - const emails = response2.data as { - email: string; - primary: boolean; - verified: boolean; - }[]; - - // get primary email - const primaryEmail = emails.find( - (email) => email.primary && email.verified, - ); - - // if no primary email, return error - if (!primaryEmail) - return res.status(HttpStatusCodes.FORBIDDEN).json({ - error: 'No primary email found', - }); - - const data = response1.data as GitHubUserData; - data.email = primaryEmail.email; - - // get database - const db = new DatabaseDriver(); - - // check if userDisplay exists - let user = await db.getWhere('users', 'email', data.email); - - if (!user) { - // create userDisplay - const userId = await db.insert('users', { - name: data.name || data.login, - email: data.email, - githubId: data.id, - }); - - // check if userDisplay was created - if (userId < 0) throw new Error('Could not create userDisplay'); - - // get userDisplay - user = new User( - data.name, - data.email, - 0, - '', - userId, - null, - data.id.toString(), - ); - } - - // check if userDisplay has githubId if not, - // update userDisplay with githubId merging accounts - if (!user.githubId) { - // update userDisplay - const userId = await db.update('users', user.id, { - githubId: data.id, - }); - - // check if userDisplay was updated - if (!userId) throw new Error('Could not update userDisplay'); - } - - //check if userDisplay has userInfo - const userInfo = await db.getWhere('userInfo', 'userId', user.id); - - // if not, create userInfo - if (!userInfo) { - const userInfoId = await db.insert('userInfo', { - userId: user.id, - profilePictureUrl: data.avatar_url, - bio: data.bio, - quote: '', - blogUrl: data.blog, - websiteUrl: '', - githubUrl: `https://github.com/${data.login}`, - }); - - // check if userInfo was created - if (userInfoId < 0) throw new Error('Could not create userInfo'); - } - - // save session - return await saveSession(res, user); - } catch (error) { - handleExternalAuthError(error, res); - } -}); - -AuthRouter.delete(Paths.Auth.Logout, validateSession); -AuthRouter.delete(Paths.Auth.Logout, async (req: RequestWithSession, res) => { - // get session - const token = req.session?.token; - - // if no session, return error - if (!token) { - return res.status(HttpStatusCodes.UNAUTHORIZED).json({ - error: 'No session found', - }); - } - - // get database - const db = new DatabaseDriver(); - - // delete session - const session = await db.getWhere<{ id: bigint }>('sessions', 'token', token); - - if (!session) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Session not found' }); - - await db.delete('sessions', session.id); - - // clear previous cookie header - res.header('Set-Cookie', ''); - - // set cookie - return res - .cookie('token', '', { - httpOnly: false, - secure: EnvVars.NodeEnv === NodeEnvs.Production, - maxAge: 0, - sameSite: 'strict', - }) - .status(HttpStatusCodes.OK) - .json({ - message: 'Logout successful', - }); -}); +AuthRouter.delete(Paths.Auth.Logout, validateSession, authLogout); export default AuthRouter; diff --git a/src/util/sessionManager.ts b/src/util/sessionManager.ts index c2673dc..d396216 100644 --- a/src/util/sessionManager.ts +++ b/src/util/sessionManager.ts @@ -51,6 +51,50 @@ export function saveSession(res: Response, token: string): boolean { } } +export async function deleteSession(token: string): Promise { + // get database + const db = new DatabaseDriver(); + + // delete session + const session = await db.getWhere<{ id: bigint }>('sessions', 'token', token); + + if (!session) { + return false; + } + + await db.delete('sessions', session.id); + + return true; +} + +export async function deleteClearSession( + res: Response, + token: string, +): Promise { + // delete session + const deleted = await deleteSession(token); + + if (!deleted) { + res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + error: 'Internal server error', + }); + return false; + } + + // clear previous cookie header + res.header('Set-Cookie', ''); + + // set cookie + res.cookie('token', '', { + httpOnly: false, + secure: EnvVars.NodeEnv === NodeEnvs.Production, + maxAge: 0, + sameSite: 'strict', + }); + + return true; +} + export async function createSaveSession( res: Response, userId: bigint, From d00c4c9d528ca91d8f0704f30f84aee84af0fb38 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 15:54:45 +0300 Subject: [PATCH 013/118] Update package.json & package-lock.json to reflect version upgrades and package removals. --- package-lock.json | 3792 ++++------------------------- package.json | 24 +- src/controllers/authController.ts | 2 +- 3 files changed, 548 insertions(+), 3270 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f0868e..86ba093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "navigo-learn-api", "version": "0.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -12,7 +12,7 @@ "axios": "^1.4.0", "bcrypt": "^5.1.0", "cookie-parser": "^1.4.6", - "dotenv": "^16.0.3", + "dotenv": "^16.3.1", "express": "^4.18.2", "express-async-errors": "^3.1.1", "helmet": "^7.0.0", @@ -20,11 +20,11 @@ "jet-logger": "^1.3.1", "jet-validator": "^1.1.1", "jsonfile": "^6.1.0", - "jsonwebtoken": "^9.0.0", - "mariadb": "^3.1.2", - "module-alias": "^2.2.2", + "jsonwebtoken": "^9.0.1", + "mariadb": "^3.2.0", + "module-alias": "^2.2.3", "morgan": "^1.10.0", - "ts-command-line-args": "^2.5.0" + "ts-command-line-args": "^2.5.1" }, "devDependencies": { "@types/bcrypt": "^5.0.0", @@ -32,21 +32,21 @@ "@types/express": "^4.17.17", "@types/find": "^0.2.1", "@types/fs-extra": "^11.0.1", - "@types/jasmine": "^4.3.1", + "@types/jasmine": "^4.3.5", "@types/jsonfile": "^6.1.1", "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", - "@types/node": "^20.1.2", + "@types/node": "^20.4.2", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "eslint": "^8.40.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.45.0", "eslint-plugin-node": "^11.1.0", "find": "^0.3.0", "fs-extra": "^11.1.1", - "jasmine": "^4.6.0", + "jasmine": "^5.0.2", "nodemon": "^3.0.1", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", @@ -102,14 +102,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -125,18 +125,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -166,6 +166,73 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -192,9 +259,9 @@ } }, "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -210,28 +277,6 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@morgan-stanley/ts-mocking-bird": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@morgan-stanley/ts-mocking-bird/-/ts-mocking-bird-0.6.4.tgz", - "integrity": "sha512-57VJIflP8eR2xXa9cD1LUawh+Gh+BVQfVu0n6GALyg/AqV/Nz25kDRvws3i9kIe1PTrbsZZOYpsYp6bXPd6nVA==", - "dependencies": { - "lodash": "^4.17.16", - "uuid": "^7.0.3" - }, - "peerDependencies": { - "jasmine": "2.x || 3.x || 4.x", - "jest": "26.x || 27.x || 28.x", - "typescript": ">=4.2" - }, - "peerDependenciesMeta": { - "jasmine": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -267,6 +312,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -286,9 +341,9 @@ "dev": true }, "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, "node_modules/@types/bcrypt": { @@ -347,9 +402,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.34", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", - "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", "dev": true, "dependencies": { "@types/node": "*", @@ -379,16 +434,22 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, "node_modules/@types/jasmine": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", - "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.5.tgz", + "integrity": "sha512-9YHUdvuNDDRJYXZwHqSsO72Ok0vmqoJbNn73ttyITQp/VA60SarnZ+MPLD37rJAhVoKp+9BWOvJP5tHIRfZylQ==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/jsonfile": { @@ -425,9 +486,9 @@ } }, "node_modules/@types/node": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.2.tgz", - "integrity": "sha512-CTO/wa8x+rZU626cL2BlbCDzydgnFNgc19h4YvizpTO88MFQxab8wqisxaofQJ/9bLGugRdWIuX/TbIs6VVF6g==", + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", "dev": true }, "node_modules/@types/qs": { @@ -459,19 +520,20 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } }, "node_modules/@types/superagent": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.17.tgz", - "integrity": "sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.18.tgz", + "integrity": "sha512-LOWgpacIV8GHhrsQU+QMZuomfqXiqzz3ILLkCtKx3Us6AmomFViuzKT9D693QTKgyut2oCytMG8/efOop+DB+w==", "dev": true, "dependencies": { "@types/cookiejar": "*", @@ -488,32 +550,35 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", - "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.0.0.tgz", + "integrity": "sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", + "@eslint-community/regexpp": "^4.5.0", + "@typescript-eslint/scope-manager": "6.0.0", + "@typescript-eslint/type-utils": "6.0.0", + "@typescript-eslint/utils": "6.0.0", + "@typescript-eslint/visitor-keys": "6.0.0", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.0", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -522,25 +587,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", - "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.0.0.tgz", + "integrity": "sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/scope-manager": "6.0.0", + "@typescript-eslint/types": "6.0.0", + "@typescript-eslint/typescript-estree": "6.0.0", + "@typescript-eslint/visitor-keys": "6.0.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -549,16 +615,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", - "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.0.0.tgz", + "integrity": "sha512-o4q0KHlgCZTqjuaZ25nw5W57NeykZT9LiMEG4do/ovwvOcPnDO1BI5BQdCsUkjxFyrCL0cSzLjvIMfR9uo7cWg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" + "@typescript-eslint/types": "6.0.0", + "@typescript-eslint/visitor-keys": "6.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -566,25 +632,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", - "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.0.0.tgz", + "integrity": "sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", + "@typescript-eslint/typescript-estree": "6.0.0", + "@typescript-eslint/utils": "6.0.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -593,12 +659,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", - "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.0.0.tgz", + "integrity": "sha512-Zk9KDggyZM6tj0AJWYYKgF0yQyrcnievdhG0g5FqyU3Y2DRxJn4yWY21sJC0QKBckbsdKKjYDV2yVrrEvuTgxg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -606,21 +672,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", - "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.0.0.tgz", + "integrity": "sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", + "@typescript-eslint/types": "6.0.0", + "@typescript-eslint/visitor-keys": "6.0.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.0", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -633,42 +699,42 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", - "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.0.0.tgz", + "integrity": "sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", + "@eslint-community/eslint-utils": "^4.3.0", + "@types/json-schema": "^7.0.11", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/scope-manager": "6.0.0", + "@typescript-eslint/types": "6.0.0", + "@typescript-eslint/typescript-estree": "6.0.0", "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "semver": "^7.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", - "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.0.0.tgz", + "integrity": "sha512-cvJ63l8c0yXdeT5POHpL0Q1cZoRcmRKFCtSjNGJxPkcP571EfZMcNbzWAc7oK3D1dRzm/V5EwtkANTZxqvuuUA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.0.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -693,9 +759,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1409,13 +1475,22 @@ } }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1460,16 +1535,16 @@ } }, "node_modules/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -1480,7 +1555,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1488,22 +1563,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -1614,9 +1686,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -1639,12 +1711,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -1800,9 +1872,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -1982,6 +2054,34 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2106,12 +2206,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -2196,6 +2297,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2215,6 +2322,17 @@ "node": ">=8" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -2366,9 +2484,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -2430,24 +2548,88 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jackspeak": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", + "integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jasmine": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.6.0.tgz", - "integrity": "sha512-iq7HQ5M8ydNUspjd9vbFW9Lu+6lQ1QLDIqjl0WysEllF5EJZy8XaUyNlhCJVwOx2YFzqTtARWbS56F/f0PzRFw==", - "devOptional": true, + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.0.2.tgz", + "integrity": "sha512-fXgPcWfDhENJJVktFZc/JJ+TpdOQIMJTbn6BgSOIneBagrHtKvnyA8Ag6uD8eF2m7cSESG7K/Hfj/Hk5asAwNg==", + "dev": true, "dependencies": { - "glob": "^7.1.6", - "jasmine-core": "^4.6.0" + "glob": "^10.2.2", + "jasmine-core": "~5.0.1" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", - "devOptional": true + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.0.1.tgz", + "integrity": "sha512-D4bRej8CplwNtNGyTPD++cafJlZUphzZNV+MSAnbD3er4D0NjL4x9V+mu/SI+5129utnCBen23JwEuBZA9vlpQ==", + "dev": true + }, + "node_modules/jasmine/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jasmine/node_modules/glob": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jasmine/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/jet-logger": { "version": "1.3.1", @@ -2465,16 +2647,6 @@ "express": "^4.18.2" } }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2523,9 +2695,9 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==", "dependencies": { "jws": "^3.2.2", "lodash": "^4.17.21", @@ -2637,9 +2809,9 @@ "dev": true }, "node_modules/mariadb": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.1.2.tgz", - "integrity": "sha512-ILlC54fkXkvizTJZC1uP7f/REBxuu1k+OWzpiIITIEdS+dGIjFe/Ob3EW9KrdtBa38l3z+odz6elva0RG/y5og==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.0.tgz", + "integrity": "sha512-IH2nidQat1IBMxP5gjuNxG6dADtz1PESEC6rKrcATen5v3ngFyZITjehyYiwNfz3zUNQupfYmVntz93M+Pz8pQ==", "dependencies": { "@types/geojson": "^7946.0.10", "@types/node": "^17.0.45", @@ -2803,9 +2975,9 @@ } }, "node_modules/module-alias": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", - "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, "node_modules/morgan": { "version": "1.10.0", @@ -2877,9 +3049,9 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, "node_modules/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3129,6 +3301,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", + "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -3165,15 +3362,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -3593,6 +3790,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3604,6 +3816,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3725,9 +3950,9 @@ } }, "node_modules/tar": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz", - "integrity": "sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -3804,12 +4029,23 @@ "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", "dev": true }, + "node_modules/ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-command-line-args": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.0.tgz", - "integrity": "sha512-Ff7Xt04WWCjj/cmPO9eWTJX3qpBZWuPWyQYG1vnxJao+alWWYjwJBc5aYz3h5p5dE08A6AnpkgiCtP/0KXXBYw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "dependencies": { - "@morgan-stanley/ts-mocking-bird": "^0.6.2", "chalk": "^4.1.0", "command-line-args": "^5.1.1", "command-line-usage": "^6.1.0", @@ -3876,27 +4112,6 @@ "node": ">=6" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3937,6 +4152,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3997,14 +4213,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4076,3063 +4284,133 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "requires": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - } - }, - "@morgan-stanley/ts-mocking-bird": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@morgan-stanley/ts-mocking-bird/-/ts-mocking-bird-0.6.4.tgz", - "integrity": "sha512-57VJIflP8eR2xXa9cD1LUawh+Gh+BVQfVu0n6GALyg/AqV/Nz25kDRvws3i9kIe1PTrbsZZOYpsYp6bXPd6nVA==", - "requires": { - "lodash": "^4.17.16", - "uuid": "^7.0.3" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, - "@types/bcrypt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", - "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/cookie-parser": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", - "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, - "@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.34", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", - "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "@types/find": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@types/find/-/find-0.2.1.tgz", - "integrity": "sha512-qUrCBlWoo9ij6ZEMx8G2WEjkdpofFks37/eZSDce7e0BY8lCYS2u7dVAoBy6AKRGX8z/ukllrUAyQ8h+/zlW9w==", - "dev": true - }, - "@types/fs-extra": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", - "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", - "dev": true, - "requires": { - "@types/jsonfile": "*", - "@types/node": "*" - } - }, - "@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" - }, - "@types/jasmine": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", - "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "@types/jsonfile": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", - "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "@types/morgan": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.4.tgz", - "integrity": "sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.2.tgz", - "integrity": "sha512-CTO/wa8x+rZU626cL2BlbCDzydgnFNgc19h4YvizpTO88MFQxab8wqisxaofQJ/9bLGugRdWIuX/TbIs6VVF6g==", - "dev": true - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", - "dev": true - }, - "@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", - "dev": true, - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "@types/superagent": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.17.tgz", - "integrity": "sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w==", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "@types/supertest": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", - "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", - "dev": true, - "requires": { - "@types/superagent": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", - "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", - "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", - "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", - "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", - "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", - "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", - "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", - "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "bcrypt": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", - "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.10", - "node-addon-api": "^5.0.0" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", - "requires": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - } - }, - "command-line-usage": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", - "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", - "requires": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" - } - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, - "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "requires": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - } - }, - "eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "requires": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", - "dev": true - }, - "espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "express-async-errors": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", - "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", - "requires": {} - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "find": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", - "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", - "dev": true, - "requires": { - "traverse-chain": "~0.1.0" - } - }, - "find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "requires": { - "array-back": "^3.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "dev": true, - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - } - }, - "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "helmet": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.0.0.tgz", - "integrity": "sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==" - }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "inserturlparams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/inserturlparams/-/inserturlparams-1.0.1.tgz", - "integrity": "sha512-63noT2JShi5jH4+UZxlUr2vWQ5ARSkJ1t651WZjWFWLoh9qiPaY9KOGN/HqOeNmaHJog508IjDa0hJ6Wl86bWg==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "jasmine": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.6.0.tgz", - "integrity": "sha512-iq7HQ5M8ydNUspjd9vbFW9Lu+6lQ1QLDIqjl0WysEllF5EJZy8XaUyNlhCJVwOx2YFzqTtARWbS56F/f0PzRFw==", - "devOptional": true, - "requires": { - "glob": "^7.1.6", - "jasmine-core": "^4.6.0" - } - }, - "jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", - "devOptional": true - }, - "jet-logger": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jet-logger/-/jet-logger-1.3.1.tgz", - "integrity": "sha512-BSsTm88Y7a+XtXKpZM71qm0ulH+bNI13rR+BzeQStfjpE/6n3fX3FZpKF/WZh52h1e6gEAOjuFlkmdzGBQnwPg==", - "requires": { - "colors": "1.3.0" - } - }, - "jet-validator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jet-validator/-/jet-validator-1.1.1.tgz", - "integrity": "sha512-F/L+UtXAkd5Dm+eIRca0BgCb5/sYenHqixHOt2tWYkMFd8AUDqA6U8dD3mQzXL+SVxCVOd5Ftcf9c9HGbUIhlw==", - "requires": { - "express": "^4.18.2" - } - }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "mariadb": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.1.2.tgz", - "integrity": "sha512-ILlC54fkXkvizTJZC1uP7f/REBxuu1k+OWzpiIITIEdS+dGIjFe/Ob3EW9KrdtBa38l3z+odz6elva0RG/y5og==", - "requires": { - "@types/geojson": "^7946.0.10", - "@types/node": "^17.0.45", - "denque": "^2.1.0", - "iconv-lite": "^0.6.3", - "lru-cache": "^7.14.0" - }, - "dependencies": { - "@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "module-alias": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", - "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" - }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "requires": { - "ee-first": "1.1.1" - } - } - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, - "node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "nodemon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", - "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", - "dev": true, - "requires": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "requires": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "reduce-flatten": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", - "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==" - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "requires": { - "semver": "^7.5.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", - "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, "dependencies": { - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true - } - } - }, - "supertest": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", - "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^8.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "table-layout": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", - "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", - "requires": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "dependencies": { - "array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" - }, - "typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" - } - } - }, - "tar": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz", - "integrity": "sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" + "engines": { + "node": ">=10" }, - "dependencies": { - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "requires": { - "abbrev": "1" - } - } - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", - "dev": true - }, - "ts-command-line-args": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.0.tgz", - "integrity": "sha512-Ff7Xt04WWCjj/cmPO9eWTJX3qpBZWuPWyQYG1vnxJao+alWWYjwJBc5aYz3h5p5dE08A6AnpkgiCtP/0KXXBYw==", - "requires": { - "@morgan-stanley/ts-mocking-bird": "^0.6.2", - "chalk": "^4.1.0", - "command-line-args": "^5.1.1", - "command-line-usage": "^6.1.0", - "string-format": "^2.0.0" - } - }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, - "tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "requires": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "requires": { - "tslib": "^1.8.1" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" - }, - "typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" - }, - "undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "wordwrapjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", - "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", - "requires": { - "reduce-flatten": "^2.0.0", - "typical": "^5.2.0" - }, "dependencies": { - "typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" - } + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "yn": { + "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 1a7e654..a68ee46 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "axios": "^1.4.0", "bcrypt": "^5.1.0", "cookie-parser": "^1.4.6", - "dotenv": "^16.0.3", + "dotenv": "^16.3.1", "express": "^4.18.2", "express-async-errors": "^3.1.1", "helmet": "^7.0.0", @@ -42,11 +42,11 @@ "jet-logger": "^1.3.1", "jet-validator": "^1.1.1", "jsonfile": "^6.1.0", - "jsonwebtoken": "^9.0.0", - "mariadb": "^3.1.2", - "module-alias": "^2.2.2", + "jsonwebtoken": "^9.0.1", + "mariadb": "^3.2.0", + "module-alias": "^2.2.3", "morgan": "^1.10.0", - "ts-command-line-args": "^2.5.0" + "ts-command-line-args": "^2.5.1" }, "devDependencies": { "@types/bcrypt": "^5.0.0", @@ -54,21 +54,21 @@ "@types/express": "^4.17.17", "@types/find": "^0.2.1", "@types/fs-extra": "^11.0.1", - "@types/jasmine": "^4.3.1", + "@types/jasmine": "^4.3.5", "@types/jsonfile": "^6.1.1", "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", - "@types/node": "^20.1.2", + "@types/node": "^20.4.2", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "eslint": "^8.40.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.45.0", "eslint-plugin-node": "^11.1.0", "find": "^0.3.0", "fs-extra": "^11.1.1", - "jasmine": "^4.6.0", + "jasmine": "^5.0.2", "nodemon": "^3.0.1", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index f53fd2e..d0b04e6 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -11,7 +11,7 @@ import { import { UserInfo } from '@src/models/UserInfo'; import { checkEmail } from '@src/util/EmailUtil'; import EnvVars from '@src/constants/EnvVars'; -import axios from 'axios/index'; +import axios from 'axios'; import logger from 'jet-logger'; import { RequestWithSession } from '@src/middleware/session'; From 7b396e2489a86a4ba5c9068b692b83b716c56aa5 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 16:12:16 +0300 Subject: [PATCH 014/118] Update validation process and error handling for auth registration to make it consistent with previous behaviour. --- spec/tests/authrouter.spec.ts | 4 ++-- src/controllers/authController.ts | 23 ++++++++++++----------- src/routes/AuthRouter.ts | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/spec/tests/authrouter.spec.ts b/spec/tests/authrouter.spec.ts index 55ccb5e..12ac00b 100644 --- a/spec/tests/authrouter.spec.ts +++ b/spec/tests/authrouter.spec.ts @@ -102,7 +102,7 @@ describe('Login Router', () => { // login and expect 403 forbidden await request(app) .get('/api/auth/google-callback') - .expect(HttpStatusCodes.FORBIDDEN); + .expect(HttpStatusCodes.BAD_REQUEST); }); // google callback test with invalid code @@ -130,7 +130,7 @@ describe('Login Router', () => { // login and expect 403 forbidden await request(app) .get('/api/auth/github-callback') - .expect(HttpStatusCodes.FORBIDDEN); + .expect(HttpStatusCodes.BAD_REQUEST); }); // GitHub callback test with invalid code diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index d0b04e6..5e839da 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -137,7 +137,7 @@ async function getUserByEmail( } function _handleNotOkay(res: Response, error: number): unknown { - logger.err(error, true); + if (EnvVars.NodeEnv !== 'test') logger.err(error, true); if (error >= HttpStatusCode.InternalServerError) return res.status(HttpStatusCode.BadGateway).json({ @@ -194,13 +194,9 @@ export async function authRegister( req: RequestWithBody, res: Response, ): Promise { - const { email, password, name } = req.body; + const { email, password } = req.body; - if ( - typeof password !== 'string' || - typeof email !== 'string' || - typeof name !== 'string' - ) + if (typeof password !== 'string' || typeof email !== 'string') return _invalidBody(res); // get database @@ -213,13 +209,18 @@ export async function authRegister( if (!checkEmail(email) || password.length < 8) return _invalidBody(res); // create user - const userId = await insertUser(db, email, name, saltPassword(password)); + const userId = await insertUser( + db, + email, + email.split('@')[0], + saltPassword(password), + ); if (userId === -1n) return _serverError(res); // create session and save it if (await createSaveSession(res, BigInt(userId))) return res - .status(HttpStatusCode.Ok) + .status(HttpStatusCode.Created) .json({ message: 'Registration successful', success: true }); } @@ -351,7 +352,7 @@ export async function authGoogleCallback( .status(HttpStatusCode.Ok) .json({ message: 'Registration successful', success: true }); } catch (e) { - logger.err(e, true); + if (EnvVars.NodeEnv !== 'test') logger.err(e, true); return _serverError(res); } } @@ -506,7 +507,7 @@ export async function authGitHubCallback( } } } catch (e) { - logger.err(e, true); + if (EnvVars.NodeEnv !== 'test') logger.err(e, true); return _serverError(res); } } diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index f50b1bb..28b6c6d 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -20,7 +20,7 @@ AuthRouter.post(Paths.Auth.Login, validateBody('email', 'password'), authLogin); AuthRouter.post( Paths.Auth.Register, - validateBody('email', 'password', 'name'), + validateBody('email', 'password'), authRegister, ); From c84cf61390a1167c05840152ccbaba05ac79ca03 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:18:17 +0300 Subject: [PATCH 015/118] Fixes #43 --- src/server.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/server.ts b/src/server.ts index 2228f87..4c62810 100644 --- a/src/server.ts +++ b/src/server.ts @@ -28,9 +28,11 @@ const app = express(); // **** Setup **** // // Basic middleware -app.use(express.json({ - limit: '10mb' -})); +app.use( + express.json({ + limit: '10mb', + }), +); app.use(express.urlencoded({ extended: true })); app.use(cookieParser(EnvVars.CookieProps.Secret)); @@ -65,20 +67,14 @@ app.use((_: Request, res: Response) => { }); // Add error handler -app.use( - ( - err: Error, - _: Request, - res: Response, - ) => { - logger.err(err, true); - let status = HttpStatusCodes.INTERNAL_SERVER_ERROR; - if (err instanceof RouteError) { - status = err.status; - } - return res.status(status).json({ error: err.message }); - }, -); +app.use((err: Error, _: Request, res: Response) => { + logger.err(err, true); + let status = HttpStatusCodes.INTERNAL_SERVER_ERROR; + if (err instanceof RouteError) { + status = err.status; + } + return res.status(status).json({ error: err.message }); +}); // ** Front-End Content ** // From 00e1d69637cf653c246018248be76ddbe40dff2a Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:22:07 +0300 Subject: [PATCH 016/118] Enforce start of string in OAuth URL regex tests. Fixes #44 --- spec/tests/authrouter.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/tests/authrouter.spec.ts b/spec/tests/authrouter.spec.ts index 12ac00b..5ce40fd 100644 --- a/spec/tests/authrouter.spec.ts +++ b/spec/tests/authrouter.spec.ts @@ -93,7 +93,7 @@ describe('Login Router', () => { .expect(HttpStatusCodes.FOUND) .expect( 'Location', - new RegExp('https:\\/\\/accounts.google.com\\/o\\/oauth2.+'), + new RegExp('^https:\\/\\/accounts.google.com\\/o\\/oauth2.+'), ); }); @@ -121,7 +121,7 @@ describe('Login Router', () => { .expect(HttpStatusCodes.FOUND) .expect( 'Location', - new RegExp('https:\\/\\/github.com\\/login\\/oauth.+'), + new RegExp('^https:\\/\\/github\\.com\\/login\\/oauth.+'), ); }); From 62134618ccc181dd845f3d6f2e502046e994530d Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:24:38 +0300 Subject: [PATCH 017/118] Remove console.log from RoadmapsTabsInfo Fixes #45 --- src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts index 96d7fea..e36a87b 100644 --- a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts +++ b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts @@ -1,14 +1,12 @@ import { Router } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Paths from '@src/constants/Paths'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import { ITabInfo, TabInfo } from '@src/models/TabInfo'; import Database from '@src/util/DatabaseDriver'; import * as console from 'console'; import { IRoadmap } from '@src/models/Roadmap'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const RoadmapTabsInfo = Router({ mergeParams: true }); @@ -71,9 +69,10 @@ RoadmapTabsInfo.get(Paths.Roadmaps.TabsInfo.Get, async (req, res) => { .json({ error: 'RoadmapId is invalid.' }); } - if (!stringId) return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'TabID not found.' }); + if (!stringId) + return res + .status(HttpStatusCodes.BAD_REQUEST) + .json({ error: 'TabID not found.' }); // get database connection const db = new Database(); @@ -87,8 +86,10 @@ RoadmapTabsInfo.get(Paths.Roadmaps.TabsInfo.Get, async (req, res) => { roadmapId, ); - if (!tabData) return res.status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'TabInfo not found.' }); + if (!tabData) + return res + .status(HttpStatusCodes.NOT_FOUND) + .json({ error: 'TabInfo not found.' }); const result = { id: tabData?.id.toString(), @@ -102,31 +103,28 @@ RoadmapTabsInfo.get(Paths.Roadmaps.TabsInfo.Get, async (req, res) => { }); RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, validateSession); -RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, - async (req: RequestWithSession, - res) => { +RoadmapTabsInfo.post( + Paths.Roadmaps.TabsInfo.Update, + async (req: RequestWithSession, res) => { const stringId = req.params?.tabInfoId; const roadmapId = BigInt(req.params?.roadmapId || -1); - if (roadmapId < 0) return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'RoadmapId is invalid.' }); + if (roadmapId < 0) + return res + .status(HttpStatusCodes.BAD_REQUEST) + .json({ error: 'RoadmapId is invalid.' }); - if (!stringId) return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'TabID not found.' }); + if (!stringId) + return res + .status(HttpStatusCodes.BAD_REQUEST) + .json({ error: 'TabID not found.' }); // get database connection const db = new Database(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const sentTabData = req.body?.tabInfo as ITabInfo; const newContent = sentTabData.content; - console.log('new content', newContent); - const roadmapReq = db.getWhere( - 'roadmaps', - 'id', - roadmapId, - ); + const roadmapReq = db.getWhere('roadmaps', 'id', roadmapId); // gets previous data from the table const tabDataReq = db.getWhere( 'tabsInfo', @@ -138,30 +136,35 @@ RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, const roadmap = await roadmapReq; - if (!roadmap) return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap not found.' }); + if (!roadmap) + return res + .status(HttpStatusCodes.NOT_FOUND) + .json({ error: 'Roadmap not found.' }); - if (roadmap.ownerId !== req.session?.userId) return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'You don\'t have permission to edit this roadmap.' }); + if (roadmap.ownerId !== req.session?.userId) + return res + .status(HttpStatusCodes.FORBIDDEN) + .json({ error: "You don't have permission to edit this roadmap." }); const tabData = await tabDataReq; - if (!tabData) return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue not found.' }); + if (!tabData) + return res + .status(HttpStatusCodes.NOT_FOUND) + .json({ error: 'Issue not found.' }); tabData.content = newContent; const success = await db.update('tabsInfo', tabData.id, tabData); - if (!success) return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); + if (!success) + return res + .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) + .json({ error: 'Issue could not be saved to database.' }); // return success return res.status(HttpStatusCodes.OK).json({ success: true }); - }); + }, +); export default RoadmapTabsInfo; From 110b2e53dcb736e267cd75edb1f1cba1742321a6 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:42:33 +0300 Subject: [PATCH 018/118] Rate limiting the ammoun tof auth calls you can make --- package-lock.json | 23 +++++++++++++++++++++++ package.json | 2 ++ spec/tests/authrouter.spec.ts | 2 +- src/routes/AuthRouter.ts | 35 +++++++++++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86ba093..b9f37c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "express-async-errors": "^3.1.1", + "express-rate-limit": "^6.7.1", "helmet": "^7.0.0", "inserturlparams": "^1.0.1", "jet-logger": "^1.3.1", @@ -30,6 +31,7 @@ "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", + "@types/express-rate-limit": "^6.0.0", "@types/find": "^0.2.1", "@types/fs-extra": "^11.0.1", "@types/jasmine": "^4.3.5", @@ -401,6 +403,16 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-rate-limit": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/express-rate-limit/-/express-rate-limit-6.0.0.tgz", + "integrity": "sha512-nZxo3nwU20EkTl/f2eGdndQkDIJYwkXIX4S3Vrp2jMdSdFJ6AWtIda8gOz0wiMuOFoeH/UUlCAiacz3x3eWNFA==", + "deprecated": "This is a stub types definition. express-rate-limit provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "express-rate-limit": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.35", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", @@ -1844,6 +1856,17 @@ "express": "^4.16.2" } }, + "node_modules/express-rate-limit": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.1.tgz", + "integrity": "sha512-eH4VgI64Nowd2vC5Xylx0lLYovWIp2gRFtTklWDbhSDydGAPQUjvr1B7aQ2/ZADrAi6bJ51qSizKIXWAZ1WCQw==", + "engines": { + "node": ">= 14.0.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", diff --git a/package.json b/package.json index a68ee46..cd9e4a7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "express-async-errors": "^3.1.1", + "express-rate-limit": "^6.7.1", "helmet": "^7.0.0", "inserturlparams": "^1.0.1", "jet-logger": "^1.3.1", @@ -52,6 +53,7 @@ "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", + "@types/express-rate-limit": "^6.0.0", "@types/find": "^0.2.1", "@types/fs-extra": "^11.0.1", "@types/jasmine": "^4.3.5", diff --git a/spec/tests/authrouter.spec.ts b/spec/tests/authrouter.spec.ts index 5ce40fd..150b826 100644 --- a/spec/tests/authrouter.spec.ts +++ b/spec/tests/authrouter.spec.ts @@ -93,7 +93,7 @@ describe('Login Router', () => { .expect(HttpStatusCodes.FOUND) .expect( 'Location', - new RegExp('^https:\\/\\/accounts.google.com\\/o\\/oauth2.+'), + new RegExp('^https:\\/\\/accounts\\.google\\.com\\/o\\/oauth2.+'), ); }); diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 28b6c6d..c4caf84 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -13,14 +13,36 @@ import { authRegister, } from '@src/controllers/authController'; import validateBody from '@src/validators/validateBody'; +import { rateLimit } from 'express-rate-limit'; const AuthRouter = Router(); +const LoginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // limit each IP to 100 requests per windowMs + message: 'Too many login attempts, please try again later.', + }), + RegisterLimiter = rateLimit({ + windowMs: 360 * 1000, // 1 hour + max: 5, // limit each IP to 100 requests per windowMs + message: 'Too many register attempts, please try again later.', + }), + ResetPasswordLimiter = rateLimit({ + windowMs: 360 * 1000, // 1 hour + max: 10, // limit each IP to 100 requests per windowMs + message: 'Too many reset password attempts, please try again later.', + }); -AuthRouter.post(Paths.Auth.Login, validateBody('email', 'password'), authLogin); +AuthRouter.post( + Paths.Auth.Login, + LoginLimiter, + validateBody('email', 'password'), + authLogin, +); AuthRouter.post( Paths.Auth.Register, validateBody('email', 'password'), + RegisterLimiter, authRegister, ); @@ -28,15 +50,20 @@ AuthRouter.post( Paths.Auth.ChangePassword, validateSession, validateBody('password', 'newPassword'), + ResetPasswordLimiter, authChangePassword, ); -AuthRouter.post(Paths.Auth.ForgotPassword, authForgotPassword); +AuthRouter.post( + Paths.Auth.ForgotPassword, + ResetPasswordLimiter, + authForgotPassword, +); -AuthRouter.get(Paths.Auth.GoogleLogin, authGoogle); +AuthRouter.get(Paths.Auth.GoogleLogin, LoginLimiter, authGoogle); AuthRouter.get(Paths.Auth.GoogleCallback, authGoogleCallback); -AuthRouter.get(Paths.Auth.GithubLogin, authGitHub); +AuthRouter.get(Paths.Auth.GithubLogin, LoginLimiter, authGitHub); AuthRouter.get(Paths.Auth.GithubCallback, authGitHubCallback); AuthRouter.delete(Paths.Auth.Logout, validateSession, authLogout); From 153fdc2a29912079b61be4550051738b05a15b4f Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:47:39 +0300 Subject: [PATCH 019/118] Fixing rate limitng in tests --- src/routes/AuthRouter.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index c4caf84..483e568 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -14,21 +14,22 @@ import { } from '@src/controllers/authController'; import validateBody from '@src/validators/validateBody'; import { rateLimit } from 'express-rate-limit'; +import EnvVars from '@src/constants/EnvVars'; const AuthRouter = Router(); const LoginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: 10, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, // limit each IP to 100 requests per windowMs message: 'Too many login attempts, please try again later.', }), RegisterLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: 5, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 5 : 99999, // limit each IP to 100 requests per windowMs message: 'Too many register attempts, please try again later.', }), ResetPasswordLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: 10, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, // limit each IP to 100 requests per windowMs message: 'Too many reset password attempts, please try again later.', }); From 4b03bdc6452d335c14cf30775d0c14b1738a65f5 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 17:50:59 +0300 Subject: [PATCH 020/118] Rate limiting as first middleware --- src/routes/AuthRouter.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 483e568..aa880e7 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -19,17 +19,17 @@ import EnvVars from '@src/constants/EnvVars'; const AuthRouter = Router(); const LoginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, message: 'Too many login attempts, please try again later.', }), RegisterLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv !== 'test' ? 5 : 99999, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 5 : 99999, message: 'Too many register attempts, please try again later.', }), ResetPasswordLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, // limit each IP to 100 requests per windowMs + max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, message: 'Too many reset password attempts, please try again later.', }); @@ -42,16 +42,16 @@ AuthRouter.post( AuthRouter.post( Paths.Auth.Register, - validateBody('email', 'password'), RegisterLimiter, + validateBody('email', 'password'), authRegister, ); AuthRouter.post( Paths.Auth.ChangePassword, + ResetPasswordLimiter, validateSession, validateBody('password', 'newPassword'), - ResetPasswordLimiter, authChangePassword, ); From 3b6ad5a27e719babe6ed0c1c52187338be44d9e7 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 18:18:20 +0300 Subject: [PATCH 021/118] Updated envs --- env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env b/env index fcef617..f026a8b 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit fcef6176b5f4acfd07b0531d59d344668076f003 +Subproject commit f026a8b26ee7834e2b0e80f4519d9d1ec6bd10d3 From db2f8ea5378eb7d321ecd8d9af4a9a9474132aa6 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 18:18:20 +0300 Subject: [PATCH 022/118] Updated envs --- env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env b/env index fcef617..f026a8b 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit fcef6176b5f4acfd07b0531d59d344668076f003 +Subproject commit f026a8b26ee7834e2b0e80f4519d9d1ec6bd10d3 From eff6472ee0f60aef7441d6ef899d0afebe8e5192 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 23:08:30 +0300 Subject: [PATCH 023/118] Moved general purpose code for database querying and API responses into separate files. --- src/controllers/authController.ts | 247 ++++++------------ src/controllers/helpers/apiResponses.ts | 83 ++++++ src/controllers/helpers/databaseManagement.ts | 73 ++++++ 3 files changed, 236 insertions(+), 167 deletions(-) create mode 100644 src/controllers/helpers/apiResponses.ts create mode 100644 src/controllers/helpers/databaseManagement.ts diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 5e839da..6b7115a 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -2,7 +2,7 @@ import { RequestWithBody } from '@src/validators/validateBody'; import { Response } from 'express'; import DatabaseDriver from '@src/util/DatabaseDriver'; import User from '@src/models/User'; -import { HttpStatusCode } from 'axios'; +import axios, { HttpStatusCode } from 'axios'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; import { createSaveSession, @@ -11,9 +11,30 @@ import { import { UserInfo } from '@src/models/UserInfo'; import { checkEmail } from '@src/util/EmailUtil'; import EnvVars from '@src/constants/EnvVars'; -import axios from 'axios'; import logger from 'jet-logger'; import { RequestWithSession } from '@src/middleware/session'; +import { + getUser, + getUserByEmail, + getUserInfo, + insertUser, + insertUserInfo, + updateUser, + updateUserInfo, +} from '@src/controllers/helpers/databaseManagement'; +import { + accountCreated, + emailConflict, + externalBadGateway, + invalidBody, + invalidLogin, + loginSuccessful, + logoutSuccessful, + notImplemented, + passwordChanged, + serverError, + unauthorized, +} from '@src/controllers/helpers/apiResponses'; /* * Interfaces @@ -38,120 +59,16 @@ interface GitHubUserData { /* * Helpers */ -function _invalidLogin(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - error: 'Invalid email or password', - success: false, - }); -} - -function _invalidBody(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - error: 'Invalid request body', - success: false, - }); -} - -function _conflict(res: Response): void { - res.status(HttpStatusCode.Conflict).json({ - error: 'Email already in use', - success: false, - }); -} - -function _serverError(res: Response): void { - res.status(HttpStatusCode.InternalServerError).json({ - error: 'Internal server error', - success: false, - }); -} - -async function insertUser( - db: DatabaseDriver, - email: string, - name: string, - pwdHash: string, - userInfo?: UserInfo, -): Promise { - const userId = await db.insert('users', { - email, - name, - pwdHash, - }); - - if (await insertUserInfo(db, userId, userInfo)) { - return userId; - } else { - return -1n; - } -} - -async function insertUserInfo( - db: DatabaseDriver, - userId: bigint, - userInfo?: UserInfo, -): Promise { - if (!userInfo) userInfo = new UserInfo(userId); - userInfo.userId = userId; - return (await db.insert('userInfo', userInfo)) >= 0; -} - -async function updateUser( - db: DatabaseDriver, - userId: bigint, - user: User, - userInfo?: UserInfo, -): Promise { - if (userInfo) if (!(await updateUserInfo(db, userId, userInfo))) return false; - - return await db.update('users', userId, user); -} - -async function updateUserInfo( - db: DatabaseDriver, - userId: bigint, - userInfo: UserInfo, -): Promise { - return await db.update('userInfo', userId, userInfo); -} - -async function getUserInfo( - db: DatabaseDriver, - userId: bigint, -): Promise { - return await db.get('userInfo', userId); -} - -async function getUser( - db: DatabaseDriver, - userId: bigint, -): Promise { - return await db.get('users', userId); -} - -async function getUserByEmail( - db: DatabaseDriver, - email: string, -): Promise { - return await db.getWhere('users', 'email', email); -} - -function _handleNotOkay(res: Response, error: number): unknown { +export function _handleNotOkay(res: Response, error: number): unknown { if (EnvVars.NodeEnv !== 'test') logger.err(error, true); - if (error >= HttpStatusCode.InternalServerError) - return res.status(HttpStatusCode.BadGateway).json({ - error: 'Remote resource error', - success: false, - }); + if (error >= (HttpStatusCode.InternalServerError as number)) + return externalBadGateway(res); - if (error === HttpStatusCode.Unauthorized) - return res.status(HttpStatusCode.Unauthorized).json({ - error: 'Unauthorized', - success: false, - }); + if (error === (HttpStatusCode.Unauthorized as number)) + return unauthorized(res); - return _serverError(res); + return serverError(res); } /* @@ -164,30 +81,29 @@ export async function authLogin( const { email, password } = req.body; if (typeof email !== 'string' || typeof password !== 'string') - return _invalidLogin(res); + return invalidLogin(res); // get database const db = new DatabaseDriver(); // check if user exists const user = await getUserByEmail(db, email); - if (!user) return _invalidLogin(res); + if (!user) return invalidLogin(res); // check if password is correct const isCorrect = comparePassword(password, user.pwdHash || ''); - if (!isCorrect) return _invalidLogin(res); + if (!isCorrect) return invalidLogin(res); // check userInfo table for user const userInfo = await getUserInfo(db, user.id); if (!userInfo) if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) - return _serverError(res); + return serverError(res); // create session and save it - if (await createSaveSession(res, user.id)) - return res - .status(HttpStatusCode.Ok) - .json({ message: 'Login successful', success: true }); + if (await createSaveSession(res, user.id)) return loginSuccessful(res); + + return serverError(res); } export async function authRegister( @@ -197,16 +113,16 @@ export async function authRegister( const { email, password } = req.body; if (typeof password !== 'string' || typeof email !== 'string') - return _invalidBody(res); + return invalidBody(res); // get database const db = new DatabaseDriver(); // check if user exists const user = await getUserByEmail(db, email); - if (!!user) return _conflict(res); + if (!!user) return emailConflict(res); - if (!checkEmail(email) || password.length < 8) return _invalidBody(res); + if (!checkEmail(email) || password.length < 8) return invalidBody(res); // create user const userId = await insertUser( @@ -215,13 +131,12 @@ export async function authRegister( email.split('@')[0], saltPassword(password), ); - if (userId === -1n) return _serverError(res); + if (userId === -1n) return serverError(res); // create session and save it - if (await createSaveSession(res, BigInt(userId))) - return res - .status(HttpStatusCode.Created) - .json({ message: 'Registration successful', success: true }); + if (await createSaveSession(res, BigInt(userId))) return accountCreated(res); + + return serverError(res); } export async function authChangePassword( @@ -231,33 +146,30 @@ export async function authChangePassword( const { password, newPassword } = req.body; if (typeof password !== 'string' || typeof newPassword !== 'string') - return _invalidBody(res); + return invalidBody(res); // get database const db = new DatabaseDriver(); // check if user is logged in - if (!req.session?.userId) return _serverError(res); + if (!req.session?.userId) return serverError(res); const userId = req.session.userId; // check if user exists const user = await getUser(db, userId); - if (!user) return _serverError(res); + if (!user) return serverError(res); // check if password is correct const isCorrect = comparePassword(password, user.pwdHash || ''); - if (!isCorrect) return _invalidLogin(res); + if (!isCorrect) return invalidLogin(res); user.pwdHash = saltPassword(newPassword); // update password in ussr const result = await updateUser(db, userId, user); - if (result) - return res - .status(HttpStatusCode.Ok) - .json({ message: 'Password changed', success: true }); - else return _serverError(res); + if (result) return passwordChanged(res); + else return serverError(res); } // eslint-disable-next-line @typescript-eslint/require-await @@ -265,10 +177,8 @@ export async function authForgotPassword( _: unknown, res: Response, ): Promise { - // TODO: implement - return res - .status(HttpStatusCode.NotImplemented) - .json({ error: 'Not implemented', success: false }); + // TODO: implement after SMTP server is set up + return notImplemented(res); } export function authGoogle(_: unknown, res: Response): unknown { @@ -287,7 +197,7 @@ export async function authGoogleCallback( ): Promise { const code = req.query.code; - if (typeof code !== 'string') return _invalidBody(res); + if (typeof code !== 'string') return invalidBody(res); try { // get access token @@ -301,12 +211,12 @@ export async function authGoogleCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return _serverError(res); + if (!response.data) return serverError(res); // get access token from response const data = response.data as { access_token?: string }; const accessToken = data.access_token; - if (!accessToken) return _serverError(res); + if (!accessToken) return serverError(res); // get user info response = await axios.get( @@ -318,7 +228,7 @@ export async function authGoogleCallback( }, ); const userData = response?.data as GoogleUserData; - if (!userData) return _serverError(res); + if (!userData) return serverError(res); // get database const db = new DatabaseDriver(); @@ -335,25 +245,25 @@ export async function authGoogleCallback( user.googleId = userData.id; // update user - if (!(await updateUser(db, user.id, user))) return _serverError(res); + if (!(await updateUser(db, user.id, user))) return serverError(res); // check userInfo table for user const userInfo = await getUserInfo(db, user.id); if (!userInfo) if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) - return _serverError(res); + return serverError(res); } // check if user was created - if (user.id === -1n) return _serverError(res); + if (user.id === -1n) return serverError(res); // create session and save it if (await createSaveSession(res, BigInt(user.id))) - return res - .status(HttpStatusCode.Ok) - .json({ message: 'Registration successful', success: true }); + return loginSuccessful(res); + + return serverError(res); } catch (e) { if (EnvVars.NodeEnv !== 'test') logger.err(e, true); - return _serverError(res); + return serverError(res); } } @@ -373,7 +283,7 @@ export async function authGitHubCallback( ): Promise { const code = req.query.code; - if (typeof code !== 'string') return _invalidBody(res); + if (typeof code !== 'string') return invalidBody(res); try { // get access token @@ -394,12 +304,12 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return _serverError(res); + if (!response.data) return serverError(res); // get access token from response const data = response.data as { access_token?: string }; const accessToken = data.access_token; - if (!accessToken) return _serverError(res); + if (!accessToken) return serverError(res); // get user info from github response = await axios.get('https://api.github.com/user', { @@ -411,11 +321,11 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return _serverError(res); + if (!response.data) return serverError(res); // get user data const userData = response.data as GitHubUserData; - if (!userData) return _serverError(res); + if (!userData) return serverError(res); // if email is not public, get it from github if (userData.email == '') { @@ -442,7 +352,7 @@ export async function authGitHubCallback( } // check if email is valid - if (userData.email == '') return _serverError(res); + if (userData.email == '') return serverError(res); // get database const db = new DatabaseDriver(); @@ -492,7 +402,7 @@ export async function authGitHubCallback( ), )) ) - return _serverError(res); + return serverError(res); } else { if (userInfo.bio == '') userInfo.bio = userData.bio; if (userInfo.profilePictureUrl == '') @@ -503,12 +413,17 @@ export async function authGitHubCallback( // update user info if (!(await updateUserInfo(db, user.id, userInfo))) - return _serverError(res); + return serverError(res); } } + + // create session and save it + if (await createSaveSession(res, user.id)) return loginSuccessful(res); + + return serverError(res); } catch (e) { if (EnvVars.NodeEnv !== 'test') logger.err(e, true); - return _serverError(res); + return serverError(res); } } @@ -516,14 +431,12 @@ export async function authLogout( req: RequestWithSession, res: Response, ): Promise { - if (!req.session) return _serverError(res); + if (!req.session) return serverError(res); - // delete session + // delete session and set cookie to expire if (!(await deleteClearSession(res, req.session.token))) - return _serverError(res); + return serverError(res); // return success - return res - .status(HttpStatusCode.Ok) - .json({ message: 'Logout successful', success: true }); + return logoutSuccessful(res); } diff --git a/src/controllers/helpers/apiResponses.ts b/src/controllers/helpers/apiResponses.ts new file mode 100644 index 0000000..1f4fac2 --- /dev/null +++ b/src/controllers/helpers/apiResponses.ts @@ -0,0 +1,83 @@ +import { Response } from 'express'; +import { HttpStatusCode } from 'axios'; + +/* + ! Failure responses + */ + +export function emailConflict(res: Response): void { + res.status(HttpStatusCode.Conflict).json({ + error: 'Email already in use', + success: false, + }); +} + +export function externalBadGateway(res: Response): void { + res.status(HttpStatusCode.BadGateway).json({ + error: 'Remote resource error', + success: false, + }); +} + +export function invalidBody(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + error: 'Invalid request body', + success: false, + }); +} + +export function invalidLogin(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + error: 'Invalid email or password', + success: false, + }); +} + +export function notImplemented(res: Response): void { + res.status(HttpStatusCode.NotImplemented).json({ + error: 'Not implemented', + success: false, + }); +} + +export function serverError(res: Response): void { + res.status(HttpStatusCode.InternalServerError).json({ + error: 'Internal server error', + success: false, + }); +} + +export function unauthorized(res: Response): void { + res.status(HttpStatusCode.Unauthorized).json({ + error: 'Unauthorized', + success: false, + }); +} + +/* + ? Success responses + */ +// ! Authentication Responses +export function accountCreated(res: Response): void { + res + .status(HttpStatusCode.Created) + .json({ message: 'Registration successful', success: true }); +} + +export function loginSuccessful(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'Login successful', success: true }); +} + +export function logoutSuccessful(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'Logout successful', success: true }); +} + +export function passwordChanged(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'Password changed successfully', success: true }); +} diff --git a/src/controllers/helpers/databaseManagement.ts b/src/controllers/helpers/databaseManagement.ts new file mode 100644 index 0000000..d60216e --- /dev/null +++ b/src/controllers/helpers/databaseManagement.ts @@ -0,0 +1,73 @@ +import DatabaseDriver from '@src/util/DatabaseDriver'; +import { UserInfo } from '@src/models/UserInfo'; +import User from '@src/models/User'; + +export async function getUser( + db: DatabaseDriver, + userId: bigint, +): Promise { + return await db.get('users', userId); +} + +export async function getUserByEmail( + db: DatabaseDriver, + email: string, +): Promise { + return await db.getWhere('users', 'email', email); +} + +export async function getUserInfo( + db: DatabaseDriver, + userId: bigint, +): Promise { + return await db.get('userInfo', userId); +} + +export async function insertUser( + db: DatabaseDriver, + email: string, + name: string, + pwdHash: string, + userInfo?: UserInfo, +): Promise { + const userId = await db.insert('users', { + email, + name, + pwdHash, + }); + + if (await insertUserInfo(db, userId, userInfo)) { + return userId; + } else { + return -1n; + } +} + +export async function insertUserInfo( + db: DatabaseDriver, + userId: bigint, + userInfo?: UserInfo, +): Promise { + if (!userInfo) userInfo = new UserInfo(userId); + userInfo.userId = userId; + return (await db.insert('userInfo', userInfo)) >= 0; +} + +export async function updateUser( + db: DatabaseDriver, + userId: bigint, + user: User, + userInfo?: UserInfo, +): Promise { + if (userInfo) if (!(await updateUserInfo(db, userId, userInfo))) return false; + + return await db.update('users', userId, user); +} + +export async function updateUserInfo( + db: DatabaseDriver, + userId: bigint, + userInfo: UserInfo, +): Promise { + return await db.update('userInfo', userId, userInfo); +} From edc2df3bfea0e7c686b9782e2422ca5bb54231c6 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 17 Jul 2023 23:24:21 +0300 Subject: [PATCH 024/118] Base of UsersRouter.ts logic moved into its own controller. --- src/controllers/helpers/apiResponses.ts | 7 ++++ src/controllers/helpers/databaseManagement.ts | 17 ++++++++ src/controllers/usersController.ts | 27 ++++++++++++ src/routes/UsersRouter.ts | 41 ++----------------- 4 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 src/controllers/usersController.ts diff --git a/src/controllers/helpers/apiResponses.ts b/src/controllers/helpers/apiResponses.ts index 1f4fac2..3b7acce 100644 --- a/src/controllers/helpers/apiResponses.ts +++ b/src/controllers/helpers/apiResponses.ts @@ -81,3 +81,10 @@ export function passwordChanged(res: Response): void { .status(HttpStatusCode.Ok) .json({ message: 'Password changed successfully', success: true }); } + +// ! User Responses +export function userDeleted(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'Account successfully deleted', success: true }); +} diff --git a/src/controllers/helpers/databaseManagement.ts b/src/controllers/helpers/databaseManagement.ts index d60216e..8444c3c 100644 --- a/src/controllers/helpers/databaseManagement.ts +++ b/src/controllers/helpers/databaseManagement.ts @@ -2,6 +2,23 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import { UserInfo } from '@src/models/UserInfo'; import User from '@src/models/User'; +// TODO: add the annoying grace period for deleting users +export async function deleteUser( + db: DatabaseDriver, + userId: bigint, +): Promise { + return await db.delete('users', userId); +} + +// ! uncomment if needed in the future deleteUser cascade deletes +// ! should handle this +// export async function deleteUserInfo( +// db: DatabaseDriver, +// userId: bigint, +// ): Promise { +// return await db.delete('userInfo', userId); +// } + export async function getUser( db: DatabaseDriver, userId: bigint, diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts new file mode 100644 index 0000000..6b5a1ab --- /dev/null +++ b/src/controllers/usersController.ts @@ -0,0 +1,27 @@ +import { Response } from 'express'; +import { RequestWithSession } from '@src/middleware/session'; +import { + serverError, + userDeleted, +} from '@src/controllers/helpers/apiResponses'; +import DatabaseDriver from '@src/util/DatabaseDriver'; +import { deleteUser } from '@src/controllers/helpers/databaseManagement'; + +/* + ! Main route controllers + */ +export async function usersDeleteUser(req: RequestWithSession, res: Response) { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return serverError(res); + + // delete user from database + if (await deleteUser(db, userId)) return userDeleted(res); + + // send error json + return serverError(res); +} diff --git a/src/routes/UsersRouter.ts b/src/routes/UsersRouter.ts index 4f6e9bb..31624f4 100644 --- a/src/routes/UsersRouter.ts +++ b/src/routes/UsersRouter.ts @@ -1,50 +1,17 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; +import validateSession from '@src/validators/validateSession'; import UsersGet from '@src/routes/usersRoutes/UsersGet'; -import { - RequestWithSession, -} from '@src/middleware/session'; -import DatabaseDriver from '@src/util/DatabaseDriver'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import UsersUpdate from '@src/routes/usersRoutes/UsersUpdate'; -import validateSession from "@src/validators/validateSession"; +import { usersDeleteUser } from '@src/controllers/usersController'; const UsersRouter = Router(); -// Get routes +// Get and Update routes UsersRouter.use(Paths.Users.Get.Base, UsersGet); - -// Update routes UsersRouter.use(Paths.Users.Update.Base, UsersUpdate); // Delete route - delete user - requires session -UsersRouter.delete(Paths.Users.Delete, validateSession); -UsersRouter.delete(Paths.Users.Delete, async (req: RequestWithSession, res) => { - // get database - const db = new DatabaseDriver(); - - // get userId from request - const userId = req.session?.userId; - - if (userId === undefined) { - // send error json - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - } - - // delete user from database - const success = await db.delete('users', userId); - - if (success) { - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - } else { - // send error json - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to delete user' }); - } -}); +UsersRouter.delete(Paths.Users.Delete, validateSession, usersDeleteUser); export default UsersRouter; From 3e40c231c24e62e11ce433f30ade965db3711c3d Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 18 Jul 2023 01:09:36 +0300 Subject: [PATCH 025/118] Refactor code to improve user profile fetching This commit revamps how user profiles and stats are fetched. Now, specific controllers and database helper functions are created to handle fetching user profiles and stats, where previously everything was done within the route handler. This improves code modularity and keeps route handlers clean. In addition, a validateUser middleware was introduced. This middleware validates user id, checks if the user exists in the database, and returns a response if the user cannot be found. This reduces the number of database calls within route handlers and avoids code duplication. Rate limiters in AuthRouter has been updated to differentiate production and non-production environments. The JSONStringify helper function has also been introduced to handle BigInt values when stringifying objects for JSON response, ensuring compatibility across different environments. This commit greatly improves code organization and readability, and it boosts the performance of the application. --- spec/tests/usersrouter.spec.ts | 42 ++-- src/controllers/authController.ts | 4 +- src/controllers/usersController.ts | 70 ++++++- src/{controllers => }/helpers/apiResponses.ts | 78 ++++++++ .../helpers/databaseManagement.ts | 50 ++++- src/routes/AuthRouter.ts | 6 +- src/routes/UsersRouter.ts | 4 +- src/routes/usersRoutes/UsersGet.ts | 183 ++++++------------ src/util/JSONStringify.ts | 7 + src/validators/validateUser.ts | 35 ++++ 10 files changed, 325 insertions(+), 154 deletions(-) rename src/{controllers => }/helpers/apiResponses.ts (55%) rename src/{controllers => }/helpers/databaseManagement.ts (66%) create mode 100644 src/util/JSONStringify.ts create mode 100644 src/validators/validateUser.ts diff --git a/spec/tests/usersrouter.spec.ts b/spec/tests/usersrouter.spec.ts index 87d3724..5ca6af5 100644 --- a/spec/tests/usersrouter.spec.ts +++ b/spec/tests/usersrouter.spec.ts @@ -16,7 +16,7 @@ async function checkProfile( .expect( !!loginCookie || !!target ? HttpStatusCodes.OK - : HttpStatusCodes.NOT_FOUND, + : HttpStatusCodes.BAD_REQUEST, ) .expect('Content-Type', /json/) .expect((res) => { @@ -29,7 +29,6 @@ async function checkProfile( return; } - expect(objKeys).toContain('type'); expect(objKeys).toContain('name'); expect(objKeys).toContain('profilePictureUrl'); expect(objKeys).toContain('userId'); @@ -232,8 +231,7 @@ describe('Users Router', () => { expect(res.body).toBeDefined(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); }); }); @@ -248,8 +246,7 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); }); }); @@ -264,8 +261,7 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); }); }); @@ -291,8 +287,9 @@ describe('Users Router', () => { expect(res.body).toBeDefined(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( + true, + ); }); }); @@ -307,8 +304,9 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( + true, + ); }); }); @@ -323,8 +321,9 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( + true, + ); }); }); @@ -350,8 +349,9 @@ describe('Users Router', () => { expect(res.body).toBeDefined(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( + true, + ); }); }); @@ -366,8 +366,9 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( + true, + ); }); }); @@ -382,8 +383,9 @@ describe('Users Router', () => { // expect it to be an array // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)) - .toBe(true); + expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( + true, + ); }); }); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 6b7115a..f9e0e0a 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -21,7 +21,7 @@ import { insertUserInfo, updateUser, updateUserInfo, -} from '@src/controllers/helpers/databaseManagement'; +} from '@src/helpers/databaseManagement'; import { accountCreated, emailConflict, @@ -34,7 +34,7 @@ import { passwordChanged, serverError, unauthorized, -} from '@src/controllers/helpers/apiResponses'; +} from '@src/helpers/apiResponses'; /* * Interfaces diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 6b5a1ab..9642329 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -3,14 +3,27 @@ import { RequestWithSession } from '@src/middleware/session'; import { serverError, userDeleted, -} from '@src/controllers/helpers/apiResponses'; + userMiniProfile, + userNotFound, + userProfile, +} from '@src/helpers/apiResponses'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import { deleteUser } from '@src/controllers/helpers/databaseManagement'; +import { + deleteUser, + getUser, + getUserInfo, + getUserStats, + isUserFollowing, +} from '@src/helpers/databaseManagement'; +import { RequestWithTargetUserId } from '@src/validators/validateUser'; /* ! Main route controllers */ -export async function usersDeleteUser(req: RequestWithSession, res: Response) { +export async function usersDelete( + req: RequestWithSession, + res: Response, +): Promise { // get database const db = new DatabaseDriver(); @@ -25,3 +38,54 @@ export async function usersDeleteUser(req: RequestWithSession, res: Response) { // send error json return serverError(res); } + +/* + ! UsersGet route controllers + */ +export async function usersGetProfile( + req: RequestWithTargetUserId, + res: Response, +): Promise { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.targetUserId; + const issuerUserId = req.issuerUserId; + if (userId === undefined || issuerUserId === undefined) + return serverError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + const stats = await getUserStats(db, userId); + const isFollowing = await isUserFollowing(db, issuerUserId, userId); + + // check if user exists + if (!user || !userInfo) return userNotFound(res); + + // send user json + return userProfile(res, user, userInfo, stats, isFollowing); +} + +export async function usersGetMiniProfile( + req: RequestWithTargetUserId, + res: Response, +): Promise { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.targetUserId; + if (!userId) return serverError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + + // check if user exists + if (!user || !userInfo) return userNotFound(res); + + // send user json + return userMiniProfile(res, user, userInfo); +} diff --git a/src/controllers/helpers/apiResponses.ts b/src/helpers/apiResponses.ts similarity index 55% rename from src/controllers/helpers/apiResponses.ts rename to src/helpers/apiResponses.ts index 3b7acce..b18e494 100644 --- a/src/controllers/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -1,5 +1,9 @@ import { Response } from 'express'; import { HttpStatusCode } from 'axios'; +import User from '@src/models/User'; +import { UserInfo } from '@src/models/UserInfo'; +import { UserStats } from '@src/helpers/databaseManagement'; +import JSONStringify from '@src/util/JSONStringify'; /* ! Failure responses @@ -33,6 +37,13 @@ export function invalidLogin(res: Response): void { }); } +export function invalidParameters(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + error: 'Invalid request paramteres', + success: false, + }); +} + export function notImplemented(res: Response): void { res.status(HttpStatusCode.NotImplemented).json({ error: 'Not implemented', @@ -47,6 +58,13 @@ export function serverError(res: Response): void { }); } +export function userNotFound(res: Response): void { + res.status(HttpStatusCode.NotFound).json({ + error: "User couldn't be found", + success: false, + }); +} + export function unauthorized(res: Response): void { res.status(HttpStatusCode.Unauthorized).json({ error: 'Unauthorized', @@ -57,7 +75,9 @@ export function unauthorized(res: Response): void { /* ? Success responses */ + // ! Authentication Responses + export function accountCreated(res: Response): void { res .status(HttpStatusCode.Created) @@ -83,8 +103,66 @@ export function passwordChanged(res: Response): void { } // ! User Responses + export function userDeleted(res: Response): void { res .status(HttpStatusCode.Ok) .json({ message: 'Account successfully deleted', success: true }); } + +export function userProfile( + res: Response, + user: User, + userInfo: UserInfo, + userStats: UserStats, + isFollowing: boolean, +): void { + const { roadmapsCount, issueCount, followerCount, followingCount } = + userStats, + { profilePictureUrl, bio, quote, blogUrl, websiteUrl, githubUrl } = + userInfo, + { name, githubId, googleId } = user; + res + .status(HttpStatusCode.Ok) + .contentType('application/json') + .send( + JSONStringify({ + name, + profilePictureUrl, + userId: user.id, + bio, + quote, + blogUrl, + websiteUrl, + githubUrl, + roadmapsCount, + issueCount, + followerCount, + followingCount, + isFollowing, + githubLink: !!githubId, + googleLink: !!googleId, + success: true, + }), + ); +} + +export function userMiniProfile( + res: Response, + user: User, + userInfo: UserInfo, +): void { + const { profilePictureUrl } = userInfo, + { name } = user; + res + .status(HttpStatusCode.Ok) + .contentType('application/json') + .send( + JSONStringify({ + name, + profilePictureUrl, + userId: user.id, + success: true, + }), + ); +} diff --git a/src/controllers/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts similarity index 66% rename from src/controllers/helpers/databaseManagement.ts rename to src/helpers/databaseManagement.ts index 8444c3c..ce90437 100644 --- a/src/controllers/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -2,6 +2,20 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import { UserInfo } from '@src/models/UserInfo'; import User from '@src/models/User'; +/* + * Interfaces + */ +export interface UserStats { + roadmapsCount: bigint; + issueCount: bigint; + followerCount: bigint; + followingCount: bigint; +} + +/* + * Functions + */ + // TODO: add the annoying grace period for deleting users export async function deleteUser( db: DatabaseDriver, @@ -37,7 +51,41 @@ export async function getUserInfo( db: DatabaseDriver, userId: bigint, ): Promise { - return await db.get('userInfo', userId); + return await db.getWhere('userInfo', 'userId', userId); +} + +export async function getUserStats( + db: DatabaseDriver, + userId: bigint, +): Promise { + const roadmapsCount = await db.countWhere('roadmaps', 'ownerId', userId); + const issueCount = await db.countWhere('issues', 'userId', userId); + const followerCount = await db.countWhere('followers', 'userId', userId); + const followingCount = await db.countWhere('followers', 'followerId', userId); + + return { + roadmapsCount, + issueCount, + followerCount, + followingCount, + }; +} + +export async function isUserFollowing( + db: DatabaseDriver, + targetId: bigint, + authUserId: bigint, +): Promise { + if (targetId === authUserId) return true; + return ( + (await db.countWhere( + 'followers', + 'userId', + targetId, + 'followerId', + authUserId, + )) > 0 + ); } export async function insertUser( diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index aa880e7..e6e11d5 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -19,17 +19,17 @@ import EnvVars from '@src/constants/EnvVars'; const AuthRouter = Router(); const LoginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, + max: EnvVars.NodeEnv === 'production' ? 10 : 99999, message: 'Too many login attempts, please try again later.', }), RegisterLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv !== 'test' ? 5 : 99999, + max: EnvVars.NodeEnv === 'production' ? 5 : 99999, message: 'Too many register attempts, please try again later.', }), ResetPasswordLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv !== 'test' ? 10 : 99999, + max: EnvVars.NodeEnv === 'production' ? 10 : 99999, message: 'Too many reset password attempts, please try again later.', }); diff --git a/src/routes/UsersRouter.ts b/src/routes/UsersRouter.ts index 31624f4..270939a 100644 --- a/src/routes/UsersRouter.ts +++ b/src/routes/UsersRouter.ts @@ -3,7 +3,7 @@ import Paths from '@src/constants/Paths'; import validateSession from '@src/validators/validateSession'; import UsersGet from '@src/routes/usersRoutes/UsersGet'; import UsersUpdate from '@src/routes/usersRoutes/UsersUpdate'; -import { usersDeleteUser } from '@src/controllers/usersController'; +import { usersDelete } from '@src/controllers/usersController'; const UsersRouter = Router(); @@ -12,6 +12,6 @@ UsersRouter.use(Paths.Users.Get.Base, UsersGet); UsersRouter.use(Paths.Users.Update.Base, UsersUpdate); // Delete route - delete user - requires session -UsersRouter.delete(Paths.Users.Delete, validateSession, usersDeleteUser); +UsersRouter.delete(Paths.Users.Delete, validateSession, usersDelete); export default UsersRouter; diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 80900f2..1f7e094 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -1,17 +1,19 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; import User from '@src/models/User'; -import { IUserInfo } from '@src/models/UserInfo'; import { Roadmap, RoadmapMini } from '@src/models/Roadmap'; import { Issue } from '@src/models/Issue'; import { Follower } from '@src/models/Follower'; import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; +import validateUser from '@src/validators/validateUser'; +import { + usersGetMiniProfile, + usersGetProfile, +} from '@src/controllers/usersController'; // ! What would I do without StackOverflow? // ! https://stackoverflow.com/a/60848873 @@ -31,87 +33,9 @@ function getUserId(req: RequestWithSession): bigint | undefined { return userId; } -UsersGet.get(Paths.Users.Get.Profile, async (req: RequestWithSession, res) => { - // get userId from request - const userId = getUserId(req); - - if (!userId) - // send error json - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'User not found' }); - - // get database - const db = new DatabaseDriver(); - - // get user from database - const user = await db.get('users', userId); - const userInfo = await db.getWhere('userInfo', 'userId', userId); - const roadmapsCount = await db.countWhere('roadmaps', 'ownerId', userId); - const issueCount = await db.countWhere('issues', 'userId', userId); - const followerCount = await db.countWhere('followers', 'userId', userId); - const followingCount = await db.countWhere('followers', 'followerId', userId); +UsersGet.get(Paths.Users.Get.Profile, validateUser(), usersGetProfile); - const isFollowing = !!(await db.getWhere( - 'followers', - 'userId', - req.session?.userId || -1, - 'followerId', - userId)); - - if (!user || !userInfo) { - res.status(HttpStatusCodes.NOT_FOUND).json({ error: 'User not found' }); - return; - } - - res.status(HttpStatusCodes.OK).json({ - type: 'profile', - name: user.name, - profilePictureUrl: userInfo.profilePictureUrl || '', - userId: user.id.toString(), - bio: userInfo.bio || '', - quote: userInfo.quote || '', - blogUrl: userInfo.blogUrl || '', - roadmapsCount: roadmapsCount.toString() || '0', - issueCount: issueCount.toString() || '0', - followerCount: followerCount.toString() || '0', - followingCount: followingCount.toString() || '0', - websiteUrl: userInfo.websiteUrl || '', - githubUrl: userInfo.githubUrl || '', - githubLink: !!user.githubId, - googleLink: !!user.googleId, - isFollowing, - }); -}); - -UsersGet.get( - Paths.Users.Get.MiniProfile, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'User not found' }); - - const db = new DatabaseDriver(); - - const user = await db.get('users', userId); - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - if (!user || !userInfo) { - res.status(HttpStatusCodes.NOT_FOUND).json({ error: 'User not found' }); - return; - } - - res.status(HttpStatusCodes.OK).json({ - type: 'mini', - name: user.name, - profilePictureUrl: userInfo.profilePictureUrl || '', - userId: user.id.toString(), - }); - }, -); +UsersGet.get(Paths.Users.Get.MiniProfile, validateUser(), usersGetMiniProfile); UsersGet.get( Paths.Users.Get.UserRoadmaps, @@ -155,11 +79,9 @@ UsersGet.get( id: roadmap.id.toString(), name: roadmap.name, description: roadmap.description || '', - likes: (await db.countWhere( - 'roadmapLikes', - 'roadmapId', - roadmap.id, - )).toString(), + likes: ( + await db.countWhere('roadmapLikes', 'roadmapId', roadmap.id) + ).toString(), isLiked: !!(await db.getWhere( 'roadmapLikes', 'userId', @@ -194,17 +116,22 @@ UsersGet.get( const issues = await db.getAllWhere('issues', 'userId', userId); - res.status(HttpStatusCodes.OK).json(JSON.stringify({ - type: 'issues', - userId: userId.toString(), - issues: issues, - }, (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - })); + res.status(HttpStatusCodes.OK).json( + JSON.stringify( + { + type: 'issues', + userId: userId.toString(), + issues: issues, + }, + (_, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }, + ), + ); }, ); @@ -226,17 +153,22 @@ UsersGet.get( userId, ); - res.status(HttpStatusCodes.OK).json(JSON.stringify({ - type: 'followers', - userId: userId.toString(), - followers: followers, - }, (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - })); + res.status(HttpStatusCodes.OK).json( + JSON.stringify( + { + type: 'followers', + userId: userId.toString(), + followers: followers, + }, + (_, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }, + ), + ); }, ); @@ -258,17 +190,22 @@ UsersGet.get( userId, ); - res.status(HttpStatusCodes.OK).json(JSON.stringify({ - type: 'following', - userId: userId.toString(), - following: following, - }, (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - })); + res.status(HttpStatusCodes.OK).json( + JSON.stringify( + { + type: 'following', + userId: userId.toString(), + following: following, + }, + (_, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }, + ), + ); }, ); diff --git a/src/util/JSONStringify.ts b/src/util/JSONStringify.ts new file mode 100644 index 0000000..868e1c4 --- /dev/null +++ b/src/util/JSONStringify.ts @@ -0,0 +1,7 @@ +export default function JSONStringify(obj: unknown): string { + return JSON.stringify(obj, (key, value) => { + if (typeof value === 'bigint') return value.toString(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }); +} diff --git a/src/validators/validateUser.ts b/src/validators/validateUser.ts new file mode 100644 index 0000000..24c9b82 --- /dev/null +++ b/src/validators/validateUser.ts @@ -0,0 +1,35 @@ +import { NextFunction, Response } from 'express'; +import { RequestWithSession } from '@src/middleware/session'; +import { invalidParameters } from '@src/helpers/apiResponses'; + +export interface RequestWithTargetUserId extends RequestWithSession { + targetUserId?: bigint; + issuerUserId?: bigint; +} + +export default function validateUser( + ownUserOnly = false, +): ( + req: RequestWithTargetUserId, + res: Response, + next: NextFunction, +) => unknown { + return ( + req: RequestWithTargetUserId, + res: Response, + next: NextFunction, + ): unknown => { + // get userId from request :userId param + req.targetUserId = BigInt(req.params.userId ?? req.session?.userId ?? -1n); + req.issuerUserId = BigInt(req.session?.userId ?? -1n); + + // if userId is -1n, return error + if (req.targetUserId === -1n) return invalidParameters(res); + + // if ownUserOnly is true, set targetUserId to issuerUserId + if (req.issuerUserId !== req.targetUserId && ownUserOnly) + req.targetUserId = req.issuerUserId; + + next(); + }; +} From 61ca011001a7c6f7445fcc54341a1e0d17aff805 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 18 Jul 2023 17:20:48 +0300 Subject: [PATCH 026/118] Refactor session validation and add DB check Refactored the session validation function to improve readability by separating the invalid session response into its own function. Also, added a database check to ensure that the session token exists in the database, providing another layer of security. This change makes unauthorized access much less likely. --- src/validators/validateSession.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/validators/validateSession.ts b/src/validators/validateSession.ts index 651a7fc..55e7b09 100644 --- a/src/validators/validateSession.ts +++ b/src/validators/validateSession.ts @@ -1,20 +1,32 @@ import { NextFunction, Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession } from '@src/middleware/session'; +import DatabaseDriver from '@src/util/DatabaseDriver'; -export default function ( +function invalidSession(res: Response): void { + res.status(HttpStatusCodes.UNAUTHORIZED).json({ + error: 'Invalid session', + success: false, + }); +} + +export default async function ( req: RequestWithSession, res: Response, next: NextFunction, -): void { +): Promise { // if session isn't set, return forbidden if (!req.session) { - res - .status(HttpStatusCodes.UNAUTHORIZED) - .json({ error: 'Token not found, please login' }); - return; + return invalidSession(res); } + // get db session + const db = new DatabaseDriver(); + + // count how maynt sessions there are with token + if ((await db.countWhere('sessions', 'token', req.session.token)) !== 1n) + return invalidSession(res); + // call next() next(); -} \ No newline at end of file +} From f9baadc4943b3c769d0eaf7791a274240a79df98 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 18 Jul 2023 17:21:08 +0300 Subject: [PATCH 027/118] Refactor session validation and add DB check Refactored the session validation function to improve readability by separating the invalid session response into its own function. Also, added a database check to ensure that the session token exists in the database, providing another layer of security. This change makes unauthorized access much less likely. --- src/validators/validateSession.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/validators/validateSession.ts b/src/validators/validateSession.ts index 55e7b09..eaf75bd 100644 --- a/src/validators/validateSession.ts +++ b/src/validators/validateSession.ts @@ -16,9 +16,7 @@ export default async function ( next: NextFunction, ): Promise { // if session isn't set, return forbidden - if (!req.session) { - return invalidSession(res); - } + if (!req.session) return invalidSession(res); // get db session const db = new DatabaseDriver(); From 515f7579e7813a65705b57fa2269690dd6c49a90 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 18 Jul 2023 17:22:31 +0300 Subject: [PATCH 028/118] spelling mistake --- src/validators/validateSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validators/validateSession.ts b/src/validators/validateSession.ts index eaf75bd..ed70f00 100644 --- a/src/validators/validateSession.ts +++ b/src/validators/validateSession.ts @@ -21,7 +21,7 @@ export default async function ( // get db session const db = new DatabaseDriver(); - // count how maynt sessions there are with token + // count how many sessions there are with token if ((await db.countWhere('sessions', 'token', req.session.token)) !== 1n) return invalidSession(res); From 84703184f6654be29f0bf637ccc42ffee8c42121 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 15 Aug 2023 18:02:40 +0300 Subject: [PATCH 029/118] Fixing github login --- src/controllers/authController.ts | 45 ++++++++++++++----------------- src/util/sessionManager.ts | 1 - 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index f9e0e0a..837bd2f 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -327,29 +327,27 @@ export async function authGitHubCallback( const userData = response.data as GitHubUserData; if (!userData) return serverError(res); - // if email is not public, get it from github - if (userData.email == '') { - response = await axios.get('https://api.github.com/user/emails', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', - 'X-GitHub-Api-Version': '2022-11-28', - 'X-OAuth-Scopes': 'userDisplay:email', - }, - }); + // get email from github + response = await axios.get('https://api.github.com/user/emails', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + 'X-OAuth-Scopes': 'userDisplay:email', + }, + }); - const emails = response.data as { - email: string; - primary: boolean; - verified: boolean; - }[]; + const emails = response.data as { + email: string; + primary: boolean; + verified: boolean; + }[]; - // check if response is valid - if (response.status !== 200) return _handleNotOkay(res, response.status); + // check if response is valid + if (response.status !== 200) return _handleNotOkay(res, response.status); - // get primary email - userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; - } + // get primary email + userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; // check if email is valid if (userData.email == '') return serverError(res); @@ -401,8 +399,7 @@ export async function authGitHubCallback( `https://github.com/${userData.login}`, ), )) - ) - return serverError(res); + ) return serverError(res); } else { if (userInfo.bio == '') userInfo.bio = userData.bio; if (userInfo.profilePictureUrl == '') @@ -412,15 +409,13 @@ export async function authGitHubCallback( userInfo.githubUrl = `https://github.com/${userData.login}`; // update user info - if (!(await updateUserInfo(db, user.id, userInfo))) + if (!(await updateUserInfo(db, userInfo.id, userInfo))) return serverError(res); } } // create session and save it if (await createSaveSession(res, user.id)) return loginSuccessful(res); - - return serverError(res); } catch (e) { if (EnvVars.NodeEnv !== 'test') logger.err(e, true); return serverError(res); diff --git a/src/util/sessionManager.ts b/src/util/sessionManager.ts index d396216..19bb4eb 100644 --- a/src/util/sessionManager.ts +++ b/src/util/sessionManager.ts @@ -40,7 +40,6 @@ export function saveSession(res: Response, token: string): boolean { }); return false; } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access res.cookie('token', token, { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days httpOnly: false, From fb77591f86847cf27f9624564d43d33c06005f59 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 15 Aug 2023 18:22:09 +0300 Subject: [PATCH 030/118] Adding staging config --- env.example/staging.env | 37 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 38 insertions(+) create mode 100644 env.example/staging.env diff --git a/env.example/staging.env b/env.example/staging.env new file mode 100644 index 0000000..76c4dfa --- /dev/null +++ b/env.example/staging.env @@ -0,0 +1,37 @@ +## Environment ## +NODE_ENV=development + +## Server ## +PORT=3001 +HOST=localhost + +## Setup jet-logger ## +JET_LOGGER_MODE=CONSOLE +JET_LOGGER_FILEPATH=jet-logger.log +JET_LOGGER_TIMESTAMP=TRUE +JET_LOGGER_FORMAT=LINE + +## Authentication ## +COOKIE_DOMAIN=localhost +COOKIE_PATH=/ +SECURE_COOKIE=false +JWT_SECRET=xxxxxxxxxxxxxx +COOKIE_SECRET=xxxxxxxxxxxxxx +# expires in 3 days +COOKIE_EXP=259200000 + +## Database Authentication +MARIADB_HOST=localhost +MARIADB_USER=xxxxxxx +MARIADB_PASSWORD=xxxxxx +MARIADB_DATABASE=xxxxxxx + +## Google Authentication +GOOGLE_CLIENT_ID=xxxxxxxxx +GOOGLE_CLIENT_SECRET=xxxxxxxxx +GOOGLE_REDIRECT_URI=xxxxxxxxxxx + +## GITHUB Authentication +GITHUB_CLIENT_ID=xxxxxxxxxxx +GITHUB_CLIENT_SECRET=xxxxxxxxxxxx +GITHUB_REDIRECT_URI=xxxxxxxxxxxxxx \ No newline at end of file diff --git a/package.json b/package.json index cd9e4a7..fb87efb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "npx eslint --ext .ts src/", "lint:tests": "npx eslint --ext .ts spec/", "start": "node -r module-alias/register ./dist --env=production", + "staging": "node -r module-alias/register ./dist --env=staging", "dev": "nodemon", "test": "nodemon --config ./spec/nodemon.json", "test:no-reloading": "npx ts-node --files -r tsconfig-paths/register ./spec" From d39e8f6e03474013902ca7224332d781dcc4c42d Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 15 Aug 2023 18:22:32 +0300 Subject: [PATCH 031/118] updating ENV files --- env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env b/env index f026a8b..a9f299d 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit f026a8b26ee7834e2b0e80f4519d9d1ec6bd10d3 +Subproject commit a9f299d52284837bade9be9f0f6b733f0b6b70bd From 816d09a74c63e0d61b6b38281a72ba90d98549ad Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 15 Aug 2023 19:58:12 +0300 Subject: [PATCH 032/118] testing CI/CD --- .github/testfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/testfile diff --git a/.github/testfile b/.github/testfile new file mode 100644 index 0000000..e69de29 From 1d0448b03a6fc305339b4fa4f554f9335d0d8e2d Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 15 Aug 2023 20:04:43 +0300 Subject: [PATCH 033/118] testing CI/CD --- .github/testfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/testfile diff --git a/.github/testfile b/.github/testfile deleted file mode 100644 index e69de29..0000000 From 02fd52a9f9bd5adda0d5d50e205f80eafabafc71 Mon Sep 17 00:00:00 2001 From: sopy Date: Sun, 3 Sep 2023 18:19:00 +0300 Subject: [PATCH 034/118] Tests for TDD - part 1 --- env.example/development.env | 1 - env.example/production.env | 1 - env.example/staging.env | 1 - env.example/test.env | 1 - spec/tests/authrouter.spec.ts | 184 ------------------------ spec/tests/routes/auth.spec.ts | 136 ++++++++++++++++++ spec/tests/usersrouter.spec.ts | 24 ++-- spec/tests/{ => utils}/authutil.spec.ts | 0 spec/tests/{ => utils}/database.spec.ts | 2 +- spec/types/supertest/index.d.ts | 13 +- spec/utils/randomString.ts | 3 + src/constants/EnvVars.ts | 78 +++++++--- src/constants/misc.ts | 5 +- src/server.ts | 17 +-- tsconfig.json | 5 +- 15 files changed, 230 insertions(+), 241 deletions(-) delete mode 100644 spec/tests/authrouter.spec.ts create mode 100644 spec/tests/routes/auth.spec.ts rename spec/tests/{ => utils}/authutil.spec.ts (100%) rename spec/tests/{ => utils}/database.spec.ts (98%) create mode 100644 spec/utils/randomString.ts diff --git a/env.example/development.env b/env.example/development.env index 76c4dfa..bc1f0d2 100644 --- a/env.example/development.env +++ b/env.example/development.env @@ -15,7 +15,6 @@ JET_LOGGER_FORMAT=LINE COOKIE_DOMAIN=localhost COOKIE_PATH=/ SECURE_COOKIE=false -JWT_SECRET=xxxxxxxxxxxxxx COOKIE_SECRET=xxxxxxxxxxxxxx # expires in 3 days COOKIE_EXP=259200000 diff --git a/env.example/production.env b/env.example/production.env index 9951d66..99d9417 100644 --- a/env.example/production.env +++ b/env.example/production.env @@ -15,7 +15,6 @@ JET_LOGGER_FORMAT=LINE COOKIE_DOMAIN=localhost COOKIE_PATH=/ SECURE_COOKIE=false -JWT_SECRET=xxxxxxxxxxxxxx COOKIE_SECRET=xxxxxxxxxxxxxx # expires in 3 days COOKIE_EXP=259200000 diff --git a/env.example/staging.env b/env.example/staging.env index 76c4dfa..bc1f0d2 100644 --- a/env.example/staging.env +++ b/env.example/staging.env @@ -15,7 +15,6 @@ JET_LOGGER_FORMAT=LINE COOKIE_DOMAIN=localhost COOKIE_PATH=/ SECURE_COOKIE=false -JWT_SECRET=xxxxxxxxxxxxxx COOKIE_SECRET=xxxxxxxxxxxxxx # expires in 3 days COOKIE_EXP=259200000 diff --git a/env.example/test.env b/env.example/test.env index 5ac4435..07258db 100644 --- a/env.example/test.env +++ b/env.example/test.env @@ -15,7 +15,6 @@ JET_LOGGER_FORMAT=LINE COOKIE_DOMAIN=localhost COOKIE_PATH=/ SECURE_COOKIE=false -JWT_SECRET=xxxxxxxxxxxxxx COOKIE_SECRET=xxxxxxxxxxxxxx # expires in 3 days COOKIE_EXP=259200000 diff --git a/spec/tests/authrouter.spec.ts b/spec/tests/authrouter.spec.ts deleted file mode 100644 index 150b826..0000000 --- a/spec/tests/authrouter.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import app from '@src/server'; -import request from 'supertest'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; -import User from '@src/models/User'; - -describe('Login Router', () => { - // generate random email - const email = Math.random().toString(36).substring(2, 15) + '@test.com'; - // generate random password - const password = Math.random().toString(36).substring(2, 15); - let loginCookie: string; - - it('Login with invalid credentials', async () => { - // login with non-existent userDisplay and expect 401 - await request(app) - .post('/api/auth/login') - .send({ email, password }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('Signup with invalid credentials', async () => { - const invalidEmail = 'invalidEmail'; - - // signup with invalid email and expect 400 - await request(app) - .post('/api/auth/register') - .send({ email: invalidEmail, password }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('Signup with valid credentials', async () => { - // register a 200 response and a session cookie to be sent back - await request(app) - .post('/api/auth/register') - .send({ email, password }) - .expect(HttpStatusCodes.CREATED) - .expect('Set-Cookie', new RegExp('token=.*; Path=/;')); - }); - - it('Login with valid credentials', async () => { - // login with new userDisplay and expect 200 response and a session cookie - // to be sent back - const res = await request(app) - .post('/api/auth/login') - .send({ email, password }) - .expect(HttpStatusCodes.OK) - .expect('Set-Cookie', new RegExp('token=.*; Path=/;')); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - res.headers['set-cookie'].forEach((cookie: string) => { - if (cookie.startsWith('token=')) { - loginCookie = cookie; - } - }); - }); - - // try to change password without a session cookie - it('Change password without session cookie', async () => { - // change password and expect 401 - await request(app) - .post('/api/auth/change-password') - .send({ password, newPassword: 'newPassword' }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - // try to change password with a session cookie but invalid password - it('Change password with session cookie but invalid password', async () => { - // change password and expect 401 - await request(app) - .post('/api/auth/change-password') - .set('Cookie', loginCookie) - .send({ password: 'invalidPassword', newPassword: 'newPassword' }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - // try to change password with a session cookie - it('Change password with session cookie', async () => { - // change password and expect 200 - await request(app) - .post('/api/auth/change-password') - .set('Cookie', loginCookie) - .send({ password, newPassword: 'newPassword' }) - .expect(HttpStatusCodes.OK); - }); - - // google login page test - it('Google login', async () => { - // login and expect redirect to login page with 302 - await request(app) - .get('/api/auth/google-login') - .expect(HttpStatusCodes.FOUND) - .expect( - 'Location', - new RegExp('^https:\\/\\/accounts\\.google\\.com\\/o\\/oauth2.+'), - ); - }); - - // google callback test with no code - it('Google callback with no code', async () => { - // login and expect 403 forbidden - await request(app) - .get('/api/auth/google-callback') - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - // google callback test with invalid code - it('Google callback with invalid code', async () => { - // login and expect 500 internal server error - await request(app) - .get('/api/auth/google-callback?code=invalid') - .expect(HttpStatusCodes.INTERNAL_SERVER_ERROR); - }); - - // GitHub login page test - it('GitHub login', async () => { - // login and expect redirect to login page with 302 - await request(app) - .get('/api/auth/github-login') - .expect(HttpStatusCodes.FOUND) - .expect( - 'Location', - new RegExp('^https:\\/\\/github\\.com\\/login\\/oauth.+'), - ); - }); - - // GitHub callback test with no code - it('GitHub callback with no code', async () => { - // login and expect 403 forbidden - await request(app) - .get('/api/auth/github-callback') - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - // GitHub callback test with invalid code - it('GitHub callback with invalid code', async () => { - // login and expect 500 internal server error - await request(app) - .get('/api/auth/github-callback?code=invalid') - .expect(HttpStatusCodes.INTERNAL_SERVER_ERROR); - }); - - it('Logout', async () => { - // logout with 200 response and a session cookie to be sent back - await request(app) - .delete('/api/auth/logout') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Set-Cookie', new RegExp('^token=; Max-Age=0; Path=\\/;')); - }); - - it('Logout without session cookie', async () => { - // logout and expect 401 - await request(app) - .delete('/api/auth/logout') - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Logout with invalid session cookie', async () => { - // logout and expect 401 - await request(app) - .delete('/api/auth/logout') - .set('Cookie', 'token=invalid') - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - // database cleanup - afterAll(async () => { - // get database - const db = new Database(); - - // get userDisplay - const user = await db.getWhere('users', 'email', email); - - if (!user) { - return; - } - - // delete userDisplay - const success = await db.delete('users', user.id); - expect(success).toBe(true); - }); -}); diff --git a/spec/tests/routes/auth.spec.ts b/spec/tests/routes/auth.spec.ts new file mode 100644 index 0000000..0fcf793 --- /dev/null +++ b/spec/tests/routes/auth.spec.ts @@ -0,0 +1,136 @@ +import { randomString } from '@spec/utils/randomString'; +import request from 'supertest'; +import app from '@src/server'; +import httpStatusCodes from '@src/constants/HttpStatusCodes'; +import User from '@src/models/User'; +import Database from '@src/util/DatabaseDriver'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; + +describe('Authentification Tests', () => { + // generate random email + const email = randomString() + '@test.com'; + // generate random password + let password = randomString(); + let loginCookie: string; + + // user journey + it('login should fail if no account is registered', async () => { + await request(app) + .post('/api/auth/login') + .send({ email, password }) + .expect(httpStatusCodes.BAD_REQUEST) + .expect(({ body }) => { + expect(body.success).toBe(false); + }); + }); + + it('user should be able to register', async () => { + await request(app) + .post('/api/auth/register') + .send({ email, password }) + .expect(httpStatusCodes.CREATED) + .expect(({ body, headers }) => { + expect(body.success).toBe(true); + expect(headers['set-cookie']).toBeDefined(); + }); + }); + + it('user should be able to login', async () => { + await request(app) + .post('/api/auth/login') + .send({ email, password }) + .expect(httpStatusCodes.OK) + .expect(({ body, headers }) => { + expect(body.success).toBe(true); + expect(headers['set-cookie']).toBeDefined(); + + headers['set-cookie'].forEach((cookie: string) => { + if (cookie.startsWith('token=')) { + loginCookie = cookie; + } + }); + }); + }); + + it('user should be able to change password', async () => { + const newPassword = randomString(); + await request(app) + .post('/api/auth/change-password') + .set('Cookie', loginCookie) + .send({ password, newPassword }) + .expect(httpStatusCodes.OK) + .expect(({ body }) => { + expect(body.success).toBe(true); + }); + + password = newPassword; + }); + + it('user should be able to logout', async () => { + await request(app) + .post('/api/auth/logout') + .set('Cookie', loginCookie) + .expect(httpStatusCodes.OK) + .expect(({ body, headers }) => { + expect(body.success).toBe(true); + expect(headers['set-cookie']).toBeDefined(); + }); + }); + + // google login page test + it('Google login', async () => { + // login and expect redirect to login page with 302 + await request(app) + .get('/api/auth/google-login') + .expect(HttpStatusCodes.FOUND) + .expect( + 'Location', + new RegExp('^https:\\/\\/accounts\\.google\\.com\\/o\\/oauth2.+'), + ); + }); + + // google callback test with no code + it('Google callback with no code', async () => { + // login and expect 403 forbidden + await request(app) + .get('/api/auth/google-callback') + .expect(HttpStatusCodes.BAD_REQUEST); + }); + + // GitHub login page test + it('GitHub login', async () => { + // login and expect redirect to login page with 302 + await request(app) + .get('/api/auth/github-login') + .expect(HttpStatusCodes.FOUND) + .expect( + 'Location', + new RegExp('^https:\\/\\/github\\.com\\/login\\/oauth.+'), + ); + }); + + // GitHub callback test with no code + it('GitHub callback with no code', async () => { + // login and expect 403 forbidden + await request(app) + .get('/api/auth/github-callback') + .expect(HttpStatusCodes.BAD_REQUEST); + }); + + // database cleanup + afterAll(async () => { + // get database + const db = new Database(); + + // get userDisplay + const user = await db.getWhere('users', 'email', email); + + if (!user) { + return; + } + + // delete userDisplay + const success = await db.delete('users', user.id); + expect(success).toBe(true); + }); +}); diff --git a/spec/tests/usersrouter.spec.ts b/spec/tests/usersrouter.spec.ts index 5ca6af5..22b7f2e 100644 --- a/spec/tests/usersrouter.spec.ts +++ b/spec/tests/usersrouter.spec.ts @@ -161,7 +161,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -218,7 +218,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -274,7 +274,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -336,7 +336,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -398,7 +398,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -459,7 +459,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -520,7 +520,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -580,7 +580,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.BAD_REQUEST) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -641,7 +641,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.UNAUTHORIZED) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -688,7 +688,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.UNAUTHORIZED) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -737,7 +737,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.UNAUTHORIZED) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); @@ -865,7 +865,7 @@ describe('Users Router', () => { .expect(HttpStatusCodes.UNAUTHORIZED) .expect('Content-Type', /json/) .expect((res) => { - expect(res.body.error).toBeDefined(); + expect(res.body.message).toBeDefined(); }); }); diff --git a/spec/tests/authutil.spec.ts b/spec/tests/utils/authutil.spec.ts similarity index 100% rename from spec/tests/authutil.spec.ts rename to spec/tests/utils/authutil.spec.ts diff --git a/spec/tests/database.spec.ts b/spec/tests/utils/database.spec.ts similarity index 98% rename from spec/tests/database.spec.ts rename to spec/tests/utils/database.spec.ts index fc36fcd..dfb17b6 100644 --- a/spec/tests/database.spec.ts +++ b/spec/tests/utils/database.spec.ts @@ -90,7 +90,7 @@ describe('Database', () => { } }); - // test for getting userDisplay by key (email) with value (userDisplay.email) like + // test for getting user by key (email) with value (userDisplay.email) like it('should get users by key with value like', async () => { // get database const db = new Database(); diff --git a/spec/types/supertest/index.d.ts b/spec/types/supertest/index.d.ts index 220c09c..777f2a8 100644 --- a/spec/types/supertest/index.d.ts +++ b/spec/types/supertest/index.d.ts @@ -1,13 +1,14 @@ -import { IUser } from '@src/models/User'; import 'supertest'; declare module 'supertest' { - export interface Response { - headers: Record; + headers: { + 'set-cookie': string[]; + }; body: { - error: string; - users: IUser[]; + success: boolean; + message: string; + data: unknown; }; } -} \ No newline at end of file +} diff --git a/spec/utils/randomString.ts b/spec/utils/randomString.ts new file mode 100644 index 0000000..a340879 --- /dev/null +++ b/spec/utils/randomString.ts @@ -0,0 +1,3 @@ +export function randomString() { + return Math.random().toString(36).substring(2, 15); +} diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index 072fabb..420782c 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -4,44 +4,76 @@ /* eslint-disable node/no-process-env */ +import { NodeEnvs } from '@src/constants/misc'; + +interface IEnvVars { + NodeEnv: NodeEnvs; + Port: number; + CookieProps: { + Key: string; + Secret: string; + Options: { + httpOnly: boolean; + signed: boolean; + path: string; + maxAge: number; + domain: string; + secure: boolean; + }; + }; + DBCred: { + host: string; + user: string; + port: number; + password: string; + database: string; + }; + Google: { + ClientID: string; + ClientSecret: string; + RedirectUri: string; + }; + GitHub: { + ClientID: string; + ClientSecret: string; + RedirectUri: string; + }; +} + const EnvVars = { - NodeEnv: (process.env.NODE_ENV ?? ''), - Port: (process.env.PORT ?? 0), + NodeEnv: process.env.NODE_ENV ?? '', + Port: process.env.PORT ?? 0, CookieProps: { Key: 'ExpressGeneratorTs', - Secret: (process.env.COOKIE_SECRET ?? ''), + Secret: process.env.COOKIE_SECRET ?? '', // Casing to match express cookie options Options: { - httpOnly: true, + httpOnly: false, signed: true, - path: (process.env.COOKIE_PATH ?? ''), + path: process.env.COOKIE_PATH ?? '/', maxAge: Number(process.env.COOKIE_EXP ?? 0), - domain: (process.env.COOKIE_DOMAIN ?? ''), - secure: (process.env.SECURE_COOKIE === 'true'), + domain: process.env.COOKIE_DOMAIN ?? '', + secure: process.env.SECURE_COOKIE === 'true', }, }, - Jwt: { - Secret: (process.env.JWT_SECRET ?? ''), - Exp: (process.env.COOKIE_EXP ?? ''), // exp at the same time as the cookie - }, DBCred: { - host: (process.env.MARIADB_HOST ?? ''), - user: (process.env.MARIADB_USER ?? ''), - port: (process.env.MARIADB_PORT ?? 3306), - password: (process.env.MARIADB_PASSWORD ?? ''), - database: (process.env.MARIADB_DATABASE ?? ''), + host: process.env.MARIADB_HOST ?? '', + user: process.env.MARIADB_USER ?? '', + port: process.env.MARIADB_PORT ?? 3306, + password: process.env.MARIADB_PASSWORD ?? '', + database: process.env.MARIADB_DATABASE ?? '', }, Google: { - ClientID: (process.env.GOOGLE_CLIENT_ID ?? ''), - ClientSecret: (process.env.GOOGLE_CLIENT_SECRET ?? ''), - RedirectUri: (process.env.GOOGLE_REDIRECT_URI ?? ''), + ClientID: process.env.GOOGLE_CLIENT_ID ?? '', + ClientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '', + RedirectUri: process.env.GOOGLE_REDIRECT_URI ?? '', }, GitHub: { - ClientID: (process.env.GITHUB_CLIENT_ID ?? ''), - ClientSecret: (process.env.GITHUB_CLIENT_SECRET ?? ''), - RedirectUri: (process.env.GITHUB_REDIRECT_URI ?? ''), + ClientID: process.env.GITHUB_CLIENT_ID ?? '', + ClientSecret: process.env.GITHUB_CLIENT_SECRET ?? '', + RedirectUri: process.env.GITHUB_REDIRECT_URI ?? '', }, -} as const; +} as Readonly; export default EnvVars; export { EnvVars }; diff --git a/src/constants/misc.ts b/src/constants/misc.ts index 32dacbc..280176a 100644 --- a/src/constants/misc.ts +++ b/src/constants/misc.ts @@ -1,5 +1,6 @@ export enum NodeEnvs { Dev = 'development', Test = 'test', - Production = 'production' -} \ No newline at end of file + Staging = 'staging', + Production = 'production', +} diff --git a/src/server.ts b/src/server.ts index 4c62810..9b84f6a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,6 @@ import cookieParser from 'cookie-parser'; import morgan from 'morgan'; -import path from 'path'; import helmet from 'helmet'; import express, { Request, Response } from 'express'; import logger from 'jet-logger'; @@ -63,7 +62,9 @@ app.use(Paths.Base, BaseRouter); // if no response is sent, send 404 app.use((_: Request, res: Response) => { - res.status(HttpStatusCodes.NOT_FOUND).json({ error: 'Route not Found' }); + res + .status(HttpStatusCodes.NOT_FOUND) + .json({ success: false, message: 'Route not Found' }); }); // Add error handler @@ -73,18 +74,18 @@ app.use((err: Error, _: Request, res: Response) => { if (err instanceof RouteError) { status = err.status; } - return res.status(status).json({ error: err.message }); + return res.status(status).json({ success: false, message: err.message }); }); // ** Front-End Content ** // // Set views directory (html) -const viewsDir = path.join(__dirname, 'views'); -app.set('views', viewsDir); - +// const viewsDir = path.join(__dirname, 'views'); +// app.set('views', viewsDir); +// // Set static directory (js and css). -const staticDir = path.join(__dirname, 'public'); -app.use(express.static(staticDir)); +// const staticDir = path.join(__dirname, 'public'); +// app.use(express.static(staticDir)); // **** Export default **** // diff --git a/tsconfig.json b/tsconfig.json index 4e47447..194923c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -76,7 +76,10 @@ "paths": { "@src/*": [ "src/*" - ] + ], + "@spec/*": [ + "spec/*" + ], }, "useUnknownInCatchVariables": false, "lib": [ From 19bbae8f9b65a5874ca74d71e9e72773682fb8d1 Mon Sep 17 00:00:00 2001 From: sopy Date: Sun, 3 Sep 2023 18:56:32 +0300 Subject: [PATCH 035/118] Update SQL database setup --- src/sql/setup.sql | 140 ++++++++++++++++++---------------------------- 1 file changed, 54 insertions(+), 86 deletions(-) diff --git a/src/sql/setup.sql b/src/sql/setup.sql index b82b7b3..e1097a4 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -1,17 +1,18 @@ -create table if not exists users +create table users ( id bigint auto_increment primary key, - name varchar(255) not null, - email varchar(255) not null, - role int default 0 not null, - pwdHash varchar(255) default '' not null, - googleId varchar(255) null, - githubId varchar(255) null, - createdAt timestamp default current_timestamp() not null + avatar text null, + name varchar(255) not null, + email varchar(255) not null, + role int default 0 null, + pwdHash varchar(255) null, + googleId varchar(255) null, + githubId varchar(255) null, + createdAt timestamp default current_timestamp() not null ); -create table if not exists followers +create table followers ( id bigint auto_increment primary key, @@ -26,29 +27,30 @@ create table if not exists followers on delete cascade ); -create index if not exists followers_followerId_index +create index followers_followerId_index on followers (followerId); -create index if not exists followers_userId_index +create index followers_userId_index on followers (userId); -create table if not exists roadmaps +create table roadmaps ( id bigint auto_increment primary key, - name varchar(255) not null, - description varchar(255) not null, - ownerId bigint not null, - createdAt timestamp not null, - updatedAt timestamp not null, - isPublic tinyint(1) not null, - data longtext not null, + name varchar(255) not null, + description varchar(255) not null, + userId bigint not null, + isPublic tinyint(1) not null, + isDraft tinyint(1) null, + data longtext not null, + createdAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() not null, constraint roadmaps_users_id_fk - foreign key (ownerId) references users (id) + foreign key (userId) references users (id) on delete cascade ); -create table if not exists issues +create table issues ( id bigint auto_increment primary key, @@ -58,7 +60,7 @@ create table if not exists issues title varchar(255) not null, content text null, createdAt timestamp default current_timestamp() null, - updatedAt timestamp null, + updatedAt timestamp default current_timestamp() not null, constraint issues_roadmaps_id_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, @@ -67,7 +69,7 @@ create table if not exists issues on delete cascade ); -create table if not exists issueComments +create table issueComments ( id bigint auto_increment primary key, @@ -75,7 +77,7 @@ create table if not exists issueComments userId bigint not null, content text not null, createdAt timestamp default current_timestamp() not null, - updatedAt timestamp null, + updatedAt timestamp default current_timestamp() not null, constraint issueComments_issues_id_fk foreign key (issueId) references issues (id) on delete cascade, @@ -84,27 +86,28 @@ create table if not exists issueComments on delete cascade ); -create index if not exists issueComments_issueId_createdAt_index +create index issueComments_issueId_createdAt_index on issueComments (issueId, createdAt); -create index if not exists issueComments_userid_index +create index issueComments_userid_index on issueComments (userId); -create index if not exists issues_roadmapId_createdAt_index +create index issues_roadmapId_createdAt_index on issues (roadmapId asc, createdAt desc); -create index if not exists issues_title_index +create index issues_title_index on issues (title); -create index if not exists issues_userId_index +create index issues_userId_index on issues (userId); -create table if not exists roadmapLikes +create table roadmapLikes ( id bigint auto_increment primary key, roadmapId bigint not null, userId bigint not null, + value int null, createdAt timestamp default current_timestamp() null, constraint roadmaplikes_roadmaps_id_fk foreign key (roadmapId) references roadmaps (id) @@ -114,27 +117,10 @@ create table if not exists roadmapLikes on delete cascade ); -create index if not exists roadmapLikes_roadmapId_index +create index roadmapLikes_roadmapId_index on roadmapLikes (roadmapId); -create table if not exists roadmapTags -( - id bigint auto_increment - primary key, - roadmapId bigint not null, - tagName varchar(255) not null, - constraint roadmapTags_roadmaps_id_fk - foreign key (roadmapId) references roadmaps (id) - on delete cascade -); - -create index if not exists roadmapTags_roadmapId_index - on roadmapTags (roadmapId); - -create index if not exists roadmapTags_tagName_index - on roadmapTags (tagName); - -create table if not exists roadmapViews +create table roadmapViews ( id bigint auto_increment primary key, @@ -150,22 +136,22 @@ create table if not exists roadmapViews on delete cascade ); -create index if not exists roadmapViews_roadmapId_createdAt_index +create index roadmapViews_roadmapId_createdAt_index on roadmapViews (roadmapId, createdAt); -create index if not exists roadmaps_createdAt_index +create index roadmaps_createdAt_index on roadmaps (createdAt desc); -create index if not exists roadmaps_description_index +create index roadmaps_description_index on roadmaps (description); -create index if not exists roadmaps_name_index +create index roadmaps_name_index on roadmaps (name); -create index if not exists roadmaps_owner_index - on roadmaps (ownerId); +create index roadmaps_owner_index + on roadmaps (userId); -create table if not exists sessionTable +create table sessionTable ( id bigint auto_increment primary key, @@ -177,51 +163,33 @@ create table if not exists sessionTable on delete cascade ); -create index if not exists sessionTable_expires_index +create index sessionTable_expires_index on sessionTable (expires); -create index if not exists sessions_index +create index sessions_index on sessionTable (userId, token); -create table if not exists tabsInfo +create table userInfo ( - id bigint auto_increment - primary key, - roadmapId bigint not null, - userId bigint not null, - content text null, - stringId varchar(255) not null, - constraint tabInfo_roadmaps_id_fk - foreign key (roadmapId) references roadmaps (id) - on delete cascade, - constraint tabInfo_users_id_fk - foreign key (userId) references users (id) - on delete cascade -); - -create table if not exists userInfo -( - id bigint auto_increment + id bigint auto_increment primary key, - userId bigint not null, - profilePictureUrl varchar(255) null, - bio varchar(255) null, - quote varchar(255) null, - blogUrl varchar(255) null, - websiteUrl varchar(255) null, - githubUrl varchar(255) null, + userId bigint not null, + bio varchar(255) null, + quote varchar(255) null, + websiteUrl varchar(255) null, + githubUrl varchar(255) null, constraint userInfo_users_id_fk foreign key (userId) references users (id) on delete cascade ); -create index if not exists userInfo_index +create index userInfo_index on userInfo (userId); -create index if not exists users_index +create index users_index on users (email, name); -create view if not exists sessions as +create view sessions as select `navigo`.`sessionTable`.`id` AS `id`, `navigo`.`sessionTable`.`userId` AS `userId`, `navigo`.`sessionTable`.`token` AS `token`, From 6017b9504b7684912e4086272593ff7e7b5c2815 Mon Sep 17 00:00:00 2001 From: sopy Date: Sun, 3 Sep 2023 19:04:58 +0300 Subject: [PATCH 036/118] Always return JSON on error --- src/server.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index 9b84f6a..7fb7274 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser'; import morgan from 'morgan'; import helmet from 'helmet'; -import express, { Request, Response } from 'express'; +import express, { NextFunction, Request, Response } from 'express'; import logger from 'jet-logger'; import { sessionMiddleware } from '@src/middleware/session'; @@ -68,13 +68,15 @@ app.use((_: Request, res: Response) => { }); // Add error handler -app.use((err: Error, _: Request, res: Response) => { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { logger.err(err, true); let status = HttpStatusCodes.INTERNAL_SERVER_ERROR; if (err instanceof RouteError) { status = err.status; } - return res.status(status).json({ success: false, message: err.message }); + + res.status(status).json({ success: false, message: err.message }); }); // ** Front-End Content ** // From 6839dfcb279e3349bcfad64d9eb5c791d9cd0b2c Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 12:58:05 +0300 Subject: [PATCH 037/118] Remove unused model files and refactor existing model classes This commit removes unnecessary files related to unused models including `Tag`, `TabInfo` and refactors existing model classes, `User`, `RoadmapView`, `Roadmap`, and `Comment`. The refactoring includes updating the class attributes to encapsulation methodology (private attributes and getter/setter methods), and updating class constructors to take in full object parameters rather than individual properties. This change was necessary to improve the project's maintainability and code readability. It reduces the risk of bugs in the long term and enhances conformity with the principles of object-oriented programming. --- src/models/Comment.ts | 66 ------------ src/models/Follower.ts | 75 ++++++++------ src/models/Issue.ts | 167 +++++++++++++++++------------- src/models/IssueComment.ts | 89 ++++++++++++++++ src/models/Roadmap.ts | 204 +++++++++++++++++++------------------ src/models/RoadmapLike.ts | 70 +++++++++++++ src/models/RoadmapView.ts | 107 ++++++++++--------- src/models/Session.ts | 57 +++++++++++ src/models/TabInfo.ts | 65 ------------ src/models/Tags.ts | 5 - src/models/User.ts | 189 ++++++++++++++++++---------------- src/models/UserInfo.ts | 126 +++++++++++------------ 12 files changed, 683 insertions(+), 537 deletions(-) delete mode 100644 src/models/Comment.ts create mode 100644 src/models/IssueComment.ts create mode 100644 src/models/RoadmapLike.ts create mode 100644 src/models/Session.ts delete mode 100644 src/models/TabInfo.ts delete mode 100644 src/models/Tags.ts diff --git a/src/models/Comment.ts b/src/models/Comment.ts deleted file mode 100644 index 121d86b..0000000 --- a/src/models/Comment.ts +++ /dev/null @@ -1,66 +0,0 @@ -const INVALID_CONSTRUCTOR_PARAM = 'content, issueId, and userId args ' + - 'must be strings or numbers.'; - -export interface IComment { - id: bigint; - issueId: bigint; - userId: bigint; - content: string; - createdAt: Date; - updatedAt: Date; -} - -export class Comment implements IComment { - public id: bigint; - public issueId: bigint; - public userId: bigint; - public content: string; - public createdAt: Date; - public updatedAt: Date; - - public constructor( - content: string, - issueId: bigint | number, - userId: bigint | number, - id?: bigint, - createdAt?: Date | string, - updatedAt?: Date | string, - ) { - this.id = BigInt(id ?? -1); - this.issueId = BigInt(issueId); - this.userId = BigInt(userId); - this.content = content; - this.createdAt = (createdAt instanceof Date) ? - createdAt : new Date(createdAt ?? Date.now()); - - this.updatedAt = (updatedAt instanceof Date) ? - updatedAt : new Date(updatedAt ?? Date.now()); - } - - public static from(obj: unknown): Comment { - if (!Comment.isComment(obj)) throw new Error(INVALID_CONSTRUCTOR_PARAM); - - return new Comment( - obj.content, - obj.issueId, - obj.userId, - obj.id, - obj.createdAt, - obj.updatedAt, - ); - } - - public static isComment(obj: unknown): obj is Comment { - return ( - !!obj && - typeof obj === 'object' && - 'content' in obj && - 'issueId' in obj && - 'userId' in obj && - 'id' in obj && - 'createdAt' in obj && - 'updatedAt' in obj - ); - } -} - diff --git a/src/models/Follower.ts b/src/models/Follower.ts index fa90c2a..eee9983 100644 --- a/src/models/Follower.ts +++ b/src/models/Follower.ts @@ -1,40 +1,57 @@ -export const INVALID_CONSTRUCTOR_PARAM = 'Invalid constructor parameter'; - +// Interface export interface IFollower { - id: bigint; - followerId: bigint; - userId: bigint; + readonly id?: bigint; + readonly followerId: bigint; + readonly userId: bigint; + readonly createdAt: Date; } +// Class export class Follower implements IFollower { - public followerId: bigint; - public id: bigint; - public userId: bigint; - - public constructor( - followerId: bigint, - userId: bigint, - id: bigint | null = null, - ) { - this.followerId = followerId; - this.userId = userId; - this.id = (id ?? -1) as bigint; + private _id?: bigint; + private _followerId: bigint; + private _userId: bigint; + private _createdAt: Date; + + public constructor({ id, followerId, userId, createdAt }: IFollower) { + this._id = id; + this._followerId = followerId; + this._userId = userId; + this._createdAt = createdAt; + } + + // Method to modify the properties + public set({ id, followerId, userId, createdAt }: IFollower): void { + if (id !== undefined) this._id = id; + if (followerId !== undefined) this._followerId = followerId; + if (userId !== undefined) this._userId = userId; + if (createdAt !== undefined) this._createdAt = createdAt; } - public static from(param: object): Follower { - if (!Follower.isFollower(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } + public get id(): bigint | undefined { + return this._id; + } - return new Follower( - param.followerId, - param.userId, - param.id, - ); + public get followerId(): bigint { + return this._followerId; } - public static isFollower(param: object): param is IFollower { - return 'followerId' in param && 'userId' in param && 'id' in param; + public get userId(): bigint { + return this._userId; } -} \ No newline at end of file + public get createdAt(): Date { + return this._createdAt; + } + + // Static method to check if an object is of type IFollower + public static isFollower(obj: unknown): obj is IFollower { + return ( + typeof obj === 'object' && + obj !== null && + 'followerId' in obj && + 'userId' in obj && + 'createdAt' in obj + ); + } +} diff --git a/src/models/Issue.ts b/src/models/Issue.ts index 3ca6f29..2db7f57 100644 --- a/src/models/Issue.ts +++ b/src/models/Issue.ts @@ -1,86 +1,109 @@ -export const INVALID_CONSTRUCTOR_PARAM = 'Invalid constructor parameter'; - +// Interface export interface IIssue { - id: bigint; - roadmapId: bigint; - userId: bigint; - open: boolean; - title: string; - content: string; - createdAt: Date; - updatedAt: Date; + readonly id?: bigint; + readonly roadmapId: bigint; + readonly userId: bigint; + readonly open: boolean; + readonly title: string; + readonly content?: string | null; + readonly createdAt?: Date; + readonly updatedAt: Date; } +// Class export class Issue implements IIssue { - public id: bigint; - public roadmapId: bigint; - public userId: bigint; - public open: boolean; - public title: string; - public content: string; - public createdAt: Date; - public updatedAt: Date; + private _id?: bigint; + private _roadmapId: bigint; + private _userId: bigint; + private _open: boolean; + private _title: string; + private _content?: string | null; + private _createdAt?: Date; + private _updatedAt: Date; - public constructor( - roadmapId: bigint, - userId: bigint, - open: boolean, - title: string, - content: string, - id: bigint | null = null, - createdAt: Date = new Date(), - updatedAt: Date = new Date(), - ) { - this.id = (id ?? -1) as bigint; - this.roadmapId = roadmapId; - this.userId = userId; - this.open = open; - this.title = title; - this.content = content; - this.createdAt = createdAt; - this.updatedAt = updatedAt; + public constructor({ + id, + roadmapId, + userId, + open, + title, + content = null, + createdAt, + updatedAt, + }: IIssue) { + this._id = id; + this._roadmapId = roadmapId; + this._userId = userId; + this._open = open; + this._title = title; + this._content = content; + this._createdAt = createdAt; + this._updatedAt = updatedAt; } - public static from(param: object): Issue { - if (!Issue.isIssue(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } + // Method to modify the properties + public set({ + id, + roadmapId, + userId, + open, + title, + content, + createdAt, + updatedAt, + }: IIssue): void { + if (id !== undefined) this._id = id; + if (roadmapId !== undefined) this._roadmapId = roadmapId; + if (userId !== undefined) this._userId = userId; + if (open !== undefined) this._open = open; + if (title !== undefined) this._title = title; + if (content !== undefined) this._content = content; + if (createdAt !== undefined) this._createdAt = createdAt; + if (updatedAt !== undefined) this._updatedAt = updatedAt; + } - return new Issue( - param.roadmapId, - param.userId, - param.open, - param.title, - param.content, - param.id, - param.createdAt, - param.updatedAt, - ); + public get id(): bigint | undefined { + return this._id; } - public static isIssue(param: object): param is IIssue { - return ( - 'id' in param && - 'roadmapId' in param && - 'userId' in param && - 'open' in param && - 'title' in param && - 'content' in param && - 'createdAt' in param && - 'updatedAt' in param - ); + public get roadmapId(): bigint { + return this._roadmapId; } - public toJSONSafe(): unknown { - return { - id: this.id.toString(), - roadmapId: this.roadmapId.toString(), - userId: this.userId.toString(), - open: this.open, - title: this.title, - content: this.content, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - }; + public get userId(): bigint { + return this._userId; + } + + public get open(): boolean { + return this._open; + } + + public get title(): string { + return this._title; + } + + public get content(): string | null | undefined { + return this._content; + } + + public get createdAt(): Date | undefined { + return this._createdAt; + } + + public get updatedAt(): Date { + return this._updatedAt; + } + + // Static method to check if an object is of type IIssue + public static isIssue(obj: unknown): obj is IIssue { + return ( + typeof obj === 'object' && + obj !== null && + 'roadmapId' in obj && + 'userId' in obj && + 'open' in obj && + 'title' in obj && + 'updatedAt' in obj + ); } } diff --git a/src/models/IssueComment.ts b/src/models/IssueComment.ts new file mode 100644 index 0000000..77511bc --- /dev/null +++ b/src/models/IssueComment.ts @@ -0,0 +1,89 @@ +// Interface +export interface IIssueComment { + readonly id?: bigint; + readonly issueId: bigint; + readonly userId: bigint; + readonly content: string; + readonly createdAt: Date; + readonly updatedAt: Date; +} + +// Class +export class IssueComment implements IIssueComment { + private _id?: bigint; + private _issueId: bigint; + private _userId: bigint; + private _content: string; + private _createdAt: Date; + private _updatedAt: Date; + + public constructor({ + id, + issueId, + userId, + content, + createdAt, + updatedAt, + }: IIssueComment) { + this._id = id; + this._issueId = issueId; + this._userId = userId; + this._content = content; + this._createdAt = createdAt; + this._updatedAt = updatedAt; + } + + // Method to modify the properties + public set({ + id, + issueId, + userId, + content, + createdAt, + updatedAt, + }: IIssueComment): void { + if (id !== undefined) this._id = id; + if (issueId !== undefined) this._issueId = issueId; + if (userId !== undefined) this._userId = userId; + if (content !== undefined) this._content = content; + if (createdAt !== undefined) this._createdAt = createdAt; + if (updatedAt !== undefined) this._updatedAt = updatedAt; + } + + public get id(): bigint | undefined { + return this._id; + } + + public get issueId(): bigint { + return this._issueId; + } + + public get userId(): bigint { + return this._userId; + } + + public get content(): string { + return this._content; + } + + public get createdAt(): Date { + return this._createdAt; + } + + public get updatedAt(): Date { + return this._updatedAt; + } + + // Static method to check if an object is of type IIssueComment + public static isIssueComment(obj: unknown): obj is IIssueComment { + return ( + typeof obj === 'object' && + obj !== null && + 'issueId' in obj && + 'userId' in obj && + 'content' in obj && + 'createdAt' in obj && + 'updatedAt' in obj + ); + } +} diff --git a/src/models/Roadmap.ts b/src/models/Roadmap.ts index 43fda60..f016a93 100644 --- a/src/models/Roadmap.ts +++ b/src/models/Roadmap.ts @@ -1,111 +1,121 @@ -// variables -export const INVALID_CONSTRUCTOR_PARAM = 'Invalid constructor parameter'; - -// interface +// Interface export interface IRoadmap { - id?: bigint; - ownerId: bigint; - name: string; - description: string; - createdAt: Date; - updatedAt: Date; - isPublic: boolean; - data: string; // base64 encoded json -} - -export interface RoadmapMini { - id: bigint | string; - name: string; - description: string; - likes: bigint | string; - isLiked: boolean | number; - ownerName: string; - ownerId: bigint | string; + readonly id?: bigint; + readonly name: string; + readonly description: string; + readonly userId: bigint; + readonly isPublic: boolean; + readonly isDraft?: boolean; + readonly data: string; + readonly createdAt: Date; + readonly updatedAt: Date; } -// class +// Class export class Roadmap implements IRoadmap { - public id: bigint; - public ownerId: bigint; - public name: string; - public description: string; - public createdAt: Date; - public updatedAt: Date; - public isPublic: boolean; - public data: string; // base64 encoded json + private _id?: bigint; + private _name: string; + private _description: string; + private _userId: bigint; + private _isPublic: boolean; + private _isDraft?: boolean; + private _data: string; + private _createdAt: Date; + private _updatedAt: Date; - /** - * Constructor() - */ - public constructor( - ownerId: bigint | string, - name: string, - description: string, - data: string, - createdAt: Date | string = new Date(), - updatedAt: Date | string = new Date(), - isPublic = true, - id: bigint | string | null = null, - ) { - this.id = BigInt(id ?? -1); - this.ownerId = BigInt(ownerId); - this.name = name; - this.description = description; - this.createdAt = createdAt instanceof Date ? - createdAt : new Date(createdAt); - this.updatedAt = updatedAt instanceof Date ? - updatedAt : new Date(updatedAt); - this.isPublic = isPublic; - this.data = data; + public constructor({ + id, + name, + description, + userId, + isPublic, + isDraft = false, + data, + createdAt, + updatedAt, + }: IRoadmap) { + this._id = id; + this._name = name; + this._description = description; + this._userId = userId; + this._isPublic = isPublic; + this._isDraft = isDraft; + this._data = data; + this._createdAt = createdAt; + this._updatedAt = updatedAt; } - // Get roadmap instance from object. - public static from(param: object): Roadmap { - // Check is roadmap - if (!Roadmap.isRoadmap(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } + // Method to modify the properties + public set({ + id, + name, + description, + userId, + isPublic, + isDraft, + data, + createdAt, + updatedAt, + }: IRoadmap): void { + if (id !== undefined) this._id = id; + if (name !== undefined) this._name = name; + if (description !== undefined) this._description = description; + if (userId !== undefined) this._userId = userId; + if (isPublic !== undefined) this._isPublic = isPublic; + if (isDraft !== undefined) this._isDraft = isDraft; + if (data !== undefined) this._data = data; + if (createdAt !== undefined) this._createdAt = createdAt; + if (updatedAt !== undefined) this._updatedAt = updatedAt; + } - // Create instance - return new Roadmap( - param.ownerId, - param.name, - param.description, - param.data, - param.createdAt, - param.updatedAt, - param.isPublic, - param.id, - ); + public get id(): bigint | undefined { + return this._id; } - // Check if object is a roadmap. - public static isRoadmap(param: object): param is IRoadmap { - return ( - param && - 'id' in param && - 'ownerId' in param && - 'name' in param && - 'description' in param && - 'createdAt' in param && - 'updatedAt' in param && - 'isPublic' in param && - 'data' in param - ); + public get name(): string { + return this._name; } - // toJSONSafe() - public toJSONSafe(): object { - return { - id: this.id.toString(), - ownerId: this.ownerId.toString(), - name: this.name, - description: this.description, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - isPublic: this.isPublic, - data: this.data, - }; + public get description(): string { + return this._description; + } + + public get userId(): bigint { + return this._userId; + } + + public get isPublic(): boolean { + return this._isPublic; } -} + public get isDraft(): boolean | undefined { + return this._isDraft; + } + + public get data(): string { + return this._data; + } + + public get createdAt(): Date { + return this._createdAt; + } + + public get updatedAt(): Date { + return this._updatedAt; + } + + // Static method to check if an object is of type IRoadmap + public static isRoadmap(obj: unknown): obj is IRoadmap { + return ( + typeof obj === 'object' && + obj !== null && + 'name' in obj && + 'description' in obj && + 'userId' in obj && + 'isPublic' in obj && + 'data' in obj && + 'createdAt' in obj && + 'updatedAt' in obj + ); + } +} diff --git a/src/models/RoadmapLike.ts b/src/models/RoadmapLike.ts new file mode 100644 index 0000000..caade1f --- /dev/null +++ b/src/models/RoadmapLike.ts @@ -0,0 +1,70 @@ +// TypeScript Interface +export interface IRoadmapLike { + readonly id?: bigint; + readonly roadmapId: bigint; + readonly userId: bigint; + readonly value?: number; + readonly createdAt?: Date; +} + +// TypeScript Class +export class RoadmapLike implements IRoadmapLike { + private _id?: bigint; + private _roadmapId: bigint; + private _userId: bigint; + private _value?: number; + private _createdAt?: Date; + + public constructor({ + id, + roadmapId, + userId, + value, + createdAt, + }: IRoadmapLike) { + this._id = id; + this._roadmapId = roadmapId; + this._userId = userId; + this._value = value; + this._createdAt = createdAt; + } + + // Method to modify the properties + public set({ id, roadmapId, userId, value, createdAt }: IRoadmapLike): void { + if (id !== undefined) this._id = id; + if (roadmapId !== undefined) this._roadmapId = roadmapId; + if (userId !== undefined) this._userId = userId; + if (value !== undefined) this._value = value; + if (createdAt !== undefined) this._createdAt = createdAt; + } + + public get id(): bigint | undefined { + return this._id; + } + + public get roadmapId(): bigint { + return this._roadmapId; + } + + public get userId(): bigint { + return this._userId; + } + + public get value(): number | undefined { + return this._value; + } + + public get createdAt(): Date | undefined { + return this._createdAt; + } + + // Static method to check if an object is of type IRoadmapLike + public static isRoadmapLike(obj: unknown): obj is IRoadmapLike { + return ( + typeof obj === 'object' && + obj !== null && + 'roadmapId' in obj && + 'userId' in obj + ); + } +} diff --git a/src/models/RoadmapView.ts b/src/models/RoadmapView.ts index a06e4cf..faaf821 100644 --- a/src/models/RoadmapView.ts +++ b/src/models/RoadmapView.ts @@ -1,59 +1,66 @@ +// TypeScript Interface export interface IRoadmapView { - id: bigint; - userId: bigint; - roadmapId: bigint; - createdAt: Date; - full: boolean; + readonly id?: bigint; + readonly userId: bigint; + readonly roadmapId: bigint; + readonly full: boolean; + readonly createdAt: Date; } +// TypeScript Class export class RoadmapView implements IRoadmapView { - public id: bigint; - public userId: bigint; - public roadmapId: bigint; - public createdAt: Date; - public full: boolean; - - public constructor( - userId: bigint | number, - roadmapId: bigint | number, - full: boolean, - createdAt: Date | string = new Date(), - id?: bigint | number | string, - ) { - this.id = BigInt(id || 0); - this.userId = BigInt(userId); - this.roadmapId = BigInt(roadmapId); - this.full = full; - - if (typeof createdAt === 'string') { - this.createdAt = new Date(createdAt); - } else this.createdAt = createdAt; - } - - // Get roadmap instance from object. - public static from(param: object): RoadmapView { - // chekc if param is a roadmapView instance - if (!this.isRoadmapView(param)) throw new Error('Invalid param'); - - // return new roadmapView instance - return new RoadmapView( - param.userId, - param.roadmapId, - param.full, - param.createdAt, - param.id, - ); + private _id?: bigint; + private _userId: bigint; + private _roadmapId: bigint; + private _full: boolean; + private _createdAt: Date; + + public constructor({ id, userId, roadmapId, full, createdAt }: IRoadmapView) { + this._id = id; + this._userId = userId; + this._roadmapId = roadmapId; + this._full = full; + this._createdAt = createdAt; + } + + // Method to modify the properties + public set({ id, userId, roadmapId, full, createdAt }: IRoadmapView): void { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (roadmapId !== undefined) this._roadmapId = roadmapId; + if (full !== undefined) this._full = full; + if (createdAt !== undefined) this._createdAt = createdAt; + } + + public get id(): bigint | undefined { + return this._id; + } + + public get userId(): bigint { + return this._userId; } - // Check if object is a roadmapView - public static isRoadmapView(param: object): param is IRoadmapView { + public get roadmapId(): bigint { + return this._roadmapId; + } + + public get full(): boolean { + return this._full; + } + + public get createdAt(): Date { + return this._createdAt; + } + + // Static method to check if an object is of type IRoadmapView + public static isRoadmapView(obj: unknown): obj is IRoadmapView { return ( - param && - 'id' in param && - 'userId' in param && - 'full' in param && - 'roadmapId' in param && - 'createdAt' in param + typeof obj === 'object' && + obj !== null && + 'userId' in obj && + 'roadmapId' in obj && + 'full' in obj && + 'createdAt' in obj ); } -} \ No newline at end of file +} diff --git a/src/models/Session.ts b/src/models/Session.ts new file mode 100644 index 0000000..ee33c03 --- /dev/null +++ b/src/models/Session.ts @@ -0,0 +1,57 @@ +// Interface +export interface ISession { + readonly id?: bigint; + readonly userId: bigint; + readonly token: string; + readonly expires: Date; +} + +// Class +export class Session implements ISession { + private _id?: bigint; + private _userId: bigint; + private _token: string; + private _expires: Date; + + public constructor({ id, userId, token, expires }: ISession) { + this._id = id; + this._userId = userId; + this._token = token; + this._expires = expires; + } + + // Method to modify the properties + public set({ id, userId, token, expires }: ISession): void { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (token !== undefined) this._token = token; + if (expires !== undefined) this._expires = expires; + } + + public get id(): bigint | undefined { + return this._id; + } + + public get userId(): bigint { + return this._userId; + } + + public get token(): string { + return this._token; + } + + public get expires(): Date { + return this._expires; + } + + // Static method to check if an object is of type ISession + public static isSession(obj: unknown): obj is ISession { + return ( + typeof obj === 'object' && + obj !== null && + 'userId' in obj && + 'token' in obj && + 'expires' in obj + ); + } +} diff --git a/src/models/TabInfo.ts b/src/models/TabInfo.ts deleted file mode 100644 index 835de48..0000000 --- a/src/models/TabInfo.ts +++ /dev/null @@ -1,65 +0,0 @@ -export const INVALID_CONSTRUCTOR_PARAM = 'Invalid constructor parameter'; - -export interface ITabInfo { - id: bigint; - stringId: string; - roadmapId: bigint; - userId: bigint; - content: string; -} - -export class TabInfo implements ITabInfo { - public id: bigint; - public stringId: string; - public roadmapId: bigint; - public userId: bigint; - public content: string; - - public constructor( - roadmapId: bigint, - userId: bigint, - content: string, // base64 encoded json - stringId: string, - id: bigint | null = null, - ) { - this.stringId = stringId; - this.id = (id ?? -1) as bigint; - this.roadmapId = roadmapId; - this.userId = userId; - this.content = content; - } - - public static isTabInfo(param: object): param is ITabInfo { - return ( - 'id' in param && - 'stringId' in param && - 'roadmapId' in param && - 'userId' in param && - 'content' in param - ); - } - - public toJSONSafe(): object { - return { - id: this.id.toString(), - stringId: this.stringId, - roadmapId: this.roadmapId.toString(), - userId: this.userId.toString(), - content: this.content, - }; - } - - public static from(param: object): ITabInfo { - if (!TabInfo.isTabInfo(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } - - return new TabInfo( - param.roadmapId, - param.userId, - param.content, - param.stringId, - param.id, - ); - } -} diff --git a/src/models/Tags.ts b/src/models/Tags.ts deleted file mode 100644 index 993fb01..0000000 --- a/src/models/Tags.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type Tag = { - id: bigint; - roadmapId: bigint; - name: string; -} \ No newline at end of file diff --git a/src/models/User.ts b/src/models/User.ts index 7f4e903..bb0cca0 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,104 +1,117 @@ -// **** Variables **** // +// Interface +export interface IUser { + readonly id?: bigint; + readonly avatar?: string | null; + readonly name: string; + readonly email: string; + readonly role?: number | null; + readonly pwdHash?: string | null; + readonly googleId?: string | null; + readonly githubId?: string | null; + readonly createdAt: Date; +} -const INVALID_CONSTRUCTOR_PARAM = - 'nameOrObj arg must a string or an object ' + - 'with the appropriate user keys.'; +// Class +export class User implements IUser { + private _id?: bigint; + private _avatar?: string | null; + private _name: string; + private _email: string; + private _role?: number | null; + private _pwdHash?: string | null; + private _googleId?: string | null; + private _githubId?: string | null; + private _createdAt: Date; -export enum UserRoles { - Standard, - Admin, -} + public constructor({ + id, + avatar = null, + name, + email, + role = null, + pwdHash = null, + googleId = null, + githubId = null, + createdAt, + }: IUser) { + this._id = id; + this._avatar = avatar; + this._name = name; + this._email = email; + this._role = role; + this._pwdHash = pwdHash; + this._googleId = googleId; + this._githubId = githubId; + this._createdAt = createdAt; + } -// **** Types **** // + // Method to modify the properties + public set({ + id, + avatar, + name, + email, + role, + pwdHash, + googleId, + githubId, + createdAt, + }: IUser): void { + if (id !== undefined) this._id = id; + if (avatar !== undefined) this._avatar = avatar; + if (name !== undefined) this._name = name; + if (email !== undefined) this._email = email; + if (role !== undefined) this._role = role; + if (pwdHash !== undefined) this._pwdHash = pwdHash; + if (googleId !== undefined) this._googleId = googleId; + if (githubId !== undefined) this._githubId = githubId; + if (createdAt !== undefined) this._createdAt = createdAt; + } -export interface IUser { - id: bigint; - name: string; - email: string; - pwdHash?: string; - role?: UserRoles; - googleId?: string | null; - githubId?: string | null; -} + public get id(): bigint | undefined { + return this._id; + } -export interface ISessionUser { - id: number; - email: string; - name: string; - role: IUser['role']; -} + public get avatar(): string | null | undefined { + return this._avatar; + } -// **** User **** // + public get name(): string { + return this._name; + } -class User implements IUser { - public id: bigint; - public name: string; - public email: string; - public role?: UserRoles; - public pwdHash?: string; - public googleId?: string | null; - public githubId?: string | null; + public get email(): string { + return this._email; + } - /** - * Constructor() - */ - public constructor( - name?: string, - email?: string, - role?: UserRoles, - pwdHash?: string, - id?: bigint, // id last cause usually set by db - googleId?: string | null, - githubId?: string | null, - ) { - this.name = name ?? ''; - this.email = email ?? ''; - this.role = role ?? UserRoles.Standard; - this.pwdHash = pwdHash ?? ''; - this.id = BigInt(id ?? -1); - this.googleId = googleId ?? null; - this.githubId = githubId ?? null; + public get role(): number | null | undefined { + return this._role; } - /** - * Get userDisplay instance from object. - */ - public static from(param: object): User { - // Check is userDisplay - if (!User.isUser(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } - // Get userDisplay instance - const p = param as IUser; - return new User( - p.name, - p.email, - p.role, - p.pwdHash, - p.id, - p.googleId, - p.githubId, - ); + public get pwdHash(): string | null | undefined { + return this._pwdHash; + } + + public get googleId(): string | null | undefined { + return this._googleId; + } + + public get githubId(): string | null | undefined { + return this._githubId; + } + + public get createdAt(): Date { + return this._createdAt; } - /** - * Is this an object which contains all the userDisplay keys. - */ - public static isUser(this: void, arg: unknown): boolean { + // Static method to check if an object is of type IUser + public static isUser(obj: unknown): obj is IUser { return ( - !!arg && - typeof arg === 'object' && - 'id' in arg && - 'email' in arg && - 'name' in arg && - 'role' in arg && - 'pwdHash' in arg && - 'googleId' in arg && - 'githubId' in arg + typeof obj === 'object' && + obj !== null && + 'name' in obj && + 'email' in obj && + 'createdAt' in obj ); } } - -// **** Export default **** // - -export default User; diff --git a/src/models/UserInfo.ts b/src/models/UserInfo.ts index 4618195..53433ad 100644 --- a/src/models/UserInfo.ts +++ b/src/models/UserInfo.ts @@ -1,78 +1,74 @@ -const INVALID_CONSTRUCTOR_PARAM = - 'nameOrObj arg must a string or an object ' + - 'with the appropriate user keys.'; - +// TypeScript Interface export interface IUserInfo { - id: bigint; - userId: bigint; - profilePictureUrl: string; - bio: string; - quote: string; - blogUrl: string; - websiteUrl: string; - githubUrl: string; + readonly id?: bigint; + readonly userId: bigint; + readonly bio?: string | null; + readonly quote?: string | null; + readonly websiteUrl?: string | null; + readonly githubUrl?: string | null; } +// TypeScript Class export class UserInfo implements IUserInfo { - public id: bigint; - public userId: bigint; - public profilePictureUrl: string; - public bio: string; - public quote: string; - public blogUrl: string; - public websiteUrl: string; - public githubUrl: string; + private _id?: bigint; + private _userId: bigint; + private _bio?: string | null; + private _quote?: string | null; + private _websiteUrl?: string | null; + private _githubUrl?: string | null; + + public constructor({ + id, + userId, + bio = null, + quote = null, + websiteUrl = null, + githubUrl = null, + }: IUserInfo) { + this._id = id; + this._userId = userId; + this._bio = bio; + this._quote = quote; + this._websiteUrl = websiteUrl; + this._githubUrl = githubUrl; + } - public constructor( - userId: bigint, - profilePictureUrl = '', - bio = '', - quote = '', - blogUrl = '', - websiteUrl = '', - githubUrl = '', - id = BigInt(-1), // id last cause usually set by db - ) { - this.userId = userId; - this.profilePictureUrl = profilePictureUrl; - this.bio = bio; - this.quote = quote; - this.blogUrl = blogUrl; - this.websiteUrl = websiteUrl; - this.githubUrl = githubUrl; - this.id = id; + // Method to modify the properties + public set({ id, userId, bio, quote, websiteUrl, githubUrl }: IUserInfo) { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (bio !== undefined) this._bio = bio; + if (quote !== undefined) this._quote = quote; + if (websiteUrl !== undefined) this._websiteUrl = websiteUrl; + if (githubUrl !== undefined) this._githubUrl = githubUrl; } - public static from(param: object): UserInfo { - // check if param is user - if (!UserInfo.isUserInfo(param)) { - throw new Error(INVALID_CONSTRUCTOR_PARAM); - } + public get id(): bigint | undefined { + return this._id; + } + + public get userId(): bigint { + return this._userId; + } - const info = param as IUserInfo; + public get bio(): string | null | undefined { + return this._bio; + } + + public get quote(): string | null | undefined { + return this._quote; + } + + public get websiteUrl(): string | null | undefined { + return this._websiteUrl; + } - // create user - return new UserInfo( - info.userId, - info.profilePictureUrl, - info.bio, - info.quote, - info.blogUrl, - info.websiteUrl, - info.githubUrl, - ); + public get githubUrl(): string | null | undefined { + return this._githubUrl; } - public static isUserInfo(param: object): boolean { - return ( - 'id' in param && - 'userId' in param && - 'profilePictureUrl' in param && - 'bio' in param && - 'quote' in param && - 'blogUrl' in param && - 'websiteUrl' in param && - 'githubUrl' in param - ); + // Static method to check if an object is of type IUserInfo + public static isUserInfo(obj: unknown): obj is IUserInfo { + return typeof obj === 'object' && obj !== null && 'userId' in obj; } } From 411455320df35c5ebdbfabbfe88be5daeb51b0c1 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:16:11 +0300 Subject: [PATCH 038/118] Create eslint.yml --- .github/workflows/eslint.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/eslint.yml diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000..002205e --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,50 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# ESLint is a tool for identifying and reporting on patterns +# found in ECMAScript/JavaScript code. +# More details at https://github.com/eslint/eslint +# and https://eslint.org + +name: ESLint + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '18 4 * * 6' + +jobs: + eslint: + name: Run eslint scanning + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install ESLint + run: | + npm install eslint@8.10.0 + npm install @microsoft/eslint-formatter-sarif@2.1.7 + + - name: Run ESLint + run: npx eslint . + --config .eslintrc.js + --ext .js,.jsx,.ts,.tsx + --format @microsoft/eslint-formatter-sarif + --output-file eslint-results.sarif + continue-on-error: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: eslint-results.sarif + wait-for-processing: true From 529524b895a5479cc153673bef32e1c20ec6944a Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:20:59 +0300 Subject: [PATCH 039/118] Update eslint.yml Updated to use the right config --- .github/workflows/eslint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 002205e..52f4e8e 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -37,7 +37,7 @@ jobs: - name: Run ESLint run: npx eslint . - --config .eslintrc.js + --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif From 6bab1fed43a8f8857a721d9d397504d7a796dcf0 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:31:34 +0300 Subject: [PATCH 040/118] Update eslint.yml --- .github/workflows/eslint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 52f4e8e..74219be 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -35,6 +35,13 @@ jobs: npm install eslint@8.10.0 npm install @microsoft/eslint-formatter-sarif@2.1.7 + - name: Run ESLint with --fix + run: npx eslint . --config .eslintrc.js --ext .js,.jsx,.ts,.tsx --fix + + - name: Commit changes (if any) + run: | + git diff --exit-code || (git config --global user.email "github-actions@github.com" && git config --global user.name "GitHub Actions" && git commit -am "Fix ESLint issues" && git push) + - name: Run ESLint run: npx eslint . --config .eslintrc.json From eae2904089643665e655177764ac695e4d52b132 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:33:36 +0300 Subject: [PATCH 041/118] Update eslint.yml --- .github/workflows/eslint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 74219be..2b3bfe2 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -36,7 +36,7 @@ jobs: npm install @microsoft/eslint-formatter-sarif@2.1.7 - name: Run ESLint with --fix - run: npx eslint . --config .eslintrc.js --ext .js,.jsx,.ts,.tsx --fix + run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix - name: Commit changes (if any) run: | From b0ad9cb56216ae4525be983b6e480b28539feaf7 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:38:55 +0300 Subject: [PATCH 042/118] Update eslint.yml --- .github/workflows/eslint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 2b3bfe2..c0ff782 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -36,7 +36,7 @@ jobs: npm install @microsoft/eslint-formatter-sarif@2.1.7 - name: Run ESLint with --fix - run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix + run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" - name: Commit changes (if any) run: | From 62b2c01397bd81c6362156758bb2208df759542e Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:48:41 +0300 Subject: [PATCH 043/118] Update eslint.yml --- .github/workflows/eslint.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index c0ff782..7fdf740 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -22,6 +22,8 @@ jobs: eslint: name: Run eslint scanning runs-on: ubuntu-latest + env: + PAT_TOKEN: ${{ secrets.PAT }} permissions: contents: read security-events: write @@ -32,15 +34,15 @@ jobs: - name: Install ESLint run: | - npm install eslint@8.10.0 - npm install @microsoft/eslint-formatter-sarif@2.1.7 + npm install --save-dev eslint@8.10.0 + npm install --save-dev @microsoft/eslint-formatter-sarif@2.1.7 - name: Run ESLint with --fix run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" - name: Commit changes (if any) run: | - git diff --exit-code || (git config --global user.email "github-actions@github.com" && git config --global user.name "GitHub Actions" && git commit -am "Fix ESLint issues" && git push) + git diff --exit-code || (git config --global user.email "github-actions@github.com" && git config --global user.name "GitHub Actions" && git commit -am "Fix ESLint issues" && git push https://$PAT_TOKEN@github.com/NavigoLearn/API.git) - name: Run ESLint run: npx eslint . From 44814ee2a825c63d4578be1ad1b83313ad0dbbaf Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:53:32 +0300 Subject: [PATCH 044/118] Update eslint.yml --- .github/workflows/eslint.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 7fdf740..616cef3 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -22,8 +22,6 @@ jobs: eslint: name: Run eslint scanning runs-on: ubuntu-latest - env: - PAT_TOKEN: ${{ secrets.PAT }} permissions: contents: read security-events: write @@ -41,8 +39,10 @@ jobs: run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" - name: Commit changes (if any) - run: | - git diff --exit-code || (git config --global user.email "github-actions@github.com" && git config --global user.name "GitHub Actions" && git commit -am "Fix ESLint issues" && git push https://$PAT_TOKEN@github.com/NavigoLearn/API.git) + uses: ad-m/github-push-action@v0.7.0 + with: + branch: ${{ github.ref }} + github_token: ${{ secrets.PAT }} - name: Run ESLint run: npx eslint . From fd26824680d4268c9e111bb164194450b034d2ff Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 14:56:28 +0300 Subject: [PATCH 045/118] Update eslint.yml --- .github/workflows/eslint.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 616cef3..833de56 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -37,9 +37,15 @@ jobs: - name: Run ESLint with --fix run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" + + - name: Commit files + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -a -m "Add changes" - name: Commit changes (if any) - uses: ad-m/github-push-action@v0.7.0 + uses: ad-m/github-push-action@v0.6.0 with: branch: ${{ github.ref }} github_token: ${{ secrets.PAT }} From 032fa51fd500ae599a29d272a04fdc90a663638f Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 15:06:53 +0300 Subject: [PATCH 046/118] Update eslint.yml --- .github/workflows/eslint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 833de56..a4170e2 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -23,7 +23,7 @@ jobs: name: Run eslint scanning runs-on: ubuntu-latest permissions: - contents: read + contents: write # required to comit eslint fix security-events: write actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status steps: From 715e41cf841590fab51a9ba3072bd35362c8d9d9 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 15:20:40 +0300 Subject: [PATCH 047/118] Update eslint.yml --- .github/workflows/eslint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index a4170e2..bb22c87 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -44,8 +44,8 @@ jobs: git config --local user.name "github-actions[bot]" git commit -a -m "Add changes" - - name: Commit changes (if any) - uses: ad-m/github-push-action@v0.6.0 + - name: Push changes + uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef with: branch: ${{ github.ref }} github_token: ${{ secrets.PAT }} From 187bd085b67e5db9b1a02615b765cba85e397add Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Sep 2023 12:25:33 +0000 Subject: [PATCH 048/118] Add changes --- package-lock.json | 250 +++++++----------- package.json | 3 +- src/helpers/apiResponses.ts | 2 +- src/routes/RoadmapsRouter.ts | 2 +- src/routes/roadmapsRoutes/RoadmapIssues.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 2 +- .../issuesRoutes/CommentsRouter.ts | 2 +- .../issuesRoutes/IssuesUpdate.ts | 2 +- src/routes/usersRoutes/UsersUpdate.ts | 22 +- 10 files changed, 121 insertions(+), 168 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9f37c7..83cbb8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "ts-command-line-args": "^2.5.1" }, "devDependencies": { + "@microsoft/eslint-formatter-sarif": "^2.1.7", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", @@ -42,7 +43,7 @@ "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.45.0", + "eslint": "^8.10.0", "eslint-plugin-node": "^11.1.0", "find": "^0.3.0", "fs-extra": "^11.1.1", @@ -104,14 +105,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", + "espree": "^9.4.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -126,42 +127,20 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.5" + "minimatch": "^3.0.4" }, "engines": { "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -279,6 +258,18 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@microsoft/eslint-formatter-sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-formatter-sarif/-/eslint-formatter-sarif-2.1.7.tgz", + "integrity": "sha512-gDNc2elHjX0eqk34HxxRxEwEL49SrvXImOoK1bZHq7IDYfuY1xY/CUx8/gOWgvwf6Qv2Yy3HirzjIvKXKH82vQ==", + "dev": true, + "dependencies": { + "eslint": "^8.9.0", + "jschardet": "latest", + "lodash": "^4.17.14", + "utf8": "^3.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1547,48 +1538,46 @@ } }, "node_modules/eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz", + "integrity": "sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.2.0", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", - "esquery": "^1.4.2", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", "ignore": "^5.2.0", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "bin": { "eslint": "bin/eslint.js" @@ -1713,6 +1702,33 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -2023,22 +2039,6 @@ "node": ">=4.0.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -2209,6 +2209,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -2274,9 +2280,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2556,15 +2562,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2682,6 +2679,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jschardet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.0.0.tgz", + "integrity": "sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2764,21 +2770,6 @@ "node": ">= 0.8.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3242,36 +3233,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3292,15 +3253,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4223,6 +4175,12 @@ "punycode": "^2.1.0" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4236,6 +4194,12 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4422,18 +4386,6 @@ "engines": { "node": ">=6" } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index fb87efb..8722a74 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "ts-command-line-args": "^2.5.1" }, "devDependencies": { + "@microsoft/eslint-formatter-sarif": "^2.1.7", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", @@ -65,7 +66,7 @@ "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.45.0", + "eslint": "^8.10.0", "eslint-plugin-node": "^11.1.0", "find": "^0.3.0", "fs-extra": "^11.1.1", diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index b18e494..833b84b 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -60,7 +60,7 @@ export function serverError(res: Response): void { export function userNotFound(res: Response): void { res.status(HttpStatusCode.NotFound).json({ - error: "User couldn't be found", + error: 'User couldn\'t be found', success: false, }); } diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 846697d..a6e104a 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -13,7 +13,7 @@ import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; import RoadmapTabsInfo from '@src/routes/roadmapsRoutes/RoadmapsTabsInfo'; import envVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const RoadmapsRouter = Router(); diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts index 3384910..c1422b4 100644 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -9,7 +9,7 @@ import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; import IssuesUpdate from '@src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate'; import Comments from '@src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const RoadmapIssues = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts index e36a87b..6bf53f0 100644 --- a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts +++ b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts @@ -144,7 +144,7 @@ RoadmapTabsInfo.post( if (roadmap.ownerId !== req.session?.userId) return res .status(HttpStatusCodes.FORBIDDEN) - .json({ error: "You don't have permission to edit this roadmap." }); + .json({ error: 'You don\'t have permission to edit this roadmap.' }); const tabData = await tabDataReq; diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index aca7157..fab62be 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -8,7 +8,7 @@ import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; import { Tag } from '@src/models/Tags'; import User from '@src/models/User'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const RoadmapsUpdate = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts index 20503b3..04a9811 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts @@ -9,7 +9,7 @@ import { Issue } from '@src/models/Issue'; import User from '@src/models/User'; import Database from '@src/util/DatabaseDriver'; import { Comment } from '@src/models/Comment'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const CommentsRouter = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts index 254f268..f91f734 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts @@ -7,7 +7,7 @@ import { } from '@src/middleware/session'; import { Issue } from '@src/models/Issue'; import { Roadmap } from '@src/models/Roadmap'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const IssuesUpdate = Router({ mergeParams: true }); diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index caeab5d..2173aa8 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -10,7 +10,7 @@ import { checkEmail } from '@src/util/EmailUtil'; import { comparePassword } from '@src/util/LoginUtil'; import User from '@src/models/User'; import { UserInfo } from '@src/models/UserInfo'; -import validateSession from "@src/validators/validateSession"; +import validateSession from '@src/validators/validateSession'; const UsersUpdate = Router({ mergeParams: true }); @@ -89,8 +89,8 @@ UsersUpdate.post( async (req: RequestWithSession, res) => { // get userId from request const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access let bio: string = req.body?.bio; // send error json @@ -138,8 +138,8 @@ UsersUpdate.post( async (req: RequestWithSession, res) => { // get userId from request const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access let quote: string = req.body?.quote; // send error json @@ -225,8 +225,8 @@ UsersUpdate.post( async (req: RequestWithSession, res) => { // get userId from request const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access let blogUrl: string = req.body?.blogUrl; // send error json @@ -273,8 +273,8 @@ UsersUpdate.post( Paths.Users.Update.WebsiteUrl, async (req: RequestWithSession, res) => { const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access let websiteUrl: string = req.body?.websiteUrl; // send error json @@ -321,8 +321,8 @@ UsersUpdate.post( Paths.Users.Update.GithubUrl, async (req: RequestWithSession, res) => { const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access let githubUrl: string = req.body?.githubUrl; // send error json From c8c84467704a783faa9a93fac6366b6f265d0753 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 16:26:45 +0300 Subject: [PATCH 049/118] Enhance data model clarity, apply defaults for building objects In response to a need for improved clarity, labels were updated for better distinction between complete object interfaces and construction interfaces across all model files. Also, default values were implemented in constructors to allow the creation of incomplete model objects. This change aims to enhance flexibility during object creation and as well to improve readability and maintainability of the codebase. --- src/models/Follower.ts | 23 ++++++++++++++---- src/models/Issue.ts | 36 ++++++++++++++++++---------- src/models/IssueComment.ts | 26 +++++++++++++------- src/models/Roadmap.ts | 36 ++++++++++++++++++---------- src/models/RoadmapLike.ts | 33 +++++++++++++++---------- src/models/RoadmapView.ts | 27 ++++++++++++++++----- src/models/Session.ts | 21 ++++++++++++---- src/models/User.ts | 49 ++++++++++++++++++++++++-------------- src/models/UserInfo.ts | 36 ++++++++++++++++++---------- src/sql/setup.sql | 24 +++++++++---------- 10 files changed, 209 insertions(+), 102 deletions(-) diff --git a/src/models/Follower.ts b/src/models/Follower.ts index eee9983..211127e 100644 --- a/src/models/Follower.ts +++ b/src/models/Follower.ts @@ -1,19 +1,32 @@ -// Interface +// Interface for full Follower object export interface IFollower { - readonly id?: bigint; + readonly id: bigint; readonly followerId: bigint; readonly userId: bigint; readonly createdAt: Date; } +// Interface for constructing a Follower +interface IFollowerConstructor { + readonly id?: bigint; + readonly followerId: bigint; + readonly userId: bigint; + readonly createdAt?: Date; +} + // Class export class Follower implements IFollower { - private _id?: bigint; + private _id: bigint; private _followerId: bigint; private _userId: bigint; private _createdAt: Date; - public constructor({ id, followerId, userId, createdAt }: IFollower) { + public constructor({ + id = -1n, + followerId, + userId, + createdAt = new Date(), + }: IFollowerConstructor) { this._id = id; this._followerId = followerId; this._userId = userId; @@ -28,7 +41,7 @@ export class Follower implements IFollower { if (createdAt !== undefined) this._createdAt = createdAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } diff --git a/src/models/Issue.ts b/src/models/Issue.ts index 2db7f57..1c4d4bc 100644 --- a/src/models/Issue.ts +++ b/src/models/Issue.ts @@ -1,5 +1,17 @@ -// Interface +// Interface for full Issue object export interface IIssue { + readonly id: bigint; + readonly roadmapId: bigint; + readonly userId: bigint; + readonly open: boolean; + readonly title: string; + readonly content: string | null; + readonly createdAt: Date; + readonly updatedAt: Date; +} + +// Interface for constructing an Issue +interface IIssueConstructor { readonly id?: bigint; readonly roadmapId: bigint; readonly userId: bigint; @@ -7,30 +19,30 @@ export interface IIssue { readonly title: string; readonly content?: string | null; readonly createdAt?: Date; - readonly updatedAt: Date; + readonly updatedAt?: Date; } // Class export class Issue implements IIssue { - private _id?: bigint; + private _id: bigint; private _roadmapId: bigint; private _userId: bigint; private _open: boolean; private _title: string; - private _content?: string | null; - private _createdAt?: Date; + private _content: string | null; + private _createdAt: Date; private _updatedAt: Date; public constructor({ - id, + id = -1n, roadmapId, userId, open, title, content = null, - createdAt, - updatedAt, - }: IIssue) { + createdAt = new Date(), + updatedAt = new Date(), + }: IIssueConstructor) { this._id = id; this._roadmapId = roadmapId; this._userId = userId; @@ -62,7 +74,7 @@ export class Issue implements IIssue { if (updatedAt !== undefined) this._updatedAt = updatedAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } @@ -82,11 +94,11 @@ export class Issue implements IIssue { return this._title; } - public get content(): string | null | undefined { + public get content(): string | null { return this._content; } - public get createdAt(): Date | undefined { + public get createdAt(): Date { return this._createdAt; } diff --git a/src/models/IssueComment.ts b/src/models/IssueComment.ts index 77511bc..0ddf2a6 100644 --- a/src/models/IssueComment.ts +++ b/src/models/IssueComment.ts @@ -1,6 +1,6 @@ -// Interface +// Interface for full IssueComment object export interface IIssueComment { - readonly id?: bigint; + readonly id: bigint; readonly issueId: bigint; readonly userId: bigint; readonly content: string; @@ -8,9 +8,19 @@ export interface IIssueComment { readonly updatedAt: Date; } +// Interface for constructing an IssueComment +interface IIssueCommentConstructor { + readonly id?: bigint; + readonly issueId: bigint; + readonly userId: bigint; + readonly content: string; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} + // Class export class IssueComment implements IIssueComment { - private _id?: bigint; + private _id: bigint; private _issueId: bigint; private _userId: bigint; private _content: string; @@ -18,13 +28,13 @@ export class IssueComment implements IIssueComment { private _updatedAt: Date; public constructor({ - id, + id = -1n, issueId, userId, content, - createdAt, - updatedAt, - }: IIssueComment) { + createdAt = new Date(), + updatedAt = new Date(), + }: IIssueCommentConstructor) { this._id = id; this._issueId = issueId; this._userId = userId; @@ -50,7 +60,7 @@ export class IssueComment implements IIssueComment { if (updatedAt !== undefined) this._updatedAt = updatedAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } diff --git a/src/models/Roadmap.ts b/src/models/Roadmap.ts index f016a93..101844f 100644 --- a/src/models/Roadmap.ts +++ b/src/models/Roadmap.ts @@ -1,39 +1,51 @@ -// Interface +// Interface for full Roadmap object export interface IRoadmap { - readonly id?: bigint; + readonly id: bigint; readonly name: string; readonly description: string; readonly userId: bigint; readonly isPublic: boolean; - readonly isDraft?: boolean; + readonly isDraft: boolean; readonly data: string; readonly createdAt: Date; readonly updatedAt: Date; } +// Interface for constructing a Roadmap +interface IRoadmapConstructor { + readonly id?: bigint; + readonly name: string; + readonly description: string; + readonly userId: bigint; + readonly isPublic?: boolean; + readonly isDraft?: boolean; + readonly data: string; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} // Class export class Roadmap implements IRoadmap { - private _id?: bigint; + private _id: bigint; private _name: string; private _description: string; private _userId: bigint; private _isPublic: boolean; - private _isDraft?: boolean; + private _isDraft: boolean; private _data: string; private _createdAt: Date; private _updatedAt: Date; public constructor({ - id, + id = 0n, name, description, userId, - isPublic, + isPublic = true, isDraft = false, data, - createdAt, - updatedAt, - }: IRoadmap) { + createdAt = new Date(), + updatedAt = new Date(), + }: IRoadmapConstructor) { this._id = id; this._name = name; this._description = description; @@ -68,7 +80,7 @@ export class Roadmap implements IRoadmap { if (updatedAt !== undefined) this._updatedAt = updatedAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } @@ -88,7 +100,7 @@ export class Roadmap implements IRoadmap { return this._isPublic; } - public get isDraft(): boolean | undefined { + public get isDraft(): boolean { return this._isDraft; } diff --git a/src/models/RoadmapLike.ts b/src/models/RoadmapLike.ts index caade1f..5002905 100644 --- a/src/models/RoadmapLike.ts +++ b/src/models/RoadmapLike.ts @@ -1,5 +1,14 @@ -// TypeScript Interface +// interface for RoadmapLike export interface IRoadmapLike { + readonly id: bigint; + readonly roadmapId: bigint; + readonly userId: bigint; + readonly value: number; + readonly createdAt: Date; +} + +// Interface for constructing a RoadmapLike +interface IRoadmapLikeConstructor { readonly id?: bigint; readonly roadmapId: bigint; readonly userId: bigint; @@ -7,21 +16,21 @@ export interface IRoadmapLike { readonly createdAt?: Date; } -// TypeScript Class +// Class export class RoadmapLike implements IRoadmapLike { - private _id?: bigint; + private _id: bigint; private _roadmapId: bigint; private _userId: bigint; - private _value?: number; - private _createdAt?: Date; + private _value: number; + private _createdAt: Date; public constructor({ - id, + id = -1n, roadmapId, userId, - value, - createdAt, - }: IRoadmapLike) { + value = 1, + createdAt = new Date(), + }: IRoadmapLikeConstructor) { this._id = id; this._roadmapId = roadmapId; this._userId = userId; @@ -38,7 +47,7 @@ export class RoadmapLike implements IRoadmapLike { if (createdAt !== undefined) this._createdAt = createdAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } @@ -50,11 +59,11 @@ export class RoadmapLike implements IRoadmapLike { return this._userId; } - public get value(): number | undefined { + public get value(): number { return this._value; } - public get createdAt(): Date | undefined { + public get createdAt(): Date { return this._createdAt; } diff --git a/src/models/RoadmapView.ts b/src/models/RoadmapView.ts index faaf821..228af36 100644 --- a/src/models/RoadmapView.ts +++ b/src/models/RoadmapView.ts @@ -1,21 +1,36 @@ -// TypeScript Interface +// Interface for full RoadmapView object export interface IRoadmapView { - readonly id?: bigint; + readonly id: bigint; readonly userId: bigint; readonly roadmapId: bigint; readonly full: boolean; readonly createdAt: Date; } -// TypeScript Class +// Interface for constructing a RoadmapView +interface IRoadmapViewConstructor { + readonly id?: bigint; + readonly userId?: bigint; + readonly roadmapId: bigint; + readonly full?: boolean; + readonly createdAt?: Date; +} + +// Class export class RoadmapView implements IRoadmapView { - private _id?: bigint; + private _id: bigint; private _userId: bigint; private _roadmapId: bigint; private _full: boolean; private _createdAt: Date; - public constructor({ id, userId, roadmapId, full, createdAt }: IRoadmapView) { + public constructor({ + id = -1n, + userId = -1n, + roadmapId, + full = false, + createdAt = new Date(), + }: IRoadmapViewConstructor) { this._id = id; this._userId = userId; this._roadmapId = roadmapId; @@ -32,7 +47,7 @@ export class RoadmapView implements IRoadmapView { if (createdAt !== undefined) this._createdAt = createdAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } diff --git a/src/models/Session.ts b/src/models/Session.ts index ee33c03..f896abd 100644 --- a/src/models/Session.ts +++ b/src/models/Session.ts @@ -1,5 +1,13 @@ -// Interface +// Interface for full Session object export interface ISession { + readonly id: bigint; + readonly userId: bigint; + readonly token: string; + readonly expires: Date; +} + +// Interface for constructing a Session +interface ISessionConstructor { readonly id?: bigint; readonly userId: bigint; readonly token: string; @@ -8,12 +16,17 @@ export interface ISession { // Class export class Session implements ISession { - private _id?: bigint; + private _id: bigint; private _userId: bigint; private _token: string; private _expires: Date; - public constructor({ id, userId, token, expires }: ISession) { + public constructor({ + id = -1n, + userId, + token, + expires, + }: ISessionConstructor) { this._id = id; this._userId = userId; this._token = token; @@ -28,7 +41,7 @@ export class Session implements ISession { if (expires !== undefined) this._expires = expires; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } diff --git a/src/models/User.ts b/src/models/User.ts index bb0cca0..0a0775f 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,5 +1,18 @@ -// Interface +// Interface for full User object export interface IUser { + readonly id: bigint; + readonly avatar: string | null; + readonly name: string; + readonly email: string; + readonly role: number | null; + readonly pwdHash: string | null; + readonly googleId: string | null; + readonly githubId: string | null; + readonly createdAt: Date; +} + +// Interface for constructing a User +interface IUserConstructor { readonly id?: bigint; readonly avatar?: string | null; readonly name: string; @@ -8,23 +21,23 @@ export interface IUser { readonly pwdHash?: string | null; readonly googleId?: string | null; readonly githubId?: string | null; - readonly createdAt: Date; + readonly createdAt?: Date; } // Class export class User implements IUser { - private _id?: bigint; - private _avatar?: string | null; + private _id: bigint; + private _avatar: string | null; private _name: string; private _email: string; - private _role?: number | null; - private _pwdHash?: string | null; - private _googleId?: string | null; - private _githubId?: string | null; + private _role: number | null; + private _pwdHash: string | null; + private _googleId: string | null; + private _githubId: string | null; private _createdAt: Date; public constructor({ - id, + id = -1n, avatar = null, name, email, @@ -32,8 +45,8 @@ export class User implements IUser { pwdHash = null, googleId = null, githubId = null, - createdAt, - }: IUser) { + createdAt = new Date(), + }: IUserConstructor) { this._id = id; this._avatar = avatar; this._name = name; @@ -56,7 +69,7 @@ export class User implements IUser { googleId, githubId, createdAt, - }: IUser): void { + }: IUserConstructor): void { if (id !== undefined) this._id = id; if (avatar !== undefined) this._avatar = avatar; if (name !== undefined) this._name = name; @@ -68,11 +81,11 @@ export class User implements IUser { if (createdAt !== undefined) this._createdAt = createdAt; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } - public get avatar(): string | null | undefined { + public get avatar(): string | null { return this._avatar; } @@ -84,19 +97,19 @@ export class User implements IUser { return this._email; } - public get role(): number | null | undefined { + public get role(): number | null { return this._role; } - public get pwdHash(): string | null | undefined { + public get pwdHash(): string | null { return this._pwdHash; } - public get googleId(): string | null | undefined { + public get googleId(): string | null { return this._googleId; } - public get githubId(): string | null | undefined { + public get githubId(): string | null { return this._githubId; } diff --git a/src/models/UserInfo.ts b/src/models/UserInfo.ts index 53433ad..30ea98e 100644 --- a/src/models/UserInfo.ts +++ b/src/models/UserInfo.ts @@ -1,5 +1,15 @@ -// TypeScript Interface +// Interface for full UserInfo object export interface IUserInfo { + readonly id: bigint; + readonly userId: bigint; + readonly bio: string | null; + readonly quote: string | null; + readonly websiteUrl: string | null; + readonly githubUrl: string | null; +} + +// Interface for constructing a UserInfo +interface IUserInfoConstructor { readonly id?: bigint; readonly userId: bigint; readonly bio?: string | null; @@ -10,21 +20,21 @@ export interface IUserInfo { // TypeScript Class export class UserInfo implements IUserInfo { - private _id?: bigint; + private _id: bigint; private _userId: bigint; - private _bio?: string | null; - private _quote?: string | null; - private _websiteUrl?: string | null; - private _githubUrl?: string | null; + private _bio: string | null; + private _quote: string | null; + private _websiteUrl: string | null; + private _githubUrl: string | null; public constructor({ - id, + id = -1n, userId, bio = null, quote = null, websiteUrl = null, githubUrl = null, - }: IUserInfo) { + }: IUserInfoConstructor) { this._id = id; this._userId = userId; this._bio = bio; @@ -43,7 +53,7 @@ export class UserInfo implements IUserInfo { if (githubUrl !== undefined) this._githubUrl = githubUrl; } - public get id(): bigint | undefined { + public get id(): bigint { return this._id; } @@ -51,19 +61,19 @@ export class UserInfo implements IUserInfo { return this._userId; } - public get bio(): string | null | undefined { + public get bio(): string | null { return this._bio; } - public get quote(): string | null | undefined { + public get quote(): string | null { return this._quote; } - public get websiteUrl(): string | null | undefined { + public get websiteUrl(): string | null { return this._websiteUrl; } - public get githubUrl(): string | null | undefined { + public get githubUrl(): string | null { return this._githubUrl; } diff --git a/src/sql/setup.sql b/src/sql/setup.sql index e1097a4..05a4d76 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -37,14 +37,14 @@ create table roadmaps ( id bigint auto_increment primary key, - name varchar(255) not null, - description varchar(255) not null, - userId bigint not null, - isPublic tinyint(1) not null, - isDraft tinyint(1) null, - data longtext not null, - createdAt timestamp default current_timestamp() not null, - updatedAt timestamp default current_timestamp() not null, + name varchar(255) not null, + description varchar(255) not null, + userId bigint not null, + isPublic tinyint(1) default 1 not null, + isDraft tinyint(1) default 0 not null, + data longtext not null, + createdAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() not null, constraint roadmaps_users_id_fk foreign key (userId) references users (id) on delete cascade @@ -124,10 +124,10 @@ create table roadmapViews ( id bigint auto_increment primary key, - userId bigint default -1 not null, - roadmapId bigint not null, - full tinyint(1) not null, - createdAt timestamp default current_timestamp() not null, + userId bigint default -1 not null, + roadmapId bigint not null, + full tinyint(1) default 0 not null, + createdAt timestamp default current_timestamp() not null, constraint roadmapViews_roadmaps_id_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, From cdd60fd70aa88f503788ffd662f25b193eb0a51f Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:48:38 +0300 Subject: [PATCH 050/118] Remove old test files for Roadmap Comment and Issue Deleted 'commentsrouter.spec.ts' and 'issuesrouter.spec.ts' files which became outdated after refactoring and consolidating tests for Roadmap Comment and Roadmap Issue feature. We made the changes to clean up the test structure, removing unused test files due to changes in the application state management logic. --- spec/tests/roadmaprouter.spec.ts | 534 ------------- spec/tests/roadmaps/commentsrouter.spec.ts | 488 ------------ spec/tests/roadmaps/issuesrouter.spec.ts | 398 ---------- spec/tests/routes/auth.spec.ts | 4 +- spec/tests/routes/users.spec.ts | 0 spec/tests/usersrouter.spec.ts | 882 --------------------- spec/tests/utils/database.spec.ts | 147 ++-- spec/utils/createUser.ts | 1 + 8 files changed, 57 insertions(+), 2397 deletions(-) delete mode 100644 spec/tests/roadmaprouter.spec.ts delete mode 100644 spec/tests/roadmaps/commentsrouter.spec.ts delete mode 100644 spec/tests/roadmaps/issuesrouter.spec.ts create mode 100644 spec/tests/routes/users.spec.ts delete mode 100644 spec/tests/usersrouter.spec.ts create mode 100644 spec/utils/createUser.ts diff --git a/spec/tests/roadmaprouter.spec.ts b/spec/tests/roadmaprouter.spec.ts deleted file mode 100644 index e255044..0000000 --- a/spec/tests/roadmaprouter.spec.ts +++ /dev/null @@ -1,534 +0,0 @@ -import Database from '@src/util/DatabaseDriver'; -import { Roadmap } from '@src/models/Roadmap'; -import User from '@src/models/User'; -import app from '@src/server'; -import request from 'supertest'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import EnvVars from '@src/constants/EnvVars'; -import axios from 'axios'; - -describe('Roadmap Router', () => { - let user: User, user2: User, token: string, token2: string, roadmap: Roadmap; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let server: any; - - beforeAll(async () => { - server = app.listen(EnvVars.Port); - // generate email - const email = Math.random().toString(36).substring(2, 15) + '@test.com'; - const email2 = Math.random().toString(36).substring(2, 15) + '@test.com'; - // generate password - const password = Math.random().toString(36).substring(2, 15); - const password2 = Math.random().toString(36).substring(2, 15); - - // register userDisplay - const res = await request(app) - .post('/api/auth/register') - .send({ email, password }) - .expect(HttpStatusCodes.CREATED); - - // get token - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token = res.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - const res2 = await request(app) - .post('/api/auth/register') - .send({ email: email2, password: password2 }) - .expect(HttpStatusCodes.CREATED); - - // get token - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token2 = res2.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - // get database - const db = new Database(); - - // get userDisplay - const dbuser = await db.getWhere('users', 'email', email); - const dbuser2 = await db.getWhere('users', 'email', email2); - - // if userDisplay is undefined cancel tests - if (!dbuser || !dbuser2) { - throw new Error('User is undefined'); - } - - // set userDisplay - user = dbuser; - user2 = dbuser2; - - // set roadmap - roadmap = new Roadmap( - user.id, - 'Test Roadmap', - 'This is a test roadmap', - 'Test', - ); - }); - - afterAll(async () => { - // close server - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - await server.close(); - - // get database - const db = new Database(); - - // delete userDisplay - will cascade delete roadmap - await db.delete('users', user.id); - await db.delete('users', user2.id); - }); - - /* - ! Create Roadmap Test - */ - - it('should create a roadmap', async () => { - // create roadmap - const res = await request(app) - .post('/api/roadmaps/create') - .set('Cookie', `token=${token}`) - .send({ roadmap: roadmap.toJSONSafe() }) - .expect(HttpStatusCodes.CREATED); - - // if id is undefined - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!res.body?.id) { - throw new Error('Id is undefined'); - } - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/ban-ts-comment,@typescript-eslint/no-unsafe-member-access - const id = (res.body.id as number) || -1; - - // set id - roadmap.id = BigInt(id); - - // get database - const db = new Database(); - - // get roadmap from database - const dbroadmap: Roadmap = - (await db.get('roadmaps', roadmap.id)) || ({} as Roadmap); - - // check if roadmap matches dbroadmap - expect(roadmap.id).toEqual(dbroadmap.id); - expect(roadmap.name).toEqual(dbroadmap.name); - expect(roadmap.description).toEqual(dbroadmap.description); - expect(roadmap.ownerId).toEqual(dbroadmap.ownerId); - }); - - it('should not create a roadmap if userDisplay is not logged in', async () => { - // create roadmap - await request(app) - .post('/api/roadmaps/create') - .send({ roadmap: roadmap.toJSONSafe() }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not create a roadmap if token not found', async () => { - // create roadmap - await request(app) - .post('/api/roadmaps/create') - .send({ roadmap: roadmap.toJSONSafe() }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not create a roadmap if token is invalid', async () => { - // create roadmap - await request(app) - .post('/api/roadmaps/create') - .set('Cookie', 'token=invalidtoken') - .send({ roadmap: roadmap.toJSONSafe() }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - /* - ! Get Roadmap Tests - */ - - it('should get a roadmap', async () => { - // get roadmap - const res = await request(app) - .get(`/api/roadmaps/${roadmap.id}`) - .expect(HttpStatusCodes.OK); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument - const resRoadmap: Roadmap = Roadmap.from(res.body); - - expect(roadmap.name).toEqual(resRoadmap.name); - expect(roadmap.description).toEqual(resRoadmap.description); - expect(roadmap.ownerId).toEqual(resRoadmap.ownerId); - }); - - it('Should fail to get a roadmap that does not exist', async () => { - // get roadmap - await request(app) - .get(`/api/roadmaps/${roadmap.id + BigInt(1)}`) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('Should get a roadmap minified', async () => { - // get roadmap - const res = await request(app) - .get(`/api/roadmaps/${roadmap.id}/mini`) - .expect(HttpStatusCodes.OK); - - type MiniRoadmap = { - id: bigint; - name: string; - description: string; - ownerId: bigint; - }; - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument - const resRoadmap: MiniRoadmap = JSON.parse(res.text); - - // ensure types are correct - resRoadmap.id = BigInt(resRoadmap.id); - resRoadmap.ownerId = BigInt(resRoadmap.ownerId); - - // check if roadmap matches dbroadmap - expect(roadmap.id).toEqual(resRoadmap.id); - expect(roadmap.name).toEqual(resRoadmap.name); - expect(roadmap.description).toEqual(resRoadmap.description); - expect(roadmap.ownerId).toEqual(resRoadmap.ownerId); - }); - - it('Should fail to get a roadmap minified that does not exist', async () => { - // get roadmap - await request(app) - .get(`/api/roadmaps/${roadmap.id + BigInt(1)}/mini`) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('Should be a able to get roadmap tags', async () => { - await request(app) - .get(`/api/roadmaps/${roadmap.id}/tags`) - .expect(HttpStatusCodes.OK); - }); - - it('Should fail to get roadmap tags if roadmap does not exist', async () => { - await request(app) - .get(`/api/roadmaps/${roadmap.id + BigInt(1)}/tags`) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('Should be able to get roadmap owner profile', async () => { - const res = await axios.get( - // eslint-disable-next-line max-len - `http://localhost:${EnvVars.Port}/api/roadmaps/${roadmap.id}/owner`, - ); - - expect(res.status).toEqual(HttpStatusCodes.OK); - }); - - it('Should fail to get roadmap owner profile if roadmap does not exist', async () => { - await request(app) - .get(`/api/roadmaps/${roadmap.id + BigInt(1)}/owner`) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('Should be able to get roadmap owner profile minified', async () => { - const res = await axios.get( - // eslint-disable-next-line max-len - `http://localhost:${EnvVars.Port}/api/roadmaps/${roadmap.id}/owner/mini`, - ); - - expect(res.status).toEqual(HttpStatusCodes.OK); - }); - - // eslint-disable-next-line max-len - it('Should fail to get roadmap owner profile minified if roadmap does not exist', async () => { - await request(app) - .get(`/api/roadmaps/${roadmap.id + BigInt(1)}/owner/mini`) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - /* - `! Update Roadmap Tests - */ - - it('Should be able to update a roadmap\'s title', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/title`) - .set('Cookie', `token=${token}`) - .send({ title: 'new title' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s title if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/title`) - .send({ title: 'new title' }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s title if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/title`) - .set('Cookie', `token=${token2}`) - .send({ title: 'new title' }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('Should be able to update a roadmap\'s description', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/description`) - .set('Cookie', `token=${token}`) - .send({ description: 'new description' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s description if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/description`) - .send({ description: 'new description' }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s description if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/description`) - .set('Cookie', `token=${token2}`) - .send({ description: 'new description' }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('Should be able to update a roadmap\'s tags', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/tags`) - .set('Cookie', `token=${token}`) - .send({ tags: ['new tag'] }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should be able to update a roadmap\'s tags with multiple tags', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/tags`) - .set('Cookie', `token=${token}`) - .send({ tags: ['new tag', 'new tag 2'] }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should be able to update a roadmap\'s tags with empty array', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/tags`) - .set('Cookie', `token=${token}`) - .send({ tags: [] }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s tags if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/tags`) - .send({ tags: ['new tag'] }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s tags if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/tags`) - .set('Cookie', `token=${token2}`) - .send({ tags: ['new tag'] }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('Should be able to update a roadmap\'s visibility', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/visibility`) - .set('Cookie', `token=${token}`) - .send({ visibility: false }) - - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s visibility if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/visibility`) - .send({ visibility: 'public' }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s visibility if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/visibility`) - .set('Cookie', `token=${token2}`) - .send({ visibility: 'public' }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('Should be able to update a roadmap\'s owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/owner`) - .set('Cookie', `token=${token}`) - .send({ newOwnerId: user2.id.toString() }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - - // change owner back - await request(app) - .post(`/api/roadmaps/${roadmap.id}/owner`) - .set('Cookie', `token=${token2}`) - .send({ newOwnerId: user.id.toString() }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s owner if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/owner`) - .send({ newOwnerId: user2.id.toString() }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s owner if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/owner`) - .set('Cookie', `token=${token2}`) - .send({ newOwnerId: user.id.toString() }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('Should be able to update a roadmap\'s data', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/data`) - .set('Cookie', `token=${token}`) - .send({ data: 'testi g' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body.success).toEqual(true); - }); - }); - - it('Should fail to update a roadmap\'s data if not logged in', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/data`) - .send({ data: 'test s' }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('Should fail to update a roadmap\'s data if not owner', async () => { - await request(app) - .post(`/api/roadmaps/${roadmap.id}/data`) - .set('Cookie', `token=${token2}`) - .send({ data: 'test s' }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - /* - ! like/dislike roadmap test - */ - it('should like a roadmap', async () => { - // like roadmap - await request(app) - .get(`/api/roadmaps/${roadmap.id}/like`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK); - }); - - it('shouldn\'t be able to like a roadmap twice', async () => { - // like roadmap - await request(app) - .get(`/api/roadmaps/${roadmap.id}/like`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('should dislike a roadmap', async () => { - // dislike roadmap - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/like`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK); - }); - - it('shouldn\'t be able to dislike a roadmap twice', async () => { - // dislike roadmap - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/like`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - /* - ! Delete Roadmap Test - */ - - it('should Delete a roadmap', async () => { - // delete roadmap - await request(app) - .delete(`/api/roadmaps/${roadmap.id}`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK); - - // get database - const db = new Database(); - - // get roadmap from database - const dbroadmap = await db.get('roadmaps', roadmap.id); - - // check if roadmap matches dbroadmap - expect(dbroadmap).toBeUndefined(); - }); - - it('should fail to delete a roadmap if not logged in', async () => { - // delete roadmap - await request(app) - .delete(`/api/roadmaps/${roadmap.id}`) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); -}); diff --git a/spec/tests/roadmaps/commentsrouter.spec.ts b/spec/tests/roadmaps/commentsrouter.spec.ts deleted file mode 100644 index 4938909..0000000 --- a/spec/tests/roadmaps/commentsrouter.spec.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { Issue } from '@src/models/Issue'; -import { Roadmap } from '@src/models/Roadmap'; -import User from '@src/models/User'; -import app from '@src/server'; -import request from 'supertest'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; -import { Comment } from '@src/models/Comment'; - -describe('CommentsRouter', () => { - let user1: User, user2: User, user3: User; - let token1: string, token2: string, token3: string; - let roadmap1: Roadmap, roadmap2: Roadmap; - let issue1: Issue, issue3: Issue, issue4: Issue; - let comment1: Comment; - - beforeAll(async () => { - // create users - let res = await request(app) - .post('/api/auth/register') - .send({ email: 'user1@email.com', password: 'password1' }) - .expect(HttpStatusCodes.CREATED); - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token1 = res.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - res = await request(app) - .post('/api/auth/register') - .send({ email: 'user2@email.com', password: 'password2' }) - .expect(HttpStatusCodes.CREATED); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token2 = res.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - res = await request(app) - .post('/api/auth/register') - .send({ email: 'user3@email.com', password: 'password3' }) - .expect(HttpStatusCodes.CREATED); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token3 = res.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - // get database - const db = new Database(); - - // get users - let user = await db.getWhere('users', 'email', 'user1@email.com'); - if (!user) throw new Error('User 1 not found.'); - user1 = user; - - user = await db.getWhere('users', 'email', 'user2@email.com'); - if (!user) throw new Error('User 2 not found.'); - user2 = user; - - user = await db.getWhere('users', 'email', 'user3@email.com'); - if (!user) throw new Error('User 3 not found.'); - user3 = user; - - // create roadmaps - await request(app) - .post('/api/roadmaps/create') - .set('Cookie', [`token=${token1}`]) - .send({ - roadmap: new Roadmap(user1.id, 'roadmap1', 'test', 'st').toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - await request(app) - .post('/api/roadmaps/create') - .set('Cookie', [`token=${token2}`]) - .send({ - roadmap: new Roadmap( - user2.id, - 'roadmap2', - 'test', - 'ksf', - undefined, - undefined, - false, - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - - // get roadmaps - let roadmap = await db.getWhere('roadmaps', 'ownerId', user1.id); - if (!roadmap) throw new Error('Roadmap 1 not found.'); - roadmap1 = roadmap; - - roadmap = await db.getWhere('roadmaps', 'ownerId', user2.id); - if (!roadmap) throw new Error('Roadmap 2 not found.'); - roadmap2 = roadmap; - - // create issues - await request(app) - .post(`/api/roadmaps/${roadmap1.id}/issues/create`) - .set('Cookie', [`token=${token1}`]) - .send({ - issue: new Issue( - roadmap1.id, - user1.id, - true, - 'issue1', - 'fd', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - - await request(app) - .post(`/api/roadmaps/${roadmap2.id}/issues/create`) - .set('Cookie', [`token=${token2}`]) - .send({ - issue: new Issue( - roadmap2.id, - user2.id, - true, - 'issue3', - 'ksf', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - - await request(app) - .post(`/api/roadmaps/${roadmap2.id}/issues/create`) - .set('Cookie', [`token=${token2}`]) - .send({ - issue: new Issue( - roadmap2.id, - user2.id, - true, - 'issue4', - 'ksf', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - - // get issues - let issue = await db.getWhere('issues', 'title', 'issue1'); - if (!issue) throw new Error('Issue 1 not found.'); - issue1 = issue; - - issue = await db.getWhere('issues', 'title', 'issue3'); - if (!issue) throw new Error('Issue 3 not found.'); - issue3 = issue; - - issue = await db.getWhere('issues', 'title', 'issue4'); - if (!issue) throw new Error('Issue 4 not found.'); - issue4 = issue; - }); - - afterAll(async () => { - // get database - const db = new Database(); - // delete users (cascade deletes roadmaps, issues, and comments) - await db.delete('users', user1.id); - await db.delete('users', user2.id); - await db.delete('users', user3.id); - }); - - it('should create a comment', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/create`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.CREATED) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body?.success).toBe(true); - }); - - // get database - const db = new Database(); - // get comment - const comment = await db.getWhere( - 'issueComments', - 'userId', - user1.id, - ); - if (!comment) throw new Error('Comment not found.'); - // check comment - expect(comment.content).toBe(new Array(100).fill('a').join('')); - expect(comment.issueId).toBe(issue1.id); - expect(comment.userId).toBe(user1.id); - - // set comment - comment1 = comment; - }); - - it('should not create a comment with invalid content', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/create`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: '', - }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should not create a comment with invalid issue id', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${ - Number(issue4.id) + 1 - }/comments/create`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not create a comment with invalid roadmap id', async () => { - await request(app) - .post( - `/api/roadmaps/${ - Number(roadmap2.id) + 1 - }/issues/${issue1.id.toString()}/comments/create`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not create a comment without login', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/create`, - ) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should fail to create a comment with invalid token', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/create`, - ) - .set('Cookie', [`token=${token1}a`]) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should fail to create a comment on private roadmap', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap2.id.toString()}/issues/${issue3.id.toString()}/comments/create`, - ) - .set('Cookie', [`token=${token3}`]) - .send({ - content: new Array(100).fill('a').join(''), - }) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('should get comments', async () => { - await request(app) - .get( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // parse body comments - // eslint-disable-next-line max-len,@typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const comments = res.body?.comments as Array; - expect(comments).toBeDefined(); - expect(comments.length).toBe(1); - }); - }); - - it('should not get comments with invalid issue id', async () => { - await request(app) - .get( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${ - Number(issue4.id) + 1 - }/comments`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not get comments with invalid roadmap id', async () => { - await request(app) - .get( - `/api/roadmaps/${ - Number(roadmap1.id) + 1 - }/issues/${issue1.id.toString()}/comments`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should get comments without login', async () => { - await request(app) - .get( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments`, - ) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // parse body comments - // eslint-disable-next-line max-len,@typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const comments = res.body?.comments as Array; - expect(comments).toBeDefined(); - expect(comments.length).toBe(1); - }); - }); - - it('should be able to update a comment', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body?.success).toBe(true); - }); - }); - - it('should not update a comment with invalid content', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: '', - }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should not update a comment with invalid comment id', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${ - Number(comment1.id) + 1 - }/`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not update a comment with invalid issue id', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${ - Number(issue1.id) + 1 - }/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should not update a comment with invalid roadmap id', async () => { - await request(app) - .post( - `/api/roadmaps/${ - Number(roadmap2.id) + 21 - }/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not update a comment without login', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not update a comment with invalid token', async () => { - await request(app) - .post( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}a`]) - .send({ - content: new Array(100).fill('b').join(''), - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not be able to delete a comment with invalid comment id', async () => { - await request(app) - .delete( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${ - Number(comment1.id) + 1 - }/`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.NOT_FOUND); - }); - - it('should not be able to delete a comment with invalid issue id', async () => { - await request(app) - .delete( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${ - Number(issue1.id) + 1 - }/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should not be able to delete a comment with invalid roadmap id', async () => { - await request(app) - .delete( - `/api/roadmaps/${ - Number(roadmap1.id) + 1 - }/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.BAD_REQUEST); - }); - - it('should not be able to delete a comment without login', async () => { - await request(app) - .delete( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not be able to delete a comment with invalid token', async () => { - await request(app) - .delete( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}a`]) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should be able to delete a comment', async () => { - await request(app) - .delete( - `/api/roadmaps/${roadmap1.id.toString()}/issues/${issue1.id.toString()}/comments/${comment1.id.toString()}/`, - ) - .set('Cookie', [`token=${token1}`]) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res.body?.success).toBe(true); - }); - }); -}); diff --git a/spec/tests/roadmaps/issuesrouter.spec.ts b/spec/tests/roadmaps/issuesrouter.spec.ts deleted file mode 100644 index 992a338..0000000 --- a/spec/tests/roadmaps/issuesrouter.spec.ts +++ /dev/null @@ -1,398 +0,0 @@ -import User from '@src/models/User'; -import { Roadmap } from '@src/models/Roadmap'; -import { Issue } from '@src/models/Issue'; -import app from '@src/server'; -import request from 'supertest'; -import Database from '@src/util/DatabaseDriver'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; - -describe('Roadmap Issues', () => { - let user: User, user2: User; - let token: string, token2: string; - let roadmap: Roadmap; - let issueid: bigint, issueid2: bigint; - - beforeAll(async () => { - // generate email - const email = Math.random().toString(36).substring(2, 15) + '@test.com'; - const email2 = Math.random().toString(36).substring(2, 15) + '@test.com'; - // generate password - const password = Math.random().toString(36).substring(2, 15); - const password2 = Math.random().toString(36).substring(2, 15); - - // register user - const res = await request(app) - .post('/api/auth/register') - .send({ email, password }) - .expect(HttpStatusCodes.CREATED); - const res2 = await request(app) - .post('/api/auth/register') - .send({ email: email2, password: password2 }) - .expect(HttpStatusCodes.CREATED); - - // get user token - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token = res.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - token2 = res2.header['set-cookie'][0].split(';')[0].split('=')[1] as string; - - // get database - const db = new Database(); - - // get user - const dbuser = await db.getWhere('users', 'email', email); - - // get user2 - const dbuser2 = await db.getWhere('users', 'email', email2); - - // check if user exists - if (!dbuser || !dbuser2) throw new Error('User not found'); - - // set user - user = dbuser; - user2 = dbuser2; - - // create roadmap - const res3 = await request(app) - .post('/api/roadmaps/create') - .set('Cookie', [`token=${token}`]) - .send({ - roadmap: new Roadmap( - user.id, - 'Test Roadmap', - 'Test Description', - 'datra', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED); - - // get roadmap - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const dbroadmap = await db.get('roadmaps', res3.body.id); - - // check if roadmap exists - if (!dbroadmap) throw new Error('Roadmap not found'); - - // set roadmap - roadmap = dbroadmap; - }); - - afterAll(async () => { - // get database - const db = new Database(); - - // delete user - const success = await db.delete('users', user.id); - const success2 = await db.delete('users', user2.id); - - // check if user was deleted - expect(success).toBe(true); - expect(success2).toBe(true); - }); - - /* - ! Create Issue Tests - */ - - it('should create an issue if user is roadmap owner', async () => { - // create issue - const res = await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/create`) - .set('Cookie', `token=${token}`) - .send({ - issue: new Issue( - roadmap.id, - user2.id, - true, - 'datra', - 'Test content', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.id).toBeDefined(); - }); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - issueid = res.body.id; - }); - - it('should create an issue if user is not roadmap owner', async () => { - // create issue - const res = await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/create`) - .set('Cookie', `token=${token2}`) - .send({ - issue: new Issue( - roadmap.id, - user2.id, - true, - 'datra', - 'Test content', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.CREATED) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.id).toBeDefined(); - }); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - issueid2 = res.body.id; - }); - - it('should fail to create an issue if not logged in', async () => { - // create issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/create`) - .send({ - issue: new Issue( - roadmap.id, - user.id, - true, - 'datra', - 'Test content', - ).toJSONSafe(), - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - /* - ! Get Issue Tests - */ - - it('should get an issue', async () => { - // get issue - await request(app) - .get(`/api/roadmaps/${roadmap.id}/issues/${issueid}`) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.issue).toBeDefined(); - }); - }); - - it('should fail to get an issue that doesn\'t exist', async () => { - // get issue - await request(app).get(`/api/roadmaps/${roadmap.id}/issues/${issueid}2`); - }); - - /* - ! Update Issue Tests - */ - - it('should fail to update an issue if not logged in', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid}`) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should be able to update title of issue', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid}/title`) - .set('Cookie', `token=${token}`) - .send({ - title: 'New Test Title', - }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should not be able to update title of issue if not owner', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/title`) - .set('Cookie', `token=${token}`) - .send({ - title: 'New Test Title', - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not be able to update title of issue if not logged in', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/title`) - .send({ - title: 'New Test Title', - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should be able to update content of issue', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid}/content`) - .set('Cookie', `token=${token}`) - .send({ - content: 'New Test Content', - }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should not be able to update content of issue if not owner', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/content`) - .set('Cookie', `token=${token}`) - .send({ - content: 'New Test Content', - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should not be able to update content of issue if not logged in', async () => { - // update issue - await request(app) - .post(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/content`) - .send({ - content: 'New Test Content', - }) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should be able to open issue', async () => { - // update issue - await request(app) - .get(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should be able to open issue if roadmap owner', async () => { - // update issue - await request(app) - .get(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/status`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should not be able to open issue if not owner of issue or roadmap', async () => { - // update issue - await request(app) - .get(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .set('Cookie', `token=${token2}`) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('should not be able to open issue if not logged in', async () => { - // update issue - await request(app) - .get(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should be able to close issue', async () => { - // update issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should be able to close issue if roadmap owner', async () => { - // update issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid2}/status`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(res?.body?.success).toBe(true); - }); - }); - - it('should not be able to close issue if not owner of issue or roadmap', async () => { - // update issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .set('Cookie', `token=${token2}`) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('should not be able to close issue if not logged in', async () => { - // update issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}/status`) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - /* - ! Delete Issue Tests - */ - - it('should fail to delete an issue if not logged in', async () => { - // delete issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}`) - .expect(HttpStatusCodes.UNAUTHORIZED); - }); - - it('should fail to delete an issue if not owner of issue/roadmap', async () => { - // delete issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}`) - .set('Cookie', `token=${token2}`) - .expect(HttpStatusCodes.FORBIDDEN); - }); - - it('should delete an issue', async () => { - // delete issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid}`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK); - }); - - it('should be able to delete an issue if owner of roadmap', async () => { - // delete issue - await request(app) - .delete(`/api/roadmaps/${roadmap.id}/issues/${issueid2}`) - .set('Cookie', `token=${token}`) - .expect(HttpStatusCodes.OK); - }); -}); diff --git a/spec/tests/routes/auth.spec.ts b/spec/tests/routes/auth.spec.ts index 0fcf793..d8d780f 100644 --- a/spec/tests/routes/auth.spec.ts +++ b/spec/tests/routes/auth.spec.ts @@ -2,7 +2,7 @@ import { randomString } from '@spec/utils/randomString'; import request from 'supertest'; import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import Database from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; @@ -68,7 +68,7 @@ describe('Authentification Tests', () => { it('user should be able to logout', async () => { await request(app) - .post('/api/auth/logout') + .delete('/api/auth/logout') .set('Cookie', loginCookie) .expect(httpStatusCodes.OK) .expect(({ body, headers }) => { diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/spec/tests/usersrouter.spec.ts b/spec/tests/usersrouter.spec.ts deleted file mode 100644 index 22b7f2e..0000000 --- a/spec/tests/usersrouter.spec.ts +++ /dev/null @@ -1,882 +0,0 @@ -import app from '@src/server'; -import request from 'supertest'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; -import { IUser } from '@src/models/User'; - -async function checkProfile( - target?: string, - mini?: boolean, - loginCookie?: string, -): Promise { - // get userDisplay and expect 200 response - await request(app) - .get('/api/users/' + (!!target ? target : '') + (mini ? '/mini' : '')) - .set('Cookie', loginCookie ?? '') - .expect( - !!loginCookie || !!target - ? HttpStatusCodes.OK - : HttpStatusCodes.BAD_REQUEST, - ) - .expect('Content-Type', /json/) - .expect((res) => { - const receivedUser: object = res.body; - // check for keys - const objKeys = Object.keys(receivedUser); - - if (!target && !loginCookie) { - expect(objKeys).toContain('error'); - return; - } - - expect(objKeys).toContain('name'); - expect(objKeys).toContain('profilePictureUrl'); - expect(objKeys).toContain('userId'); - if (mini) return; - expect(objKeys).toContain('bio'); - expect(objKeys).toContain('quote'); - expect(objKeys).toContain('blogUrl'); - expect(objKeys).toContain('websiteUrl'); - expect(objKeys).toContain('githubUrl'); - expect(objKeys).toContain('roadmapsCount'); - expect(objKeys).toContain('issueCount'); - expect(objKeys).toContain('followerCount'); - expect(objKeys).toContain('followingCount'); - expect(objKeys).toContain('githubLink'); - expect(objKeys).toContain('googleLink'); - }); -} - -describe('Users Router', () => { - let email: string; - let password: string; - let loginCookie: string; - let userId: bigint; - - // second userDisplay for testing - let email2: string; - let password2: string; - let userId2: bigint; - - // set up a userDisplay to run tests on - beforeAll(async () => { - // generate random email - email = Math.random().toString(36).substring(2, 15) + '@test.com'; - email2 = Math.random().toString(36).substring(2, 15) + '@test.com'; - // generate random password - password = Math.random().toString(36).substring(2, 15); - password2 = Math.random().toString(36).substring(2, 15); - - // register a 200 response and a session cookie to be sent back - const reqData = await request(app) - .post('/api/auth/register') - .send({ email, password }) - .expect(HttpStatusCodes.CREATED); - - await request(app) - .post('/api/auth/register') - .send({ email: email2, password: password2 }) - .expect(HttpStatusCodes.CREATED); - - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - reqData.headers['set-cookie'].forEach((cookie: string) => { - if (cookie.startsWith('token=')) { - loginCookie = cookie; - } - }); - - // get database - const db = new Database(); - - // get userDisplay from database - const user = await db.getWhere('users', 'email', email); - const user2 = await db.getWhere('users', 'email', email2); - - // get userDisplay id - userId = user?.id ?? BigInt(-1); - userId2 = user2?.id ?? BigInt(-1); - - if (userId < 0 || userId2 < 0) { - // can't run tests without a userDisplay - throw new Error('Failed to create userDisplay'); - } - }); - - // delete the userDisplay after tests - afterAll(async () => { - // get database - const db = new Database(); - - // see if userDisplay exists - const user = await db.getWhere('users', 'email', email); - const user2 = await db.getWhere('users', 'email', email2); - - // if userDisplay exists, delete them - if (user) await db.delete('users', user.id); - if (user2) await db.delete('users', user2.id); - }); - - /** - * Test getting a userDisplay's profile - */ - it('Get profile with no target or login cookie', async () => { - await checkProfile(undefined, false, undefined); - }); - - it('Get mini profile with no target and login cookie', async () => { - await checkProfile(undefined, true, undefined); - }); - - it('Get profile with target and no login cookie', async () => { - await checkProfile(userId.toString(), false, undefined); - }); - - it('Get mini profile with target and no login cookie', async () => { - await checkProfile(userId.toString(), true, undefined); - }); - - it('Get profile with no target and login cookie', async () => { - await checkProfile(undefined, false, loginCookie); - }); - - it('Get mini profile with no target and login cookie', async () => { - await checkProfile(undefined, true, loginCookie); - }); - - it('Get profile with target and login cookie', async () => { - await checkProfile(userId.toString(), false, loginCookie); - }); - - it('Get mini profile with target and login cookie', async () => { - await checkProfile(userId.toString(), true, loginCookie); - }); - - /** - * Test getting userDisplay's roadmaps - */ - it('Get userDisplay roadmaps with no target or login cookie', async () => { - await request(app) - .get('/api/users/roadmaps') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay roadmaps with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/roadmaps') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(res.body?.roadmaps)).toBe(true); - }); - }); - - it('Get userDisplay roadmaps with no target and login cookie', async () => { - await request(app) - .get('/api/users/roadmaps') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(res.body?.roadmaps)).toBe(true); - }); - }); - - it('Get userDisplay roadmaps with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/roadmaps') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(res.body?.roadmaps)).toBe(true); - }); - }); - - /** - * Test getting userDisplay's issues - */ - it('Get userDisplay issues with no target or login cookie', async () => { - await request(app) - .get('/api/users/issues') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay issues with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/issues') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); - }); - }); - - it('Get userDisplay issues with no target and login cookie', async () => { - await request(app) - .get('/api/users/issues') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); - }); - }); - - it('Get userDisplay issues with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/issues') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.issues)).toBe(true); - }); - }); - - /** - * Test getting userDisplay's followers - */ - it('Get userDisplay followers with no target or login cookie', async () => { - await request(app) - .get('/api/users/followers') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay followers with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/followers') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( - true, - ); - }); - }); - - it('Get userDisplay followers with no target and login cookie', async () => { - await request(app) - .get('/api/users/followers') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( - true, - ); - }); - }); - - it('Get userDisplay followers with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/followers') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.followers)).toBe( - true, - ); - }); - }); - - /** - * Test getting userDisplay's following - */ - it('Get userDisplay following with no target or login cookie', async () => { - await request(app) - .get('/api/users/following') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay following with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/following') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( - true, - ); - }); - }); - - it('Get userDisplay following with no target and login cookie', async () => { - await request(app) - .get('/api/users/following') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( - true, - ); - }); - }); - - it('Get userDisplay following with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/following') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an array - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(Array.isArray(JSON.parse(res.body || '{}')?.following)).toBe( - true, - ); - }); - }); - - /** - * Test getting userDisplay's Roadmap count - */ - it('Get userDisplay Roadmap count with no target or login cookie', async () => { - await request(app) - .get('/api/users/roadmap-count') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay Roadmap count with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/roadmap-count') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.roadmapCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay Roadmap count with no target and login cookie', async () => { - await request(app) - .get('/api/users/roadmap-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.roadmapCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay Roadmap count with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/roadmap-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.roadmapCount)).toBeGreaterThanOrEqual(0); - }); - }); - - /** - * Test getting userDisplay's issue count - */ - - it('Get userDisplay issue count with no target or login cookie', async () => { - await request(app) - .get('/api/users/issue-count') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay issue count with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/issue-count') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.issueCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay issue count with no target and login cookie', async () => { - await request(app) - .get('/api/users/issue-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.issueCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay issue count with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/issue-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.issueCount)).toBeGreaterThanOrEqual(0); - }); - }); - - /** - * Test getting userDisplay's follower count - */ - - it('Get userDisplay follower count with no target or login cookie', async () => { - await request(app) - .get('/api/users/follower-count') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay follower count with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/follower-count') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followerCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay follower count with no target and login cookie', async () => { - await request(app) - .get('/api/users/follower-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followerCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay follower count with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/follower-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followerCount)).toBeGreaterThanOrEqual(0); - }); - }); - - /** - * Test getting userDisplay's following count - */ - it('Get userDisplay following count with no target or login cookie', async () => { - await request(app) - .get('/api/users/following-count') - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Get userDisplay following count with target and no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/following-count') - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followingCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay following count with no target and login cookie', async () => { - await request(app) - .get('/api/users/following-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followingCount)).toBeGreaterThanOrEqual(0); - }); - }); - - it('Get userDisplay following count with target and login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/following-count') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - // expect it to be an number >= 0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(parseInt(res.body?.followingCount)).toBeGreaterThanOrEqual(0); - }); - }); - - /** - ! Test following a userDisplay - */ - - it('Follow self with no login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/follow') - .expect(HttpStatusCodes.UNAUTHORIZED) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Follow self with login cookie', async () => { - await request(app) - .get('/api/users/' + userId.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.FORBIDDEN) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Follow userDisplay with login cookie', async () => { - await request(app) - .get('/api/users/' + userId2.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Follow userDisplay with login cookie again', async () => { - await request(app) - .get('/api/users/' + userId2.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - /** - ! Test unfollowing a userDisplay - */ - - it('Unfollow self with no login cookie', async () => { - await request(app) - .delete('/api/users/' + userId.toString() + '/follow') - .expect(HttpStatusCodes.UNAUTHORIZED) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Unfollow self with login cookie', async () => { - await request(app) - .delete('/api/users/' + userId.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.FORBIDDEN) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Unfollow userDisplay with login cookie', async () => { - await request(app) - .delete('/api/users/' + userId2.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Unfollow userDisplay with login cookie again', async () => { - await request(app) - .delete('/api/users/' + userId2.toString() + '/follow') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - /** - ! Test Updating the User - */ - - // all will fail because no login cookie so only test one - it('Update userDisplay with no login cookie', async () => { - await request(app) - .post('/api/users/') - .send({}) - .expect(HttpStatusCodes.UNAUTHORIZED) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Update userDisplay pfp with login cookie', async () => { - await request(app) - .post('/api/users/profile-picture') - .set('Cookie', loginCookie) - .send({ - avatarURL: - 'https://www.google.com/images/branding/googlelogo/2x/' + - 'googlelogo_color_272x92dp.png', - }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay Bio with login cookie', async () => { - await request(app) - .post('/api/users/bio') - .set('Cookie', loginCookie) - .send({ bio: 'I am a test userDisplay' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay quote with login cookie', async () => { - await request(app) - .post('/api/users/quote') - .set('Cookie', loginCookie) - .send({ quote: 'I am a test userDisplay' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay name with login cookie', async () => { - await request(app) - .post('/api/users/name') - .set('Cookie', loginCookie) - .send({ name: 'Test User' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay blog url with login cookie', async () => { - await request(app) - .post('/api/users/blog-url') - .set('Cookie', loginCookie) - .send({ blogUrl: 'https://www.google.com' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay website url with login cookie', async () => { - await request(app) - .post('/api/users/website-url') - .set('Cookie', loginCookie) - .send({ websiteUrl: 'https://youtu.be/dQw4w9WgXcQ' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay github url with login cookie', async () => { - await request(app) - .post('/api/users/github-url') - .set('Cookie', loginCookie) - .send({ githubUrl: 'https://github.com/NavigoLearn' }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay email with the same email', async () => { - await request(app) - .post('/api/users/email') - .set('Cookie', loginCookie) - .send({ email, password }) - .expect(HttpStatusCodes.BAD_REQUEST) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - it('Update userDisplay email with login cookie', async () => { - email = `testuser${Math.floor(Math.random() * 1000000)}@test.com`; - await request(app) - .post('/api/users/email') - .set('Cookie', loginCookie) - .send({ email, password }) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); - - /* - ! Test Deleting the User - */ - - // will fail because no login cookie - it('Delete userDisplay with no login cookie', async () => { - await request(app) - .delete('/api/users/') - .expect(HttpStatusCodes.UNAUTHORIZED) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body.message).toBeDefined(); - }); - }); - - it('Delete userDisplay with login cookie', async () => { - await request(app) - .delete('/api/users/') - .set('Cookie', loginCookie) - .expect(HttpStatusCodes.OK) - .expect('Content-Type', /json/) - .expect((res) => { - expect(res.body).toBeDefined(); - }); - }); -}); diff --git a/spec/tests/utils/database.spec.ts b/spec/tests/utils/database.spec.ts index dfb17b6..c05bf44 100644 --- a/spec/tests/utils/database.spec.ts +++ b/spec/tests/utils/database.spec.ts @@ -1,34 +1,40 @@ import Database from '@src/util/DatabaseDriver'; -import User from '@src/models/User'; +import { IUser, User } from '@src/models/User'; +import { randomString } from '@spec/utils/randomString'; + +function testUserAttributes(user: IUser, user2?: IUser) { + expect(user2).not.toBe(undefined); + Object.keys(user).forEach((key) => { + if (Object.prototype.hasOwnProperty.call(user2, key)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(user2[key]).toBe(user[key]); + } + }); +} describe('Database', () => { const users: User[] = []; + const db = new Database(); // test for inserting users it('should insert users', async () => { - // get database - const db = new Database(); - // add create 10 users for (let i = 0; i < 10; i++) { // get random details - const username = Math.random().toString(36).substring(2, 15); + const username = randomString(); const email = username + '@test.com'; - const user = new User( - username, + const user = new User({ + name: username, email, - 0, - 'password', - undefined, - undefined, - undefined, - ); + pwdHash: 'password', + }); // add userDisplay to database - const id = await db.insert('users', user); + const id = await db.insert('users', user.toObject()); // add id to userDisplay - user.id = id; + user.set({ id: id }); // add userDisplay to users array users.push(user); @@ -39,16 +45,13 @@ describe('Database', () => { // test for updating users it('should update users', async () => { - // get database - const db = new Database(); - // update all users for (const user of users) { // change username - user.name = Math.random().toString(36).substring(2, 15); + user.set({ name: randomString() }); // update userDisplay - const success = await db.update('users', user.id, user); + const success = await db.update('users', user.id, user.toObject()); expect(success).toBe(true); } @@ -56,133 +59,97 @@ describe('Database', () => { // test for getting userDisplay by id it('should get users by id', async () => { - // get database - const db = new Database(); - // get all users for (const user of users) { - const user2 = await db.get('users', user.id); + const user2 = await db.get('users', user.id); - expect(user2).not.toBe(undefined); - expect(user2?.id).toBe(user.id); - expect(user2?.email).toBe(user.email); - expect(user2?.name).toBe(user.name); - expect(user2?.pwdHash).toBe(user.pwdHash); - expect(user2?.role).toBe(user.role); + expect(user2).not.toBe(null); + if (user2 === null) return; + testUserAttributes(user, user2); } }); // test for getting userDisplay by key (email) it('should get users by key', async () => { - // get database - const db = new Database(); - // get all users for (const user of users) { - const user2 = await db.getWhere('users', 'email', user.email); + const user2 = await db.getWhere('users', 'email', user.email); - expect(user2).not.toBe(undefined); - expect(user2?.id).toBe(user.id); - expect(user2?.email).toBe(user.email); - expect(user2?.name).toBe(user.name); - expect(user2?.pwdHash).toBe(user.pwdHash); - expect(user2?.role).toBe(user.role); + expect(user2).not.toBe(null); + if (user2 === null) return; + testUserAttributes(user, user2); } }); // test for getting user by key (email) with value (userDisplay.email) like it('should get users by key with value like', async () => { - // get database - const db = new Database(); - // get all users for (const user of users) { // process email to get only the username const email = user.email.split('@')[0] + '%'; - const user2 = await db.getWhereLike('users', 'email', email); + const user2 = await db.getWhereLike('users', 'email', email); - expect(user2).not.toBe(undefined); - expect(user2?.id).toBe(user.id); - expect(user2?.email).toBe(user.email); - expect(user2?.name).toBe(user.name); - expect(user2?.pwdHash).toBe(user.pwdHash); - expect(user2?.role).toBe(user.role); + expect(user2).not.toBe(null); + if (user2 === null) return; + testUserAttributes(user, user2); } }); // test for getting all users it('should get all users', async () => { - // get database - const db = new Database(); - // get all users - const users2 = await db.getAll('users'); + const users2 = await db.getAll('users'); - expect(users2).not.toBe(undefined); - expect(users2?.length).toBeGreaterThanOrEqual(users.length); + expect(users2).not.toBe(null); + if (!users2) return; + expect(users2.length).toEqual(users.length); }); // test for getting all users where key (pwdHash) is value (password) it('should get all users where key is value', async () => { - // get database - const db = new Database(); - // get all users - const users2 = await db.getAllWhere('users', 'pwdHash', 'password'); + const users2 = await db.getAllWhere('users', 'pwdHash', 'password'); - expect(users2).not.toBe(undefined); - expect(users2?.length).toBe(users.length); + expect(users2).not.toBe(null); + if (!users2) return; + expect(users2.length).toBe(users.length); for (const user of users) { - const user2 = users2?.find((u) => u.id === user.id); + const user2 = users2.find((u) => u.id === user.id); expect(user2).not.toBe(undefined); - expect(user2?.id).toBe(user.id); - expect(user2?.email).toBe(user.email); - expect(user2?.name).toBe(user.name); - expect(user2?.pwdHash).toBe(user.pwdHash); - expect(user2?.role).toBe(user.role); + if (user2 === null) return; + testUserAttributes(user, user2); } }); // get all users where key (pwdHash) is value (password) like it('should get all users where key is value like', async () => { - // get database - const db = new Database(); - // get all users - const users2 = await db.getAllWhereLike('users', 'pwdHash', 'pass%'); + const users2 = await db.getAllWhereLike('users', 'pwdHash', 'pass%'); - expect(users2).not.toBe(undefined); - expect(users2?.length).toBe(users.length); + expect(users2).not.toBe(null); + if (!users2) return; + expect(users2.length).toBe(users.length); for (const user of users) { - const user2 = users2?.find((u) => u.id === user.id); + const user2 = users2.find((u) => u.id === user.id); expect(user2).not.toBe(undefined); - expect(user2?.id).toBe(user.id); - expect(user2?.email).toBe(user.email); - expect(user2?.name).toBe(user.name); - expect(user2?.pwdHash).toBe(user.pwdHash); - expect(user2?.role).toBe(user.role); + if (user2 === null) return; + testUserAttributes(user, user2); } }); // test for counting users it('should count users', async () => { - // get database - const db = new Database(); - // get count const count = await db.count('users'); - expect(count).toBeGreaterThanOrEqual(users.length); + expect(count).toEqual(BigInt(users.length)); }); // test for counting users where key (pwdHash) is value (password) it('should count users where key is value', async () => { - // get database - const db = new Database(); - // get count const count = BigInt(await db.countWhere('users', 'pwdHash', 'password')); const size = BigInt(users.length); @@ -192,9 +159,6 @@ describe('Database', () => { // test for counting users where key (pwdHash) is value (password) like it('should count users where key is value like', async () => { - // get database - const db = new Database(); - // get count const count = BigInt(await db.countWhereLike('users', 'pwdHash', 'pass%')); const size = BigInt(users.length); @@ -204,9 +168,6 @@ describe('Database', () => { // test for deleting users it('should delete users', async () => { - // get database - const db = new Database(); - // delete all users for (const user of users) { const success = await db.delete('users', user.id); diff --git a/spec/utils/createUser.ts b/spec/utils/createUser.ts new file mode 100644 index 0000000..4b4aba7 --- /dev/null +++ b/spec/utils/createUser.ts @@ -0,0 +1 @@ +export function createUser() {} From 3faa33dcd01c05a7300a5d90912b6c233d572877 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:49:03 +0300 Subject: [PATCH 051/118] Added modification interfaces and methods to models Added a separate interface for modifications to each model, making it clearer what type of modifications can be made. Also implemented 'toObject' method to each model to convert the class instance into an object. This change adds an abstraction layer to data manipulation, making modifications more explicit and controlled, and serialization easier. --- src/models/Follower.ts | 25 ++++++++++++++++++++++++- src/models/Issue.ts | 28 +++++++++++++++++++++++++++- src/models/IssueComment.ts | 24 +++++++++++++++++++++++- src/models/Roadmap.ts | 31 ++++++++++++++++++++++++++++++- src/models/RoadmapLike.ts | 28 +++++++++++++++++++++++++++- src/models/RoadmapView.ts | 28 +++++++++++++++++++++++++++- src/models/Session.ts | 20 +++++++++++++++++++- src/models/User.ts | 30 +++++++++++++++++++++++++++++- src/models/UserInfo.ts | 33 +++++++++++++++++++++++++++++++-- 9 files changed, 237 insertions(+), 10 deletions(-) diff --git a/src/models/Follower.ts b/src/models/Follower.ts index 211127e..53d34d6 100644 --- a/src/models/Follower.ts +++ b/src/models/Follower.ts @@ -14,6 +14,14 @@ interface IFollowerConstructor { readonly createdAt?: Date; } +// Interface for modifying a Follower +interface IFollowerModifications { + readonly id?: bigint; + readonly followerId?: bigint; + readonly userId?: bigint; + readonly createdAt?: Date; +} + // Class export class Follower implements IFollower { private _id: bigint; @@ -34,7 +42,12 @@ export class Follower implements IFollower { } // Method to modify the properties - public set({ id, followerId, userId, createdAt }: IFollower): void { + public set({ + id, + followerId, + userId, + createdAt, + }: IFollowerModifications): void { if (id !== undefined) this._id = id; if (followerId !== undefined) this._followerId = followerId; if (userId !== undefined) this._userId = userId; @@ -67,4 +80,14 @@ export class Follower implements IFollower { 'createdAt' in obj ); } + + // toObject method + public toObject(): IFollower { + return { + id: this._id, + followerId: this._followerId, + userId: this._userId, + createdAt: this._createdAt, + }; + } } diff --git a/src/models/Issue.ts b/src/models/Issue.ts index 1c4d4bc..bb283d0 100644 --- a/src/models/Issue.ts +++ b/src/models/Issue.ts @@ -22,6 +22,18 @@ interface IIssueConstructor { readonly updatedAt?: Date; } +// Interface for modifying an Issue +interface IIssueModifications { + readonly id?: bigint; + readonly roadmapId?: bigint; + readonly userId?: bigint; + readonly open?: boolean; + readonly title?: string; + readonly content?: string | null; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} + // Class export class Issue implements IIssue { private _id: bigint; @@ -63,7 +75,7 @@ export class Issue implements IIssue { content, createdAt, updatedAt, - }: IIssue): void { + }: IIssueModifications): void { if (id !== undefined) this._id = id; if (roadmapId !== undefined) this._roadmapId = roadmapId; if (userId !== undefined) this._userId = userId; @@ -118,4 +130,18 @@ export class Issue implements IIssue { 'updatedAt' in obj ); } + + // toObject method + public toObject(): IIssue { + return { + id: this._id, + roadmapId: this._roadmapId, + userId: this._userId, + open: this._open, + title: this._title, + content: this._content, + createdAt: this._createdAt, + updatedAt: this._updatedAt, + }; + } } diff --git a/src/models/IssueComment.ts b/src/models/IssueComment.ts index 0ddf2a6..6576e8d 100644 --- a/src/models/IssueComment.ts +++ b/src/models/IssueComment.ts @@ -18,6 +18,16 @@ interface IIssueCommentConstructor { readonly updatedAt?: Date; } +// Interface for modifying an IssueComment +interface IIssueCommentModifications { + readonly id?: bigint; + readonly issueId?: bigint; + readonly userId?: bigint; + readonly content?: string; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} + // Class export class IssueComment implements IIssueComment { private _id: bigint; @@ -51,7 +61,7 @@ export class IssueComment implements IIssueComment { content, createdAt, updatedAt, - }: IIssueComment): void { + }: IIssueCommentModifications): void { if (id !== undefined) this._id = id; if (issueId !== undefined) this._issueId = issueId; if (userId !== undefined) this._userId = userId; @@ -96,4 +106,16 @@ export class IssueComment implements IIssueComment { 'updatedAt' in obj ); } + + // toObject method + public toObject(): IIssueComment { + return { + id: this._id, + issueId: this._issueId, + userId: this._userId, + content: this._content, + createdAt: this._createdAt, + updatedAt: this._updatedAt, + }; + } } diff --git a/src/models/Roadmap.ts b/src/models/Roadmap.ts index 101844f..eb01cd0 100644 --- a/src/models/Roadmap.ts +++ b/src/models/Roadmap.ts @@ -23,6 +23,20 @@ interface IRoadmapConstructor { readonly createdAt?: Date; readonly updatedAt?: Date; } + +// Interface for modifying a Roadmap +interface IRoadmapModifications { + readonly id?: bigint; + readonly name?: string; + readonly description?: string; + readonly userId?: bigint; + readonly isPublic?: boolean; + readonly isDraft?: boolean; + readonly data?: string; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} + // Class export class Roadmap implements IRoadmap { private _id: bigint; @@ -68,7 +82,7 @@ export class Roadmap implements IRoadmap { data, createdAt, updatedAt, - }: IRoadmap): void { + }: IRoadmapModifications): void { if (id !== undefined) this._id = id; if (name !== undefined) this._name = name; if (description !== undefined) this._description = description; @@ -130,4 +144,19 @@ export class Roadmap implements IRoadmap { 'updatedAt' in obj ); } + + // toObject method + public toObject(): IRoadmap { + return { + id: this._id, + name: this._name, + description: this._description, + userId: this._userId, + isPublic: this._isPublic, + isDraft: this._isDraft, + data: this._data, + createdAt: this._createdAt, + updatedAt: this._updatedAt, + }; + } } diff --git a/src/models/RoadmapLike.ts b/src/models/RoadmapLike.ts index 5002905..6014b76 100644 --- a/src/models/RoadmapLike.ts +++ b/src/models/RoadmapLike.ts @@ -16,6 +16,15 @@ interface IRoadmapLikeConstructor { readonly createdAt?: Date; } +// Interface for modifying a RoadmapLike +interface IRoadmapLikeModifications { + readonly id?: bigint; + readonly roadmapId?: bigint; + readonly userId?: bigint; + readonly value?: number; + readonly createdAt?: Date; +} + // Class export class RoadmapLike implements IRoadmapLike { private _id: bigint; @@ -39,7 +48,13 @@ export class RoadmapLike implements IRoadmapLike { } // Method to modify the properties - public set({ id, roadmapId, userId, value, createdAt }: IRoadmapLike): void { + public set({ + id, + roadmapId, + userId, + value, + createdAt, + }: IRoadmapLikeModifications): void { if (id !== undefined) this._id = id; if (roadmapId !== undefined) this._roadmapId = roadmapId; if (userId !== undefined) this._userId = userId; @@ -76,4 +91,15 @@ export class RoadmapLike implements IRoadmapLike { 'userId' in obj ); } + + // toObject method + public toObject(): IRoadmapLike { + return { + id: this._id, + roadmapId: this._roadmapId, + userId: this._userId, + value: this._value, + createdAt: this._createdAt, + }; + } } diff --git a/src/models/RoadmapView.ts b/src/models/RoadmapView.ts index 228af36..aea902f 100644 --- a/src/models/RoadmapView.ts +++ b/src/models/RoadmapView.ts @@ -16,6 +16,15 @@ interface IRoadmapViewConstructor { readonly createdAt?: Date; } +// Interface for modifying a RoadmapView +interface IRoadmapViewModifications { + readonly id?: bigint; + readonly userId?: bigint; + readonly roadmapId?: bigint; + readonly full?: boolean; + readonly createdAt?: Date; +} + // Class export class RoadmapView implements IRoadmapView { private _id: bigint; @@ -39,7 +48,13 @@ export class RoadmapView implements IRoadmapView { } // Method to modify the properties - public set({ id, userId, roadmapId, full, createdAt }: IRoadmapView): void { + public set({ + id, + userId, + roadmapId, + full, + createdAt, + }: IRoadmapViewModifications): void { if (id !== undefined) this._id = id; if (userId !== undefined) this._userId = userId; if (roadmapId !== undefined) this._roadmapId = roadmapId; @@ -78,4 +93,15 @@ export class RoadmapView implements IRoadmapView { 'createdAt' in obj ); } + + // toObject method + public toObject(): IRoadmapView { + return { + id: this._id, + userId: this._userId, + roadmapId: this._roadmapId, + full: this._full, + createdAt: this._createdAt, + }; + } } diff --git a/src/models/Session.ts b/src/models/Session.ts index f896abd..0401be4 100644 --- a/src/models/Session.ts +++ b/src/models/Session.ts @@ -14,6 +14,14 @@ interface ISessionConstructor { readonly expires: Date; } +// Interface for modifying a Session +interface ISessionModifications { + readonly id?: bigint; + readonly userId?: bigint; + readonly token?: string; + readonly expires?: Date; +} + // Class export class Session implements ISession { private _id: bigint; @@ -34,7 +42,7 @@ export class Session implements ISession { } // Method to modify the properties - public set({ id, userId, token, expires }: ISession): void { + public set({ id, userId, token, expires }: ISessionModifications): void { if (id !== undefined) this._id = id; if (userId !== undefined) this._userId = userId; if (token !== undefined) this._token = token; @@ -67,4 +75,14 @@ export class Session implements ISession { 'expires' in obj ); } + + // toObject method to convert the class instance to an object + public toObject(): ISession { + return { + id: this._id, + userId: this._userId, + token: this._token, + expires: this._expires, + }; + } } diff --git a/src/models/User.ts b/src/models/User.ts index 0a0775f..0a7f826 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -24,6 +24,19 @@ interface IUserConstructor { readonly createdAt?: Date; } +// Interface for modifying a User +interface IUserModifications { + readonly id?: bigint; + readonly avatar?: string | null; + readonly name?: string; + readonly email?: string; + readonly role?: number | null; + readonly pwdHash?: string | null; + readonly googleId?: string | null; + readonly githubId?: string | null; + readonly createdAt?: Date; +} + // Class export class User implements IUser { private _id: bigint; @@ -69,7 +82,7 @@ export class User implements IUser { googleId, githubId, createdAt, - }: IUserConstructor): void { + }: IUserModifications): void { if (id !== undefined) this._id = id; if (avatar !== undefined) this._avatar = avatar; if (name !== undefined) this._name = name; @@ -127,4 +140,19 @@ export class User implements IUser { 'createdAt' in obj ); } + + // toObject method + public toObject(): IUser { + return { + id: this._id, + avatar: this._avatar, + name: this._name, + email: this._email, + role: this._role, + pwdHash: this._pwdHash, + googleId: this._googleId, + githubId: this._githubId, + createdAt: this._createdAt, + }; + } } diff --git a/src/models/UserInfo.ts b/src/models/UserInfo.ts index 30ea98e..c503ff4 100644 --- a/src/models/UserInfo.ts +++ b/src/models/UserInfo.ts @@ -18,7 +18,17 @@ interface IUserInfoConstructor { readonly githubUrl?: string | null; } -// TypeScript Class +// Interface for modifying a UserInfo +interface IUserInfoModifications { + readonly id?: bigint; + readonly userId?: bigint; + readonly bio?: string | null; + readonly quote?: string | null; + readonly websiteUrl?: string | null; + readonly githubUrl?: string | null; +} + +// Class export class UserInfo implements IUserInfo { private _id: bigint; private _userId: bigint; @@ -44,7 +54,14 @@ export class UserInfo implements IUserInfo { } // Method to modify the properties - public set({ id, userId, bio, quote, websiteUrl, githubUrl }: IUserInfo) { + public set({ + id, + userId, + bio, + quote, + websiteUrl, + githubUrl, + }: IUserInfoModifications) { if (id !== undefined) this._id = id; if (userId !== undefined) this._userId = userId; if (bio !== undefined) this._bio = bio; @@ -81,4 +98,16 @@ export class UserInfo implements IUserInfo { public static isUserInfo(obj: unknown): obj is IUserInfo { return typeof obj === 'object' && obj !== null && 'userId' in obj; } + + // toObject method + public toObject(): IUserInfo { + return { + id: this._id, + userId: this._userId, + bio: this._bio, + quote: this._quote, + websiteUrl: this._websiteUrl, + githubUrl: this._githubUrl, + }; + } } From 28fca455be86b4b79354cf91039113525761e37b Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:49:23 +0300 Subject: [PATCH 052/118] Add "if not exists" conditions and modify constraint names in SQL setup script The SQL setup script has been updated to use "if not exists" conditions when creating tables and indexes. This helps to prevent errors and ensures proper setup when the script is run on a system where some tables or indexes may already exist. Additionally, the names of relationship constraints have been modified to reflect the correct column names, improving semantics and clarity. --- src/sql/setup.sql | 82 +++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/sql/setup.sql b/src/sql/setup.sql index 05a4d76..a81cad7 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -1,4 +1,4 @@ -create table users +create table if not exists users ( id bigint auto_increment primary key, @@ -12,28 +12,28 @@ create table users createdAt timestamp default current_timestamp() not null ); -create table followers +create table if not exists followers ( id bigint auto_increment primary key, followerId bigint not null, userId bigint not null, createdAt timestamp default current_timestamp() not null, - constraint followers_users_id_fk + constraint followers_userId_followerId_fk foreign key (followerId) references users (id) on delete cascade, - constraint followers_users_id_fk2 + constraint followers_userId_userId_fk foreign key (userId) references users (id) on delete cascade ); -create index followers_followerId_index +create index if not exists followers_followerId_index on followers (followerId); -create index followers_userId_index +create index if not exists followers_userId_index on followers (userId); -create table roadmaps +create table if not exists roadmaps ( id bigint auto_increment primary key, @@ -45,12 +45,12 @@ create table roadmaps data longtext not null, createdAt timestamp default current_timestamp() not null, updatedAt timestamp default current_timestamp() not null, - constraint roadmaps_users_id_fk + constraint roadmaps_userId_fk foreign key (userId) references users (id) on delete cascade ); -create table issues +create table if not exists issues ( id bigint auto_increment primary key, @@ -61,15 +61,15 @@ create table issues content text null, createdAt timestamp default current_timestamp() null, updatedAt timestamp default current_timestamp() not null, - constraint issues_roadmaps_id_fk + constraint issues_roadmapId_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, - constraint issues_users_id_fk + constraint issues_userId_fk foreign key (userId) references users (id) on delete cascade ); -create table issueComments +create table if not exists issueComments ( id bigint auto_increment primary key, @@ -78,30 +78,30 @@ create table issueComments content text not null, createdAt timestamp default current_timestamp() not null, updatedAt timestamp default current_timestamp() not null, - constraint issueComments_issues_id_fk + constraint issueComments_issuesId_fk foreign key (issueId) references issues (id) on delete cascade, - constraint issueComments_users_id_fk + constraint issueComments_usersId_fk foreign key (userId) references users (id) on delete cascade ); -create index issueComments_issueId_createdAt_index +create index if not exists issueComments_issueId_createdAt_index on issueComments (issueId, createdAt); -create index issueComments_userid_index +create index if not exists issueComments_userid_index on issueComments (userId); -create index issues_roadmapId_createdAt_index +create index if not exists issues_roadmapId_createdAt_index on issues (roadmapId asc, createdAt desc); -create index issues_title_index +create index if not exists issues_title_index on issues (title); -create index issues_userId_index +create index if not exists issues_userId_index on issues (userId); -create table roadmapLikes +create table if not exists roadmapLikes ( id bigint auto_increment primary key, @@ -109,67 +109,67 @@ create table roadmapLikes userId bigint not null, value int null, createdAt timestamp default current_timestamp() null, - constraint roadmaplikes_roadmaps_id_fk + constraint roadmapLikes_roadmapId_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, - constraint roadmaplikes_users_id_fk + constraint roadmapLikes_userId_fk foreign key (userId) references users (id) on delete cascade ); -create index roadmapLikes_roadmapId_index +create index if not exists roadmapLikes_roadmapId_index on roadmapLikes (roadmapId); -create table roadmapViews +create table if not exists roadmapViews ( id bigint auto_increment primary key, userId bigint default -1 not null, roadmapId bigint not null, full tinyint(1) default 0 not null, - createdAt timestamp default current_timestamp() not null, - constraint roadmapViews_roadmaps_id_fk + createdAt timestamp default current_timestamp() not null, + constraint roadmapViews_roadmapsId_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, - constraint roadmapViews_users_id_fk + constraint roadmapViews_userId_fk foreign key (userId) references users (id) on delete cascade ); -create index roadmapViews_roadmapId_createdAt_index +create index if not exists roadmapViews_roadmapId_createdAt_index on roadmapViews (roadmapId, createdAt); -create index roadmaps_createdAt_index +create index if not exists roadmaps_createdAt_index on roadmaps (createdAt desc); -create index roadmaps_description_index +create index if not exists roadmaps_description_index on roadmaps (description); -create index roadmaps_name_index +create index if not exists roadmaps_name_index on roadmaps (name); -create index roadmaps_owner_index +create index if not exists roadmaps_owner_index on roadmaps (userId); -create table sessionTable +create table if not exists sessionTable ( id bigint auto_increment primary key, userId bigint not null, token varchar(255) not null, expires timestamp not null, - constraint sessions_users_id_fk + constraint sessions_usersId_fk foreign key (userId) references users (id) on delete cascade ); -create index sessionTable_expires_index +create index if not exists sessionTable_expires_index on sessionTable (expires); -create index sessions_index +create index if not exists sessionTable_userId_token_index on sessionTable (userId, token); -create table userInfo +create table if not exists userInfo ( id bigint auto_increment primary key, @@ -178,18 +178,18 @@ create table userInfo quote varchar(255) null, websiteUrl varchar(255) null, githubUrl varchar(255) null, - constraint userInfo_users_id_fk + constraint userInfo_usersId_fk foreign key (userId) references users (id) on delete cascade ); -create index userInfo_index +create index if not exists userInfo_userId_index on userInfo (userId); -create index users_index +create index if not exists users_email_name_index on users (email, name); -create view sessions as +create view if not exists sessions as select `navigo`.`sessionTable`.`id` AS `id`, `navigo`.`sessionTable`.`userId` AS `userId`, `navigo`.`sessionTable`.`token` AS `token`, From 46c09721145cf91fbe3db193a5702cf2101a9562 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:50:10 +0300 Subject: [PATCH 053/118] Refactor DatabaseDriver and remove eslint-disable-next-line usage The DatabaseDriver utility file offers cleaner code with several improvements. First, the user import semantics have been adjusted for an exact match. The handling of Result types has been improved to use specific type or null instead of undefined, improving type checking and semantic readability. Also, a DataType has been added to provide a more precise type definition than the previously used object. The unnecessary eslint-disable-next-line have been removed by improving type-checking and coercion methods, leading to safer and cleaner code. Overall, this refactoring makes the DatabaseDriver utility code more clean, and understandable. Also, it potentially eliminates hidden bugs or corner cases related to type checking and data handling. --- src/util/DatabaseDriver.ts | 178 +++++++++++++++---------------------- 1 file changed, 73 insertions(+), 105 deletions(-) diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 0fa533e..4b821aa 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -3,7 +3,7 @@ import { createPool, Pool } from 'mariadb'; import fs from 'fs'; import path from 'path'; import logger from 'jet-logger'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; // database credentials const { DBCred } = EnvVars; @@ -48,6 +48,8 @@ interface Data { values: never[]; } +type DataType = bigint | string | number | Date | null; + // config interface interface DatabaseConfig { host: string; @@ -74,8 +76,10 @@ interface ResultSetHeader { warningStatus: number; } -// eslint-disable-next-line @typescript-eslint/ban-types -function processData(data: Object, discardId = true): Data { +function processData( + data: object | Record, + discardId = true, +): Data { // get keys and values const keys = Object.keys(data); const values = Object.values(data) as never[]; @@ -92,7 +96,7 @@ function processData(data: Object, discardId = true): Data { function parseResult( result: RowDataPacket[] | ResultSetHeader, -): RowDataPacket[] | undefined { +): RowDataPacket[] | null { if (result && Object.keys(result).length > 0) { const keys = Object.keys((result as RowDataPacket[])[0]); for (const element of keys) { @@ -111,8 +115,8 @@ function parseResult( function getFirstResult( result: RowDataPacket[] | ResultSetHeader, -): T | undefined { - return parseResult(result)?.[0] as T; +): T | null { + return (parseResult(result)?.[0] as T) || null; } class Database { @@ -128,8 +132,11 @@ class Database { this._setup(); } - public async insert(table: string, data: object, discardId = true): - Promise { + public async insert( + table: string, + data: object | Record, + discardId = true, + ): Promise { const { keys, values } = processData(data, discardId); // check if keys are trusted @@ -140,13 +147,12 @@ class Database { const sql = `INSERT INTO ${table} (${keys.join(',')}) VALUES (${values.map(() => '?').join(',')})`; // execute query - const result = await this._query(sql, values); + const result = (await this._query(sql, values)) as ResultSetHeader; // return insert id - let insertId = BigInt(-1); + let insertId = -1n; if (result) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - insertId = BigInt((result as ResultSetHeader)?.insertId); + insertId = BigInt(result.insertId); } return insertId; } @@ -154,7 +160,7 @@ class Database { public async update( table: string, id: bigint, - data: object, + data: object | Record, discardId = true, ): Promise { const { keys, values } = processData(data, discardId); @@ -168,15 +174,13 @@ class Database { const sql = `UPDATE ${table} SET ${sqlKeys} WHERE id = ?`; - const params = [ ...values, id ]; + const params = [...values, id]; // execute query - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = await this._query(sql, params); + const result = (await this._query(sql, params)) as ResultSetHeader; let affectedRows = -1; if (result) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - affectedRows = (result as ResultSetHeader)?.affectedRows || -1; + affectedRows = result.affectedRows || -1; } // return true if affected rows > 0 else false @@ -187,25 +191,45 @@ class Database { const sql = `DELETE FROM ${table} WHERE id = ?`; - const result = await this._query(sql, [ id ]); + const result = (await this._query(sql, [id])) as ResultSetHeader; let affectedRows = -1; if (result) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - affectedRows = (result as ResultSetHeader)?.affectedRows || -1; + affectedRows = result.affectedRows || -1; } // return true if affected rows > 0 else false return affectedRows > 0; } - public async get(table: string, id: bigint): Promise { + private _buildWhereQuery = ( + like: boolean, + ...values: unknown[] + ): { keyString: string; params: unknown[] } | null => { + let keyString = ''; + let params: unknown[] = []; + + for (let i = 0; i < values.length - 1; i += 2) { + const key = values[i]; + + if (typeof key !== 'string' || !trustedColumns.includes(key)) return null; + + if (i > 0) keyString += ' AND '; + keyString += `${key} ${like ? 'LIKE' : '='} ?`; + + params = [...params, values[i + 1]]; + } + + return { keyString, params }; + }; + + public async get(table: string, id: bigint): Promise { // create sql query - select * from table where id = ? const sql = `SELECT * FROM ${table} WHERE id = ?`; // execute query - const result = await this._query(sql, [ id ]); + const result = await this._query(sql, [id]); // check if T has any properties that are JSON // if so parse them @@ -215,14 +239,14 @@ class Database { public async getWhere( table: string, ...values: unknown[] - ): Promise { + ): Promise { return this._getWhere(table, false, ...values); } public async getWhereLike( table: string, ...values: unknown[] - ): Promise { + ): Promise { return this._getWhere(table, true, ...values); } @@ -230,36 +254,19 @@ class Database { table: string, like: boolean, ...values: unknown[] - ): Promise { - // format values - let keyString = ''; - let params: unknown[] = []; + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return null; - for (let i = 0; i < values.length - 1; i += 2) { - const key = values[i]; - - // check if key is trusted and is a string - if (typeof key !== 'string' || !trustedColumns.includes(key)) - return undefined; - - if (i > 0) keyString += ' AND '; - keyString += `${key} ${like ? 'LIKE' : '='} ?`; - params = [ ...params, values[i + 1] ]; - } - - // create sql query const sql = `SELECT * FROM ${table} - WHERE ${keyString}`; - - // execute query - const result = await this._query(sql, params); + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); - // if so parse them return getFirstResult(result); } - public async getAll(table: string): Promise { + public async getAll(table: string): Promise { // create sql query - select * from table const sql = `SELECT * FROM ${table}`; @@ -269,20 +276,20 @@ class Database { // check if T has any properties that are JSON // if so parse them - return parseResult(result) as T[]; + return parseResult(result) as T[] | null; } public async getAllWhere( table: string, ...values: unknown[] - ): Promise { + ): Promise { return this._getAllWhere(table, false, ...values); } public async getAllWhereLike( table: string, ...values: unknown[] - ): Promise { + ): Promise { return this._getAllWhere(table, true, ...values); } @@ -290,34 +297,16 @@ class Database { table: string, like: boolean, ...values: unknown[] - ): Promise { - // format values - let keyString = ''; - let params: unknown[] = []; - - for (let i = 0; i < values.length - 1; i += 2) { - const key = values[i]; - - // check if key is trusted and is a string - if (typeof key !== 'string' || !trustedColumns.includes(key)) - return undefined; + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return null; - if (i > 0) keyString += ' AND '; - keyString += `${key} ${like ? 'LIKE' : '='} ?`; - params = [ ...params, values[i + 1] ]; - } - - // create sql query const sql = `SELECT * FROM ${table} - WHERE ${keyString}`; + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); - // execute query - const result = await this._query(sql, params); - - // check if T has any properties that are JSON - // if so parse them - return parseResult(result) as T[]; + return parseResult(result) as T[] | null; } public async count(table: string): Promise { @@ -351,31 +340,14 @@ class Database { like: boolean, ...values: unknown[] ): Promise { - // format values - let keyString = ''; - let params: unknown[] = []; - - for (let i = 0; i < values.length - 1; i += 2) { - const key = values[i]; + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return BigInt(0); - // check if key is trusted and is a string - if (typeof key !== 'string' || !trustedColumns.includes(key)) - return BigInt(0); - - if (i > 0) keyString += ' AND '; - keyString += `${key} ${like ? 'LIKE' : '='} ?`; - params = [ ...params, values[i + 1] ]; - } - - // create sql query - select count(*) from table where key = ? const sql = `SELECT COUNT(*) FROM ${table} - WHERE ${keyString}`; + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); - // execute query - const result = await this._query(sql, params); - - // return count return (result as CountDataPacket[])[0]['COUNT(*)']; } @@ -412,15 +384,11 @@ class Database { } // create dummy user - const user = new User( - 'Unknown User', - 'unknown', - 0, - '', - BigInt(-1), - '', - '', - ); + const user = new User({ + id: -1n, + name: 'Unknown User', + email: 'unknown', + }); // see if user exists const existingUser = await this.get('users', user.id); From bc73592d91aeb0990ee11e1067392c489a97fdd3 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:52:59 +0300 Subject: [PATCH 054/118] Refactor code to enhance readability and flexibility --- src/controllers/authController.ts | 124 ++++++++++++++++------------- src/controllers/usersController.ts | 2 +- src/helpers/apiResponses.ts | 41 ++++------ src/helpers/databaseManagement.ts | 46 +++++------ 4 files changed, 110 insertions(+), 103 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 837bd2f..ac9a5b3 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,7 +1,7 @@ import { RequestWithBody } from '@src/validators/validateBody'; import { Response } from 'express'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import axios, { HttpStatusCode } from 'axios'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; import { @@ -20,7 +20,6 @@ import { insertUser, insertUserInfo, updateUser, - updateUserInfo, } from '@src/helpers/databaseManagement'; import { accountCreated, @@ -35,6 +34,7 @@ import { serverError, unauthorized, } from '@src/helpers/apiResponses'; +import { NodeEnvs } from '@src/constants/misc'; /* * Interfaces @@ -60,7 +60,7 @@ interface GitHubUserData { * Helpers */ export function _handleNotOkay(res: Response, error: number): unknown { - if (EnvVars.NodeEnv !== 'test') logger.err(error, true); + if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(error, true); if (error >= (HttpStatusCode.InternalServerError as number)) return externalBadGateway(res); @@ -97,7 +97,7 @@ export async function authLogin( // check userInfo table for user const userInfo = await getUserInfo(db, user.id); if (!userInfo) - if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) + if (!(await insertUserInfo(db, user.id, new UserInfo({ userId: user.id })))) return serverError(res); // create session and save it @@ -127,9 +127,11 @@ export async function authRegister( // create user const userId = await insertUser( db, - email, - email.split('@')[0], - saltPassword(password), + new User({ + email, + name: email.split('@')[0], + pwdHash: saltPassword(password), + }), ); if (userId === -1n) return serverError(res); @@ -163,7 +165,7 @@ export async function authChangePassword( const isCorrect = comparePassword(password, user.pwdHash || ''); if (!isCorrect) return invalidLogin(res); - user.pwdHash = saltPassword(newPassword); + user.set({ pwdHash: saltPassword(newPassword) }); // update password in ussr const result = await updateUser(db, userId, user); @@ -172,11 +174,7 @@ export async function authChangePassword( else return serverError(res); } -// eslint-disable-next-line @typescript-eslint/require-await -export async function authForgotPassword( - _: unknown, - res: Response, -): Promise { +export function authForgotPassword(_: unknown, res: Response): unknown { // TODO: implement after SMTP server is set up return notImplemented(res); } @@ -238,11 +236,16 @@ export async function authGoogleCallback( if (!user) { // create user - user = new User(userData.name, userData.email, 0, ''); - user.googleId = userData.id; - user.id = await insertUser(db, userData.email, userData.name, ''); + user = new User({ + name: userData.name, + email: userData.email, + googleId: userData.id, + }); + user.set({ + id: await insertUser(db, user), + }); } else { - user.googleId = userData.id; + user.set({ googleId: userData.id }); // update user if (!(await updateUser(db, user.id, user))) return serverError(res); @@ -250,7 +253,13 @@ export async function authGoogleCallback( // check userInfo table for user const userInfo = await getUserInfo(db, user.id); if (!userInfo) - if (!(await insertUserInfo(db, user.id, new UserInfo(user.id)))) + if ( + !(await insertUserInfo( + db, + user.id, + new UserInfo({ userId: user.id }), + )) + ) return serverError(res); } // check if user was created @@ -262,7 +271,7 @@ export async function authGoogleCallback( return serverError(res); } catch (e) { - if (EnvVars.NodeEnv !== 'test') logger.err(e, true); + if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(e, true); return serverError(res); } } @@ -311,7 +320,7 @@ export async function authGitHubCallback( const accessToken = data.access_token; if (!accessToken) return serverError(res); - // get user info from github + // get user info from GitHub response = await axios.get('https://api.github.com/user', { headers: { Authorization: `Bearer ${accessToken}`, @@ -360,25 +369,27 @@ export async function authGitHubCallback( if (!user) { // create user - user = new User(userData.login, userData.email, 0, ''); - user.githubId = userData.id.toString(); - user.id = await insertUser( - db, - userData.email, - userData.name || userData.login, - '', - new UserInfo( - -1n, - userData.avatar_url, - userData.bio, - '', - userData.blog, - '', - `https://github.com/${userData.login}`, + user = new User({ + name: userData.name || userData.login, + avatar: userData.avatar_url, + email: userData.email, + githubId: userData.id.toString(), + }); + + user.set({ + id: await insertUser( + db, + user, + new UserInfo({ + userId: user.id, + bio: userData.bio, + websiteUrl: userData.blog, + githubUrl: `https://github.com/${userData.login}`, + }), ), - ); + }); } else { - user.githubId = userData.id.toString(); + user.set({ githubId: userData.id.toString() }); await updateUser(db, user.id, user); // get user info @@ -389,27 +400,30 @@ export async function authGitHubCallback( !(await insertUserInfo( db, user.id, - new UserInfo( - user.id, - userData.avatar_url, - userData.bio, - '', - userData.blog, - '', - `https://github.com/${userData.login}`, - ), + new UserInfo({ + userId: user.id, + bio: userData.bio, + websiteUrl: userData.blog, + githubUrl: `https://github.com/${userData.login}`, + }), )) - ) return serverError(res); + ) + return serverError(res); } else { - if (userInfo.bio == '') userInfo.bio = userData.bio; - if (userInfo.profilePictureUrl == '') - userInfo.profilePictureUrl = userData.avatar_url; - if (userInfo.blogUrl == '') userInfo.blogUrl = userData.blog; - if (userInfo.githubUrl == '') - userInfo.githubUrl = `https://github.com/${userData.login}`; + // update user info + user.set({ + avatar: user.avatar || userData.avatar_url, + }); + + userInfo.set({ + bio: userInfo.bio || userData.bio, + websiteUrl: userInfo.websiteUrl || userData.blog, + githubUrl: + userInfo.githubUrl || `https://github.com/${userData.login}`, + }); // update user info - if (!(await updateUserInfo(db, userInfo.id, userInfo))) + if (!(await updateUser(db, user.id, user, userInfo))) return serverError(res); } } @@ -417,7 +431,7 @@ export async function authGitHubCallback( // create session and save it if (await createSaveSession(res, user.id)) return loginSuccessful(res); } catch (e) { - if (EnvVars.NodeEnv !== 'test') logger.err(e, true); + if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(e, true); return serverError(res); } } diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 9642329..f0a5f33 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -87,5 +87,5 @@ export async function usersGetMiniProfile( if (!user || !userInfo) return userNotFound(res); // send user json - return userMiniProfile(res, user, userInfo); + return userMiniProfile(res, user); } diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 833b84b..22dc68b 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -1,6 +1,6 @@ import { Response } from 'express'; import { HttpStatusCode } from 'axios'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import { UserInfo } from '@src/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; import JSONStringify from '@src/util/JSONStringify'; @@ -11,63 +11,63 @@ import JSONStringify from '@src/util/JSONStringify'; export function emailConflict(res: Response): void { res.status(HttpStatusCode.Conflict).json({ - error: 'Email already in use', + message: 'Email already in use', success: false, }); } export function externalBadGateway(res: Response): void { res.status(HttpStatusCode.BadGateway).json({ - error: 'Remote resource error', + message: 'Remote resource error', success: false, }); } export function invalidBody(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ - error: 'Invalid request body', + message: 'Invalid request body', success: false, }); } export function invalidLogin(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ - error: 'Invalid email or password', + message: 'Invalid email or password', success: false, }); } export function invalidParameters(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ - error: 'Invalid request paramteres', + message: 'Invalid request paramteres', success: false, }); } export function notImplemented(res: Response): void { res.status(HttpStatusCode.NotImplemented).json({ - error: 'Not implemented', + message: 'Not implemented', success: false, }); } export function serverError(res: Response): void { res.status(HttpStatusCode.InternalServerError).json({ - error: 'Internal server error', + message: 'Internal server error', success: false, }); } export function userNotFound(res: Response): void { res.status(HttpStatusCode.NotFound).json({ - error: 'User couldn\'t be found', + message: 'User couldn\'t be found', success: false, }); } export function unauthorized(res: Response): void { res.status(HttpStatusCode.Unauthorized).json({ - error: 'Unauthorized', + message: 'Unauthorized', success: false, }); } @@ -119,20 +119,18 @@ export function userProfile( ): void { const { roadmapsCount, issueCount, followerCount, followingCount } = userStats, - { profilePictureUrl, bio, quote, blogUrl, websiteUrl, githubUrl } = - userInfo, - { name, githubId, googleId } = user; + { bio, quote, websiteUrl, githubUrl } = userInfo, + { name, avatar, githubId, googleId } = user; res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ name, - profilePictureUrl, + avatar, userId: user.id, bio, quote, - blogUrl, websiteUrl, githubUrl, roadmapsCount, @@ -147,21 +145,16 @@ export function userProfile( ); } -export function userMiniProfile( - res: Response, - user: User, - userInfo: UserInfo, -): void { - const { profilePictureUrl } = userInfo, - { name } = user; +export function userMiniProfile(res: Response, user: User): void { + const { id, name, avatar } = user; res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ name, - profilePictureUrl, - userId: user.id, + avatar, + userId: id, success: true, }), ); diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index ce90437..615920d 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -1,6 +1,6 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import { UserInfo } from '@src/models/UserInfo'; -import User from '@src/models/User'; +import { IUser, User } from '@src/models/User'; /* * Interfaces @@ -36,22 +36,28 @@ export async function deleteUser( export async function getUser( db: DatabaseDriver, userId: bigint, -): Promise { - return await db.get('users', userId); +): Promise { + const user = await db.get('users', userId); + if (!user) return null; + return new User(user); } export async function getUserByEmail( db: DatabaseDriver, email: string, -): Promise { - return await db.getWhere('users', 'email', email); +): Promise { + const user = await db.getWhere('users', 'email', email); + if (!user) return null; + return new User(user); } export async function getUserInfo( db: DatabaseDriver, userId: bigint, -): Promise { - return await db.getWhere('userInfo', 'userId', userId); +): Promise { + const userInfo = await db.get('userInfo', userId); + if (!userInfo) return null; + return new UserInfo(userInfo); } export async function getUserStats( @@ -90,19 +96,13 @@ export async function isUserFollowing( export async function insertUser( db: DatabaseDriver, - email: string, - name: string, - pwdHash: string, + user: User, userInfo?: UserInfo, ): Promise { - const userId = await db.insert('users', { - email, - name, - pwdHash, - }); - - if (await insertUserInfo(db, userId, userInfo)) { - return userId; + user.set({ id: await db.insert('users', user.toObject()) }); + + if (await insertUserInfo(db, user.id, userInfo)) { + return user.id; } else { return -1n; } @@ -113,9 +113,9 @@ export async function insertUserInfo( userId: bigint, userInfo?: UserInfo, ): Promise { - if (!userInfo) userInfo = new UserInfo(userId); - userInfo.userId = userId; - return (await db.insert('userInfo', userInfo)) >= 0; + if (!userInfo) userInfo = new UserInfo({ userId }); + userInfo.set({ userId }); + return (await db.insert('userInfo', userInfo.toObject())) >= 0; } export async function updateUser( @@ -126,7 +126,7 @@ export async function updateUser( ): Promise { if (userInfo) if (!(await updateUserInfo(db, userId, userInfo))) return false; - return await db.update('users', userId, user); + return await db.update('users', userId, user.toObject()); } export async function updateUserInfo( @@ -134,5 +134,5 @@ export async function updateUserInfo( userId: bigint, userInfo: UserInfo, ): Promise { - return await db.update('userInfo', userId, userInfo); + return await db.update('userInfo', userId, userInfo.toObject()); } From 12c34c327b1c88317d66addec6ecc94f409b2f64 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 19:54:13 +0300 Subject: [PATCH 055/118] Fixed routes to be remade --- src/routes/ExploreRouter.ts | 26 +-- src/routes/RoadmapsRouter.ts | 40 ++--- src/routes/roadmapsRoutes/RoadmapIssues.ts | 31 ++-- src/routes/roadmapsRoutes/RoadmapsGet.ts | 69 ++----- src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts | 170 ------------------ src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 108 +++-------- .../issuesRoutes/CommentsRouter.ts | 35 ++-- .../issuesRoutes/IssuesUpdate.ts | 29 +-- src/routes/usersRoutes/UsersGet.ts | 16 +- src/routes/usersRoutes/UsersUpdate.ts | 18 +- 10 files changed, 117 insertions(+), 425 deletions(-) delete mode 100644 src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts diff --git a/src/routes/ExploreRouter.ts b/src/routes/ExploreRouter.ts index 760d9fd..452cefd 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -1,14 +1,15 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; import { ExploreDB } from '@src/util/ExploreDB'; -import { RoadmapMini } from '@src/models/Roadmap'; +import { Roadmap } from '@src/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; import { RequestWithSession } from '@src/middleware/session'; import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; const ExploreRouter = Router(); -ExploreRouter.get(Paths.Explore.Default, +ExploreRouter.get( + Paths.Explore.Default, async (req: RequestWithSession, res) => { // get query, count, and page from url // eslint-disable-next-line max-len @@ -33,7 +34,7 @@ ExploreRouter.get(Paths.Explore.Default, // get roadmaps from database // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const roadmaps = await ExploreDB.searchRoadmapsByLiked( + const roadmaps = await ExploreDB.searchRoadmapsByLiked( query, userId, countNum, @@ -43,8 +44,11 @@ ExploreRouter.get(Paths.Explore.Default, const db = new Database(); // get total roadmaps - const totalRoadmaps = - await db.countWhereLike('roadmaps', 'name', `%${query}%`); + const totalRoadmaps = await db.countWhereLike( + 'roadmaps', + 'name', + `%${query}%`, + ); // page count const pageCount = Math.ceil(parseInt(totalRoadmaps.toString()) / countNum); @@ -53,11 +57,10 @@ ExploreRouter.get(Paths.Explore.Default, roadmaps.forEach((roadmap) => { addView(userId, BigInt(roadmap.id), false); - roadmap.id = roadmap.id.toString(); - roadmap.likes = roadmap.likes.toString(); - roadmap.ownerId = roadmap.ownerId.toString(); - roadmap.isLiked = Boolean(roadmap.isLiked); - + // roadmap.id = roadmap.id.toString(); + // roadmap.likes = roadmap.likes.toString(); + // roadmap.ownerId = roadmap.ownerId.toString(); + // roadmap.isLiked = Boolean(roadmap.isLiked); }); // send roadmaps res.status(200).json({ @@ -65,6 +68,7 @@ ExploreRouter.get(Paths.Explore.Default, roadmaps, pageCount, }); - }); + }, +); export default ExploreRouter; diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index a6e104a..649029c 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -1,8 +1,6 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { IRoadmap, Roadmap } from '@src/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; @@ -10,7 +8,6 @@ import GetRouter from '@src/routes/roadmapsRoutes/RoadmapsGet'; import UpdateRouter from '@src/routes/roadmapsRoutes/RoadmapsUpdate'; import * as console from 'console'; import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; -import RoadmapTabsInfo from '@src/routes/roadmapsRoutes/RoadmapsTabsInfo'; import envVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; import validateSession from '@src/validators/validateSession'; @@ -29,31 +26,17 @@ RoadmapsRouter.post( try { // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const roadmapData = req.body?.roadmap as string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const roadmapData = req.body?.roadmap as IRoadmap; - let roadmapDataJson: IRoadmap; - if (typeof roadmapData !== 'string') roadmapDataJson = roadmapData; - else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - roadmapDataJson = JSON.parse(roadmapData); - } + roadmap = new Roadmap(roadmapData); - roadmapDataJson.ownerId = session?.userId || BigInt(-1); - roadmapDataJson.id = undefined; - - // if Title is empty, edit it to be "Untitled Roadmap" - if (roadmapDataJson.name === '') { - roadmapDataJson.name = 'Untitled'; - } - - // convert date strings to date objects - roadmapDataJson.createdAt = new Date(roadmapDataJson.createdAt); - roadmapDataJson.updatedAt = new Date(roadmapDataJson.updatedAt); - - // FIX to make this work without workaround - - roadmap = Roadmap.from(roadmapDataJson); + roadmap.set({ + id: undefined, + userId: session?.userId || -1n, + name: roadmapData.name, + createdAt: new Date(), + updatedAt: new Date(), + }); } catch (e) { if (envVars.NodeEnv !== NodeEnvs.Test) console.log(e); return res @@ -119,7 +102,7 @@ RoadmapsRouter.delete( .json({ error: 'Roadmap does not exist.' }); // check if the user is owner - if (roadmap.ownerId !== session?.userId) + if (roadmap.userId !== session?.userId) return res .status(HttpStatusCodes.FORBIDDEN) .json({ error: 'User is not the owner of the roadmap.' }); @@ -139,7 +122,6 @@ RoadmapsRouter.delete( ); RoadmapsRouter.use(Paths.Roadmaps.Issues.Base, RoadmapIssues); -RoadmapsRouter.use(Paths.Roadmaps.TabsInfo.Base, RoadmapTabsInfo); /* ! like roadmaps diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts index c1422b4..05c7615 100644 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -1,9 +1,7 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { - RequestWithSession, -} from '@src/middleware/session'; -import { IIssue, Issue } from '@src/models/Issue'; +import { RequestWithSession } from '@src/middleware/session'; +import { Issue } from '@src/models/Issue'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; @@ -18,28 +16,27 @@ RoadmapIssues.post( Paths.Roadmaps.Issues.Create, async (req: RequestWithSession, res) => { //get data from body and session - let issue; + let issue: Issue; const session = req.session; try { // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const issueData = req.body?.issue as IIssue; + const issueData = req.body?.issue as Issue; - if (!issueData) { + if (!issueData || !Issue.isIssue(issueData)) { throw new Error('Issue is missing.'); } - // set userId - issueData.userId = session?.userId || BigInt(-1); - issueData.id = BigInt(-1); - issueData.roadmapId = BigInt(issueData.roadmapId); - - // update createdAt and updatedAt - issueData.createdAt = new Date(issueData.createdAt); - issueData.updatedAt = new Date(issueData.updatedAt); + issue = new Issue(issueData); - issue = Issue.from(issueData); + // set userId + issueData.set({ + userId: session?.userId, + id: -1n, + createdAt: new Date(), + updatedAt: new Date(), + }); } catch (e) { return res .status(HttpStatusCodes.BAD_REQUEST) @@ -191,7 +188,7 @@ RoadmapIssues.delete( .json({ error: 'Roadmap not found.' }); // check if userDisplay is owner - if (issue.userId !== session?.userId && roadmap.ownerId !== session?.userId) + if (issue.userId !== session?.userId && roadmap.userId !== session?.userId) return res .status(HttpStatusCodes.FORBIDDEN) .json({ error: 'User is not owner of issue or roadmap.' }); diff --git a/src/routes/roadmapsRoutes/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts index ff76a8a..5982ede 100644 --- a/src/routes/roadmapsRoutes/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -7,8 +7,7 @@ import { Roadmap } from '@src/models/Roadmap'; import axios from 'axios'; import EnvVars from '@src/constants/EnvVars'; import logger from 'jet-logger'; -import { Tag } from '@src/models/Tags'; -import User from '@src/models/User'; +import { IUser } from '@src/models/User'; import { RoadmapView } from '@src/models/RoadmapView'; const RoadmapsGet = Router({ mergeParams: true }); @@ -58,16 +57,13 @@ async function checkIfRoadmapExists( id, ); - let isLiked = false; if (req.session) { - const liked = - await new Database().getAllWhere<{ roadmapId: bigint; userId: bigint }>( - 'roadmapLikes', - 'userId', - req.session.userId, - ); + const liked = await new Database().getAllWhere<{ + roadmapId: bigint; + userId: bigint; + }>('roadmapLikes', 'userId', req.session.userId); if (liked) { if (liked.some((like) => like.roadmapId === BigInt(id))) { @@ -88,13 +84,12 @@ export async function addView( const db = new Database(); // get roadmap from database - const roadmap = - await db.get('roadmaps', roadmapId); + const roadmap = await db.get('roadmaps', roadmapId); // check if roadmap is valid if (!roadmap) return; - const view = new RoadmapView(userId, roadmapId, full); + const view = new RoadmapView({ userId, roadmapId, full }); await db.insert('roadmapViews', view); } @@ -117,7 +112,7 @@ RoadmapsGet.get( id: roadmap.id.toString(), name: roadmap.name, description: roadmap.description, - ownerId: roadmap.ownerId.toString(), + ownerId: roadmap.userId.toString(), issueCount: issueCount.toString(), likes: likes.toString(), isLiked, @@ -137,9 +132,9 @@ RoadmapsGet.get( if (!data) return; - let user = await new Database().get('users', data.roadmap.ownerId); + let user = await new Database().get('users', data.roadmap.userId); if (!user) { - user = new User('Unknown User'); + user = { id: -1n } as IUser; } const { roadmap, likes, isLiked } = data; @@ -155,49 +150,11 @@ RoadmapsGet.get( likes: likes.toString(), isLiked, ownerName: user.name, - ownerId: roadmap.ownerId.toString(), + ownerId: roadmap.userId.toString(), }); }, ); -RoadmapsGet.get( - Paths.Roadmaps.Get.Tags, - async (req: RequestWithSession, res) => { - //get data from params - const id = req.params.roadmapId; - - if (!id) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - - // get database connection - const db = new Database(); - - // check if roadmap exists - const roadmap = await db.get('roadmaps', BigInt(id)); - if (!roadmap) - return res.status(HttpStatusCodes.NOT_FOUND).json({ - error: 'Roadmap does not exist.', - }); - - // get tags from database - const tags = await db.getAllWhere('roadmapTags', 'roadmapId', id); - - // check if there are any tags - if (tags?.length === 0 || !tags) { - // return empty array - return res.status(HttpStatusCodes.OK).json({ tags: [] }); - } - - // map tags name to array - const tagNames = tags.map((tag) => tag.name); - - // return tags - return res.status(HttpStatusCodes.OK).json({ tags: tagNames }); - }, -); - RoadmapsGet.get( Paths.Roadmaps.Get.Owner, async (req: RequestWithSession, res) => { @@ -210,7 +167,7 @@ RoadmapsGet.get( // fetch /api/users/:id axios - .get(`http://localhost:${EnvVars.Port}/api/users/${roadmap.ownerId}`) + .get(`http://localhost:${EnvVars.Port}/api/users/${roadmap.userId}`) .then((response) => { res.status(response.status).json(response.data); }) @@ -233,7 +190,7 @@ RoadmapsGet.get( // fetch /api-wrapper/users/:id const user = await axios.get( - `http://localhost:${EnvVars.Port}/api/users/${roadmap.ownerId}/mini`, + `http://localhost:${EnvVars.Port}/api/users/${roadmap.userId}/mini`, ); // ? might need to check if json needs to be parsed diff --git a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts b/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts deleted file mode 100644 index 6bf53f0..0000000 --- a/src/routes/roadmapsRoutes/RoadmapsTabsInfo.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Router } from 'express'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Paths from '@src/constants/Paths'; -import { RequestWithSession } from '@src/middleware/session'; -import { ITabInfo, TabInfo } from '@src/models/TabInfo'; -import Database from '@src/util/DatabaseDriver'; -import * as console from 'console'; -import { IRoadmap } from '@src/models/Roadmap'; -import validateSession from '@src/validators/validateSession'; - -const RoadmapTabsInfo = Router({ mergeParams: true }); - -RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Create, validateSession); -RoadmapTabsInfo.post( - Paths.Roadmaps.TabsInfo.Create, - async (req: RequestWithSession, res) => { - //get data from body and session - let tabInfo; - const session = req.session; - - try { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const tabInfoData = req.body?.tabInfo as ITabInfo; - - if (!tabInfoData) { - throw new Error('tabinfo is missing.'); - } - - // set userId - tabInfoData.userId = session?.userId || BigInt(-1); - tabInfoData.id = BigInt(-1); - tabInfoData.roadmapId = BigInt(tabInfoData.roadmapId); // set as string on frontend - - tabInfo = TabInfo.from(tabInfoData); - } catch (e) { - console.log(e); - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'tabinfo data is invalid.' }); - } - - // get database connection - const db = new Database(); - - // save issue to database - console.log(tabInfo); - const id = await db.insert('tabsInfo', tabInfo); - - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return id - return res.status(HttpStatusCodes.CREATED).json({ id: id.toString() }); - }, -); - -RoadmapTabsInfo.get(Paths.Roadmaps.TabsInfo.Get, async (req, res) => { - // get issue id from params - const stringId = req.params?.tabInfoId; - const roadmapId = BigInt(req.params?.roadmapId || -1); - - if (roadmapId < 0) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'RoadmapId is invalid.' }); - } - - if (!stringId) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'TabID not found.' }); - - // get database connection - const db = new Database(); - - // the stringId is supposed to be unique and created with uuidv4 by the frontend - const tabData = await db.getWhere( - 'tabsInfo', - 'stringId', - stringId, - 'roadmapId', - roadmapId, - ); - - if (!tabData) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'TabInfo not found.' }); - - const result = { - id: tabData?.id.toString(), - stringId: tabData?.stringId, - roadmapId: tabData?.roadmapId.toString(), - userId: tabData?.userId.toString(), - content: tabData?.content, - }; - - return res.status(HttpStatusCodes.OK).json({ tabInfo: result }); -}); - -RoadmapTabsInfo.post(Paths.Roadmaps.TabsInfo.Update, validateSession); -RoadmapTabsInfo.post( - Paths.Roadmaps.TabsInfo.Update, - async (req: RequestWithSession, res) => { - const stringId = req.params?.tabInfoId; - const roadmapId = BigInt(req.params?.roadmapId || -1); - - if (roadmapId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'RoadmapId is invalid.' }); - - if (!stringId) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'TabID not found.' }); - - // get database connection - const db = new Database(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const sentTabData = req.body?.tabInfo as ITabInfo; - const newContent = sentTabData.content; - const roadmapReq = db.getWhere('roadmaps', 'id', roadmapId); - // gets previous data from the table - const tabDataReq = db.getWhere( - 'tabsInfo', - 'stringId', - stringId, - 'roadmapId', - roadmapId, - ); - - const roadmap = await roadmapReq; - - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap not found.' }); - - if (roadmap.ownerId !== req.session?.userId) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'You don\'t have permission to edit this roadmap.' }); - - const tabData = await tabDataReq; - - if (!tabData) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue not found.' }); - - tabData.content = newContent; - - const success = await db.update('tabsInfo', tabData.id, tabData); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -export default RoadmapTabsInfo; diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index fab62be..bd6216a 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -1,13 +1,10 @@ import { Response, Router } from 'express'; import Paths from '@src/constants/Paths'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/models/Roadmap'; -import { Tag } from '@src/models/Tags'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import validateSession from '@src/validators/validateSession'; const RoadmapsUpdate = Router({ mergeParams: true }); @@ -80,8 +77,10 @@ RoadmapsUpdate.post( const db = new Database(); // update roadmap - roadmap.name = title; - roadmap.updatedAt = new Date(); + roadmap.set({ + name: title, + updatedAt: new Date(), + }); const success = await db.update('roadmaps', roadmap.id, roadmap); if (!success) @@ -113,8 +112,10 @@ RoadmapsUpdate.post( const db = new Database(); // update roadmap - roadmap.description = description; - roadmap.updatedAt = new Date(); + roadmap.set({ + description, + updatedAt: new Date(), + }); const success = await db.update('roadmaps', roadmap.id, roadmap); if (!success) @@ -126,77 +127,6 @@ RoadmapsUpdate.post( }, ); -RoadmapsUpdate.post( - Paths.Roadmaps.Update.Tags, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const tags: string[] = req?.body?.tags; - if (!tags) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap tags are missing.' }); - - // check if the roadmap is valid - const data = await isRoadmapValid(req, res); - if (!data) return; - const { roadmap } = data; - - // get database connection - const db = new Database(); - - // get all tags from database - const allTags = await db.getAllWhere( - 'roadmapTags', - 'roadmapId', - roadmap.id, - ); - - const success: boolean[] = []; - - if (allTags !== undefined && allTags.length > 0) { - // filter out tags that are not in the request - const tagsToDelete = allTags.filter((tag) => !tags.includes(tag.name)); - - // delete tags that are not in the request - for (const tag of tagsToDelete) { - success.push(await db.delete('roadmapTags', tag.id)); - } - - // filter out tags that are already in the database - const tagsNames = allTags.map((e) => e.name); - const tagsToCreate = tags.filter((tag) => !tagsNames.includes(tag)); - - // create tags that are not in the database - for (const tag of tagsToCreate) { - success.push( - (await db.insert('roadmapTags', { - tagName: tag, - roadmapId: roadmap.id, - })) >= 0, - ); - } - } else { - for (const tag of tags) { - success.push( - (await db.insert('roadmapTags', { - tagName: tag, - roadmapId: roadmap.id, - })) >= 0, - ); - } - } - roadmap.updatedAt = new Date(); - - if (success.includes(false)) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); - - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - RoadmapsUpdate.post( Paths.Roadmaps.Update.Visibility, async (req: RequestWithSession, res) => { @@ -226,8 +156,10 @@ RoadmapsUpdate.post( const db = new Database(); // update roadmap - roadmap.isPublic = visibility; - roadmap.updatedAt = new Date(); + roadmap.set({ + isPublic: visibility, + updatedAt: new Date(), + }); const success = await db.update('roadmaps', roadmap.id, roadmap); if (!success) @@ -266,8 +198,10 @@ RoadmapsUpdate.post( .json({ error: 'New owner does not exist.' }); // update roadmap - roadmap.ownerId = BigInt(newOwnerId); - roadmap.updatedAt = new Date(); + roadmap.set({ + userId: newOwnerId, + updatedAt: new Date(), + }); const success = await db.update('roadmaps', roadmap.id, roadmap); if (!success) @@ -300,8 +234,10 @@ RoadmapsUpdate.post( const db = new Database(); // update roadmap - roadmap.data = data; - roadmap.updatedAt = new Date(); + roadmap.set({ + data, + updatedAt: new Date(), + }); const success = await db.update('roadmaps', roadmap.id, roadmap); if (!success) diff --git a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts index 04a9811..ce4490f 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts @@ -1,14 +1,12 @@ import { Request, Response, Router } from 'express'; import Paths from '@src/constants/Paths'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import { Roadmap } from '@src/models/Roadmap'; import { Issue } from '@src/models/Issue'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import Database from '@src/util/DatabaseDriver'; -import { Comment } from '@src/models/Comment'; +import { IssueComment } from '@src/models/IssueComment'; import validateSession from '@src/validators/validateSession'; const CommentsRouter = Router({ mergeParams: true }); @@ -106,10 +104,7 @@ async function checkUser( return { user, userId }; } -CommentsRouter.post( - Paths.Roadmaps.Issues.Comments.Create, - validateSession, -); +CommentsRouter.post(Paths.Roadmaps.Issues.Comments.Create, validateSession); CommentsRouter.post( Paths.Roadmaps.Issues.Comments.Create, async (req: RequestWithSession, res) => { @@ -129,7 +124,7 @@ CommentsRouter.post( .json({ error: 'Comment can\'t be empty.' }); // check if user is allowed to create a comment - if (!roadmap.isPublic && roadmap.ownerId !== userId) { + if (!roadmap.isPublic && roadmap.userId !== userId) { res .status(HttpStatusCodes.FORBIDDEN) .json({ error: 'Only the owner can create comments.' }); @@ -142,7 +137,7 @@ CommentsRouter.post( // create comment const commentId = await db.insert( 'issueComments', - new Comment(content, issueId, userId), + new IssueComment({ content, issueId, userId }), ); if (commentId < 0) @@ -164,7 +159,7 @@ CommentsRouter.get(Paths.Roadmaps.Issues.Comments.Get, async (req, res) => { const db = new Database(); // get comments - const comments = await db.getAllWhere( + const comments = await db.getAllWhere( 'issueComments', 'issueId', issueId, @@ -186,10 +181,7 @@ CommentsRouter.get(Paths.Roadmaps.Issues.Comments.Get, async (req, res) => { }); }); -CommentsRouter.use( - Paths.Roadmaps.Issues.Comments.Update, - validateSession, -); +CommentsRouter.use(Paths.Roadmaps.Issues.Comments.Update, validateSession); CommentsRouter.post( Paths.Roadmaps.Issues.Comments.Update, async (req: RequestWithSession, res) => { @@ -226,7 +218,7 @@ CommentsRouter.post( const db = new Database(); // get comment - const comment = await db.get('issueComments', commentId); + const comment = await db.get('issueComments', commentId); if (!comment) { res @@ -264,10 +256,7 @@ CommentsRouter.post( }, ); -CommentsRouter.use( - Paths.Roadmaps.Issues.Comments.Delete, - validateSession, -); +CommentsRouter.use(Paths.Roadmaps.Issues.Comments.Delete, validateSession); CommentsRouter.delete( Paths.Roadmaps.Issues.Comments.Delete, async (req: RequestWithSession, res) => { @@ -296,7 +285,7 @@ CommentsRouter.delete( const db = new Database(); // get comment - const comment = await db.get('issueComments', commentId); + const comment = await db.get('issueComments', commentId); if (!comment) { res @@ -312,7 +301,7 @@ CommentsRouter.delete( .json({ error: 'Comment does not belong to issue.' }); // check if the userDisplay is allowed to delete comment - if (comment.userId !== userId || roadmap.ownerId !== userId) { + if (comment.userId !== userId || roadmap.userId !== userId) { res .status(HttpStatusCodes.FORBIDDEN) .json({ error: 'Only the comment owner can delete comments.' }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts index f91f734..0ca9a9f 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts @@ -2,9 +2,7 @@ import { Response, Router } from 'express'; import Paths from '@src/constants/Paths'; import Database from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import { Issue } from '@src/models/Issue'; import { Roadmap } from '@src/models/Roadmap'; import validateSession from '@src/validators/validateSession'; @@ -72,7 +70,7 @@ async function checkArguments( // check if user is allowed to update issue if ( issue.userId !== session.userId && - (roadmapOwnerCanEdit ? roadmap?.ownerId !== session.userId : true) + (roadmapOwnerCanEdit ? roadmap.userId !== session.userId : true) ) { res .status(HttpStatusCodes.FORBIDDEN) @@ -103,8 +101,10 @@ async function statusChangeIssue( const { issueId, issue, db } = args; // update issue - issue.open = open; - issue.updatedAt = new Date(); + issue.set({ + open, + updatedAt: new Date(), + }); // save issue to database const success = await db.update('issues', issueId, issue); @@ -169,8 +169,10 @@ IssuesUpdate.post( } // update issue - issue.title = title; - issue.updatedAt = new Date(); + issue.set({ + title, + updatedAt: new Date(), + }); // save issue to database const success = await db.update('issues', issueId, issue); @@ -236,8 +238,10 @@ IssuesUpdate.post( } // update issue - issue.content = content; - issue.updatedAt = new Date(); + issue.set({ + content, + updatedAt: new Date(), + }); // save issue to database const success = await db.update('issues', issueId, issue); @@ -258,10 +262,7 @@ IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, (req, res) => statusChangeIssue(req, res, true), ); -IssuesUpdate.delete( - Paths.Roadmaps.Issues.Update.Status, - validateSession, -); +IssuesUpdate.delete(Paths.Roadmaps.Issues.Update.Status, validateSession); IssuesUpdate.delete(Paths.Roadmaps.Issues.Update.Status, (req, res) => statusChangeIssue(req, res, false), ); diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 1f7e094..fda2f06 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -3,8 +3,8 @@ import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import User from '@src/models/User'; -import { Roadmap, RoadmapMini } from '@src/models/Roadmap'; +import { User } from '@src/models/User'; +import { Roadmap } from '@src/models/Roadmap'; import { Issue } from '@src/models/Issue'; import { Follower } from '@src/models/Follower'; import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; @@ -69,13 +69,15 @@ UsersGet.get( return; } - const parsedRoadmaps: RoadmapMini[] = []; + const parsedRoadmaps: Roadmap[] = []; // convert roadmaps to RoadmapMini for (let i = 0; i < roadmaps.length; i++) { const roadmap = roadmaps[i]; - addView(viewerId, roadmap.id, false); + await addView(viewerId, roadmap.id, false); parsedRoadmaps[i] = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore id: roadmap.id.toString(), name: roadmap.name, description: roadmap.description || '', @@ -90,7 +92,9 @@ UsersGet.get( roadmap.id, )), ownerName: user.name, - ownerId: roadmap.ownerId.toString(), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + userId: roadmap.userId.toString(), }; } @@ -346,7 +350,7 @@ UsersGet.get(Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { .json({ error: 'Already following' }); // create a new follower - const follower = new Follower(followerId, userId); + const follower = new Follower({ followerId, userId }); // insert the follower into the database const insert = await db.insert('followers', follower); diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index 2173aa8..af1e96e 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -1,14 +1,12 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; -import { - RequestWithSession, -} from '@src/middleware/session'; +import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import axios from 'axios'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { checkEmail } from '@src/util/EmailUtil'; import { comparePassword } from '@src/util/LoginUtil'; -import User from '@src/models/User'; +import { User } from '@src/models/User'; import { UserInfo } from '@src/models/UserInfo'; import validateSession from '@src/validators/validateSession'; @@ -144,9 +142,7 @@ UsersUpdate.post( // send error json if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user' }); + return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); // check if quote was given if (quote.length > 255) @@ -193,9 +189,7 @@ UsersUpdate.post( // send error json if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user' }); + return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); // check if quote was given if (!name || name.length > 32) @@ -231,9 +225,7 @@ UsersUpdate.post( // send error json if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user' }); + return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); // check if quote was given if (blogUrl.length > 255) From e6fdbce1fe047f66423f87bb3c126ab13eb1d232 Mon Sep 17 00:00:00 2001 From: Sopy Date: Mon, 4 Sep 2023 19:56:32 +0300 Subject: [PATCH 056/118] Update eslint.yml --- .github/workflows/eslint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index bb22c87..aee8751 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -42,7 +42,7 @@ jobs: run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git commit -a -m "Add changes" + git commit -a -m "Add changes" || echo "No changes to commit" - name: Push changes uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef From 69a79f74b482126b5c5986c42b9d0d3f66570ba2 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 4 Sep 2023 20:05:28 +0300 Subject: [PATCH 057/118] Updated eslint.yml --- .github/workflows/eslint.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index aee8751..e4a91fe 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -37,7 +37,7 @@ jobs: - name: Run ESLint with --fix run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" - + - name: Commit files run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" @@ -47,7 +47,6 @@ jobs: - name: Push changes uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef with: - branch: ${{ github.ref }} github_token: ${{ secrets.PAT }} - name: Run ESLint From e521f22f04b407a2670e0dca369d24aa568e8ed0 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 00:59:31 +0300 Subject: [PATCH 058/118] Add get user tests and utility functions for creating and deleting a user --- spec/tests/routes/auth.spec.ts | 2 +- spec/tests/routes/users.spec.ts | 68 +++++++++++++++++++++++++++++++ spec/tests/utils/database.spec.ts | 2 +- spec/types/supertest/index.d.ts | 11 +++++ spec/types/tests/CreatedUser.ts | 9 ++++ spec/utils/createUser.ts | 53 +++++++++++++++++++++++- 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 spec/types/tests/CreatedUser.ts diff --git a/spec/tests/routes/auth.spec.ts b/spec/tests/routes/auth.spec.ts index d8d780f..4384c86 100644 --- a/spec/tests/routes/auth.spec.ts +++ b/spec/tests/routes/auth.spec.ts @@ -2,7 +2,7 @@ import { randomString } from '@spec/utils/randomString'; import request from 'supertest'; import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; -import { User } from '@src/models/User'; +import { User } from '@src/types/models/User'; import Database from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts index e69de29..dc78fbc 100644 --- a/spec/tests/routes/users.spec.ts +++ b/spec/tests/routes/users.spec.ts @@ -0,0 +1,68 @@ +import { createUser, deleteUser } from '@spec/utils/createUser'; +import request from 'supertest'; +import app from '@src/server'; +import httpStatusCodes from '@src/constants/HttpStatusCodes'; +import { CreatedUser } from '@spec/types/tests/CreatedUser'; +import JSONStringify from '@src/util/JSONStringify'; + +// ! Get User Tests +describe('Get User Tests', () => { + let user: CreatedUser; + beforeAll(async () => { + user = await createUser(); + }); + + afterAll(async () => { + // delete user + await deleteUser(user.user); + }); + + it('should return user with id', async () => { + await request(app) + .get('/api/users/' + user.user.id) + .expect(httpStatusCodes.OK) + .expect(({ body }) => { + expect(body.success).toBe(true); + expect(body.data).toBeDefined(); + // TODO: check for type to be right + }); + }); + + it('should return local user', async () => { + await request(app) + .get('/api/users/') + .set('Cookie', user.loginCookie) + .expect(httpStatusCodes.OK) + .expect(({ body }) => { + expect(body.success).toBe(true); + expect(body.data).toBeDefined(); + // TODO: check for type to be right + }); + }); + + it('should return mini user profile', async () => { + await request(app) + .get('/api/users/' + user.user.id + '/mini') + .expect(httpStatusCodes.OK) + .expect(({ body }) => { + expect(body.success).toBe(true); + expect(body.data).toEqual( + JSON.parse(JSONStringify(user.user.toObject())), + ); + }); + }); + + it('should return mini local user profile', async () => { + await request(app) + .get('/api/users/mini') + .set('Cookie', user.loginCookie) + .expect(httpStatusCodes.OK) + .expect(({ body }) => { + expect(body.success).toBe(true); + expect(body.data).toBeDefined(); + expect(body.data).toEqual( + JSON.parse(JSONStringify(user.user.toObject())), + ); + }); + }); +}); diff --git a/spec/tests/utils/database.spec.ts b/spec/tests/utils/database.spec.ts index c05bf44..ef5ea17 100644 --- a/spec/tests/utils/database.spec.ts +++ b/spec/tests/utils/database.spec.ts @@ -1,5 +1,5 @@ import Database from '@src/util/DatabaseDriver'; -import { IUser, User } from '@src/models/User'; +import { IUser, User } from '@src/types/models/User'; import { randomString } from '@spec/utils/randomString'; function testUserAttributes(user: IUser, user2?: IUser) { diff --git a/spec/types/supertest/index.d.ts b/spec/types/supertest/index.d.ts index 777f2a8..9a21bcc 100644 --- a/spec/types/supertest/index.d.ts +++ b/spec/types/supertest/index.d.ts @@ -1,5 +1,16 @@ import 'supertest'; +export interface Response { + headers: { + 'set-cookie': string[]; + }; + body: { + success: boolean; + message: string; + data: unknown; + }; +} + declare module 'supertest' { export interface Response { headers: { diff --git a/spec/types/tests/CreatedUser.ts b/spec/types/tests/CreatedUser.ts new file mode 100644 index 0000000..aa46a83 --- /dev/null +++ b/spec/types/tests/CreatedUser.ts @@ -0,0 +1,9 @@ +import { User } from '@src/types/models/User'; + +export type CreatedUser = { + email: string; + password: string; + loginCookie: string; + + user: User; +}; diff --git a/spec/utils/createUser.ts b/spec/utils/createUser.ts index 4b4aba7..65a2449 100644 --- a/spec/utils/createUser.ts +++ b/spec/utils/createUser.ts @@ -1 +1,52 @@ -export function createUser() {} +import { IUser, User } from '@src/types/models/User'; +import { randomString } from '@spec/utils/randomString'; +import request from 'supertest'; +import app from '@src/server'; +import Database from '@src/util/DatabaseDriver'; +import { CreatedUser } from '@spec/types/tests/CreatedUser'; +import httpStatusCodes from '@src/constants/HttpStatusCodes'; +import { Response } from '@spec/types/supertest'; + +export async function createUser(): Promise { + const email = randomString() + '@test.com'; + const password = randomString(); + let loginCookie: string | null = null; + + const res = (await request(app) + .post('/api/auth/register') + .send({ email, password }) + .expect(httpStatusCodes.CREATED)) as Response; + + res.headers['set-cookie'].forEach((cookie: string) => { + if (cookie.startsWith('token=')) { + loginCookie = cookie; + } + }); + + if (!loginCookie) { + throw new Error('Login cookie not found.'); + } + + const db = new Database(); + + const userDb = await db.getWhere('users', 'email', email); + + if (!userDb) { + throw new Error('User doesn\'t exists.'); + } + + const user = new User(userDb); + + return { + email, + password, + loginCookie, + user, + }; +} + +export async function deleteUser(user: User): Promise { + const db = new Database(); + + await db.delete('users', user.id); +} From 6d2b896e9f3a11766a6de2ed19c64d1725c0c267 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 01:01:03 +0300 Subject: [PATCH 059/118] Refactor model imports, moving them to types directory --- src/routes/ExploreRouter.ts | 2 +- src/routes/RoadmapsRouter.ts | 2 +- src/routes/roadmapsRoutes/RoadmapIssues.ts | 4 ++-- src/routes/roadmapsRoutes/RoadmapsGet.ts | 6 +++--- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 4 ++-- src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts | 8 ++++---- src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts | 4 ++-- src/routes/usersRoutes/UsersGet.ts | 8 ++++---- src/routes/usersRoutes/UsersUpdate.ts | 4 ++-- src/{ => types}/models/Follower.ts | 0 src/{ => types}/models/Issue.ts | 0 src/{ => types}/models/IssueComment.ts | 0 src/{ => types}/models/Roadmap.ts | 0 src/{ => types}/models/RoadmapLike.ts | 0 src/{ => types}/models/RoadmapView.ts | 0 src/{ => types}/models/Session.ts | 0 src/{ => types}/models/User.ts | 0 src/{ => types}/models/UserInfo.ts | 0 src/util/DatabaseDriver.ts | 2 +- 19 files changed, 22 insertions(+), 22 deletions(-) rename src/{ => types}/models/Follower.ts (100%) rename src/{ => types}/models/Issue.ts (100%) rename src/{ => types}/models/IssueComment.ts (100%) rename src/{ => types}/models/Roadmap.ts (100%) rename src/{ => types}/models/RoadmapLike.ts (100%) rename src/{ => types}/models/RoadmapView.ts (100%) rename src/{ => types}/models/Session.ts (100%) rename src/{ => types}/models/User.ts (100%) rename src/{ => types}/models/UserInfo.ts (100%) diff --git a/src/routes/ExploreRouter.ts b/src/routes/ExploreRouter.ts index 452cefd..e033986 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; import { ExploreDB } from '@src/util/ExploreDB'; -import { Roadmap } from '@src/models/Roadmap'; +import { Roadmap } from '@src/types/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; import { RequestWithSession } from '@src/middleware/session'; import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 649029c..038882b 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -2,7 +2,7 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { IRoadmap, Roadmap } from '@src/models/Roadmap'; +import { IRoadmap, Roadmap } from '@src/types/models/Roadmap'; import Database from '@src/util/DatabaseDriver'; import GetRouter from '@src/routes/roadmapsRoutes/RoadmapsGet'; import UpdateRouter from '@src/routes/roadmapsRoutes/RoadmapsUpdate'; diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts index 05c7615..b1c0045 100644 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; -import { Issue } from '@src/models/Issue'; +import { Issue } from '@src/types/models/Issue'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; -import { Roadmap } from '@src/models/Roadmap'; +import { Roadmap } from '@src/types/models/Roadmap'; import IssuesUpdate from '@src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate'; import Comments from '@src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter'; import validateSession from '@src/validators/validateSession'; diff --git a/src/routes/roadmapsRoutes/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts index 5982ede..4bc2cab 100644 --- a/src/routes/roadmapsRoutes/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -3,12 +3,12 @@ import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; -import { Roadmap } from '@src/models/Roadmap'; +import { Roadmap } from '@src/types/models/Roadmap'; import axios from 'axios'; import EnvVars from '@src/constants/EnvVars'; import logger from 'jet-logger'; -import { IUser } from '@src/models/User'; -import { RoadmapView } from '@src/models/RoadmapView'; +import { IUser } from '@src/types/models/User'; +import { RoadmapView } from '@src/types/models/RoadmapView'; const RoadmapsGet = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index bd6216a..a105324 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -3,8 +3,8 @@ import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; -import { Roadmap } from '@src/models/Roadmap'; -import { User } from '@src/models/User'; +import { Roadmap } from '@src/types/models/Roadmap'; +import { User } from '@src/types/models/User'; import validateSession from '@src/validators/validateSession'; const RoadmapsUpdate = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts index ce4490f..1e66fa4 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts @@ -2,11 +2,11 @@ import { Request, Response, Router } from 'express'; import Paths from '@src/constants/Paths'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession } from '@src/middleware/session'; -import { Roadmap } from '@src/models/Roadmap'; -import { Issue } from '@src/models/Issue'; -import { User } from '@src/models/User'; +import { Roadmap } from '@src/types/models/Roadmap'; +import { Issue } from '@src/types/models/Issue'; +import { User } from '@src/types/models/User'; import Database from '@src/util/DatabaseDriver'; -import { IssueComment } from '@src/models/IssueComment'; +import { IssueComment } from '@src/types/models/IssueComment'; import validateSession from '@src/validators/validateSession'; const CommentsRouter = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts index 0ca9a9f..de21c40 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts @@ -3,8 +3,8 @@ import Paths from '@src/constants/Paths'; import Database from '@src/util/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession } from '@src/middleware/session'; -import { Issue } from '@src/models/Issue'; -import { Roadmap } from '@src/models/Roadmap'; +import { Issue } from '@src/types/models/Issue'; +import { Roadmap } from '@src/types/models/Roadmap'; import validateSession from '@src/validators/validateSession'; const IssuesUpdate = Router({ mergeParams: true }); diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index fda2f06..0e76bc7 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -3,10 +3,10 @@ import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import { User } from '@src/models/User'; -import { Roadmap } from '@src/models/Roadmap'; -import { Issue } from '@src/models/Issue'; -import { Follower } from '@src/models/Follower'; +import { User } from '@src/types/models/User'; +import { Roadmap } from '@src/types/models/Roadmap'; +import { Issue } from '@src/types/models/Issue'; +import { Follower } from '@src/types/models/Follower'; import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; import validateSession from '@src/validators/validateSession'; import validateUser from '@src/validators/validateUser'; diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index af1e96e..2c34127 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -6,8 +6,8 @@ import axios from 'axios'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { checkEmail } from '@src/util/EmailUtil'; import { comparePassword } from '@src/util/LoginUtil'; -import { User } from '@src/models/User'; -import { UserInfo } from '@src/models/UserInfo'; +import { User } from '@src/types/models/User'; +import { UserInfo } from '@src/types/models/UserInfo'; import validateSession from '@src/validators/validateSession'; const UsersUpdate = Router({ mergeParams: true }); diff --git a/src/models/Follower.ts b/src/types/models/Follower.ts similarity index 100% rename from src/models/Follower.ts rename to src/types/models/Follower.ts diff --git a/src/models/Issue.ts b/src/types/models/Issue.ts similarity index 100% rename from src/models/Issue.ts rename to src/types/models/Issue.ts diff --git a/src/models/IssueComment.ts b/src/types/models/IssueComment.ts similarity index 100% rename from src/models/IssueComment.ts rename to src/types/models/IssueComment.ts diff --git a/src/models/Roadmap.ts b/src/types/models/Roadmap.ts similarity index 100% rename from src/models/Roadmap.ts rename to src/types/models/Roadmap.ts diff --git a/src/models/RoadmapLike.ts b/src/types/models/RoadmapLike.ts similarity index 100% rename from src/models/RoadmapLike.ts rename to src/types/models/RoadmapLike.ts diff --git a/src/models/RoadmapView.ts b/src/types/models/RoadmapView.ts similarity index 100% rename from src/models/RoadmapView.ts rename to src/types/models/RoadmapView.ts diff --git a/src/models/Session.ts b/src/types/models/Session.ts similarity index 100% rename from src/models/Session.ts rename to src/types/models/Session.ts diff --git a/src/models/User.ts b/src/types/models/User.ts similarity index 100% rename from src/models/User.ts rename to src/types/models/User.ts diff --git a/src/models/UserInfo.ts b/src/types/models/UserInfo.ts similarity index 100% rename from src/models/UserInfo.ts rename to src/types/models/UserInfo.ts diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 4b821aa..2ae03e2 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -3,7 +3,7 @@ import { createPool, Pool } from 'mariadb'; import fs from 'fs'; import path from 'path'; import logger from 'jet-logger'; -import { User } from '@src/models/User'; +import { User } from '@src/types/models/User'; // database credentials const { DBCred } = EnvVars; From 4e22f81b330023acb04d22b3dd9d1d82f99a1ed9 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 01:01:38 +0300 Subject: [PATCH 060/118] Fixing to match tests spec --- src/controllers/authController.ts | 4 +-- src/controllers/usersController.ts | 3 +-- src/helpers/apiResponses.ts | 41 +++++++++++++++--------------- src/helpers/databaseManagement.ts | 8 +++--- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index ac9a5b3..5a46421 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,14 +1,14 @@ import { RequestWithBody } from '@src/validators/validateBody'; import { Response } from 'express'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import { User } from '@src/models/User'; +import { User } from '@src/types/models/User'; import axios, { HttpStatusCode } from 'axios'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; import { createSaveSession, deleteClearSession, } from '@src/util/sessionManager'; -import { UserInfo } from '@src/models/UserInfo'; +import { UserInfo } from '@src/types/models/UserInfo'; import { checkEmail } from '@src/util/EmailUtil'; import EnvVars from '@src/constants/EnvVars'; import logger from 'jet-logger'; diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index f0a5f33..b0089bc 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -81,10 +81,9 @@ export async function usersGetMiniProfile( // get user from database const user = await getUser(db, userId); - const userInfo = await getUserInfo(db, userId); // check if user exists - if (!user || !userInfo) return userNotFound(res); + if (!user) return userNotFound(res); // send user json return userMiniProfile(res, user); diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 22dc68b..3057911 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { HttpStatusCode } from 'axios'; -import { User } from '@src/models/User'; -import { UserInfo } from '@src/models/UserInfo'; +import { User } from '@src/types/models/User'; +import { UserInfo } from '@src/types/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; import JSONStringify from '@src/util/JSONStringify'; @@ -126,35 +126,36 @@ export function userProfile( .contentType('application/json') .send( JSONStringify({ - name, - avatar, - userId: user.id, - bio, - quote, - websiteUrl, - githubUrl, - roadmapsCount, - issueCount, - followerCount, - followingCount, - isFollowing, - githubLink: !!githubId, - googleLink: !!googleId, + data: { + name, + avatar, + userId: user.id, + bio, + quote, + websiteUrl, + githubUrl, + roadmapsCount, + issueCount, + followerCount, + followingCount, + isFollowing, + githubLink: !!githubId, + googleLink: !!googleId, + }, + message: 'User found', success: true, }), ); } export function userMiniProfile(res: Response, user: User): void { - const { id, name, avatar } = user; res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ - name, - avatar, - userId: id, + data: user.toObject(), + message: 'User found', success: true, }), ); diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 615920d..2fc99d9 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -1,6 +1,6 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; -import { UserInfo } from '@src/models/UserInfo'; -import { IUser, User } from '@src/models/User'; +import { IUserInfo, UserInfo } from '@src/types/models/UserInfo'; +import { IUser, User } from '@src/types/models/User'; /* * Interfaces @@ -55,7 +55,7 @@ export async function getUserInfo( db: DatabaseDriver, userId: bigint, ): Promise { - const userInfo = await db.get('userInfo', userId); + const userInfo = await db.getWhere('userInfo', 'userId', userId); if (!userInfo) return null; return new UserInfo(userInfo); } @@ -64,7 +64,7 @@ export async function getUserStats( db: DatabaseDriver, userId: bigint, ): Promise { - const roadmapsCount = await db.countWhere('roadmaps', 'ownerId', userId); + const roadmapsCount = await db.countWhere('roadmaps', 'userId', userId); const issueCount = await db.countWhere('issues', 'userId', userId); const followerCount = await db.countWhere('followers', 'userId', userId); const followingCount = await db.countWhere('followers', 'followerId', userId); From 66639fc3470216974a4c8d43bb4d6bb7dc21c79f Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 13:40:19 +0300 Subject: [PATCH 061/118] Split ESlint workflow into scanning and auto fixing actions Renamed the original 'eslint.yml' workflow file to 'eslintcodescan.yml' to more accurately represent its purpose. In tandem, a new 'eslintfix.yml' workflow file was created to specifically handle the automatic fixing of ESLint issues and commits. This division allows more clarity in workflow actions and more granular control over the ESlint process. --- .../{eslint.yml => eslintcodescan.yml} | 14 -------- .github/workflows/eslintfix.yml | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 14 deletions(-) rename .github/workflows/{eslint.yml => eslintcodescan.yml} (73%) create mode 100644 .github/workflows/eslintfix.yml diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslintcodescan.yml similarity index 73% rename from .github/workflows/eslint.yml rename to .github/workflows/eslintcodescan.yml index e4a91fe..9484c65 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslintcodescan.yml @@ -35,20 +35,6 @@ jobs: npm install --save-dev eslint@8.10.0 npm install --save-dev @microsoft/eslint-formatter-sarif@2.1.7 - - name: Run ESLint with --fix - run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" - - - name: Commit files - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git commit -a -m "Add changes" || echo "No changes to commit" - - - name: Push changes - uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef - with: - github_token: ${{ secrets.PAT }} - - name: Run ESLint run: npx eslint . --config .eslintrc.json diff --git a/.github/workflows/eslintfix.yml b/.github/workflows/eslintfix.yml new file mode 100644 index 0000000..3d31a1d --- /dev/null +++ b/.github/workflows/eslintfix.yml @@ -0,0 +1,34 @@ +name: ESLint Fix on Push to Master + +on: + push: + branches: + - master + +jobs: + eslint-fix: + name: Fix ESLint Issues + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install ESLint + run: | + npm install --save-dev eslint@8.10.0 + npm install --save-dev @microsoft/eslint-formatter-sarif@2.1.7 + + - name: Run ESLint with --fix + run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" + + - name: Commit files + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -a -m "Fix ESLint issues" || echo "No changes to commit" + + - name: Push changes + uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef + with: + branch: ${{ github.ref_name }} + github_token: ${{ secrets.PAT }} From 7f835c6b371a2f4be36cb77d587e96dd10d735f4 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 13:57:48 +0300 Subject: [PATCH 062/118] Upgrade eslint config and ignored EnvVars from no-process-env ESLint configuration has been enhanced by adding a 'node' plugin and strengthening the 'no-process-env' rule to throw errors instead of warnings. In addition, the process environment variables in EnvVars.ts previously disabled ESLint, which has been brought back into the linting process. This was done for better adherence to coding standards and enhancing error detection in process environment variables. --- .eslintrc.json | 14 +++++++++----- src/constants/EnvVars.ts | 3 +++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7ef687d..89ae9cf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,8 @@ { "parser": "@typescript-eslint/parser", "plugins": [ - "@typescript-eslint" + "@typescript-eslint", + "node" ], "extends": [ "eslint:recommended", @@ -37,7 +38,7 @@ "warn", "single" ], - "node/no-process-env": 1, + "node/no-process-env": "error", "node/no-unsupported-features/es-syntax": [ "error", { @@ -51,9 +52,12 @@ ], "node/no-missing-import": 0, "node/no-unpublished-import": 0, - "@typescript-eslint/unbound-method": ["error", { - "ignoreStatic": true - }] + "@typescript-eslint/unbound-method": [ + "error", + { + "ignoreStatic": true + } + ] }, "settings": { "node": { diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index 420782c..a799f45 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -40,6 +40,8 @@ interface IEnvVars { }; } +// Config environment variables +/* eslint-disable no-process-env */ const EnvVars = { NodeEnv: process.env.NODE_ENV ?? '', Port: process.env.PORT ?? 0, @@ -74,6 +76,7 @@ const EnvVars = { RedirectUri: process.env.GITHUB_REDIRECT_URI ?? '', }, } as Readonly; +/* eslint-enable no-process-env */ export default EnvVars; export { EnvVars }; From 7b53ca122cec65f1f127c3ec470eafc4d4956976 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 14:04:40 +0300 Subject: [PATCH 063/118] no-process-env shouldn't trigger anymore --- .eslintrc.json | 1 + src/constants/EnvVars.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 89ae9cf..b873e1f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,6 +38,7 @@ "warn", "single" ], + "no-process-env": "error", "node/no-process-env": "error", "node/no-unsupported-features/es-syntax": [ "error", diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index a799f45..cf89d54 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -42,6 +42,7 @@ interface IEnvVars { // Config environment variables /* eslint-disable no-process-env */ +/* eslint-disable node/no-process-env */ const EnvVars = { NodeEnv: process.env.NODE_ENV ?? '', Port: process.env.PORT ?? 0, @@ -77,6 +78,7 @@ const EnvVars = { }, } as Readonly; /* eslint-enable no-process-env */ +/* eslint-enable node/no-process-env */ export default EnvVars; export { EnvVars }; From 5cc3f7a4649a1f1c434328e7ede3b30ad132d973 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 16:35:52 +0300 Subject: [PATCH 064/118] Remove login limiters from Google and Github auth routes --- src/routes/AuthRouter.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index e6e11d5..55a6bbd 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -15,21 +15,22 @@ import { import validateBody from '@src/validators/validateBody'; import { rateLimit } from 'express-rate-limit'; import EnvVars from '@src/constants/EnvVars'; +import { NodeEnvs } from '@src/constants/misc'; const AuthRouter = Router(); const LoginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: EnvVars.NodeEnv === 'production' ? 10 : 99999, + max: EnvVars.NodeEnv === NodeEnvs.Production ? 10 : 99999, message: 'Too many login attempts, please try again later.', }), RegisterLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv === 'production' ? 5 : 99999, + max: EnvVars.NodeEnv === NodeEnvs.Production ? 5 : 99999, message: 'Too many register attempts, please try again later.', }), ResetPasswordLimiter = rateLimit({ windowMs: 360 * 1000, // 1 hour - max: EnvVars.NodeEnv === 'production' ? 10 : 99999, + max: EnvVars.NodeEnv === NodeEnvs.Production ? 10 : 99999, message: 'Too many reset password attempts, please try again later.', }); @@ -61,10 +62,10 @@ AuthRouter.post( authForgotPassword, ); -AuthRouter.get(Paths.Auth.GoogleLogin, LoginLimiter, authGoogle); +AuthRouter.get(Paths.Auth.GoogleLogin, authGoogle); AuthRouter.get(Paths.Auth.GoogleCallback, authGoogleCallback); -AuthRouter.get(Paths.Auth.GithubLogin, LoginLimiter, authGitHub); +AuthRouter.get(Paths.Auth.GithubLogin, authGitHub); AuthRouter.get(Paths.Auth.GithubCallback, authGitHubCallback); AuthRouter.delete(Paths.Auth.Logout, validateSession, authLogout); From bb20bebd5717380dd0743300645049990edfeeaa Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 17:29:40 +0300 Subject: [PATCH 065/118] Update database operations and JSON stringify function This commit makes changes across several files related to database operations and JSON serialization. The 'toObject()' method calls previously utilized in 'apiResponses' and 'databaseManagement' have been removed for a more direct approach to dealing with user objects. This change aims to improve performance and clean up the code by reducing unnecessary conversions. Furthermore, additional logic has been added in 'JSONStringify' to deal with possible objects containing a 'toObject' method. This ensures that such objects are properly converted before stringification. Database operations, including insert and update in 'DatabaseDriver', have been updated to reflect these changes. In particular, they now accept and process instances of GenericModelClass, allowing for a more generalized process. Finally, a new file 'GenericModelClass' has been created to define a generic data model structure. Changes have been verified by adjusting user count assertions in 'database.spec' tests. --- spec/tests/utils/database.spec.ts | 4 +- src/helpers/apiResponses.ts | 2 +- src/helpers/databaseManagement.ts | 8 +-- src/types/models/GenericModelClass.ts | 6 ++ src/util/DatabaseDriver.ts | 85 ++++++++++++++++++--------- src/util/JSONStringify.ts | 14 ++++- 6 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src/types/models/GenericModelClass.ts diff --git a/spec/tests/utils/database.spec.ts b/spec/tests/utils/database.spec.ts index ef5ea17..4102364 100644 --- a/spec/tests/utils/database.spec.ts +++ b/spec/tests/utils/database.spec.ts @@ -103,7 +103,7 @@ describe('Database', () => { expect(users2).not.toBe(null); if (!users2) return; - expect(users2.length).toEqual(users.length); + expect(users2.length).toEqual(users.length + 1); }); // test for getting all users where key (pwdHash) is value (password) @@ -145,7 +145,7 @@ describe('Database', () => { // get count const count = await db.count('users'); - expect(count).toEqual(BigInt(users.length)); + expect(count).toEqual(BigInt(users.length + 1)); }); // test for counting users where key (pwdHash) is value (password) diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 3057911..3492bde 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -154,7 +154,7 @@ export function userMiniProfile(res: Response, user: User): void { .contentType('application/json') .send( JSONStringify({ - data: user.toObject(), + data: user, message: 'User found', success: true, }), diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 2fc99d9..e45163f 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -99,7 +99,7 @@ export async function insertUser( user: User, userInfo?: UserInfo, ): Promise { - user.set({ id: await db.insert('users', user.toObject()) }); + user.set({ id: await db.insert('users', user) }); if (await insertUserInfo(db, user.id, userInfo)) { return user.id; @@ -115,7 +115,7 @@ export async function insertUserInfo( ): Promise { if (!userInfo) userInfo = new UserInfo({ userId }); userInfo.set({ userId }); - return (await db.insert('userInfo', userInfo.toObject())) >= 0; + return (await db.insert('userInfo', userInfo)) >= 0; } export async function updateUser( @@ -126,7 +126,7 @@ export async function updateUser( ): Promise { if (userInfo) if (!(await updateUserInfo(db, userId, userInfo))) return false; - return await db.update('users', userId, user.toObject()); + return await db.update('users', userId, user); } export async function updateUserInfo( @@ -134,5 +134,5 @@ export async function updateUserInfo( userId: bigint, userInfo: UserInfo, ): Promise { - return await db.update('userInfo', userId, userInfo.toObject()); + return await db.update('userInfo', userId, userInfo); } diff --git a/src/types/models/GenericModelClass.ts b/src/types/models/GenericModelClass.ts new file mode 100644 index 0000000..4593025 --- /dev/null +++ b/src/types/models/GenericModelClass.ts @@ -0,0 +1,6 @@ +export interface GenericModelClass { + // Method to modify the properties + set: (obj: unknown) => void; + // Method to get Object + toObject: () => object; +} diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 2ae03e2..32d7d2e 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -4,48 +4,67 @@ import fs from 'fs'; import path from 'path'; import logger from 'jet-logger'; import { User } from '@src/types/models/User'; +import { GenericModelClass } from '@src/types/models/GenericModelClass'; // database credentials const { DBCred } = EnvVars; const trustedColumns = [ + // id columns + 'id', + 'roadmapId', + 'userId', + 'issueId', + // followers + 'followerId', + + // generic 'name', + 'data', + + // time + 'createdAt', + 'updatedAt', + + // user + 'avatar', 'email', 'role', 'pwdHash', 'googleId', 'githubId', - 'description', - 'profilePictureUrl', + + // userInfo 'bio', - 'email', 'quote', - 'blogUrl', 'websiteUrl', 'githubUrl', - 'ownerId', + + // roadmap + 'description', 'isPublic', - 'content', - 'title', + 'isDraft', + + // roadmapLikes + 'value', + + // roadmapViews + 'full', + + // roadmapIssues & comments 'open', - 'tagName', - 'id', - 'followerId', - 'issueId', - 'roadmapId', - 'userId', + 'title', + 'content', + + // session 'token', - 'createdAt', - 'updatedAt', - 'expiresAt', - 'stringId', - 'full', + 'expires', ]; // data interface interface Data { keys: string[]; - values: never[]; + values: unknown[]; } type DataType = bigint | string | number | Date | null; @@ -80,9 +99,21 @@ function processData( data: object | Record, discardId = true, ): Data { + const dataObj = data as GenericModelClass; + + // check if data is a GenericModelClass + if ( + typeof dataObj === 'object' && + dataObj !== null && + 'toObject' in dataObj && + typeof dataObj.toObject === 'function' + ) { + data = dataObj.toObject(); + } + // get keys and values const keys = Object.keys(data); - const values = Object.values(data) as never[]; + const values = Object.values(data) as unknown[]; // remove id from keys and values const idIndex = keys.indexOf('id'); @@ -91,6 +122,10 @@ function processData( values.splice(idIndex, 1); } + // check if values are trusted + if (keys.every((key) => !trustedColumns.includes(key))) + throw new Error('Untrusted data: ' + keys.join(', ')); + return { keys, values }; } @@ -139,9 +174,6 @@ class Database { ): Promise { const { keys, values } = processData(data, discardId); - // check if keys are trusted - if (keys.every((key) => !trustedColumns.includes(key))) return BigInt(-1); - // create sql query - insert into table (keys) values (values) // ? for values to be replaced by params const sql = `INSERT INTO ${table} (${keys.join(',')}) @@ -165,9 +197,6 @@ class Database { ): Promise { const { keys, values } = processData(data, discardId); - // check if keys are trusted - if (keys.every((key) => !trustedColumns.includes(key))) return false; - // create sql query - update table set key = ?, key = ? where id = ? // ? for values to be replaced by params const sqlKeys = keys.map((key) => `${key} = ?`).join(','); @@ -210,9 +239,7 @@ class Database { let params: unknown[] = []; for (let i = 0; i < values.length - 1; i += 2) { - const key = values[i]; - - if (typeof key !== 'string' || !trustedColumns.includes(key)) return null; + const key = values[i] as string; if (i > 0) keyString += ' AND '; keyString += `${key} ${like ? 'LIKE' : '='} ?`; diff --git a/src/util/JSONStringify.ts b/src/util/JSONStringify.ts index 868e1c4..0ccd4eb 100644 --- a/src/util/JSONStringify.ts +++ b/src/util/JSONStringify.ts @@ -1,7 +1,19 @@ +/* eslint-disable */ export default function JSONStringify(obj: unknown): string { return JSON.stringify(obj, (key, value) => { + // if value is a bigint, convert it to a string if (typeof value === 'bigint') return value.toString(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // if value has a toObject method, call it and return the result + else if ( + typeof value === 'object' && + value !== null && + 'toObject' in value && + typeof value.toObject === 'function' + ) { + return value.toObject(); + } + + // return value as is return value; }); } From d5fe57259fa0649b1db2ec59d5e6944d6baac0c0 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 19:32:03 +0300 Subject: [PATCH 066/118] Update api responses and add response types Refactor API response functions to have a more literal naming structure for better readability and maintainability. Added new types for user profile, mini user profile and roadmap responses. This will help to ensure data consistency and reliability when sending responses from API. All instances of API response functions have been updated to reflect the changes. Tests and validators have also been modified to match the updated response types. --- spec/tests/routes/users.spec.ts | 16 +- src/controllers/authController.ts | 117 +++++++------ src/controllers/usersController.ts | 55 ++++-- src/helpers/apiResponses.ts | 72 ++++---- src/helpers/databaseManagement.ts | 37 +++- src/routes/usersRoutes/UsersGet.ts | 207 +---------------------- src/types/response/ResRoadmap.ts | 63 +++++++ src/types/response/ResUserMiniProfile.ts | 37 ++++ src/types/response/ResUserProfile.ts | 108 ++++++++++++ src/util/DatabaseDriver.ts | 11 +- src/validators/validateUser.ts | 4 +- 11 files changed, 405 insertions(+), 322 deletions(-) create mode 100644 src/types/response/ResRoadmap.ts create mode 100644 src/types/response/ResUserMiniProfile.ts create mode 100644 src/types/response/ResUserProfile.ts diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts index dc78fbc..9bc1d9d 100644 --- a/spec/tests/routes/users.spec.ts +++ b/spec/tests/routes/users.spec.ts @@ -4,6 +4,8 @@ import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; import { CreatedUser } from '@spec/types/tests/CreatedUser'; import JSONStringify from '@src/util/JSONStringify'; +import { ResUserProfile } from '@src/types/response/ResUserProfile'; +import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; // ! Get User Tests describe('Get User Tests', () => { @@ -24,7 +26,7 @@ describe('Get User Tests', () => { .expect(({ body }) => { expect(body.success).toBe(true); expect(body.data).toBeDefined(); - // TODO: check for type to be right + expect(ResUserProfile.isProfile(body.data)).toBe(true); }); }); @@ -36,7 +38,7 @@ describe('Get User Tests', () => { .expect(({ body }) => { expect(body.success).toBe(true); expect(body.data).toBeDefined(); - // TODO: check for type to be right + expect(ResUserProfile.isProfile(body.data)).toBe(true); }); }); @@ -46,8 +48,11 @@ describe('Get User Tests', () => { .expect(httpStatusCodes.OK) .expect(({ body }) => { expect(body.success).toBe(true); + expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); expect(body.data).toEqual( - JSON.parse(JSONStringify(user.user.toObject())), + JSON.parse( + JSONStringify(new ResUserMiniProfile(user.user.toObject())), + ), ); }); }); @@ -60,8 +65,11 @@ describe('Get User Tests', () => { .expect(({ body }) => { expect(body.success).toBe(true); expect(body.data).toBeDefined(); + expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); expect(body.data).toEqual( - JSON.parse(JSONStringify(user.user.toObject())), + JSON.parse( + JSONStringify(new ResUserMiniProfile(user.user.toObject())), + ), ); }); }); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 5a46421..9b22644 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -22,17 +22,17 @@ import { updateUser, } from '@src/helpers/databaseManagement'; import { - accountCreated, - emailConflict, - externalBadGateway, - invalidBody, - invalidLogin, - loginSuccessful, - logoutSuccessful, - notImplemented, - passwordChanged, - serverError, - unauthorized, + responseAccountCreated, + responseEmailConflict, + responseExternalBadGateway, + responseInvalidBody, + responseInvalidLogin, + responseLoginSuccessful, + responseLogoutSuccessful, + responseNotImplemented, + responsePasswordChanged, + responseServerError, + responseUnauthorized, } from '@src/helpers/apiResponses'; import { NodeEnvs } from '@src/constants/misc'; @@ -63,12 +63,12 @@ export function _handleNotOkay(res: Response, error: number): unknown { if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(error, true); if (error >= (HttpStatusCode.InternalServerError as number)) - return externalBadGateway(res); + return responseExternalBadGateway(res); if (error === (HttpStatusCode.Unauthorized as number)) - return unauthorized(res); + return responseUnauthorized(res); - return serverError(res); + return responseServerError(res); } /* @@ -81,29 +81,30 @@ export async function authLogin( const { email, password } = req.body; if (typeof email !== 'string' || typeof password !== 'string') - return invalidLogin(res); + return responseInvalidLogin(res); // get database const db = new DatabaseDriver(); // check if user exists const user = await getUserByEmail(db, email); - if (!user) return invalidLogin(res); + if (!user) return responseInvalidLogin(res); // check if password is correct const isCorrect = comparePassword(password, user.pwdHash || ''); - if (!isCorrect) return invalidLogin(res); + if (!isCorrect) return responseInvalidLogin(res); // check userInfo table for user const userInfo = await getUserInfo(db, user.id); if (!userInfo) if (!(await insertUserInfo(db, user.id, new UserInfo({ userId: user.id })))) - return serverError(res); + return responseServerError(res); // create session and save it - if (await createSaveSession(res, user.id)) return loginSuccessful(res); + if (await createSaveSession(res, user.id)) + return responseLoginSuccessful(res); - return serverError(res); + return responseServerError(res); } export async function authRegister( @@ -113,16 +114,17 @@ export async function authRegister( const { email, password } = req.body; if (typeof password !== 'string' || typeof email !== 'string') - return invalidBody(res); + return responseInvalidBody(res); // get database const db = new DatabaseDriver(); // check if user exists const user = await getUserByEmail(db, email); - if (!!user) return emailConflict(res); + if (!!user) return responseEmailConflict(res); - if (!checkEmail(email) || password.length < 8) return invalidBody(res); + if (!checkEmail(email) || password.length < 8) + return responseInvalidBody(res); // create user const userId = await insertUser( @@ -133,12 +135,13 @@ export async function authRegister( pwdHash: saltPassword(password), }), ); - if (userId === -1n) return serverError(res); + if (userId === -1n) return responseServerError(res); // create session and save it - if (await createSaveSession(res, BigInt(userId))) return accountCreated(res); + if (await createSaveSession(res, BigInt(userId))) + return responseAccountCreated(res); - return serverError(res); + return responseServerError(res); } export async function authChangePassword( @@ -148,35 +151,35 @@ export async function authChangePassword( const { password, newPassword } = req.body; if (typeof password !== 'string' || typeof newPassword !== 'string') - return invalidBody(res); + return responseInvalidBody(res); // get database const db = new DatabaseDriver(); // check if user is logged in - if (!req.session?.userId) return serverError(res); + if (!req.session?.userId) return responseServerError(res); const userId = req.session.userId; // check if user exists const user = await getUser(db, userId); - if (!user) return serverError(res); + if (!user) return responseServerError(res); // check if password is correct const isCorrect = comparePassword(password, user.pwdHash || ''); - if (!isCorrect) return invalidLogin(res); + if (!isCorrect) return responseInvalidLogin(res); user.set({ pwdHash: saltPassword(newPassword) }); // update password in ussr const result = await updateUser(db, userId, user); - if (result) return passwordChanged(res); - else return serverError(res); + if (result) return responsePasswordChanged(res); + else return responseServerError(res); } export function authForgotPassword(_: unknown, res: Response): unknown { // TODO: implement after SMTP server is set up - return notImplemented(res); + return responseNotImplemented(res); } export function authGoogle(_: unknown, res: Response): unknown { @@ -195,7 +198,7 @@ export async function authGoogleCallback( ): Promise { const code = req.query.code; - if (typeof code !== 'string') return invalidBody(res); + if (typeof code !== 'string') return responseInvalidBody(res); try { // get access token @@ -209,12 +212,12 @@ export async function authGoogleCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return serverError(res); + if (!response.data) return responseServerError(res); // get access token from response const data = response.data as { access_token?: string }; const accessToken = data.access_token; - if (!accessToken) return serverError(res); + if (!accessToken) return responseServerError(res); // get user info response = await axios.get( @@ -226,7 +229,7 @@ export async function authGoogleCallback( }, ); const userData = response?.data as GoogleUserData; - if (!userData) return serverError(res); + if (!userData) return responseServerError(res); // get database const db = new DatabaseDriver(); @@ -248,7 +251,8 @@ export async function authGoogleCallback( user.set({ googleId: userData.id }); // update user - if (!(await updateUser(db, user.id, user))) return serverError(res); + if (!(await updateUser(db, user.id, user))) + return responseServerError(res); // check userInfo table for user const userInfo = await getUserInfo(db, user.id); @@ -260,19 +264,19 @@ export async function authGoogleCallback( new UserInfo({ userId: user.id }), )) ) - return serverError(res); + return responseServerError(res); } // check if user was created - if (user.id === -1n) return serverError(res); + if (user.id === -1n) return responseServerError(res); // create session and save it if (await createSaveSession(res, BigInt(user.id))) - return loginSuccessful(res); + return responseLoginSuccessful(res); - return serverError(res); + return responseServerError(res); } catch (e) { if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(e, true); - return serverError(res); + return responseServerError(res); } } @@ -292,7 +296,7 @@ export async function authGitHubCallback( ): Promise { const code = req.query.code; - if (typeof code !== 'string') return invalidBody(res); + if (typeof code !== 'string') return responseInvalidBody(res); try { // get access token @@ -313,12 +317,12 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return serverError(res); + if (!response.data) return responseServerError(res); // get access token from response const data = response.data as { access_token?: string }; const accessToken = data.access_token; - if (!accessToken) return serverError(res); + if (!accessToken) return responseServerError(res); // get user info from GitHub response = await axios.get('https://api.github.com/user', { @@ -330,11 +334,11 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return serverError(res); + if (!response.data) return responseServerError(res); // get user data const userData = response.data as GitHubUserData; - if (!userData) return serverError(res); + if (!userData) return responseServerError(res); // get email from github response = await axios.get('https://api.github.com/user/emails', { @@ -359,7 +363,7 @@ export async function authGitHubCallback( userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; // check if email is valid - if (userData.email == '') return serverError(res); + if (userData.email == '') return responseServerError(res); // get database const db = new DatabaseDriver(); @@ -408,7 +412,7 @@ export async function authGitHubCallback( }), )) ) - return serverError(res); + return responseServerError(res); } else { // update user info user.set({ @@ -424,15 +428,16 @@ export async function authGitHubCallback( // update user info if (!(await updateUser(db, user.id, user, userInfo))) - return serverError(res); + return responseServerError(res); } } // create session and save it - if (await createSaveSession(res, user.id)) return loginSuccessful(res); + if (await createSaveSession(res, user.id)) + return responseLoginSuccessful(res); } catch (e) { if (EnvVars.NodeEnv !== NodeEnvs.Test) logger.err(e, true); - return serverError(res); + return responseServerError(res); } } @@ -440,12 +445,12 @@ export async function authLogout( req: RequestWithSession, res: Response, ): Promise { - if (!req.session) return serverError(res); + if (!req.session) return responseServerError(res); // delete session and set cookie to expire if (!(await deleteClearSession(res, req.session.token))) - return serverError(res); + return responseServerError(res); // return success - return logoutSuccessful(res); + return responseLogoutSuccessful(res); } diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index b0089bc..9820dc7 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -1,17 +1,19 @@ import { Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; import { - serverError, - userDeleted, - userMiniProfile, - userNotFound, - userProfile, + responseServerError, + responseUserDeleted, + responseUserMiniProfile, + responseUserNotFound, + responseUserProfile, + userRoadmaps, } from '@src/helpers/apiResponses'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { deleteUser, getUser, getUserInfo, + getUserRoadmaps, getUserStats, isUserFollowing, } from '@src/helpers/databaseManagement'; @@ -30,13 +32,13 @@ export async function usersDelete( // get userId from request const userId = req.session?.userId; - if (userId === undefined) return serverError(res); + if (userId === undefined) return responseServerError(res); // delete user from database - if (await deleteUser(db, userId)) return userDeleted(res); + if (await deleteUser(db, userId)) return responseUserDeleted(res); // send error json - return serverError(res); + return responseServerError(res); } /* @@ -53,7 +55,7 @@ export async function usersGetProfile( const userId = req.targetUserId; const issuerUserId = req.issuerUserId; if (userId === undefined || issuerUserId === undefined) - return serverError(res); + return responseServerError(res); // get user from database const user = await getUser(db, userId); @@ -62,10 +64,10 @@ export async function usersGetProfile( const isFollowing = await isUserFollowing(db, issuerUserId, userId); // check if user exists - if (!user || !userInfo) return userNotFound(res); + if (!user || !userInfo) return responseUserNotFound(res); // send user json - return userProfile(res, user, userInfo, stats, isFollowing); + return responseUserProfile(res, user, userInfo, stats, isFollowing); } export async function usersGetMiniProfile( @@ -77,14 +79,39 @@ export async function usersGetMiniProfile( // get userId from request const userId = req.targetUserId; - if (!userId) return serverError(res); + if (!userId) return responseServerError(res); // get user from database const user = await getUser(db, userId); // check if user exists - if (!user) return userNotFound(res); + if (!user) return responseUserNotFound(res); // send user json - return userMiniProfile(res, user); + return responseUserMiniProfile(res, user); } + +export async function userGetRoadmaps( + req: RequestWithTargetUserId, + res: Response, +): Promise { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.targetUserId; + if (!userId) return responseServerError(res); + + // get user from database + const user = await getUserRoadmaps(db, userId); + + // check if user exists + if (!user) return responseUserNotFound(res); + + // send user json + return userRoadmaps(res, user); +} + +/* + ! UsersPost route controllers + */ diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 3492bde..3337caf 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -4,68 +4,72 @@ import { User } from '@src/types/models/User'; import { UserInfo } from '@src/types/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; import JSONStringify from '@src/util/JSONStringify'; +import { Roadmap } from '@src/types/models/Roadmap'; +import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; +import { ResUserProfile } from '@src/types/response/ResUserProfile'; +import { ResRoadmap } from '@src/types/response/ResRoadmap'; /* ! Failure responses */ -export function emailConflict(res: Response): void { +export function responseEmailConflict(res: Response): void { res.status(HttpStatusCode.Conflict).json({ message: 'Email already in use', success: false, }); } -export function externalBadGateway(res: Response): void { +export function responseExternalBadGateway(res: Response): void { res.status(HttpStatusCode.BadGateway).json({ message: 'Remote resource error', success: false, }); } -export function invalidBody(res: Response): void { +export function responseInvalidBody(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ message: 'Invalid request body', success: false, }); } -export function invalidLogin(res: Response): void { +export function responseInvalidLogin(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ message: 'Invalid email or password', success: false, }); } -export function invalidParameters(res: Response): void { +export function responseInvalidParameters(res: Response): void { res.status(HttpStatusCode.BadRequest).json({ message: 'Invalid request paramteres', success: false, }); } -export function notImplemented(res: Response): void { +export function responseNotImplemented(res: Response): void { res.status(HttpStatusCode.NotImplemented).json({ message: 'Not implemented', success: false, }); } -export function serverError(res: Response): void { +export function responseServerError(res: Response): void { res.status(HttpStatusCode.InternalServerError).json({ message: 'Internal server error', success: false, }); } -export function userNotFound(res: Response): void { +export function responseUserNotFound(res: Response): void { res.status(HttpStatusCode.NotFound).json({ message: 'User couldn\'t be found', success: false, }); } -export function unauthorized(res: Response): void { +export function responseUnauthorized(res: Response): void { res.status(HttpStatusCode.Unauthorized).json({ message: 'Unauthorized', success: false, @@ -78,25 +82,25 @@ export function unauthorized(res: Response): void { // ! Authentication Responses -export function accountCreated(res: Response): void { +export function responseAccountCreated(res: Response): void { res .status(HttpStatusCode.Created) .json({ message: 'Registration successful', success: true }); } -export function loginSuccessful(res: Response): void { +export function responseLoginSuccessful(res: Response): void { res .status(HttpStatusCode.Ok) .json({ message: 'Login successful', success: true }); } -export function logoutSuccessful(res: Response): void { +export function responseLogoutSuccessful(res: Response): void { res .status(HttpStatusCode.Ok) .json({ message: 'Logout successful', success: true }); } -export function passwordChanged(res: Response): void { +export function responsePasswordChanged(res: Response): void { res .status(HttpStatusCode.Ok) .json({ message: 'Password changed successfully', success: true }); @@ -104,59 +108,53 @@ export function passwordChanged(res: Response): void { // ! User Responses -export function userDeleted(res: Response): void { +export function responseUserDeleted(res: Response): void { res .status(HttpStatusCode.Ok) .json({ message: 'Account successfully deleted', success: true }); } -export function userProfile( +export function responseUserProfile( res: Response, user: User, userInfo: UserInfo, userStats: UserStats, isFollowing: boolean, ): void { - const { roadmapsCount, issueCount, followerCount, followingCount } = - userStats, - { bio, quote, websiteUrl, githubUrl } = userInfo, - { name, avatar, githubId, googleId } = user; res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ - data: { - name, - avatar, - userId: user.id, - bio, - quote, - websiteUrl, - githubUrl, - roadmapsCount, - issueCount, - followerCount, - followingCount, - isFollowing, - githubLink: !!githubId, - googleLink: !!googleId, - }, + data: new ResUserProfile(user, userInfo, userStats, isFollowing), message: 'User found', success: true, }), ); } -export function userMiniProfile(res: Response, user: User): void { +export function responseUserMiniProfile(res: Response, user: User): void { res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ - data: user, + data: new ResUserMiniProfile(user), message: 'User found', success: true, }), ); } + +export function userRoadmaps(res: Response, roadmaps: Roadmap[]): void { + res + .status(HttpStatusCode.Ok) + .contentType('application/json') + .send( + JSONStringify({ + data: roadmaps.map((roadmap) => new ResRoadmap(roadmap)), + message: 'Roadmaps found', + success: true, + }), + ); +} diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index e45163f..1151b87 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -1,13 +1,15 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import { IUserInfo, UserInfo } from '@src/types/models/UserInfo'; import { IUser, User } from '@src/types/models/User'; +import { Roadmap } from '@src/types/models/Roadmap'; /* * Interfaces */ export interface UserStats { roadmapsCount: bigint; - issueCount: bigint; + roadmapsViews: bigint; + roadmapsLikes: bigint; followerCount: bigint; followingCount: bigint; } @@ -65,18 +67,47 @@ export async function getUserStats( userId: bigint, ): Promise { const roadmapsCount = await db.countWhere('roadmaps', 'userId', userId); - const issueCount = await db.countWhere('issues', 'userId', userId); + const roadmapsViews = await db.countQuery( + ` + SELECT SUM(rl.value) AS 'result' + FROM users u + LEFT JOIN roadmaps r ON u.id = r.userId + LEFT JOIN roadmapLikes rl ON r.id = rl.roadmapId + WHERE u.id = ?; + `, + [userId], + ); + const roadmapsLikes = await db.countQuery( + ` + SELECT COUNT(rv.userId) AS 'result' + FROM roadmaps r + JOIN roadmapViews rv ON r.id = rv.roadmapId + WHERE rv.full = 1 + AND r.userId = ?; + `, + [userId], + ); const followerCount = await db.countWhere('followers', 'userId', userId); const followingCount = await db.countWhere('followers', 'followerId', userId); return { roadmapsCount, - issueCount, + roadmapsViews, + roadmapsLikes, followerCount, followingCount, }; } +export async function getUserRoadmaps( + db: DatabaseDriver, + userId: bigint, +): Promise { + const roadmaps = await db.getAllWhere('roadmaps', 'userId', userId); + if (!roadmaps) return null; + return roadmaps.map((roadmap) => new Roadmap(roadmap)); +} + export async function isUserFollowing( db: DatabaseDriver, targetId: bigint, diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 0e76bc7..e4bb2e8 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -3,14 +3,11 @@ import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import DatabaseDriver from '@src/util/DatabaseDriver'; -import { User } from '@src/types/models/User'; -import { Roadmap } from '@src/types/models/Roadmap'; -import { Issue } from '@src/types/models/Issue'; import { Follower } from '@src/types/models/Follower'; -import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; import validateSession from '@src/validators/validateSession'; import validateUser from '@src/validators/validateUser'; import { + userGetRoadmaps, usersGetMiniProfile, usersGetProfile, } from '@src/controllers/usersController'; @@ -37,107 +34,7 @@ UsersGet.get(Paths.Users.Get.Profile, validateUser(), usersGetProfile); UsersGet.get(Paths.Users.Get.MiniProfile, validateUser(), usersGetMiniProfile); -UsersGet.get( - Paths.Users.Get.UserRoadmaps, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - const viewerId = req.session?.userId || BigInt(-1); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - const db = new DatabaseDriver(); - - const roadmaps = await db.getAllWhere( - 'roadmaps', - 'ownerId', - userId, - ); - - if (!roadmaps) { - res.status(HttpStatusCodes.NOT_FOUND).json({ error: 'User not found' }); - return; - } - - // get user - const user = await db.get('users', userId); - - if (!user) { - res.status(HttpStatusCodes.NOT_FOUND).json({ error: 'User not found' }); - return; - } - - const parsedRoadmaps: Roadmap[] = []; - - // convert roadmaps to RoadmapMini - for (let i = 0; i < roadmaps.length; i++) { - const roadmap = roadmaps[i]; - await addView(viewerId, roadmap.id, false); - parsedRoadmaps[i] = { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - id: roadmap.id.toString(), - name: roadmap.name, - description: roadmap.description || '', - likes: ( - await db.countWhere('roadmapLikes', 'roadmapId', roadmap.id) - ).toString(), - isLiked: !!(await db.getWhere( - 'roadmapLikes', - 'userId', - viewerId, - 'roadmapId', - roadmap.id, - )), - ownerName: user.name, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - userId: roadmap.userId.toString(), - }; - } - - res.status(HttpStatusCodes.OK).json({ - type: 'roadmaps', - userId: userId.toString(), - roadmaps: parsedRoadmaps, - }); - }, -); - -UsersGet.get( - Paths.Users.Get.UserIssues, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - const db = new DatabaseDriver(); - - const issues = await db.getAllWhere('issues', 'userId', userId); - - res.status(HttpStatusCodes.OK).json( - JSON.stringify( - { - type: 'issues', - userId: userId.toString(), - issues: issues, - }, - (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - }, - ), - ); - }, -); +UsersGet.get(Paths.Users.Get.UserRoadmaps, validateUser(), userGetRoadmaps); UsersGet.get( Paths.Users.Get.UserFollowers, @@ -213,106 +110,6 @@ UsersGet.get( }, ); -UsersGet.get( - Paths.Users.Get.RoadmapCount, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - // get database - const db = new DatabaseDriver(); - - // get roadmap count - const roadmapCount = await db.countWhere('roadmaps', 'ownerId', userId); - - res.status(HttpStatusCodes.OK).json({ - type: 'roadmapCount', - userId: userId.toString(), - roadmapCount: roadmapCount.toString(), - }); - }, -); - -UsersGet.get( - Paths.Users.Get.IssueCount, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - // get database - const db = new DatabaseDriver(); - - // get issue count - const issueCount = await db.countWhere('issues', 'userId', userId); - - res.status(HttpStatusCodes.OK).json({ - type: 'issueCount', - userId: userId.toString(), - issueCount: issueCount.toString(), - }); - }, -); - -UsersGet.get( - Paths.Users.Get.FollowerCount, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay specified' }); - - // get database - const db = new DatabaseDriver(); - - // get follower count - const followerCount = await db.countWhere('followers', 'userId', userId); - - res.status(HttpStatusCodes.OK).json({ - type: 'followerCount', - userId: userId.toString(), - followerCount: followerCount.toString(), - }); - }, -); - -UsersGet.get( - Paths.Users.Get.FollowingCount, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay specified' }); - - // get database - const db = new DatabaseDriver(); - - // get the following count - const followingCount = await db.countWhere( - 'followers', - 'followerId', - userId, - ); - - res.status(HttpStatusCodes.OK).json({ - type: 'followingCount', - userId: userId.toString(), - followingCount: followingCount.toString(), - }); - }, -); - UsersGet.get(Paths.Users.Get.Follow, validateSession); UsersGet.get(Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { // get the target userDisplay id diff --git a/src/types/response/ResRoadmap.ts b/src/types/response/ResRoadmap.ts new file mode 100644 index 0000000..66a4e9a --- /dev/null +++ b/src/types/response/ResRoadmap.ts @@ -0,0 +1,63 @@ +import { IRoadmap } from '@src/types/models/Roadmap'; + +export interface IResRoadmap { + readonly id: bigint; + readonly name: string; + readonly description: string; + readonly userId: bigint; + readonly isPublic: boolean; + readonly isDraft: boolean; + readonly data: string; + readonly createdAt: Date; + readonly updatedAt: Date; +} + +export class ResRoadmap implements IResRoadmap { + public readonly id: bigint; + public readonly name: string; + public readonly description: string; + public readonly userId: bigint; + public readonly isPublic: boolean; + public readonly isDraft: boolean; + public readonly data: string; + public readonly createdAt: Date; + public readonly updatedAt: Date; + + public constructor({ + id = 0n, + name, + description, + userId, + isPublic = true, + isDraft = false, + data, + createdAt = new Date(), + updatedAt = new Date(), + }: IRoadmap) { + this.id = id; + this.name = name; + this.description = description; + this.userId = userId; + this.isPublic = isPublic; + this.isDraft = isDraft; + this.data = data; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static isRoadmap(obj: unknown): obj is IResRoadmap { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + 'name' in obj && + 'description' in obj && + 'userId' in obj && + 'isPublic' in obj && + 'isDraft' in obj && + 'data' in obj && + 'createdAt' in obj && + 'updatedAt' in obj + ); + } +} diff --git a/src/types/response/ResUserMiniProfile.ts b/src/types/response/ResUserMiniProfile.ts new file mode 100644 index 0000000..b0e3c0c --- /dev/null +++ b/src/types/response/ResUserMiniProfile.ts @@ -0,0 +1,37 @@ +import { IUser } from '@src/types/models/User'; + +export interface IResUserMiniProfile { + readonly id: bigint; + readonly avatar: string | null; + readonly name: string; + readonly email: string; + readonly createdAt: Date; +} + +export class ResUserMiniProfile implements IResUserMiniProfile { + public readonly id: bigint; + public readonly avatar: string | null; + public readonly name: string; + public readonly email: string; + public readonly createdAt: Date; + + public constructor({ id, avatar, name, email, createdAt }: IUser) { + this.id = id; + this.avatar = avatar; + this.name = name; + this.email = email; + this.createdAt = createdAt; + } + + public static isMiniProfile(obj: unknown): obj is IResUserMiniProfile { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + 'avatar' in obj && + 'name' in obj && + 'email' in obj && + 'createdAt' in obj + ); + } +} diff --git a/src/types/response/ResUserProfile.ts b/src/types/response/ResUserProfile.ts new file mode 100644 index 0000000..8902335 --- /dev/null +++ b/src/types/response/ResUserProfile.ts @@ -0,0 +1,108 @@ +import { IUser } from '@src/types/models/User'; +import { IUserInfo } from '@src/types/models/UserInfo'; +import { UserStats } from '@src/helpers/databaseManagement'; + +export interface IResUserProfile { + // from User + readonly id: bigint; + readonly avatar: string | null; + readonly name: string; + readonly email: string; + readonly createdAt: Date; + + // from UserInfo + readonly bio: string | null; + readonly quote: string | null; + readonly websiteUrl: string | null; + readonly githubUrl: string | null; + + // from UserStats + readonly roadmapsCount: bigint; + readonly roadmapsViews: bigint; + readonly roadmapsLikes: bigint; + readonly followerCount: bigint; + readonly followingCount: bigint; + + // from User + readonly githubLinked: boolean; + readonly googleLinked: boolean; + + // utils + readonly isFollowing: boolean; +} + +export class ResUserProfile implements IResUserProfile { + public readonly id: bigint; + public readonly avatar: string | null; + public readonly name: string; + public readonly email: string; + public readonly bio: string | null; + public readonly quote: string | null; + public readonly websiteUrl: string | null; + public readonly githubUrl: string | null; + public readonly roadmapsCount: bigint; + public readonly roadmapsViews: bigint; + public readonly roadmapsLikes: bigint; + public readonly followerCount: bigint; + public readonly followingCount: bigint; + public readonly githubLinked: boolean; + public readonly googleLinked: boolean; + public readonly createdAt: Date; + public readonly isFollowing: boolean; + + public constructor( + { id, avatar, name, email, createdAt, githubId, googleId }: IUser, + { bio, quote, websiteUrl, githubUrl }: IUserInfo, + { + roadmapsCount, + roadmapsViews, + roadmapsLikes, + followerCount, + followingCount, + }: UserStats, + isFollowing: boolean, + ) { + this.id = id; + this.avatar = avatar; + this.name = name; + this.email = email; + this.bio = bio; + this.quote = quote; + this.websiteUrl = websiteUrl; + this.githubUrl = githubUrl; + this.roadmapsCount = roadmapsCount; + this.roadmapsViews = roadmapsViews; + this.roadmapsLikes = roadmapsLikes; + this.followerCount = followerCount; + this.followingCount = followingCount; + this.githubLinked = !!githubId; + this.googleLinked = !!googleId; + this.createdAt = createdAt; + this.isFollowing = isFollowing; + } + + // function to determine if an object is a UserProfile + public static isProfile(obj: unknown): obj is IResUserProfile { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + 'avatar' in obj && + 'name' in obj && + 'email' in obj && + 'createdAt' in obj && + 'bio' in obj && + 'quote' in obj && + 'websiteUrl' in obj && + 'githubUrl' in obj && + 'roadmapsCount' in obj && + 'roadmapsViews' in obj && + 'roadmapsLikes' in obj && + 'followerCount' in obj && + 'followingCount' in obj && + 'githubLinked' in obj && + 'googleLinked' in obj && + 'isFollowing' in obj + ); + } +} diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 32d7d2e..501c0c4 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -86,6 +86,10 @@ interface CountDataPacket extends RowDataPacket { 'COUNT(*)': bigint; } +interface CountQueryPacket extends RowDataPacket { + result: bigint; +} + interface ResultSetHeader { fieldCount: number; affectedRows: number; @@ -336,6 +340,11 @@ class Database { return parseResult(result) as T[] | null; } + public async countQuery(sql: string, params?: unknown[]): Promise { + const result = await this._query(sql, params); + return (result as CountQueryPacket[])[0]['result'] || 0n; + } + public async count(table: string): Promise { // create sql query - select count(*) from table const sql = `SELECT COUNT(*) @@ -368,7 +377,7 @@ class Database { ...values: unknown[] ): Promise { const queryBuilderResult = this._buildWhereQuery(like, ...values); - if (!queryBuilderResult) return BigInt(0); + if (!queryBuilderResult) return 0n; const sql = `SELECT COUNT(*) FROM ${table} diff --git a/src/validators/validateUser.ts b/src/validators/validateUser.ts index 24c9b82..00db5ea 100644 --- a/src/validators/validateUser.ts +++ b/src/validators/validateUser.ts @@ -1,6 +1,6 @@ import { NextFunction, Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; -import { invalidParameters } from '@src/helpers/apiResponses'; +import { responseInvalidParameters } from '@src/helpers/apiResponses'; export interface RequestWithTargetUserId extends RequestWithSession { targetUserId?: bigint; @@ -24,7 +24,7 @@ export default function validateUser( req.issuerUserId = BigInt(req.session?.userId ?? -1n); // if userId is -1n, return error - if (req.targetUserId === -1n) return invalidParameters(res); + if (req.targetUserId === -1n) return responseInvalidParameters(res); // if ownUserOnly is true, set targetUserId to issuerUserId if (req.issuerUserId !== req.targetUserId && ownUserOnly) From 35cf14900bb6e49c70d280f8c176588d744a6af0 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 20:33:26 +0300 Subject: [PATCH 067/118] Add follow and unfollow functionality --- src/constants/Paths.ts | 9 +- src/controllers/usersController.ts | 70 ++++++- src/helpers/apiResponses.ts | 35 +++- src/helpers/databaseManagement.ts | 33 ++++ src/routes/usersRoutes/UsersGet.ts | 291 ++++++++--------------------- src/util/DatabaseDriver.ts | 25 ++- 6 files changed, 235 insertions(+), 228 deletions(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 9339896..6f039f0 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -93,17 +93,10 @@ const Paths = { Profile: '/', MiniProfile: '/mini', UserRoadmaps: '/roadmaps', - UserIssues: '/issues', - UserFollowers: '/followers', - UserFollowing: '/following', - RoadmapCount: '/roadmap-count', - IssueCount: '/issue-count', - FollowerCount: '/follower-count', - FollowingCount: '/following-count', Follow: '/follow', }, Update: { - Base: '/:userId([0-9]+)?', + Base: '/', ProfilePicture: '/profile-picture', Bio: '/bio', Quote: '/quote', diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 9820dc7..bd538cb 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -1,21 +1,27 @@ import { Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; import { + responseAlreadyFollowing, + responseCantFollowYourself, responseServerError, responseUserDeleted, + responseUserFollowed, responseUserMiniProfile, responseUserNotFound, responseUserProfile, - userRoadmaps, + responseUserRoadmaps, + responseUserUnfollowed, } from '@src/helpers/apiResponses'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { deleteUser, + followUser, getUser, getUserInfo, getUserRoadmaps, getUserStats, isUserFollowing, + unfollowUser, } from '@src/helpers/databaseManagement'; import { RequestWithTargetUserId } from '@src/validators/validateUser'; @@ -109,7 +115,67 @@ export async function userGetRoadmaps( if (!user) return responseUserNotFound(res); // send user json - return userRoadmaps(res, user); + return responseUserRoadmaps(res, user); +} + +export async function userFollow( + req: RequestWithTargetUserId, + res: Response, +): Promise { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const issuerId = req.issuerUserId; + const targetId = req.targetUserId; + if (!targetId || !issuerId || targetId === issuerId) + return responseCantFollowYourself(res); + + // get user from database + const user = await getUser(db, targetId); + + // check if user exists + if (!user) return responseUserNotFound(res); + + // check if user is already following + if (await isUserFollowing(db, issuerId, targetId)) + return responseAlreadyFollowing(res); + + // follow user + await followUser(db, issuerId, targetId); + + // send user json + return responseUserFollowed(res); +} + +export async function userUnfollow( + req: RequestWithTargetUserId, + res: Response, +): Promise { + // get database + const db = new DatabaseDriver(); + + // get userId from request + const issuerId = req.issuerUserId; + const targetId = req.targetUserId; + if (!targetId || !issuerId || targetId === issuerId) + return responseCantFollowYourself(res); + + // get user from database + const user = await getUser(db, targetId); + + // check if user exists + if (!user) return responseUserNotFound(res); + + // check if user is already following + if (!(await isUserFollowing(db, issuerId, targetId))) + return responseAlreadyFollowing(res); + + // follow user + await unfollowUser(db, issuerId, targetId); + + // send user json + return responseUserUnfollowed(res); } /* diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 3337caf..437eb21 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -76,6 +76,27 @@ export function responseUnauthorized(res: Response): void { }); } +export function responseCantFollowYourself(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + message: 'You can\'t follow yourself', + success: false, + }); +} + +export function responseAlreadyFollowing(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + message: 'Already following', + success: false, + }); +} + +export function responseNotFollowing(res: Response): void { + res.status(HttpStatusCode.BadRequest).json({ + message: 'Not following', + success: false, + }); +} + /* ? Success responses */ @@ -146,7 +167,7 @@ export function responseUserMiniProfile(res: Response, user: User): void { ); } -export function userRoadmaps(res: Response, roadmaps: Roadmap[]): void { +export function responseUserRoadmaps(res: Response, roadmaps: Roadmap[]): void { res .status(HttpStatusCode.Ok) .contentType('application/json') @@ -158,3 +179,15 @@ export function userRoadmaps(res: Response, roadmaps: Roadmap[]): void { }), ); } + +export function responseUserFollowed(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'User followed', success: true }); +} + +export function responseUserUnfollowed(res: Response): void { + res + .status(HttpStatusCode.Ok) + .json({ message: 'User unfollowed', success: true }); +} diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 1151b87..3f4a359 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -2,6 +2,7 @@ import DatabaseDriver from '@src/util/DatabaseDriver'; import { IUserInfo, UserInfo } from '@src/types/models/UserInfo'; import { IUser, User } from '@src/types/models/User'; import { Roadmap } from '@src/types/models/Roadmap'; +import { Follower } from '@src/types/models/Follower'; /* * Interfaces @@ -108,6 +109,38 @@ export async function getUserRoadmaps( return roadmaps.map((roadmap) => new Roadmap(roadmap)); } +export async function followUser( + db: DatabaseDriver, + targetId: bigint, + authUserId: bigint, +): Promise { + if (targetId === authUserId) return false; + return ( + (await db.insert( + 'followers', + new Follower({ + userId: targetId, + followerId: authUserId, + }), + )) >= 0 + ); +} + +export async function unfollowUser( + db: DatabaseDriver, + targetId: bigint, + authUserId: bigint, +): Promise { + if (targetId === authUserId) return false; + return await db.deleteWhere( + 'followers', + 'userId', + targetId, + 'followerId', + authUserId, + ); +} + export async function isUserFollowing( db: DatabaseDriver, targetId: bigint, diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index e4bb2e8..5fdf7e9 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -1,232 +1,101 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { RequestWithSession } from '@src/middleware/session'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import DatabaseDriver from '@src/util/DatabaseDriver'; -import { Follower } from '@src/types/models/Follower'; -import validateSession from '@src/validators/validateSession'; import validateUser from '@src/validators/validateUser'; import { + userFollow, userGetRoadmaps, usersGetMiniProfile, usersGetProfile, + userUnfollow, } from '@src/controllers/usersController'; // ! What would I do without StackOverflow? // ! https://stackoverflow.com/a/60848873 const UsersGet = Router({ mergeParams: true, strict: true }); -function getUserId(req: RequestWithSession): bigint | undefined { - // get :userId? from req.params - const query: string = req.params.userId; - - let userId = query ? BigInt(query) : undefined; - - if (userId === undefined) { - // get userId from session - userId = req?.session?.userId; - } - - return userId; -} - UsersGet.get(Paths.Users.Get.Profile, validateUser(), usersGetProfile); UsersGet.get(Paths.Users.Get.MiniProfile, validateUser(), usersGetMiniProfile); UsersGet.get(Paths.Users.Get.UserRoadmaps, validateUser(), userGetRoadmaps); -UsersGet.get( - Paths.Users.Get.UserFollowers, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - const db = new DatabaseDriver(); - - const followers = await db.getAllWhere( - 'followers', - 'userId', - userId, - ); - - res.status(HttpStatusCodes.OK).json( - JSON.stringify( - { - type: 'followers', - userId: userId.toString(), - followers: followers, - }, - (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - }, - ), - ); - }, -); - -UsersGet.get( - Paths.Users.Get.UserFollowing, - async (req: RequestWithSession, res) => { - const userId = getUserId(req); - - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - const db = new DatabaseDriver(); - - const following = await db.getAllWhere( - 'followers', - 'followerId', - userId, - ); - - res.status(HttpStatusCodes.OK).json( - JSON.stringify( - { - type: 'following', - userId: userId.toString(), - following: following, - }, - (_, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - }, - ), - ); - }, -); - -UsersGet.get(Paths.Users.Get.Follow, validateSession); -UsersGet.get(Paths.Users.Get.Follow, async (req: RequestWithSession, res) => { - // get the target userDisplay id - const userId = BigInt(req.params.userId || -1); - - // get the current userDisplay id - const followerId = req.session?.userId; - - if (userId === followerId) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'Cannot follow yourself' }); - - // if either of the ids are undefined, return a bad request - if (!followerId || !userId || userId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay specified' }); - - // get database - const db = new DatabaseDriver(); - - // check if the userDisplay is already following the target userDisplay - const following = await db.getWhere( - 'followers', - 'followerId', - followerId, - 'userId', - userId, - ); - - if (following) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Already following' }); - - // create a new follower - const follower = new Follower({ followerId, userId }); - - // insert the follower into the database - const insert = await db.insert('followers', follower); - - // if the insert was successful, return the follower - if (insert >= 0) { - return res.status(HttpStatusCodes.OK).json({ - type: 'follow', - follower: { - id: insert.toString(), - followerId: follower.followerId.toString(), - userId: follower.userId.toString(), - }, - }); - } - - // otherwise, return an error - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to follow' }); -}); - -UsersGet.delete(Paths.Users.Get.Follow, validateSession); -UsersGet.delete( - Paths.Users.Get.Follow, - async (req: RequestWithSession, res) => { - // get the target userDisplay id - const userId = BigInt(req.params.userId || -1); - - // get the current userDisplay id - const followerId = req.session?.userId; - - if (userId === followerId) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'Cannot unfollow yourself' }); - - // if either of the ids are undefined, return a bad request - if (!followerId || !userId) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay specified' }); - - // get database - const db = new DatabaseDriver(); - - // check if the userDisplay is already following the target userDisplay - const following = await db.getWhere( - 'followers', - 'followerId', - followerId, - 'userId', - userId, - ); - - if (!following) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Not following' }); - - // delete the follower from the database - const deleted = await db.delete('followers', following.id); - - // if the delete was successful, return the follower - if (deleted) { - return res.status(HttpStatusCodes.OK).json({ - type: 'unfollow', - follower: { - followerId: followerId.toString(), - userId: userId.toString(), - }, - }); - } - - // otherwise, return an error - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to unfollow' }); - }, -); +UsersGet.get(Paths.Users.Get.Follow, validateUser(), userFollow); + +UsersGet.delete(Paths.Users.Get.Follow, validateUser(), userUnfollow); + +// TODO: Following and followers lists +// UsersGet.get( +// Paths.Users.Get.UserFollowers, +// async (req: RequestWithSession, res) => { +// const userId = getUserId(req); +// +// if (userId === undefined) +// return res +// .status(HttpStatusCodes.BAD_REQUEST) +// .json({ error: 'No user specified' }); +// +// const db = new DatabaseDriver(); +// +// const followers = await db.getAllWhere( +// 'followers', +// 'userId', +// userId, +// ); +// +// res.status(HttpStatusCodes.OK).json( +// JSON.stringify( +// { +// type: 'followers', +// userId: userId.toString(), +// followers: followers, +// }, +// (_, value) => { +// if (typeof value === 'bigint') { +// return value.toString(); +// } +// // eslint-disable-next-line @typescript-eslint/no-unsafe-return +// return value; +// }, +// ), +// ); +// }, +// ); +// +// UsersGet.get( +// Paths.Users.Get.UserFollowing, +// async (req: RequestWithSession, res) => { +// const userId = getUserId(req); +// +// if (userId === undefined) +// return res +// .status(HttpStatusCodes.BAD_REQUEST) +// .json({ error: 'No user specified' }); +// +// const db = new DatabaseDriver(); +// +// const following = await db.getAllWhere( +// 'followers', +// 'followerId', +// userId, +// ); +// +// res.status(HttpStatusCodes.OK).json( +// JSON.stringify( +// { +// type: 'following', +// userId: userId.toString(), +// following: following, +// }, +// (_, value) => { +// if (typeof value === 'bigint') { +// return value.toString(); +// } +// // eslint-disable-next-line @typescript-eslint/no-unsafe-return +// return value; +// }, +// ), +// ); +// }, +// ); export default UsersGet; diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 501c0c4..76f23dc 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -226,13 +226,26 @@ class Database { WHERE id = ?`; const result = (await this._query(sql, [id])) as ResultSetHeader; - let affectedRows = -1; - if (result) { - affectedRows = result.affectedRows || -1; - } - // return true if affected rows > 0 else false - return affectedRows > 0; + return result.affectedRows > 0; + } + + public async deleteWhere( + table: string, + ...values: unknown[] + ): Promise { + const queryBuilderResult = this._buildWhereQuery(false, ...values); + if (!queryBuilderResult) return false; + + const sql = `DELETE + FROM ${table} + WHERE ${queryBuilderResult.keyString}`; + const result = (await this._query( + sql, + queryBuilderResult.params, + )) as ResultSetHeader; + + return result.affectedRows > 0; } private _buildWhereQuery = ( From 99b9c64f02e889a8f8f57ef6eead3aca67f290d1 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 20:53:35 +0300 Subject: [PATCH 068/118] Roadmaps now come with userName and userAvatar --- src/controllers/usersController.ts | 17 +++++++--- src/helpers/apiResponses.ts | 21 ++++++++++-- src/types/response/ResRoadmap.ts | 42 ++++++++++++++++-------- src/types/response/ResUserMiniProfile.ts | 5 +-- src/types/response/ResUserProfile.ts | 6 +--- 5 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index bd538cb..ad2f713 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -7,6 +7,7 @@ import { responseUserDeleted, responseUserFollowed, responseUserMiniProfile, + responseUserNoRoadmaps, responseUserNotFound, responseUserProfile, responseUserRoadmaps, @@ -24,6 +25,7 @@ import { unfollowUser, } from '@src/helpers/databaseManagement'; import { RequestWithTargetUserId } from '@src/validators/validateUser'; +import { ResRoadmap } from '@src/types/response/ResRoadmap'; /* ! Main route controllers @@ -108,14 +110,21 @@ export async function userGetRoadmaps( const userId = req.targetUserId; if (!userId) return responseServerError(res); - // get user from database - const user = await getUserRoadmaps(db, userId); + const user = await getUser(db, userId); - // check if user exists if (!user) return responseUserNotFound(res); + // get roadmaps from database + const roadmaps = await getUserRoadmaps(db, userId); + + // check if user exists + if (!roadmaps) return responseUserNoRoadmaps(res); + // send user json - return responseUserRoadmaps(res, user); + return responseUserRoadmaps( + res, + roadmaps.map((roadmap) => new ResRoadmap(roadmap, user)), + ); } export async function userFollow( diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index 437eb21..f68c69c 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -4,7 +4,6 @@ import { User } from '@src/types/models/User'; import { UserInfo } from '@src/types/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; import JSONStringify from '@src/util/JSONStringify'; -import { Roadmap } from '@src/types/models/Roadmap'; import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; import { ResUserProfile } from '@src/types/response/ResUserProfile'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; @@ -167,13 +166,29 @@ export function responseUserMiniProfile(res: Response, user: User): void { ); } -export function responseUserRoadmaps(res: Response, roadmaps: Roadmap[]): void { +export function responseUserNoRoadmaps(res: Response): void { res .status(HttpStatusCode.Ok) .contentType('application/json') .send( JSONStringify({ - data: roadmaps.map((roadmap) => new ResRoadmap(roadmap)), + data: [], + message: 'User has no roadmaps', + success: true, + }), + ); +} + +export function responseUserRoadmaps( + res: Response, + roadmaps: ResRoadmap[], +): void { + res + .status(HttpStatusCode.Ok) + .contentType('application/json') + .send( + JSONStringify({ + data: roadmaps, message: 'Roadmaps found', success: true, }), diff --git a/src/types/response/ResRoadmap.ts b/src/types/response/ResRoadmap.ts index 66a4e9a..aedb5a4 100644 --- a/src/types/response/ResRoadmap.ts +++ b/src/types/response/ResRoadmap.ts @@ -1,48 +1,62 @@ import { IRoadmap } from '@src/types/models/Roadmap'; +import { IUser } from '@src/types/models/User'; export interface IResRoadmap { readonly id: bigint; readonly name: string; readonly description: string; - readonly userId: bigint; readonly isPublic: boolean; readonly isDraft: boolean; readonly data: string; readonly createdAt: Date; readonly updatedAt: Date; + + // user + readonly userId: bigint; + readonly userAvatar: string | null; + readonly userName: string; } export class ResRoadmap implements IResRoadmap { public readonly id: bigint; public readonly name: string; public readonly description: string; - public readonly userId: bigint; public readonly isPublic: boolean; public readonly isDraft: boolean; public readonly data: string; public readonly createdAt: Date; public readonly updatedAt: Date; - public constructor({ - id = 0n, - name, - description, - userId, - isPublic = true, - isDraft = false, - data, - createdAt = new Date(), - updatedAt = new Date(), - }: IRoadmap) { + public readonly userId: bigint; + public readonly userAvatar: string | null; + public readonly userName: string; + + public constructor( + { + id = 0n, + name, + description, + userId, + isPublic = true, + isDraft = false, + data, + createdAt = new Date(), + updatedAt = new Date(), + }: IRoadmap, + { avatar, name: userName }: IUser, + ) { this.id = id; this.name = name; this.description = description; - this.userId = userId; this.isPublic = isPublic; this.isDraft = isDraft; this.data = data; this.createdAt = createdAt; this.updatedAt = updatedAt; + + this.userId = userId; + this.userAvatar = avatar; + this.userName = userName; } public static isRoadmap(obj: unknown): obj is IResRoadmap { diff --git a/src/types/response/ResUserMiniProfile.ts b/src/types/response/ResUserMiniProfile.ts index b0e3c0c..8343d1e 100644 --- a/src/types/response/ResUserMiniProfile.ts +++ b/src/types/response/ResUserMiniProfile.ts @@ -4,7 +4,6 @@ export interface IResUserMiniProfile { readonly id: bigint; readonly avatar: string | null; readonly name: string; - readonly email: string; readonly createdAt: Date; } @@ -12,14 +11,12 @@ export class ResUserMiniProfile implements IResUserMiniProfile { public readonly id: bigint; public readonly avatar: string | null; public readonly name: string; - public readonly email: string; public readonly createdAt: Date; - public constructor({ id, avatar, name, email, createdAt }: IUser) { + public constructor({ id, avatar, name, createdAt }: IUser) { this.id = id; this.avatar = avatar; this.name = name; - this.email = email; this.createdAt = createdAt; } diff --git a/src/types/response/ResUserProfile.ts b/src/types/response/ResUserProfile.ts index 8902335..11a7eba 100644 --- a/src/types/response/ResUserProfile.ts +++ b/src/types/response/ResUserProfile.ts @@ -7,7 +7,6 @@ export interface IResUserProfile { readonly id: bigint; readonly avatar: string | null; readonly name: string; - readonly email: string; readonly createdAt: Date; // from UserInfo @@ -35,7 +34,6 @@ export class ResUserProfile implements IResUserProfile { public readonly id: bigint; public readonly avatar: string | null; public readonly name: string; - public readonly email: string; public readonly bio: string | null; public readonly quote: string | null; public readonly websiteUrl: string | null; @@ -51,7 +49,7 @@ export class ResUserProfile implements IResUserProfile { public readonly isFollowing: boolean; public constructor( - { id, avatar, name, email, createdAt, githubId, googleId }: IUser, + { id, avatar, name, createdAt, githubId, googleId }: IUser, { bio, quote, websiteUrl, githubUrl }: IUserInfo, { roadmapsCount, @@ -65,7 +63,6 @@ export class ResUserProfile implements IResUserProfile { this.id = id; this.avatar = avatar; this.name = name; - this.email = email; this.bio = bio; this.quote = quote; this.websiteUrl = websiteUrl; @@ -89,7 +86,6 @@ export class ResUserProfile implements IResUserProfile { 'id' in obj && 'avatar' in obj && 'name' in obj && - 'email' in obj && 'createdAt' in obj && 'bio' in obj && 'quote' in obj && From 39daf0b41defbdf69d90186f0c74d311f357f1c8 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 22:05:37 +0300 Subject: [PATCH 069/118] Remove classes.ts and types.ts files; organize responses into dedicated modules Two files, classes.ts and types.ts, have been removed as their contents were unnecessary. To improve code organization and readability, responses were grouped into dedicated modules. This includes generalResponses.ts, userResponses.ts, authResponses.ts, and roadmapResponses.ts. Updates were also made to various route-related files to reflect these changes, ensuring they properly reference the correct response functions. Filepaths in imports were also updated, moving validators to a new validators directory within the middleware directory. --- src/constants/EnvVars.ts | 1 - src/controllers/authController.ts | 18 +- src/controllers/usersController.ts | 32 +- src/helpers/apiResponses.ts | 405 +++++++++--------- src/helpers/responses/authResponses.ts | 44 ++ src/helpers/responses/generalResponses.ts | 48 +++ src/helpers/responses/roadmapResponses.ts | 33 ++ src/helpers/responses/userResponses.ts | 90 ++++ .../validators/validateBody.ts | 0 .../validators/validateSession.ts | 0 .../validators/validateUser.ts | 4 +- src/other/classes.ts | 17 - src/other/types.ts | 3 - src/routes/AuthRouter.ts | 4 +- src/routes/RoadmapsRouter.ts | 2 +- src/routes/UsersRouter.ts | 2 +- src/routes/roadmapsRoutes/RoadmapIssues.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 2 +- .../issuesRoutes/CommentsRouter.ts | 2 +- .../issuesRoutes/IssuesUpdate.ts | 2 +- src/routes/usersRoutes/UsersGet.ts | 2 +- src/routes/usersRoutes/UsersUpdate.ts | 2 +- src/server.ts | 11 +- 23 files changed, 457 insertions(+), 269 deletions(-) create mode 100644 src/helpers/responses/authResponses.ts create mode 100644 src/helpers/responses/generalResponses.ts create mode 100644 src/helpers/responses/roadmapResponses.ts create mode 100644 src/helpers/responses/userResponses.ts rename src/{ => middleware}/validators/validateBody.ts (100%) rename src/{ => middleware}/validators/validateSession.ts (100%) rename src/{ => middleware}/validators/validateUser.ts (91%) delete mode 100644 src/other/classes.ts delete mode 100644 src/other/types.ts diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index cf89d54..41b502e 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -47,7 +47,6 @@ const EnvVars = { NodeEnv: process.env.NODE_ENV ?? '', Port: process.env.PORT ?? 0, CookieProps: { - Key: 'ExpressGeneratorTs', Secret: process.env.COOKIE_SECRET ?? '', // Casing to match express cookie options Options: { diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 9b22644..2c76cf7 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,4 +1,4 @@ -import { RequestWithBody } from '@src/validators/validateBody'; +import { RequestWithBody } from '@src/middleware/validators/validateBody'; import { Response } from 'express'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { User } from '@src/types/models/User'; @@ -21,20 +21,22 @@ import { insertUserInfo, updateUser, } from '@src/helpers/databaseManagement'; +import { NodeEnvs } from '@src/constants/misc'; import { - responseAccountCreated, - responseEmailConflict, responseExternalBadGateway, responseInvalidBody, + responseNotImplemented, + responseServerError, + responseUnauthorized, +} from '@src/helpers/responses/generalResponses'; +import { + responseAccountCreated, + responseEmailConflict, responseInvalidLogin, responseLoginSuccessful, responseLogoutSuccessful, - responseNotImplemented, responsePasswordChanged, - responseServerError, - responseUnauthorized, -} from '@src/helpers/apiResponses'; -import { NodeEnvs } from '@src/constants/misc'; +} from '@src/helpers/responses/authResponses'; /* * Interfaces diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index ad2f713..2c09666 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -1,18 +1,5 @@ import { Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; -import { - responseAlreadyFollowing, - responseCantFollowYourself, - responseServerError, - responseUserDeleted, - responseUserFollowed, - responseUserMiniProfile, - responseUserNoRoadmaps, - responseUserNotFound, - responseUserProfile, - responseUserRoadmaps, - responseUserUnfollowed, -} from '@src/helpers/apiResponses'; import DatabaseDriver from '@src/util/DatabaseDriver'; import { deleteUser, @@ -24,8 +11,25 @@ import { isUserFollowing, unfollowUser, } from '@src/helpers/databaseManagement'; -import { RequestWithTargetUserId } from '@src/validators/validateUser'; +import { + RequestWithTargetUserId, +} from '@src/middleware/validators/validateUser'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; +import { responseServerError } from '@src/helpers/responses/generalResponses'; +import { + responseAlreadyFollowing, + responseCantFollowYourself, + responseUserDeleted, + responseUserFollowed, + responseUserMiniProfile, + responseUserNotFound, + responseUserProfile, + responseUserUnfollowed, +} from '@src/helpers/responses/userResponses'; +import { + responseUserNoRoadmaps, + responseUserRoadmaps, +} from '@src/helpers/responses/roadmapResponses'; /* ! Main route controllers diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts index f68c69c..4b51bd1 100644 --- a/src/helpers/apiResponses.ts +++ b/src/helpers/apiResponses.ts @@ -1,208 +1,197 @@ -import { Response } from 'express'; -import { HttpStatusCode } from 'axios'; -import { User } from '@src/types/models/User'; -import { UserInfo } from '@src/types/models/UserInfo'; -import { UserStats } from '@src/helpers/databaseManagement'; -import JSONStringify from '@src/util/JSONStringify'; -import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; -import { ResUserProfile } from '@src/types/response/ResUserProfile'; -import { ResRoadmap } from '@src/types/response/ResRoadmap'; - -/* - ! Failure responses - */ - -export function responseEmailConflict(res: Response): void { - res.status(HttpStatusCode.Conflict).json({ - message: 'Email already in use', - success: false, - }); -} - -export function responseExternalBadGateway(res: Response): void { - res.status(HttpStatusCode.BadGateway).json({ - message: 'Remote resource error', - success: false, - }); -} - -export function responseInvalidBody(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'Invalid request body', - success: false, - }); -} - -export function responseInvalidLogin(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'Invalid email or password', - success: false, - }); -} - -export function responseInvalidParameters(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'Invalid request paramteres', - success: false, - }); -} - -export function responseNotImplemented(res: Response): void { - res.status(HttpStatusCode.NotImplemented).json({ - message: 'Not implemented', - success: false, - }); -} - -export function responseServerError(res: Response): void { - res.status(HttpStatusCode.InternalServerError).json({ - message: 'Internal server error', - success: false, - }); -} - -export function responseUserNotFound(res: Response): void { - res.status(HttpStatusCode.NotFound).json({ - message: 'User couldn\'t be found', - success: false, - }); -} - -export function responseUnauthorized(res: Response): void { - res.status(HttpStatusCode.Unauthorized).json({ - message: 'Unauthorized', - success: false, - }); -} - -export function responseCantFollowYourself(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'You can\'t follow yourself', - success: false, - }); -} - -export function responseAlreadyFollowing(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'Already following', - success: false, - }); -} - -export function responseNotFollowing(res: Response): void { - res.status(HttpStatusCode.BadRequest).json({ - message: 'Not following', - success: false, - }); -} - -/* - ? Success responses - */ - -// ! Authentication Responses - -export function responseAccountCreated(res: Response): void { - res - .status(HttpStatusCode.Created) - .json({ message: 'Registration successful', success: true }); -} - -export function responseLoginSuccessful(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'Login successful', success: true }); -} - -export function responseLogoutSuccessful(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'Logout successful', success: true }); -} - -export function responsePasswordChanged(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'Password changed successfully', success: true }); -} - -// ! User Responses - -export function responseUserDeleted(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'Account successfully deleted', success: true }); -} - -export function responseUserProfile( - res: Response, - user: User, - userInfo: UserInfo, - userStats: UserStats, - isFollowing: boolean, -): void { - res - .status(HttpStatusCode.Ok) - .contentType('application/json') - .send( - JSONStringify({ - data: new ResUserProfile(user, userInfo, userStats, isFollowing), - message: 'User found', - success: true, - }), - ); -} - -export function responseUserMiniProfile(res: Response, user: User): void { - res - .status(HttpStatusCode.Ok) - .contentType('application/json') - .send( - JSONStringify({ - data: new ResUserMiniProfile(user), - message: 'User found', - success: true, - }), - ); -} - -export function responseUserNoRoadmaps(res: Response): void { - res - .status(HttpStatusCode.Ok) - .contentType('application/json') - .send( - JSONStringify({ - data: [], - message: 'User has no roadmaps', - success: true, - }), - ); -} - -export function responseUserRoadmaps( - res: Response, - roadmaps: ResRoadmap[], -): void { - res - .status(HttpStatusCode.Ok) - .contentType('application/json') - .send( - JSONStringify({ - data: roadmaps, - message: 'Roadmaps found', - success: true, - }), - ); -} - -export function responseUserFollowed(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'User followed', success: true }); -} - -export function responseUserUnfollowed(res: Response): void { - res - .status(HttpStatusCode.Ok) - .json({ message: 'User unfollowed', success: true }); -} +// import { Response } from 'express'; +// import { HttpStatusCode } from 'axios'; +// import { User } from '@src/types/models/User'; +// import { UserInfo } from '@src/types/models/UserInfo'; +// import { UserStats } from '@src/helpers/databaseManagement'; +// import JSONStringify from '@src/util/JSONStringify'; +// import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; +// import { ResUserProfile } from '@src/types/response/ResUserProfile'; +// import { ResRoadmap } from '@src/types/response/ResRoadmap'; +// +// /* +// ! Failure responses +// */ +// +// export function responseEmailConflict(res: Response): void { +// res.status(HttpStatusCode.Conflict).json({ +// message: 'Email already in use', +// success: false, +// }); +// } +// +// export function responseExternalBadGateway(res: Response): void { +// res.status(HttpStatusCode.BadGateway).json({ +// message: 'Remote resource error', +// success: false, +// }); +// } +// +// export function responseInvalidBody(res: Response): void { +// res.status(HttpStatusCode.BadRequest).json({ +// message: 'Invalid request body', +// success: false, +// }); +// } +// +// +// export function responseInvalidParameters(res: Response): void { +// res.status(HttpStatusCode.BadRequest).json({ +// message: 'Invalid request paramteres', +// success: false, +// }); +// } +// +// export function responseNotImplemented(res: Response): void { +// res.status(HttpStatusCode.NotImplemented).json({ +// message: 'Not implemented', +// success: false, +// }); +// } +// +// export function responseServerError(res: Response): void { +// res.status(HttpStatusCode.InternalServerError).json({ +// message: 'Internal server error', +// success: false, +// }); +// } +// +// export function responseUserNotFound(res: Response): void { +// res.status(HttpStatusCode.NotFound).json({ +// message: 'User couldn\'t be found', +// success: false, +// }); +// } +// +// export function responseUnauthorized(res: Response): void { +// res.status(HttpStatusCode.Unauthorized).json({ +// message: 'Unauthorized', +// success: false, +// }); +// } +// +// export function responseCantFollowYourself(res: Response): void { +// res.status(HttpStatusCode.BadRequest).json({ +// message: 'You can\'t follow yourself', +// success: false, +// }); +// } +// +// export function responseAlreadyFollowing(res: Response): void { +// res.status(HttpStatusCode.BadRequest).json({ +// message: 'Already following', +// success: false, +// }); +// } +// +// export function responseNotFollowing(res: Response): void { +// res.status(HttpStatusCode.BadRequest).json({ +// message: 'Not following', +// success: false, +// }); +// } +// +// /* +// ? Success responses +// */ +// +// // ! Authentication Responses +// +// export function responseAccountCreated(res: Response): void { +// res +// .status(HttpStatusCode.Created) +// .json({ message: 'Registration successful', success: true }); +// } +// +// +// export function responseLogoutSuccessful(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .json({ message: 'Logout successful', success: true }); +// } +// +// export function responsePasswordChanged(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .json({ message: 'Password changed successfully', success: true }); +// } +// +// // ! User Responses +// +// export function responseUserDeleted(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .json({ message: 'Account successfully deleted', success: true }); +// } +// +// export function responseUserProfile( +// res: Response, +// user: User, +// userInfo: UserInfo, +// userStats: UserStats, +// isFollowing: boolean, +// ): void { +// res +// .status(HttpStatusCode.Ok) +// .contentType('application/json') +// .send( +// JSONStringify({ +// data: new ResUserProfile(user, userInfo, userStats, isFollowing), +// message: 'User found', +// success: true, +// }), +// ); +// } +// +// export function responseUserMiniProfile(res: Response, user: User): void { +// res +// .status(HttpStatusCode.Ok) +// .contentType('application/json') +// .send( +// JSONStringify({ +// data: new ResUserMiniProfile(user), +// message: 'User found', +// success: true, +// }), +// ); +// } +// +// export function responseUserNoRoadmaps(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .contentType('application/json') +// .send( +// JSONStringify({ +// data: [], +// message: 'User has no roadmaps', +// success: true, +// }), +// ); +// } +// +// export function responseUserRoadmaps( +// res: Response, +// roadmaps: ResRoadmap[], +// ): void { +// res +// .status(HttpStatusCode.Ok) +// .contentType('application/json') +// .send( +// JSONStringify({ +// data: roadmaps, +// message: 'Roadmaps found', +// success: true, +// }), +// ); +// } +// +// export function responseUserFollowed(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .json({ message: 'User followed', success: true }); +// } +// +// export function responseUserUnfollowed(res: Response): void { +// res +// .status(HttpStatusCode.Ok) +// .json({ message: 'User unfollowed', success: true }); +// } diff --git a/src/helpers/responses/authResponses.ts b/src/helpers/responses/authResponses.ts new file mode 100644 index 0000000..dcaf6a6 --- /dev/null +++ b/src/helpers/responses/authResponses.ts @@ -0,0 +1,44 @@ +import { Response } from 'express'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; + +/* + ! Authentication responses + */ + +export function responseInvalidLogin(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Invalid email or password', + success: false, + }); +} + +export function responseEmailConflict(res: Response): void { + res.status(HttpStatusCodes.CONFLICT).json({ + message: 'Email already in use', + success: false, + }); +} + +export function responseAccountCreated(res: Response): void { + res + .status(HttpStatusCodes.CREATED) + .json({ message: 'Registration successful', success: true }); +} + +export function responseLoginSuccessful(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'Login successful', success: true }); +} + +export function responseLogoutSuccessful(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'Logout successful', success: true }); +} + +export function responsePasswordChanged(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'Password changed successfully', success: true }); +} diff --git a/src/helpers/responses/generalResponses.ts b/src/helpers/responses/generalResponses.ts new file mode 100644 index 0000000..33439fd --- /dev/null +++ b/src/helpers/responses/generalResponses.ts @@ -0,0 +1,48 @@ +import { Response } from 'express'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; + +/* + ! General responses + */ + +export function responseExternalBadGateway(res: Response): void { + res.status(HttpStatusCodes.BAD_GATEWAY).json({ + message: 'Remote resource error', + success: false, + }); +} + +export function responseInvalidBody(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Invalid request body', + success: false, + }); +} + +export function responseInvalidParameters(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Invalid request paramteres', + success: false, + }); +} + +export function responseNotImplemented(res: Response): void { + res.status(HttpStatusCodes.NOT_IMPLEMENTED).json({ + message: 'Not implemented', + success: false, + }); +} + +export function responseServerError(res: Response): void { + res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Internal server error', + success: false, + }); +} + +export function responseUnauthorized(res: Response): void { + res.status(HttpStatusCodes.UNAUTHORIZED).json({ + message: 'Unauthorized', + success: false, + }); +} diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts new file mode 100644 index 0000000..408ec74 --- /dev/null +++ b/src/helpers/responses/roadmapResponses.ts @@ -0,0 +1,33 @@ +import { Response } from 'express'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; +import JSONStringify from '@src/util/JSONStringify'; +import { ResRoadmap } from '@src/types/response/ResRoadmap'; + +export function responseUserNoRoadmaps(res: Response): void { + res + .status(HttpStatusCodes.OK) + .contentType('application/json') + .send( + JSONStringify({ + data: [], + message: 'User has no roadmaps', + success: true, + }), + ); +} + +export function responseUserRoadmaps( + res: Response, + roadmaps: ResRoadmap[], +): void { + res + .status(HttpStatusCodes.OK) + .contentType('application/json') + .send( + JSONStringify({ + data: roadmaps, + message: 'Roadmaps found', + success: true, + }), + ); +} diff --git a/src/helpers/responses/userResponses.ts b/src/helpers/responses/userResponses.ts new file mode 100644 index 0000000..d9b5515 --- /dev/null +++ b/src/helpers/responses/userResponses.ts @@ -0,0 +1,90 @@ +import { Response } from 'express'; +import { User } from '@src/types/models/User'; +import { UserInfo } from '@src/types/models/UserInfo'; +import { UserStats } from '@src/helpers/databaseManagement'; +import JSONStringify from '@src/util/JSONStringify'; +import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; +import { ResUserProfile } from '@src/types/response/ResUserProfile'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; + +/* + ! User responses + */ + +export function responseUserNotFound(res: Response): void { + res.status(HttpStatusCodes.NOT_FOUND).json({ + message: 'User couldn\'t be found', + success: false, + }); +} + +export function responseCantFollowYourself(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'You can\'t follow yourself', + success: false, + }); +} + +export function responseAlreadyFollowing(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Already following', + success: false, + }); +} + +export function responseNotFollowing(res: Response): void { + res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Not following', + success: false, + }); +} + +export function responseUserDeleted(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'Account successfully deleted', success: true }); +} + +export function responseUserProfile( + res: Response, + user: User, + userInfo: UserInfo, + userStats: UserStats, + isFollowing: boolean, +): void { + res + .status(HttpStatusCodes.OK) + .contentType('application/json') + .send( + JSONStringify({ + data: new ResUserProfile(user, userInfo, userStats, isFollowing), + message: 'User found', + success: true, + }), + ); +} + +export function responseUserMiniProfile(res: Response, user: User): void { + res + .status(HttpStatusCodes.OK) + .contentType('application/json') + .send( + JSONStringify({ + data: new ResUserMiniProfile(user), + message: 'User found', + success: true, + }), + ); +} + +export function responseUserFollowed(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'User followed', success: true }); +} + +export function responseUserUnfollowed(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'User unfollowed', success: true }); +} diff --git a/src/validators/validateBody.ts b/src/middleware/validators/validateBody.ts similarity index 100% rename from src/validators/validateBody.ts rename to src/middleware/validators/validateBody.ts diff --git a/src/validators/validateSession.ts b/src/middleware/validators/validateSession.ts similarity index 100% rename from src/validators/validateSession.ts rename to src/middleware/validators/validateSession.ts diff --git a/src/validators/validateUser.ts b/src/middleware/validators/validateUser.ts similarity index 91% rename from src/validators/validateUser.ts rename to src/middleware/validators/validateUser.ts index 00db5ea..bbd84e3 100644 --- a/src/validators/validateUser.ts +++ b/src/middleware/validators/validateUser.ts @@ -1,6 +1,8 @@ import { NextFunction, Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; -import { responseInvalidParameters } from '@src/helpers/apiResponses'; +import { + responseInvalidParameters, +} from '@src/helpers/responses/generalResponses'; export interface RequestWithTargetUserId extends RequestWithSession { targetUserId?: bigint; diff --git a/src/other/classes.ts b/src/other/classes.ts deleted file mode 100644 index 06153ac..0000000 --- a/src/other/classes.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Miscellaneous shared classes go here. - */ - -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; - -/** - * Error with status code and message - */ -export class RouteError extends Error { - public status: HttpStatusCodes; - - public constructor(status: HttpStatusCodes, message: string) { - super(message); - this.status = status; - } -} diff --git a/src/other/types.ts b/src/other/types.ts deleted file mode 100644 index fb78afd..0000000 --- a/src/other/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Immutable = { - readonly [K in keyof T]: Immutable; -}; diff --git a/src/routes/AuthRouter.ts b/src/routes/AuthRouter.ts index 55a6bbd..6b3331d 100644 --- a/src/routes/AuthRouter.ts +++ b/src/routes/AuthRouter.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; import { authChangePassword, authForgotPassword, @@ -12,7 +12,7 @@ import { authLogout, authRegister, } from '@src/controllers/authController'; -import validateBody from '@src/validators/validateBody'; +import validateBody from '@src/middleware/validators/validateBody'; import { rateLimit } from 'express-rate-limit'; import EnvVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 038882b..f3ed4c4 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -10,7 +10,7 @@ import * as console from 'console'; import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; import envVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const RoadmapsRouter = Router(); diff --git a/src/routes/UsersRouter.ts b/src/routes/UsersRouter.ts index 270939a..bde751e 100644 --- a/src/routes/UsersRouter.ts +++ b/src/routes/UsersRouter.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; import UsersGet from '@src/routes/usersRoutes/UsersGet'; import UsersUpdate from '@src/routes/usersRoutes/UsersUpdate'; import { usersDelete } from '@src/controllers/usersController'; diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts index b1c0045..77645f8 100644 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -7,7 +7,7 @@ import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/types/models/Roadmap'; import IssuesUpdate from '@src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate'; import Comments from '@src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const RoadmapIssues = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index a105324..3ddb203 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -5,7 +5,7 @@ import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/types/models/Roadmap'; import { User } from '@src/types/models/User'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const RoadmapsUpdate = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts index 1e66fa4..d65e582 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts @@ -7,7 +7,7 @@ import { Issue } from '@src/types/models/Issue'; import { User } from '@src/types/models/User'; import Database from '@src/util/DatabaseDriver'; import { IssueComment } from '@src/types/models/IssueComment'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const CommentsRouter = Router({ mergeParams: true }); diff --git a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts index de21c40..2ff3432 100644 --- a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts +++ b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts @@ -5,7 +5,7 @@ import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession } from '@src/middleware/session'; import { Issue } from '@src/types/models/Issue'; import { Roadmap } from '@src/types/models/Roadmap'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const IssuesUpdate = Router({ mergeParams: true }); diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 5fdf7e9..3807460 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import validateUser from '@src/validators/validateUser'; +import validateUser from '@src/middleware/validators/validateUser'; import { userFollow, userGetRoadmaps, diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index 2c34127..128bd4c 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -8,7 +8,7 @@ import { checkEmail } from '@src/util/EmailUtil'; import { comparePassword } from '@src/util/LoginUtil'; import { User } from '@src/types/models/User'; import { UserInfo } from '@src/types/models/UserInfo'; -import validateSession from '@src/validators/validateSession'; +import validateSession from '@src/middleware/validators/validateSession'; const UsersUpdate = Router({ mergeParams: true }); diff --git a/src/server.ts b/src/server.ts index 7fb7274..bcc9d2b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,7 +18,6 @@ import EnvVars from '@src/constants/EnvVars'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { NodeEnvs } from '@src/constants/misc'; -import { RouteError } from '@src/other/classes'; // **** Variables **** // @@ -71,12 +70,10 @@ app.use((_: Request, res: Response) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars app.use((err: Error, req: Request, res: Response, next: NextFunction) => { logger.err(err, true); - let status = HttpStatusCodes.INTERNAL_SERVER_ERROR; - if (err instanceof RouteError) { - status = err.status; - } - - res.status(status).json({ success: false, message: err.message }); + res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: err.message, + }); }); // ** Front-End Content ** // From 10e86e4519d14152d8aac6523cb02a1760ab15b3 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 5 Sep 2023 22:23:22 +0300 Subject: [PATCH 070/118] Fixing stupid stuff I left messed up --- src/constants/EnvVars.ts | 7 ++----- src/constants/Paths.ts | 8 ++------ src/routes/roadmapsRoutes/RoadmapIssues.ts | 5 ++++- src/types/response/ResUserMiniProfile.ts | 1 - 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index 41b502e..f304bb0 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -1,8 +1,9 @@ +/* eslint-disable node/no-process-env */ +/* eslint-disable no-process-env */ /** * Environments variables declared here. */ -/* eslint-disable node/no-process-env */ import { NodeEnvs } from '@src/constants/misc'; @@ -41,8 +42,6 @@ interface IEnvVars { } // Config environment variables -/* eslint-disable no-process-env */ -/* eslint-disable node/no-process-env */ const EnvVars = { NodeEnv: process.env.NODE_ENV ?? '', Port: process.env.PORT ?? 0, @@ -76,8 +75,6 @@ const EnvVars = { RedirectUri: process.env.GITHUB_REDIRECT_URI ?? '', }, } as Readonly; -/* eslint-enable no-process-env */ -/* eslint-enable node/no-process-env */ export default EnvVars; export { EnvVars }; diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 6f039f0..c552de2 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -2,8 +2,6 @@ * Express router paths go here. */ -import { Immutable } from '@src/other/types'; - const Paths = { Base: '/api', Auth: { @@ -108,9 +106,7 @@ const Paths = { }, Delete: '/:userId([0-9]+)?', }, -}; +} as const; // **** Export **** // - -export type TPaths = Immutable; -export default Paths as TPaths; +export default Paths; diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts index 77645f8..5ef404d 100644 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ b/src/routes/roadmapsRoutes/RoadmapIssues.ts @@ -113,7 +113,10 @@ RoadmapIssues.get(Paths.Roadmaps.Issues.Get, async (req, res) => { RoadmapIssues.get(Paths.Roadmaps.Issues.GetAll, async (req, res) => { // get issue id from params - const roadmapId = BigInt(req.params?.roadmapId || -1); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const roadmapId = BigInt(req?.params?.roadmapId || -1); const db = new Database(); diff --git a/src/types/response/ResUserMiniProfile.ts b/src/types/response/ResUserMiniProfile.ts index 8343d1e..d68c1f9 100644 --- a/src/types/response/ResUserMiniProfile.ts +++ b/src/types/response/ResUserMiniProfile.ts @@ -27,7 +27,6 @@ export class ResUserMiniProfile implements IResUserMiniProfile { 'id' in obj && 'avatar' in obj && 'name' in obj && - 'email' in obj && 'createdAt' in obj ); } From ae89f288fbc7a6ecce08460c9f6eb28707d25710 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 13:26:28 +0300 Subject: [PATCH 071/118] Refactor roadmap search functionality and improve database structure Moved the existing roadmap search logic from ExploreRouter to a new dedicated ExploreController to separate routing and business logic. Furthermore, the roadmap search now allows for more flexible search criteria, including topic and order of results. Adjustments were made in the ExploreDB and the respective middleware and validation is introduced for these search parameters. Also, updated the database schema to store additional roadmap properties, including topic and isFeatured, to accommodate the new search feature and future roadmap enhancements. Added 'topic' property to the 'roadmap' table in the setup.sql file and added a new enum 'RoadmapTopic' in the Roadmap model. These amendments ensure the application handles and stores the data correctly. Corresponding changes were made in the ResRoadmap response type to include the new properties. Lastly, made amendments in eslint.json for better code formatting and added methods in the existing DatabaseDriver for additional database operations. These changes offer enhanced roadmap search functionality and prepare the application for potential future improvements. --- .eslintrc.json | 3 +- src/controllers/exploreController.ts | 28 ++++ .../validators/validateSearchParameters.ts | 88 +++++++++++++ src/routes/ExploreRouter.ts | 69 +--------- src/sql/setup.sql | 22 ++-- src/types/models/Roadmap.ts | 32 +++++ src/types/response/ResRoadmap.ts | 26 ++-- src/util/DatabaseDriver.ts | 34 +++-- src/util/ExploreDB.ts | 124 ++++++++++++------ 9 files changed, 297 insertions(+), 129 deletions(-) create mode 100644 src/controllers/exploreController.ts create mode 100644 src/middleware/validators/validateSearchParameters.ts diff --git a/.eslintrc.json b/.eslintrc.json index b873e1f..1ab9647 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,7 +32,8 @@ "semi": 1, "indent": [ "warn", - 2 + 2, + { "SwitchCase": 1 } ], "quotes": [ "warn", diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts new file mode 100644 index 0000000..9ea7b18 --- /dev/null +++ b/src/controllers/exploreController.ts @@ -0,0 +1,28 @@ +import { + RequestWithSearchParameters, +} from '@src/middleware/validators/validateSearchParameters'; +import { Response } from 'express'; +import { ExploreDB, SearchRoadmap } from '@src/util/ExploreDB'; +import HttpStatusCodes from '@src/constants/HttpStatusCodes'; + +function responseSearchRoadmaps( + res: Response, + roadmaps: SearchRoadmap[], +): unknown { + return res.status(HttpStatusCodes.OK).json({ + success: true, + message: 'Roadmaps found', + data: roadmaps, + }); +} + +export async function searchRoadmaps( + req: RequestWithSearchParameters, + res: Response, +): Promise { + const db = new ExploreDB(); + + const roadmaps = await db.getRoadmaps(req, req.session?.userId); + + return responseSearchRoadmaps(res, roadmaps); +} diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts new file mode 100644 index 0000000..1dd496b --- /dev/null +++ b/src/middleware/validators/validateSearchParameters.ts @@ -0,0 +1,88 @@ +import { RequestWithSession } from '@src/middleware/session'; +import { NextFunction, Response } from 'express'; +import { RoadmapTopic } from '@src/types/models/Roadmap'; + +interface Order { + by: string; + direction?: 'ASC' | 'DESC'; +} + +export interface SearchParameters { + search?: string; + page?: number; + limit?: number; + topic?: RoadmapTopic[]; + order?: Order; +} + +export interface RequestWithSearchParameters + extends RequestWithSession, + SearchParameters {} + +export default function ( + req: RequestWithSearchParameters, + _: Response, + next: NextFunction, +) { + // get parameters from request + const { searchParam, pageParam, limitParam, topicParam, orderParam } = + req.query; + const search = (searchParam as string) || ''; + const page = parseInt((pageParam as string) || '1'); + const limit = parseInt((limitParam as string) || '12'); + let topic = + (topicParam as RoadmapTopic[]) || + ([ + RoadmapTopic.PROGRAMMING, + RoadmapTopic.MATH, + RoadmapTopic.DESIGN, + RoadmapTopic.OTHER, + ] as RoadmapTopic[]); + let order: Order; + + const [by, direction] = (orderParam as string).split(':'); + switch (by) { + case 'views': + order = { + by: 'views', + direction: 'DESC', + }; + break; + + case 'likes': + order = { + by: 'likes', + direction: 'DESC', + }; + break; + + case 'age': + default: + order = { + by: 'r.createdAt', + direction: 'DESC', + }; + break; + } + + if (direction.toLowerCase() === 'asc') { + order.direction = 'ASC'; + } + + topic = topic.filter((t) => { + return ( + t === RoadmapTopic.PROGRAMMING || + t === RoadmapTopic.MATH || + t === RoadmapTopic.DESIGN || + t === RoadmapTopic.OTHER + ); + }); + + req.search = search; + req.page = page; + req.limit = limit; + req.topic = topic; + req.order = order; + + next(); +} diff --git a/src/routes/ExploreRouter.ts b/src/routes/ExploreRouter.ts index e033986..1386cf4 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -1,74 +1,15 @@ import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { ExploreDB } from '@src/util/ExploreDB'; -import { Roadmap } from '@src/types/models/Roadmap'; -import Database from '@src/util/DatabaseDriver'; -import { RequestWithSession } from '@src/middleware/session'; -import { addView } from '@src/routes/roadmapsRoutes/RoadmapsGet'; +import validateSearchParameters + from '@src/middleware/validators/validateSearchParameters'; +import { searchRoadmaps } from '@src/controllers/exploreController'; const ExploreRouter = Router(); ExploreRouter.get( Paths.Explore.Default, - async (req: RequestWithSession, res) => { - // get query, count, and page from url - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,prefer-const - let { query, count, page } = req.query; - - // check if query is a string - if (typeof query !== 'string') { - query = ''; - } - - // check if count is a number - let countNum = parseInt(count as string) || 9; - - // max count is 100 and min count is 1 so clip count - countNum = Math.max(1, Math.min(countNum, 100)); - - // check if page is a number - const pageNum = parseInt(page as string) - 1 || 0; - - const userId = BigInt(req.session?.userId || -1); - - // get roadmaps from database - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const roadmaps = await ExploreDB.searchRoadmapsByLiked( - query, - userId, - countNum, - pageNum, - ); - - const db = new Database(); - - // get total roadmaps - const totalRoadmaps = await db.countWhereLike( - 'roadmaps', - 'name', - `%${query}%`, - ); - - // page count - const pageCount = Math.ceil(parseInt(totalRoadmaps.toString()) / countNum); - - // process roadmaps - roadmaps.forEach((roadmap) => { - addView(userId, BigInt(roadmap.id), false); - - // roadmap.id = roadmap.id.toString(); - // roadmap.likes = roadmap.likes.toString(); - // roadmap.ownerId = roadmap.ownerId.toString(); - // roadmap.isLiked = Boolean(roadmap.isLiked); - }); - // send roadmaps - res.status(200).json({ - success: true, - roadmaps, - pageCount, - }); - }, + validateSearchParameters, + searchRoadmaps, ); export default ExploreRouter; diff --git a/src/sql/setup.sql b/src/sql/setup.sql index a81cad7..0b46341 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -37,14 +37,16 @@ create table if not exists roadmaps ( id bigint auto_increment primary key, - name varchar(255) not null, - description varchar(255) not null, - userId bigint not null, - isPublic tinyint(1) default 1 not null, - isDraft tinyint(1) default 0 not null, - data longtext not null, - createdAt timestamp default current_timestamp() not null, - updatedAt timestamp default current_timestamp() not null, + name varchar(255) not null, + description varchar(255) not null, + topic enum ('programming', 'math', 'design', 'other') not null, + userId bigint not null, + isFeatured tinyint(1) default 0 not null, + isPublic tinyint(1) default 1 not null, + isDraft tinyint(1) default 0 not null, + data longtext not null, + createdAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() not null on update current_timestamp(), constraint roadmaps_userId_fk foreign key (userId) references users (id) on delete cascade @@ -60,7 +62,7 @@ create table if not exists issues title varchar(255) not null, content text null, createdAt timestamp default current_timestamp() null, - updatedAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() null on update current_timestamp(), constraint issues_roadmapId_fk foreign key (roadmapId) references roadmaps (id) on delete cascade, @@ -77,7 +79,7 @@ create table if not exists issueComments userId bigint not null, content text not null, createdAt timestamp default current_timestamp() not null, - updatedAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() not null on update current_timestamp(), constraint issueComments_issuesId_fk foreign key (issueId) references issues (id) on delete cascade, diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index eb01cd0..285c9d8 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -1,9 +1,18 @@ +export enum RoadmapTopic { + PROGRAMMING = 'programming', + MATH = 'math', + DESIGN = 'design', + OTHER = 'other', +} + // Interface for full Roadmap object export interface IRoadmap { readonly id: bigint; readonly name: string; readonly description: string; + readonly topic: RoadmapTopic; readonly userId: bigint; + readonly isFeatured: boolean; readonly isPublic: boolean; readonly isDraft: boolean; readonly data: string; @@ -16,7 +25,9 @@ interface IRoadmapConstructor { readonly id?: bigint; readonly name: string; readonly description: string; + readonly topic?: RoadmapTopic; readonly userId: bigint; + readonly isFeatured?: boolean; readonly isPublic?: boolean; readonly isDraft?: boolean; readonly data: string; @@ -29,7 +40,9 @@ interface IRoadmapModifications { readonly id?: bigint; readonly name?: string; readonly description?: string; + readonly topic?: RoadmapTopic; readonly userId?: bigint; + // isFeatured is not modifiable readonly isPublic?: boolean; readonly isDraft?: boolean; readonly data?: string; @@ -42,7 +55,9 @@ export class Roadmap implements IRoadmap { private _id: bigint; private _name: string; private _description: string; + private _topic: RoadmapTopic; private _userId: bigint; + private _isFeatured: boolean; private _isPublic: boolean; private _isDraft: boolean; private _data: string; @@ -53,7 +68,9 @@ export class Roadmap implements IRoadmap { id = 0n, name, description, + topic = RoadmapTopic.PROGRAMMING, userId, + isFeatured = false, isPublic = true, isDraft = false, data, @@ -63,7 +80,9 @@ export class Roadmap implements IRoadmap { this._id = id; this._name = name; this._description = description; + this._topic = topic; this._userId = userId; + this._isFeatured = isFeatured; this._isPublic = isPublic; this._isDraft = isDraft; this._data = data; @@ -76,6 +95,7 @@ export class Roadmap implements IRoadmap { id, name, description, + topic, userId, isPublic, isDraft, @@ -86,6 +106,7 @@ export class Roadmap implements IRoadmap { if (id !== undefined) this._id = id; if (name !== undefined) this._name = name; if (description !== undefined) this._description = description; + if (topic !== undefined) this._topic = topic; if (userId !== undefined) this._userId = userId; if (isPublic !== undefined) this._isPublic = isPublic; if (isDraft !== undefined) this._isDraft = isDraft; @@ -106,10 +127,18 @@ export class Roadmap implements IRoadmap { return this._description; } + public get topic(): RoadmapTopic { + return this._topic; + } + public get userId(): bigint { return this._userId; } + public get isFeatured(): boolean { + return this._isFeatured; + } + public get isPublic(): boolean { return this._isPublic; } @@ -137,6 +166,7 @@ export class Roadmap implements IRoadmap { obj !== null && 'name' in obj && 'description' in obj && + 'topic' in obj && 'userId' in obj && 'isPublic' in obj && 'data' in obj && @@ -151,7 +181,9 @@ export class Roadmap implements IRoadmap { id: this._id, name: this._name, description: this._description, + topic: this._topic, userId: this._userId, + isFeatured: this._isFeatured, isPublic: this._isPublic, isDraft: this._isDraft, data: this._data, diff --git a/src/types/response/ResRoadmap.ts b/src/types/response/ResRoadmap.ts index aedb5a4..0ee1c01 100644 --- a/src/types/response/ResRoadmap.ts +++ b/src/types/response/ResRoadmap.ts @@ -1,10 +1,11 @@ -import { IRoadmap } from '@src/types/models/Roadmap'; +import { IRoadmap, RoadmapTopic } from '@src/types/models/Roadmap'; import { IUser } from '@src/types/models/User'; export interface IResRoadmap { readonly id: bigint; readonly name: string; readonly description: string; + readonly topic: RoadmapTopic; readonly isPublic: boolean; readonly isDraft: boolean; readonly data: string; @@ -21,6 +22,8 @@ export class ResRoadmap implements IResRoadmap { public readonly id: bigint; public readonly name: string; public readonly description: string; + public readonly topic: RoadmapTopic; + public readonly isFeatured: boolean; public readonly isPublic: boolean; public readonly isDraft: boolean; public readonly data: string; @@ -33,29 +36,32 @@ export class ResRoadmap implements IResRoadmap { public constructor( { - id = 0n, + id, name, description, + topic, userId, - isPublic = true, - isDraft = false, + isFeatured, + isPublic, + isDraft, data, - createdAt = new Date(), - updatedAt = new Date(), + createdAt, + updatedAt, }: IRoadmap, - { avatar, name: userName }: IUser, + { avatar: userAvatar, name: userName }: IUser, ) { this.id = id; this.name = name; this.description = description; + this.topic = topic; + this.isFeatured = isFeatured; this.isPublic = isPublic; this.isDraft = isDraft; this.data = data; this.createdAt = createdAt; this.updatedAt = updatedAt; - this.userId = userId; - this.userAvatar = avatar; + this.userAvatar = userAvatar; this.userName = userName; } @@ -66,7 +72,9 @@ export class ResRoadmap implements IResRoadmap { 'id' in obj && 'name' in obj && 'description' in obj && + 'topic' in obj && 'userId' in obj && + 'isFeatured' in obj && 'isPublic' in obj && 'isDraft' in obj && 'data' in obj && diff --git a/src/util/DatabaseDriver.ts b/src/util/DatabaseDriver.ts index 76f23dc..04f10cd 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/DatabaseDriver.ts @@ -70,7 +70,7 @@ interface Data { type DataType = bigint | string | number | Date | null; // config interface -interface DatabaseConfig { +export interface DatabaseConfig { host: string; user: string; password: string; @@ -258,15 +258,33 @@ class Database { for (let i = 0; i < values.length - 1; i += 2) { const key = values[i] as string; - if (i > 0) keyString += ' AND '; - keyString += `${key} ${like ? 'LIKE' : '='} ?`; - - params = [...params, values[i + 1]]; + if (Array.isArray(values[i + 1])) { + const arrayParams = values[i + 1] as unknown[]; + if ((values[i + 1] as unknown[]).length === 0) continue; + const subKeyString = arrayParams + .map(() => `${key} ${like ? 'LIKE' : '='} ?`) + .join(' OR '); + keyString += i > 0 ? ' AND ' : ''; + keyString += `(${subKeyString})`; + params = [...params, ...arrayParams]; + } else { + if (i > 0) keyString += ' AND '; + keyString += `${key} ${like ? 'LIKE' : '='} ?`; + params = [...params, values[i + 1]]; + } } return { keyString, params }; }; + public async getQuery( + sql: string, + params?: unknown[], + ): Promise { + const result = await this._query(sql, params); + return parseResult(result); + } + public async get(table: string, id: bigint): Promise { // create sql query - select * from table where id = ? const sql = `SELECT * @@ -294,7 +312,7 @@ class Database { return this._getWhere(table, true, ...values); } - private async _getWhere( + protected async _getWhere( table: string, like: boolean, ...values: unknown[] @@ -337,7 +355,7 @@ class Database { return this._getAllWhere(table, true, ...values); } - private async _getAllWhere( + protected async _getAllWhere( table: string, like: boolean, ...values: unknown[] @@ -384,7 +402,7 @@ class Database { return await this._countWhere(table, true, ...values); } - private async _countWhere( + protected async _countWhere( table: string, like: boolean, ...values: unknown[] diff --git a/src/util/ExploreDB.ts b/src/util/ExploreDB.ts index e1e650d..12bc299 100644 --- a/src/util/ExploreDB.ts +++ b/src/util/ExploreDB.ts @@ -1,40 +1,90 @@ -import Database from '@src/util/DatabaseDriver'; - -class ExploreDB { - public static async searchRoadmapsByLiked( - query: string, - userId: bigint, - count = 9, - page = 0, - ) { - // add % to query - query = `%${query}%`; - const sql = ` - SELECT r.id as id, - r.name as name, - r.description as description, - COUNT(l.id) as likes, - u.name as ownerName, - r.ownerId as ownerId, - CASE - WHEN EXISTS - (SELECT * - FROM roadmapLikes - WHERE roadmapId = r.id AND userId = ${userId}) - THEN 1 - ELSE 0 END AS isLiked - FROM roadmaps r - LEFT JOIN roadmapLikes l ON r.id = l.roadmapId - LEFT JOIN users u ON r.ownerId = u.id - WHERE r.name LIKE ? - OR r.description LIKE ? - GROUP BY r.id - ORDER BY likes DESC, r.id DESC - LIMIT ? OFFSET ?`; - - const db = new Database(); - - return await db._query(sql, [ query, query, count, page * count ]) as T[]; +import Database, { DatabaseConfig } from '@src/util/DatabaseDriver'; +import EnvVars from '@src/constants/EnvVars'; +import { + SearchParameters, +} from '@src/middleware/validators/validateSearchParameters'; +import { RoadmapTopic } from '@src/types/models/Roadmap'; + +// database credentials +const { DBCred } = EnvVars; + +export interface SearchRoadmap{ + id: bigint; + name: string; + description: string; + topic: RoadmapTopic; + isFeatured: boolean; + isPublic: boolean; + isDraft: boolean; + userAvatar: string | null; + userName: string; + + likeCount: number; + viewCount: number; + + isLiked: number; +} + +class ExploreDB extends Database { + public constructor(config: DatabaseConfig = DBCred as DatabaseConfig) { + super(config); + } + + public async getRoadmaps({ + search, + page, + limit, + topic, + order, + }: SearchParameters, userid?: bigint): Promise { + if(!search || !page || !limit || !topic || !order) return []; + const query = ` + SELECT + r.id as id, + r.name AS name, + r.description AS description, + r.topic AS topic, + r.isFeatured AS isFeatured, + r.isPublic AS isPublic, + r.isDraft AS isDraft, + u.id AS userId, + u.avatar AS userAvatar, + u.name AS userName, + (SELECT COUNT(*) FROM roadmapLikes WHERE roadmapId = r.id) AS likeCount, + (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, + u.avatar AS userAvatar, + u.name AS userName, + ${!!userid ? `(SELECT COUNT(*) FROM roadmapLikes; + WHERE roadmapId = r.id + AND userId = ? + ) + ` : '0'} AS isLiked + FROM + roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE + r.name LIKE ? + AND r.topic IN (?) + AND r.isPublic = 1 + AND r.isDraft = 0 + ORDER BY + r.isFeatured DESC, ${order.by} ${order.direction} + LIMIT ?, ?; + `; + + const params = []; + + if (!!userid) { + params.push(userid); + } + params.push(`%${search}%`); + params.push(topic.map((t) => t.toString()).join(',')); + params.push((page - 1) * limit); + params.push(limit); + + const result = await this.getQuery(query, params); + if (result === null) return []; + return result as unknown as SearchRoadmap[]; } } From d975267d035cf32e0375dc82dab940c8c631e60f Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 15:20:18 +0300 Subject: [PATCH 072/118] Refactor User Update route and related controllers for clarity and efficiency User update functions in the `src/routes/usersRoutes/UsersUpdate.ts` file have been refactored to use helper functions for validating request body and session, as well as controllers that were previously bloating the file. User update functionality was previously dispersed over repetitive blocks of code for each update type (name, bio, picture, etc.). Now, these have been condensed into efficient, reusable controllers, removing nearly 400 lines of redundant, hard-to-follow code. Also, `src/controllers/exploreController.ts`, `src/helpers/responses/userResponses.ts`, `src/controllers/usersController.ts`, `src/middleware/validators/validateBody.ts` and `src/middleware/validators/validateUser.ts` files were updated accordingly. These changes improve overall clarity and maintainability of the code. The user update process is now easier to understand and modify for future adjustments or feature additions. --- src/controllers/exploreController.ts | 2 +- src/controllers/usersController.ts | 178 +++++++++ src/helpers/responses/userResponses.ts | 6 + src/middleware/validators/validateBody.ts | 2 +- src/middleware/validators/validateUser.ts | 2 +- src/routes/usersRoutes/UsersUpdate.ts | 427 ++-------------------- 6 files changed, 214 insertions(+), 403 deletions(-) diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 9ea7b18..4a1c6e8 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -11,7 +11,7 @@ function responseSearchRoadmaps( ): unknown { return res.status(HttpStatusCodes.OK).json({ success: true, - message: 'Roadmaps found', + message: `Roadmaps ${roadmaps.length ? '' : 'not '}found`, data: roadmaps, }); } diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 2c09666..e43ac77 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -10,6 +10,8 @@ import { getUserStats, isUserFollowing, unfollowUser, + updateUser, + updateUserInfo, } from '@src/helpers/databaseManagement'; import { RequestWithTargetUserId, @@ -19,6 +21,7 @@ import { responseServerError } from '@src/helpers/responses/generalResponses'; import { responseAlreadyFollowing, responseCantFollowYourself, + responseProfileUpdated, responseUserDeleted, responseUserFollowed, responseUserMiniProfile, @@ -194,3 +197,178 @@ export async function userUnfollow( /* ! UsersPost route controllers */ +export async function usersPostProfile( + req: RequestWithSession, + res: Response, +): Promise { + // get variables + const { name, githubUrl, websiteUrl, quote } = req.body as { + [key: string]: string; + }; + + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return responseServerError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + + // check if user exists + if (!user || !userInfo) return responseServerError(res); + + user.set({ + name, + }); + + userInfo.set({ + githubUrl, + websiteUrl, + quote, + }); + + // save user to database + if (await updateUser(db, userId, user, userInfo)) + return responseProfileUpdated(res); + + // send error json + return responseServerError(res); +} + +export async function usersPostProfileName( + req: RequestWithSession, + res: Response, +): Promise { + // get variables + const { name } = req.body as { [key: string]: string }; + + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return responseServerError(res); + + // get user from database + const user = await getUser(db, userId); + + // check if user exists + if (!user) return responseServerError(res); + + user.set({ + name, + }); + + // save user to database + if (await updateUser(db, userId, user)) return responseProfileUpdated(res); + + // send error json + return responseServerError(res); +} + +export async function usersPostProfileGithubUrl( + req: RequestWithSession, + res: Response, +): Promise { + // get variables + const { githubUrl } = req.body as { [key: string]: string }; + + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return responseServerError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + + // check if user exists + if (!user || !userInfo) return responseServerError(res); + + userInfo.set({ + githubUrl, + }); + + // save user to database + if (await updateUserInfo(db, userId, userInfo)) + return responseProfileUpdated(res); + + // send error json + return responseServerError(res); +} + +export async function usersPostProfileWebsiteUrl( + req: RequestWithSession, + res: Response, +): Promise { + // get variables + const { websiteUrl } = req.body as { [key: string]: string }; + + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return responseServerError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + + // check if user exists + if (!user || !userInfo) return responseServerError(res); + + userInfo.set({ + websiteUrl, + }); + + // save user to database + if (await updateUserInfo(db, userId, userInfo)) + return responseProfileUpdated(res); + + // send error json + return responseServerError(res); +} + +export async function usersPostProfileQuote( + req: RequestWithSession, + res: Response, +): Promise { + // get variables + const { quote } = req.body as { [key: string]: string }; + + // get database + const db = new DatabaseDriver(); + + // get userId from request + const userId = req.session?.userId; + + if (userId === undefined) return responseServerError(res); + + // get user from database + const user = await getUser(db, userId); + const userInfo = await getUserInfo(db, userId); + + // check if user exists + if (!user || !userInfo) return responseServerError(res); + + userInfo.set({ + quote, + }); + + // save user to database + if (await updateUserInfo(db, userId, userInfo)) + return responseProfileUpdated(res); + + // send error json + return responseServerError(res); +} diff --git a/src/helpers/responses/userResponses.ts b/src/helpers/responses/userResponses.ts index d9b5515..9495d06 100644 --- a/src/helpers/responses/userResponses.ts +++ b/src/helpers/responses/userResponses.ts @@ -88,3 +88,9 @@ export function responseUserUnfollowed(res: Response): void { .status(HttpStatusCodes.OK) .json({ message: 'User unfollowed', success: true }); } + +export function responseProfileUpdated(res: Response): void { + res + .status(HttpStatusCodes.OK) + .json({ message: 'Profile updated', success: true }); +} diff --git a/src/middleware/validators/validateBody.ts b/src/middleware/validators/validateBody.ts index db337e7..41d40d6 100644 --- a/src/middleware/validators/validateBody.ts +++ b/src/middleware/validators/validateBody.ts @@ -10,7 +10,7 @@ export interface RequestWithBody extends RequestWithSession { body: IBody; } -export default function ValidateBody( +export default function ( ...requiredFields: string[] ): (req: RequestWithBody, res: Response, next: NextFunction) => unknown { return (req: RequestWithBody, res: Response, next: NextFunction): unknown => { diff --git a/src/middleware/validators/validateUser.ts b/src/middleware/validators/validateUser.ts index bbd84e3..f86a55f 100644 --- a/src/middleware/validators/validateUser.ts +++ b/src/middleware/validators/validateUser.ts @@ -9,7 +9,7 @@ export interface RequestWithTargetUserId extends RequestWithSession { issuerUserId?: bigint; } -export default function validateUser( +export default function ( ownUserOnly = false, ): ( req: RequestWithTargetUserId, diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index 128bd4c..449239d 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -1,429 +1,56 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; import { RequestWithSession } from '@src/middleware/session'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import axios from 'axios'; -import DatabaseDriver from '@src/util/DatabaseDriver'; -import { checkEmail } from '@src/util/EmailUtil'; -import { comparePassword } from '@src/util/LoginUtil'; -import { User } from '@src/types/models/User'; -import { UserInfo } from '@src/types/models/UserInfo'; import validateSession from '@src/middleware/validators/validateSession'; +import { + responseNotImplemented, +} from '@src/helpers/responses/generalResponses'; +import validateBody from '@src/middleware/validators/validateBody'; +import { + usersPostProfile, + usersPostProfileGithubUrl, + usersPostProfileName, usersPostProfileQuote, usersPostProfileWebsiteUrl, +} from '@src/controllers/usersController'; const UsersUpdate = Router({ mergeParams: true }); -UsersUpdate.post('*', validateSession); -UsersUpdate.post( - Paths.Users.Update.ProfilePicture, - async (req: RequestWithSession, res) => { - // get userId from request - const userId = req.session?.userId, - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - url: string = req.body?.avatarURL; - - // send error json - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - // if no url is specified - if (url === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No profile picture URL specified' }); - - try { - // get url and check if it is valid with axios - // if valid, update user profile picture - const axiosReq = await axios.get(url); - - // if request is successful check data type - if (axiosReq.status === 200) { - // if data is type picture update user profile picture - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - if (!axiosReq.headers['content-type']?.startsWith('image/')) - throw new Error('Invalid image URL'); - } - } catch (e) { - // if request is not successful - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Invalid image URL' }); - } - - // get database - const db = new DatabaseDriver(); - - // get id of userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update profile picture' }); - - // update user profile picture - const success = await db.update('userInfo', userInfo.id, { - profilePictureUrl: url, - }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update profile picture' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -UsersUpdate.post( - Paths.Users.Update.Bio, - async (req: RequestWithSession, res) => { - // get userId from request - const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - let bio: string = req.body?.bio; - - // send error json - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No user specified' }); - - // if no bio is specified - if (bio.length > 255) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No bio specified' }); - - if (!bio) bio = ''; - - // get database - const db = new DatabaseDriver(); - - // get id of userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update bio' }); - - // update user bio - const success = await db.update('userInfo', userInfo.id, { bio }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update bio' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); +UsersUpdate.post('*', validateSession); // ! Global middleware for file UsersUpdate.post( - Paths.Users.Update.Quote, - async (req: RequestWithSession, res) => { - // get userId from request - const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - let quote: string = req.body?.quote; - - // send error json - if (userId === undefined) - return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); - - // check if quote was given - if (quote.length > 255) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Quote provided is too long' }); - - if (!quote) quote = ''; - - // get database - const db = new DatabaseDriver(); - - // get userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update quote' }); - - //update user quote - const success = await db.update('userInfo', userInfo.id, { quote }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update quote' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + Paths.Users.Update.Base, + validateBody('name', 'githubUrl', 'websiteUrl', 'quote'), + usersPostProfile, ); UsersUpdate.post( Paths.Users.Update.Name, - async (req: RequestWithSession, res) => { - // get userId from request - const userId = req.session?.userId, - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - name: string = req.body?.name; - - // send error json - if (userId === undefined) - return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); - - // check if quote was given - if (!name || name.length > 32) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No name valid provided' }); - - // get database - const db = new DatabaseDriver(); - - //update user name - const success = await db.update('users', userId, { name }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update username' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + validateBody('name'), + usersPostProfileName, ); UsersUpdate.post( - Paths.Users.Update.BlogUrl, - async (req: RequestWithSession, res) => { - // get userId from request - const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - let blogUrl: string = req.body?.blogUrl; - - // send error json - if (userId === undefined) - return res.status(HttpStatusCodes.BAD_REQUEST).json({ error: 'No user' }); - - // check if quote was given - if (blogUrl.length > 255) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No blog url valid provided' }); - - if (!blogUrl) blogUrl = ''; - - // get database - const db = new DatabaseDriver(); - - // get userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update blog URL' }); - - //update userDisplay blog url - const success = await db.update('userInfo', userInfo.id, { blogUrl }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update blog URL' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + Paths.Users.Update.GithubUrl, + validateBody('githubUrl'), + usersPostProfileGithubUrl, ); UsersUpdate.post( Paths.Users.Update.WebsiteUrl, - async (req: RequestWithSession, res) => { - const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - let websiteUrl: string = req.body?.websiteUrl; - - // send error json - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay' }); - - // check if quote was given - if (websiteUrl.length > 255) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No website url valid provided' }); - - if (!websiteUrl) websiteUrl = ''; - - // get database - const db = new DatabaseDriver(); - - // get userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update website URL' }); - - //update userDisplay website url - const success = await db.update('userInfo', userInfo.id, { websiteUrl }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update website URL' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + validateBody('websiteUrl'), + usersPostProfileWebsiteUrl, ); -UsersUpdate.post( - Paths.Users.Update.GithubUrl, - async (req: RequestWithSession, res) => { - const userId = req.session?.userId; - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - let githubUrl: string = req.body?.githubUrl; - - // send error json - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay' }); - // check if quote was given - if (githubUrl.length > 255) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No github url valid provided' }); - - if (!githubUrl) githubUrl = ''; - - // get database - const db = new DatabaseDriver(); - - // get userInfo - const userInfo = await db.getWhere('userInfo', 'userId', userId); - - // if userInfo does not exist - if (!userInfo) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update github URL' }); - - //update userDisplay github url - const success = await db.update('userInfo', userInfo.id, { githubUrl }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update github URL' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, +UsersUpdate.post( + Paths.Users.Update.Quote, + validateBody('quote'), + usersPostProfileQuote, ); UsersUpdate.post( - Paths.Users.Update.Email, - async (req: RequestWithSession, res) => { - const userId = req.session?.userId, - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - email: string = req.body?.email, - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - password: string = req.body?.password; - - // send error json - if (userId === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No userDisplay' }); - - // check if quote was given - if (!email || email.length > 255 || !checkEmail(email)) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No email valid provided' }); - - // check if password was given - if (!password) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'No password valid provided' }); - - // get database - const db = new DatabaseDriver(); - - // get userDisplay - const user = await db.get('users', userId); - - // check if userDisplay exists - if (!user || !user?.pwdHash) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'User does not exist' }); - - // check if password is correct - if (!comparePassword(password, user.pwdHash)) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Password is not correct' }); - - // check if email is already taken - const emailTaken = await db.getWhere('users', 'email', email); - - // check if email is already taken - if (emailTaken) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Email is already taken' }); - - //update userDisplay email - const success = await db.update('users', userId, { email }); - - // if update was not successful - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update email' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); + Paths.Users.Update.ProfilePicture, + (req: RequestWithSession, res) => { + return responseNotImplemented(res); }, ); From 691be5c49875a6454f4a9bce20be4da8491a6fac Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 16:56:34 +0300 Subject: [PATCH 073/118] Update ESLint workflow to continue on failure --- .github/workflows/eslintfix.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/eslintfix.yml b/.github/workflows/eslintfix.yml index 3d31a1d..abcbdc0 100644 --- a/.github/workflows/eslintfix.yml +++ b/.github/workflows/eslintfix.yml @@ -19,16 +19,20 @@ jobs: npm install --save-dev @microsoft/eslint-formatter-sarif@2.1.7 - name: Run ESLint with --fix - run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix || echo "ESLint fix failed" + run: npx eslint . --config .eslintrc.json --ext .js,.jsx,.ts,.tsx --fix + continue-on-error: true - name: Commit files run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git commit -a -m "Fix ESLint issues" || echo "No changes to commit" + git commit -a -m "Fix ESLint issues" + continue-on-error: true - name: Push changes uses: ad-m/github-push-action@29f05e01bb17e6f28228b47437e03a7b69e1f9ef with: branch: ${{ github.ref_name }} github_token: ${{ secrets.PAT }} + continue-on-error: true + From 96e6351f86ba56658796891c247ff7a869fc3aae Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 17:45:20 +0300 Subject: [PATCH 074/118] Refactor roadmapLikes endpoints, updated DB management Refactored several roadmap and user-related endpoints for efficiency and clarity. Removed unneeded API responses from apiResponses.ts and distributed to relevant response files. Redesigned URLs structure in Paths.ts to be more concise. Some endpoint handlers were moved from the router to a new dedicated controller - roadmapController.ts. Implemented like/dislike functionality for roadmaps and added relevant database management functions. Changes will improve code maintainability and readability. --- .github/workflows/eslintfix.yml | 3 +- src/constants/Paths.ts | 31 +--- src/controllers/roadmapController.ts | 108 ++++++++++++ src/helpers/apiResponses.ts | 197 ---------------------- src/helpers/databaseManagement.ts | 45 +++++ src/helpers/responses/roadmapResponses.ts | 28 +++ src/routes/RoadmapsRouter.ts | 130 +------------- 7 files changed, 197 insertions(+), 345 deletions(-) create mode 100644 src/controllers/roadmapController.ts delete mode 100644 src/helpers/apiResponses.ts diff --git a/.github/workflows/eslintfix.yml b/.github/workflows/eslintfix.yml index abcbdc0..2c19560 100644 --- a/.github/workflows/eslintfix.yml +++ b/.github/workflows/eslintfix.yml @@ -34,5 +34,4 @@ jobs: with: branch: ${{ github.ref_name }} github_token: ${{ secrets.PAT }} - continue-on-error: true - + continue-on-error: true \ No newline at end of file diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index c552de2..64b1bab 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -9,7 +9,6 @@ const Paths = { Login: '/login', Register: '/register', ChangePassword: '/change-password', - ForgotPassword: '/forgot-password', GoogleLogin: '/google-login', GoogleCallback: '/google-callback', GithubLogin: '/github-login', @@ -18,13 +17,8 @@ const Paths = { }, Explore: { Base: '/explore', - Default: '/', - New: '/new', - Popular: '/popular', - Trending: '/trending', Search: { Base: '/search', - Users: '/users', Roadmaps: '/roadmaps', }, }, @@ -35,26 +29,22 @@ const Paths = { Base: '/:roadmapId([0-9]+)?', Roadmap: '/', MiniRoadmap: '/mini', - Tags: '/tags', Owner: '/owner', OwnerMini: '/owner/mini', }, Update: { Base: '/:roadmapId([0-9]+)', - Title: '/title', + All: '/', + Name: '/title', Description: '/description', - Tags: '/tags', + Topic: '/topic', Visibility: '/visibility', - Owner: '/owner', + Draft: '/draft', Data: '/data', }, Delete: '/:roadmapId([0-9]+)', Like: '/:roadmapId([0-9]+)/like', - Progress: { - Base: '/:roadmapId/progress', - Get: '/:userId?', - Update: '/', - }, + Dislike: '/:roadmapId([0-9]+)/dislike', Issues: { Base: '/:roadmapId([0-9]+)/issues', Create: '/create', @@ -62,6 +52,7 @@ const Paths = { GetAll: '/', Update: { Base: '/:issueId([0-9]+)', + All: '/', Title: '/title', Content: '/content', Status: '/status', @@ -75,14 +66,6 @@ const Paths = { Delete: '/:commentId', }, }, - - TabsInfo: { - Base: '/:roadmapId([0-9]+)/tabsInfo', - Create: '/create', - Get: '/:tabInfoId?', - Update: '/:tabInfoId?', - Delete: '/:tabInfoId', - }, }, Users: { Base: '/users', @@ -99,10 +82,8 @@ const Paths = { Bio: '/bio', Quote: '/quote', Name: '/name', - BlogUrl: '/blog-url', WebsiteUrl: '/website-url', GithubUrl: '/github-url', - Email: '/email', }, Delete: '/:userId([0-9]+)?', }, diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts new file mode 100644 index 0000000..ed0aea1 --- /dev/null +++ b/src/controllers/roadmapController.ts @@ -0,0 +1,108 @@ +import { RequestWithSession } from '@src/middleware/session'; +import { Response } from 'express'; +import { responseServerError } from '@src/helpers/responses/generalResponses'; +import Database from '@src/util/DatabaseDriver'; +import { RoadmapLike } from '@src/types/models/RoadmapLike'; +import { + responseRoadmapAlreadyLiked, + responseRoadmapRated, +} from '@src/helpers/responses/roadmapResponses'; +import { + getRoadmapLike, + insertRoadmapLike, + updateRoadmapLike, +} from '@src/helpers/databaseManagement'; + +export async function likeRoadmap(req: RequestWithSession, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!userId) return responseServerError(res); + if (!roadmapId) return responseServerError(res); + + const db = new Database(); + + const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + + if (!liked) { + if ( + (await insertRoadmapLike( + db, + new RoadmapLike({ + userId, + roadmapId: BigInt(roadmapId), + value: 1, + }), + )) !== -1n + ) + return responseRoadmapRated(res); + } + + if (!liked) return responseServerError(res); + if (liked.value == 1) return responseRoadmapAlreadyLiked(res); + + liked.set({ value: 1 }); + + if (await updateRoadmapLike(db, liked.id, liked)) + return responseRoadmapRated(res); + + return responseServerError(res); +} + +export async function dislikeRoadmap(req: RequestWithSession, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!userId) return responseServerError(res); + if (!roadmapId) return responseServerError(res); + + const db = new Database(); + + const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + + if (!liked) { + if ( + (await insertRoadmapLike( + db, + new RoadmapLike({ + userId, + roadmapId: BigInt(roadmapId), + value: -1, + }), + )) !== -1n + ) + return responseRoadmapRated(res); + } + + if (!liked) return responseServerError(res); + if (liked.value == -1) return responseRoadmapAlreadyLiked(res); + + liked.set({ value: -1 }); + + if (await updateRoadmapLike(db, liked.id, liked)) + return responseRoadmapRated(res); + + return responseServerError(res); +} + +export async function removeLikeRoadmap( + req: RequestWithSession, + res: Response, +) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!userId) return responseServerError(res); + if (!roadmapId) return responseServerError(res); + + const db = new Database(); + + const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + + if (!liked) return responseServerError(res); + + if (await db.delete('roadmapLikes', liked.id)) + return responseRoadmapRated(res); + + return responseServerError(res); +} diff --git a/src/helpers/apiResponses.ts b/src/helpers/apiResponses.ts deleted file mode 100644 index 4b51bd1..0000000 --- a/src/helpers/apiResponses.ts +++ /dev/null @@ -1,197 +0,0 @@ -// import { Response } from 'express'; -// import { HttpStatusCode } from 'axios'; -// import { User } from '@src/types/models/User'; -// import { UserInfo } from '@src/types/models/UserInfo'; -// import { UserStats } from '@src/helpers/databaseManagement'; -// import JSONStringify from '@src/util/JSONStringify'; -// import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; -// import { ResUserProfile } from '@src/types/response/ResUserProfile'; -// import { ResRoadmap } from '@src/types/response/ResRoadmap'; -// -// /* -// ! Failure responses -// */ -// -// export function responseEmailConflict(res: Response): void { -// res.status(HttpStatusCode.Conflict).json({ -// message: 'Email already in use', -// success: false, -// }); -// } -// -// export function responseExternalBadGateway(res: Response): void { -// res.status(HttpStatusCode.BadGateway).json({ -// message: 'Remote resource error', -// success: false, -// }); -// } -// -// export function responseInvalidBody(res: Response): void { -// res.status(HttpStatusCode.BadRequest).json({ -// message: 'Invalid request body', -// success: false, -// }); -// } -// -// -// export function responseInvalidParameters(res: Response): void { -// res.status(HttpStatusCode.BadRequest).json({ -// message: 'Invalid request paramteres', -// success: false, -// }); -// } -// -// export function responseNotImplemented(res: Response): void { -// res.status(HttpStatusCode.NotImplemented).json({ -// message: 'Not implemented', -// success: false, -// }); -// } -// -// export function responseServerError(res: Response): void { -// res.status(HttpStatusCode.InternalServerError).json({ -// message: 'Internal server error', -// success: false, -// }); -// } -// -// export function responseUserNotFound(res: Response): void { -// res.status(HttpStatusCode.NotFound).json({ -// message: 'User couldn\'t be found', -// success: false, -// }); -// } -// -// export function responseUnauthorized(res: Response): void { -// res.status(HttpStatusCode.Unauthorized).json({ -// message: 'Unauthorized', -// success: false, -// }); -// } -// -// export function responseCantFollowYourself(res: Response): void { -// res.status(HttpStatusCode.BadRequest).json({ -// message: 'You can\'t follow yourself', -// success: false, -// }); -// } -// -// export function responseAlreadyFollowing(res: Response): void { -// res.status(HttpStatusCode.BadRequest).json({ -// message: 'Already following', -// success: false, -// }); -// } -// -// export function responseNotFollowing(res: Response): void { -// res.status(HttpStatusCode.BadRequest).json({ -// message: 'Not following', -// success: false, -// }); -// } -// -// /* -// ? Success responses -// */ -// -// // ! Authentication Responses -// -// export function responseAccountCreated(res: Response): void { -// res -// .status(HttpStatusCode.Created) -// .json({ message: 'Registration successful', success: true }); -// } -// -// -// export function responseLogoutSuccessful(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .json({ message: 'Logout successful', success: true }); -// } -// -// export function responsePasswordChanged(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .json({ message: 'Password changed successfully', success: true }); -// } -// -// // ! User Responses -// -// export function responseUserDeleted(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .json({ message: 'Account successfully deleted', success: true }); -// } -// -// export function responseUserProfile( -// res: Response, -// user: User, -// userInfo: UserInfo, -// userStats: UserStats, -// isFollowing: boolean, -// ): void { -// res -// .status(HttpStatusCode.Ok) -// .contentType('application/json') -// .send( -// JSONStringify({ -// data: new ResUserProfile(user, userInfo, userStats, isFollowing), -// message: 'User found', -// success: true, -// }), -// ); -// } -// -// export function responseUserMiniProfile(res: Response, user: User): void { -// res -// .status(HttpStatusCode.Ok) -// .contentType('application/json') -// .send( -// JSONStringify({ -// data: new ResUserMiniProfile(user), -// message: 'User found', -// success: true, -// }), -// ); -// } -// -// export function responseUserNoRoadmaps(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .contentType('application/json') -// .send( -// JSONStringify({ -// data: [], -// message: 'User has no roadmaps', -// success: true, -// }), -// ); -// } -// -// export function responseUserRoadmaps( -// res: Response, -// roadmaps: ResRoadmap[], -// ): void { -// res -// .status(HttpStatusCode.Ok) -// .contentType('application/json') -// .send( -// JSONStringify({ -// data: roadmaps, -// message: 'Roadmaps found', -// success: true, -// }), -// ); -// } -// -// export function responseUserFollowed(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .json({ message: 'User followed', success: true }); -// } -// -// export function responseUserUnfollowed(res: Response): void { -// res -// .status(HttpStatusCode.Ok) -// .json({ message: 'User unfollowed', success: true }); -// } diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 3f4a359..94cd473 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -3,6 +3,7 @@ import { IUserInfo, UserInfo } from '@src/types/models/UserInfo'; import { IUser, User } from '@src/types/models/User'; import { Roadmap } from '@src/types/models/Roadmap'; import { Follower } from '@src/types/models/Follower'; +import { IRoadmapLike, RoadmapLike } from '@src/types/models/RoadmapLike'; /* * Interfaces @@ -200,3 +201,47 @@ export async function updateUserInfo( ): Promise { return await db.update('userInfo', userId, userInfo); } + +export async function getRoadmapLike( + db: DatabaseDriver, + userId: bigint, + roadmapId: bigint, +): Promise { + const like = await db.getWhere( + 'roadmapLikes', + 'userId', + userId, + 'roadmapId', + roadmapId, + ); + if (!like) return null; + return new RoadmapLike(like); +} + +export async function insertRoadmapLike( + db: DatabaseDriver, + data: IRoadmapLike, +) { + return await db.insert('roadmapLikes', data); +} + +export async function updateRoadmapLike( + db: DatabaseDriver, + id: bigint, + data: IRoadmapLike, +): Promise { + return await db.update('roadmapLikes', id, data); +} + +export async function deleteRoadmapLike( + db: DatabaseDriver, + data: IRoadmapLike, +): Promise { + return await db.deleteWhere( + 'roadmapLikes', + 'userId', + data.userId, + 'roadmapId', + data.roadmapId, + ); +} diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index 408ec74..bd6e226 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -31,3 +31,31 @@ export function responseUserRoadmaps( }), ); } + +export function responseRoadmapAlreadyLiked(res: Response) { + return res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Already liked', + success: false, + }); +} + +export function responseRoadmapAlreadyDisliked(res: Response) { + return res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Already disliked', + success: false, + }); +} + +export function responseRoadmapNotRated(res: Response) { + return res.status(HttpStatusCodes.BAD_REQUEST).json({ + message: 'Not rated', + success: false, + }); +} + +export function responseRoadmapRated(res: Response) { + return res.status(HttpStatusCodes.OK).json({ + message: 'Roadmap rated', + success: true, + }); +} diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index f3ed4c4..505e371 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -11,6 +11,10 @@ import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; import envVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; import validateSession from '@src/middleware/validators/validateSession'; +import { + dislikeRoadmap, + likeRoadmap, removeLikeRoadmap, +} from '@src/controllers/roadmapController'; const RoadmapsRouter = Router(); @@ -127,128 +131,12 @@ RoadmapsRouter.use(Paths.Roadmaps.Issues.Base, RoadmapIssues); ! like roadmaps */ RoadmapsRouter.all(Paths.Roadmaps.Like, validateSession); +RoadmapsRouter.all(Paths.Roadmaps.Dislike, validateSession); -RoadmapsRouter.get( - Paths.Roadmaps.Like, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const id = BigInt(req.params.roadmapId || -1); +RoadmapsRouter.post(Paths.Roadmaps.Like, likeRoadmap); +RoadmapsRouter.post(Paths.Roadmaps.Dislike, dislikeRoadmap); - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - - // check if session exists - if (!session) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Session is missing from user.' }); - - // get database connection - const db = new Database(); - - // check if roadmap exists - const roadmap = await db.get('roadmaps', id); - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - - // check if user has already liked the roadmap - const liked = await db.getAllWhere<{ roadmapId: bigint; userId: bigint }>( - 'roadmapLikes', - 'userId', - session.userId.toString(), - ); - - // check if likes exist - if (!liked) return res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR); - - if (liked.some((like) => like.roadmapId === id)) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User has already liked the roadmap.' }); - - // like roadmap - const success = await db.insert('roadmapLikes', { - roadmapId: id, - userId: session.userId, - }); - - // check if id is valid - if (success < 0) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be liked.' }); - - // return - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -RoadmapsRouter.delete( - Paths.Roadmaps.Like, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const id = BigInt(req.params.roadmapId || -1); - - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - - // check if session exists - if (!session) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Session is missing from user.' }); - - // get database connection - const db = new Database(); - - // check if roadmap exists - const roadmap = await db.get('roadmaps', id); - if (!roadmap) return res.status(HttpStatusCodes.NOT_FOUND); - - // check if user has already liked the roadmap - const liked = await db.getAllWhere<{ - id: bigint; - roadmapId: bigint; - userId: bigint; - }>('roadmapLikes', 'userId', session.userId.toString()); - - // check if likes exist - if (!liked) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap likes could not be retrieved from database.' }); - - // find id of roadmap liked - const likedRoadmap = liked.find((like) => like.roadmapId === id); - - // check if user has liked the roadmap - if (!likedRoadmap) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User has not liked the roadmap.' }); - - // delete roadmap like - const success = await db.delete('roadmapLikes', likedRoadmap.id); - - // check if id is valid - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap like could not be deleted from database.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); +RoadmapsRouter.delete(Paths.Roadmaps.Like, removeLikeRoadmap); +RoadmapsRouter.delete(Paths.Roadmaps.Dislike, removeLikeRoadmap); export default RoadmapsRouter; From 27a367ee92919519988180acff1fe3633e3495c1 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 17:46:59 +0300 Subject: [PATCH 075/118] Update server response for non-liked roadmap Changed the server response error when a non-liked roadmap is retrieved from 'serverError' to 'roadmapNotRated'. This change improves the accuracy of the response error, helping with debug and user experience. --- src/controllers/roadmapController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index ed0aea1..aab500a 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -4,7 +4,7 @@ import { responseServerError } from '@src/helpers/responses/generalResponses'; import Database from '@src/util/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { - responseRoadmapAlreadyLiked, + responseRoadmapAlreadyLiked, responseRoadmapNotRated, responseRoadmapRated, } from '@src/helpers/responses/roadmapResponses'; import { @@ -99,7 +99,7 @@ export async function removeLikeRoadmap( const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); - if (!liked) return responseServerError(res); + if (!liked) return responseRoadmapNotRated(res); if (await db.delete('roadmapLikes', liked.id)) return responseRoadmapRated(res); From 5f4f1d51b7c575fddd40ede47658188ebfa43688 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 18:45:50 +0300 Subject: [PATCH 076/118] Small fix for something I broke --- src/constants/Paths.ts | 1 + src/routes/ExploreRouter.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 85 ++++++++++----------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 64b1bab..46394d7 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -9,6 +9,7 @@ const Paths = { Login: '/login', Register: '/register', ChangePassword: '/change-password', + ForgotPassword: '/forgot-password', GoogleLogin: '/google-login', GoogleCallback: '/google-callback', GithubLogin: '/github-login', diff --git a/src/routes/ExploreRouter.ts b/src/routes/ExploreRouter.ts index 1386cf4..0e6d9b1 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -7,7 +7,7 @@ import { searchRoadmaps } from '@src/controllers/exploreController'; const ExploreRouter = Router(); ExploreRouter.get( - Paths.Explore.Default, + Paths.Explore.Search.Base + Paths.Explore.Search.Roadmaps, validateSearchParameters, searchRoadmaps, ); diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index 3ddb203..aecde05 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -4,7 +4,6 @@ import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import Database from '@src/util/DatabaseDriver'; import { Roadmap } from '@src/types/models/Roadmap'; -import { User } from '@src/types/models/User'; import validateSession from '@src/middleware/validators/validateSession'; const RoadmapsUpdate = Router({ mergeParams: true }); @@ -58,7 +57,7 @@ async function isRoadmapValid( RoadmapsUpdate.post('*', validateSession); RoadmapsUpdate.post( - Paths.Roadmaps.Update.Title, + Paths.Roadmaps.Update.Name, async (req: RequestWithSession, res) => { // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment @@ -171,47 +170,47 @@ RoadmapsUpdate.post( }, ); -RoadmapsUpdate.post( - Paths.Roadmaps.Update.Owner, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument - const newOwnerId = BigInt(req?.body?.newOwnerId || -1); - if (newOwnerId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap new owner id is missing.' }); - - // check if the roadmap is valid - const data = await isRoadmapValid(req, res); - if (!data) return; - const { roadmap } = data; - - // get database connection - const db = new Database(); - - // check if the new owner exists - const newOwner = await db.get('users', BigInt(newOwnerId)); - if (!newOwner) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'New owner does not exist.' }); - - // update roadmap - roadmap.set({ - userId: newOwnerId, - updatedAt: new Date(), - }); - const success = await db.update('roadmaps', roadmap.id, roadmap); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); - - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); +// RoadmapsUpdate.post( +// Paths.Roadmaps.Update.Owner, +// async (req: RequestWithSession, res) => { +// eslint-disable-next-line max-len +// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument +// const newOwnerId = BigInt(req?.body?.newOwnerId || -1); +// if (newOwnerId < 0) +// return res +// .status(HttpStatusCodes.BAD_REQUEST) +// .json({ error: 'Roadmap new owner id is missing.' }); +// +// // check if the roadmap is valid +// const data = await isRoadmapValid(req, res); +// if (!data) return; +// const { roadmap } = data; +// +// // get database connection +// const db = new Database(); +// +// // check if the new owner exists +// const newOwner = await db.get('users', BigInt(newOwnerId)); +// if (!newOwner) +// return res +// .status(HttpStatusCodes.NOT_FOUND) +// .json({ error: 'New owner does not exist.' }); +// +// // update roadmap +// roadmap.set({ +// userId: newOwnerId, +// updatedAt: new Date(), +// }); +// const success = await db.update('roadmaps', roadmap.id, roadmap); +// +// if (!success) +// return res +// .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) +// .json({ error: 'Roadmap could not be updated.' }); +// +// return res.status(HttpStatusCodes.OK).json({ success: true }); +// }, +// ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Data, From 112ca49fd80732876b9d34854cb65873bb327ade Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 19:30:24 +0300 Subject: [PATCH 077/118] Add create and delete functionalities for roadmap Several new functions are added to support creating and deleting roadmaps. The functions are added in roadmapResponses, roadmapController, RoadmapsRouter files for handling HTTP responses better and ensuring system consistency. The aim is to enhance user interaction with the application by facilitating the creation and deletion of roadmaps. --- src/controllers/roadmapController.ts | 69 ++++++++++++- src/helpers/databaseManagement.ts | 31 ++++++ src/helpers/responses/roadmapResponses.ts | 29 ++++++ src/routes/RoadmapsRouter.ts | 119 ++-------------------- 4 files changed, 137 insertions(+), 111 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index aab500a..c784813 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -4,14 +4,79 @@ import { responseServerError } from '@src/helpers/responses/generalResponses'; import Database from '@src/util/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { - responseRoadmapAlreadyLiked, responseRoadmapNotRated, + responseNotAllowed, + responseRoadmapAlreadyLiked, + responseRoadmapCreated, + responseRoadmapDeleted, + responseRoadmapNotFound, + responseRoadmapNotRated, responseRoadmapRated, } from '@src/helpers/responses/roadmapResponses'; import { - getRoadmapLike, + getRoadmap, + getRoadmapLike, insertRoadmap, insertRoadmapLike, updateRoadmapLike, } from '@src/helpers/databaseManagement'; +import { RequestWithBody } from '@src/middleware/validators/validateBody'; +import { Roadmap, RoadmapTopic } from '@src/types/models/Roadmap'; + +export async function createRoadmap(req: RequestWithBody, res: Response) { + // guaranteed to exist by middleware + const { name, description, data } = req.body; + + // non guaranteed to exist by middleware of type Roadmap + let { topic, isPublic, isDraft } = req.body; + + const userId = req.session?.userId; + + if (!userId || !name || !description || !data) + return responseServerError(res); + + const db = new Database(); + + if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) + topic = undefined; + + if (isPublic !== true && isPublic !== false) isPublic = true; + if (isDraft !== true && isDraft !== false) isDraft = false; + + const roadmap = new Roadmap({ + name: name as string, + description: description as string, + topic: topic as RoadmapTopic | undefined, + userId, + isPublic: isPublic as boolean, + isDraft: isDraft as boolean , + data: data as string, + }); + + const id = await insertRoadmap(db, roadmap); + + if (id !== -1n) return responseRoadmapCreated(res, id); + + return responseServerError(res); +} + +export async function deleteRoadmap(req: RequestWithSession, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!userId) return responseServerError(res); + if (!roadmapId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmap(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + if (await db.delete('roadmaps', BigInt(roadmapId))) + return responseRoadmapDeleted(res); + + return responseServerError(res); +} export async function likeRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 94cd473..7c0e747 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -202,6 +202,37 @@ export async function updateUserInfo( return await db.update('userInfo', userId, userInfo); } +export async function getRoadmap( + db: DatabaseDriver, + roadmapId: bigint, +): Promise { + const roadmap = await db.get('roadmaps', roadmapId); + if (!roadmap) return null; + return new Roadmap(roadmap); +} + +export async function insertRoadmap( + db: DatabaseDriver, + roadmap: Roadmap, +): Promise { + return await db.insert('roadmaps', roadmap); +} + +export async function updateRoadmap( + db: DatabaseDriver, + roadmapId: bigint, + roadmap: Roadmap, +): Promise { + return await db.update('roadmaps', roadmapId, roadmap); +} + +export async function deleteRoadmap( + db: DatabaseDriver, + roadmapId: bigint, +): Promise { + return await db.delete('roadmaps', roadmapId); +} + export async function getRoadmapLike( db: DatabaseDriver, userId: bigint, diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index bd6e226..967815e 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -3,6 +3,35 @@ import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import JSONStringify from '@src/util/JSONStringify'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; +export function responseRoadmapNotFound(res: Response): void { + res.status(HttpStatusCodes.NOT_FOUND).json({ + message: 'Roadmap not found', + success: false, + }); +} + +export function responseNotAllowed(res: Response): void { + res.status(HttpStatusCodes.METHOD_NOT_ALLOWED).json({ + message: 'Not allowed to perform this action', + success: false, + }); +} + +export function responseRoadmapCreated(res: Response, id: bigint): void { + res.status(HttpStatusCodes.CREATED).json({ + data: { id: id.toString() }, + message: 'Roadmap created', + success: true, + }); +} + +export function responseRoadmapDeleted(res: Response): void { + res.status(HttpStatusCodes.OK).json({ + message: 'Roadmap deleted', + success: true, + }); +} + export function responseUserNoRoadmaps(res: Response): void { res .status(HttpStatusCodes.OK) diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 505e371..a41d9a2 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -1,132 +1,33 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; -import { RequestWithSession } from '@src/middleware/session'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { IRoadmap, Roadmap } from '@src/types/models/Roadmap'; -import Database from '@src/util/DatabaseDriver'; import GetRouter from '@src/routes/roadmapsRoutes/RoadmapsGet'; import UpdateRouter from '@src/routes/roadmapsRoutes/RoadmapsUpdate'; -import * as console from 'console'; import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; -import envVars from '@src/constants/EnvVars'; -import { NodeEnvs } from '@src/constants/misc'; import validateSession from '@src/middleware/validators/validateSession'; import { + createRoadmap, + deleteRoadmap, dislikeRoadmap, - likeRoadmap, removeLikeRoadmap, + likeRoadmap, + removeLikeRoadmap, } from '@src/controllers/roadmapController'; +import validateBody from '@src/middleware/validators/validateBody'; const RoadmapsRouter = Router(); -RoadmapsRouter.post(Paths.Roadmaps.Create, validateSession); RoadmapsRouter.post( Paths.Roadmaps.Create, - async (req: RequestWithSession, res) => { - //get data from body and session - let roadmap; - const session = req.session; - - // check if the roadmap is valid - try { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const roadmapData = req.body?.roadmap as IRoadmap; - - roadmap = new Roadmap(roadmapData); - - roadmap.set({ - id: undefined, - userId: session?.userId || -1n, - name: roadmapData.name, - createdAt: new Date(), - updatedAt: new Date(), - }); - } catch (e) { - if (envVars.NodeEnv !== NodeEnvs.Test) console.log(e); - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap is not a valid roadmap object.' }); - } - - //check if session exists - if (!session) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Session is missing from user.' }); - - // get database connection - const db = new Database(); - - // save roadmap to database - const id = await db.insert('roadmaps', roadmap); - - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be saved to database.' }); - - // return id - return res.status(HttpStatusCodes.CREATED).json({ id: id.toString() }); - }, + validateSession, + validateBody('name', 'description', 'data'), + createRoadmap, ); RoadmapsRouter.use(Paths.Roadmaps.Get.Base, GetRouter); - RoadmapsRouter.use(Paths.Roadmaps.Update.Base, UpdateRouter); - -RoadmapsRouter.delete(Paths.Roadmaps.Delete, validateSession); -RoadmapsRouter.delete( - Paths.Roadmaps.Delete, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const id = BigInt(req.params.roadmapId || -1); - - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - - // check if session exists - if (!session) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Session is missing from user.' }); - - // get database connection - const db = new Database(); - - // check if roadmap exists - const roadmap = await db.get('roadmaps', id); - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - - // check if the user is owner - if (roadmap.userId !== session?.userId) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User is not the owner of the roadmap.' }); - - // delete roadmap from database - const success = await db.delete('roadmaps', id); - - // check if id is valid - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be deleted from database.' }); - - // return id - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - RoadmapsRouter.use(Paths.Roadmaps.Issues.Base, RoadmapIssues); +RoadmapsRouter.delete(Paths.Roadmaps.Delete, validateSession, deleteRoadmap); + /* ! like roadmaps */ From 092e55e90bc745cdf780a997201d8dcc4bf688a8 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 19:44:45 +0300 Subject: [PATCH 078/118] Remove issue-related logic from routes and models FEATURE SCRAPPED: removed issue-related logic from routing files and the Issue model. The removed code includes routing logic for managing, updating, and deleting issues and their comments, and the Issue model itself. --- src/routes/RoadmapsRouter.ts | 2 - src/routes/roadmapsRoutes/RoadmapIssues.ts | 215 ------------ .../issuesRoutes/CommentsRouter.ts | 324 ------------------ .../issuesRoutes/IssuesUpdate.ts | 270 --------------- src/sql/setup.sql | 51 --- src/types/models/Issue.ts | 147 -------- src/types/models/IssueComment.ts | 121 ------- 7 files changed, 1130 deletions(-) delete mode 100644 src/routes/roadmapsRoutes/RoadmapIssues.ts delete mode 100644 src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts delete mode 100644 src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts delete mode 100644 src/types/models/Issue.ts delete mode 100644 src/types/models/IssueComment.ts diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index a41d9a2..47d1305 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -2,7 +2,6 @@ import Paths from '@src/constants/Paths'; import { Router } from 'express'; import GetRouter from '@src/routes/roadmapsRoutes/RoadmapsGet'; import UpdateRouter from '@src/routes/roadmapsRoutes/RoadmapsUpdate'; -import RoadmapIssues from '@src/routes/roadmapsRoutes/RoadmapIssues'; import validateSession from '@src/middleware/validators/validateSession'; import { createRoadmap, @@ -24,7 +23,6 @@ RoadmapsRouter.post( RoadmapsRouter.use(Paths.Roadmaps.Get.Base, GetRouter); RoadmapsRouter.use(Paths.Roadmaps.Update.Base, UpdateRouter); -RoadmapsRouter.use(Paths.Roadmaps.Issues.Base, RoadmapIssues); RoadmapsRouter.delete(Paths.Roadmaps.Delete, validateSession, deleteRoadmap); diff --git a/src/routes/roadmapsRoutes/RoadmapIssues.ts b/src/routes/roadmapsRoutes/RoadmapIssues.ts deleted file mode 100644 index 5ef404d..0000000 --- a/src/routes/roadmapsRoutes/RoadmapIssues.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Router } from 'express'; -import Paths from '@src/constants/Paths'; -import { RequestWithSession } from '@src/middleware/session'; -import { Issue } from '@src/types/models/Issue'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; -import { Roadmap } from '@src/types/models/Roadmap'; -import IssuesUpdate from '@src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate'; -import Comments from '@src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter'; -import validateSession from '@src/middleware/validators/validateSession'; - -const RoadmapIssues = Router({ mergeParams: true }); - -RoadmapIssues.post(Paths.Roadmaps.Issues.Create, validateSession); -RoadmapIssues.post( - Paths.Roadmaps.Issues.Create, - async (req: RequestWithSession, res) => { - //get data from body and session - let issue: Issue; - const session = req.session; - - try { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - const issueData = req.body?.issue as Issue; - - if (!issueData || !Issue.isIssue(issueData)) { - throw new Error('Issue is missing.'); - } - - issue = new Issue(issueData); - - // set userId - issueData.set({ - userId: session?.userId, - id: -1n, - createdAt: new Date(), - updatedAt: new Date(), - }); - } catch (e) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue data is invalid.' }); - } - - // get database connection - const db = new Database(); - - // save issue to database - const id = await db.insert('issues', issue); - - // check if id is valid - if (id < 0) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return id - return res.status(HttpStatusCodes.CREATED).json({ id: id.toString() }); - }, -); - -RoadmapIssues.get(Paths.Roadmaps.Issues.Get, async (req, res) => { - // get issue id from params - const issueId = BigInt(req.params?.issueId || -1); - const roadmapId = BigInt(req.params?.roadmapId || -1); - - // get database connection - const db = new Database(); - - if (roadmapId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is invalid.' }); - - // get roadmap from database - const roadmap = await db.get('roadmaps', roadmapId); - - // check if roadmap exists - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap not found.' }); - - // check if issue id is valid - if (issueId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue id is invalid.' }); - // get issue from database - const issue = await db.get('issues', issueId); - - // check if issue exists - if (!issue) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue not found.' }); - - // return issue - return res.status(HttpStatusCodes.OK).json({ - issue: { - id: issue.id.toString(), - title: issue.title, - content: issue.content, - open: issue.open, - roadmapId: issue.roadmapId.toString(), - userId: issue.userId.toString(), - createdAt: issue.createdAt, - updatedAt: issue.updatedAt, - }, - }); -}); - -RoadmapIssues.get(Paths.Roadmaps.Issues.GetAll, async (req, res) => { - // get issue id from params - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const roadmapId = BigInt(req?.params?.roadmapId || -1); - - const db = new Database(); - - if (roadmapId < 0) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is invalid.' }); - - // get roadmap from database - const roadmap = await db.get('roadmaps', roadmapId); - - // check if roadmap exists - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap not found.' }); - - // check if issue id is valid - let issues = await db.getAllWhere('issues', 'roadmapId', roadmapId); - - if (!issues) issues = []; - - const result = issues - .filter((issue) => issue.roadmapId === roadmapId) - .map((issue) => { - return { - id: issue.id.toString(), - title: issue.title, - content: issue.content, - roadmapId: issue.roadmapId.toString(), - userId: issue.userId.toString(), - open: Boolean(issue.open), - createdAt: issue.createdAt, - updatedAt: issue.updatedAt, - }; - }); - - // return issues - return res.status(HttpStatusCodes.OK).json({ issues: result }); -}); - -RoadmapIssues.post(Paths.Roadmaps.Issues.Update.Base, validateSession); -RoadmapIssues.use(Paths.Roadmaps.Issues.Update.Base, IssuesUpdate); - -// Delete Issue -RoadmapIssues.delete(Paths.Roadmaps.Issues.Delete, validateSession); -RoadmapIssues.delete( - Paths.Roadmaps.Issues.Delete, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const id = BigInt(req.params.issueId); - - // get database connection - const db = new Database(); - - // get issue from the database - const issue = await db.get('issues', id); - - // check if issue exists - if (!issue) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue not found.' }); - - const roadmap = await db.get('roadmaps', issue.roadmapId); - - // check if roadmap exists - if (!roadmap) - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap not found.' }); - - // check if userDisplay is owner - if (issue.userId !== session?.userId && roadmap.userId !== session?.userId) - return res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User is not owner of issue or roadmap.' }); - - // delete issue from database - const success = await db.delete('issues', id); - - // check if issue was deleted - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be deleted.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -RoadmapIssues.use(Paths.Roadmaps.Issues.Comments.Base, Comments); - -export default RoadmapIssues; diff --git a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts b/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts deleted file mode 100644 index d65e582..0000000 --- a/src/routes/roadmapsRoutes/issuesRoutes/CommentsRouter.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Paths from '@src/constants/Paths'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { RequestWithSession } from '@src/middleware/session'; -import { Roadmap } from '@src/types/models/Roadmap'; -import { Issue } from '@src/types/models/Issue'; -import { User } from '@src/types/models/User'; -import Database from '@src/util/DatabaseDriver'; -import { IssueComment } from '@src/types/models/IssueComment'; -import validateSession from '@src/middleware/validators/validateSession'; - -const CommentsRouter = Router({ mergeParams: true }); - -async function checkParams( - req: Request, - res: Response, -): Promise< - | { - issue: Issue; - issueId: bigint; - roadmap: Roadmap; - roadmapId: bigint; - } - | undefined -> { - // get data from body and session - const roadmapId = BigInt(req.params.roadmapId || -1); - const issueId = BigInt(req.params.issueId || -1); - - // check if ids are valid - if (issueId < 0) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'IssueId is invalid.' }); - return; - } - - if (roadmapId < 0) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'RoadmapId is invalid.' }); - } - - // get database connection - const db = new Database(); - - // check if roadmap exists - const roadmap = await db.get('roadmaps', roadmapId); - - if (!roadmap) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - return; - } - - // check if issue exists - const issue = await db.get('issues', issueId); - - if (!issue) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue does not exist.' }); - return; - } - - // check if issue belongs to roadmap - if (issue.roadmapId !== roadmapId) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue does not belong to roadmap.' }); - return; - } - - return { issue, issueId, roadmap, roadmapId }; -} - -async function checkUser( - req: RequestWithSession, - res: Response, -): Promise< - | { - user: User; - userId: bigint; - } - | undefined -> { - // get user id from session - const userId = BigInt(req.session?.userId || -1); - - // get database connection - const db = new Database(); - - // get user - const user = await db.get('users', userId); - - if (!user) { - res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Token user can\'t be found.' }); - return; - } - - return { user, userId }; -} - -CommentsRouter.post(Paths.Roadmaps.Issues.Comments.Create, validateSession); -CommentsRouter.post( - Paths.Roadmaps.Issues.Comments.Create, - async (req: RequestWithSession, res) => { - // get comment data from body - const { content } = req.body as { content: string }; - - const args = await checkParams(req, res); - const userArgs = await checkUser(req, res); - if (!args || !userArgs) return; - const { issueId, roadmap } = args; - const { userId } = userArgs; - - // if comment is empty - if (!content) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Comment can\'t be empty.' }); - - // check if user is allowed to create a comment - if (!roadmap.isPublic && roadmap.userId !== userId) { - res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'Only the owner can create comments.' }); - return; - } - - // get database connection - const db = new Database(); - - // create comment - const commentId = await db.insert( - 'issueComments', - new IssueComment({ content, issueId, userId }), - ); - - if (commentId < 0) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to create comment.' }); - - // send success json - return res.status(HttpStatusCodes.CREATED).json({ success: true }); - }, -); - -CommentsRouter.get(Paths.Roadmaps.Issues.Comments.Get, async (req, res) => { - const args = await checkParams(req, res); - if (!args) return; - const { issueId } = args; - - // get database connection - const db = new Database(); - - // get comments - const comments = await db.getAllWhere( - 'issueComments', - 'issueId', - issueId, - ); - - const commentsData = comments?.map((comment) => ({ - id: comment.id.toString(), - content: comment.content, - issueId: comment.issueId.toString(), - userId: comment.userId.toString(), - createdAt: comment.createdAt, - updatedAt: comment.updatedAt, - })); - - // send success json - return res.status(HttpStatusCodes.OK).json({ - success: true, - comments: commentsData, - }); -}); - -CommentsRouter.use(Paths.Roadmaps.Issues.Comments.Update, validateSession); -CommentsRouter.post( - Paths.Roadmaps.Issues.Comments.Update, - async (req: RequestWithSession, res) => { - // get comment data from body - const { content } = req.body as { content: string }; - // get comment id from params - const commentId = BigInt(req.params.commentId || -1); - - // check if params are valid - const args = await checkParams(req, res); - if (!args) return; - const { issueId } = args; - - // get user info - const userArgs = await checkUser(req, res); - if (!userArgs) return; - const { userId } = userArgs; - - // check if comment id is valid - if (commentId < 0) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'CommentId is invalid.' }); - return; - } - - // if comment is empty - if (!content) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Comment can\'t be empty.' }); - - // get database connection - const db = new Database(); - - // get comment - const comment = await db.get('issueComments', commentId); - - if (!comment) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Comment does not exist.' }); - return; - } - - // check if comment belongs to issue - if (comment.issueId !== issueId) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Comment does not belong to issue.' }); - return; - } - - // check if user is allowed to update comment - if (comment.userId !== userId) { - res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'Only the owner can update comments.' }); - return; - } - - // update comment - const success = await db.update('issueComments', commentId, { content }); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to update comment.' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -CommentsRouter.use(Paths.Roadmaps.Issues.Comments.Delete, validateSession); -CommentsRouter.delete( - Paths.Roadmaps.Issues.Comments.Delete, - async (req: RequestWithSession, res) => { - // get comment id from params - const commentId = BigInt(req.params.commentId || -1); - - // check if ids are valid - const args = await checkParams(req, res); - if (!args) return; - const { issue, roadmap } = args; - - // get userDisplay info - const userArgs = await checkUser(req, res); - if (!userArgs) return; - const { userId } = userArgs; - - // check if comment id is valid - if (commentId < 0) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'CommentId is invalid.' }); - return; - } - - // get database connection - const db = new Database(); - - // get comment - const comment = await db.get('issueComments', commentId); - - if (!comment) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Comment does not exist.' }); - return; - } - - // check if issue is valid - if (comment.issueId !== issue.id) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Comment does not belong to issue.' }); - - // check if the userDisplay is allowed to delete comment - if (comment.userId !== userId || roadmap.userId !== userId) { - res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'Only the comment owner can delete comments.' }); - return; - } - - // delete comment - const success = await db.delete('issueComments', commentId); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Failed to delete comment.' }); - - // send success json - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -export default CommentsRouter; diff --git a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts b/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts deleted file mode 100644 index 2ff3432..0000000 --- a/src/routes/roadmapsRoutes/issuesRoutes/IssuesUpdate.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Response, Router } from 'express'; -import Paths from '@src/constants/Paths'; -import Database from '@src/util/DatabaseDriver'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import { RequestWithSession } from '@src/middleware/session'; -import { Issue } from '@src/types/models/Issue'; -import { Roadmap } from '@src/types/models/Roadmap'; -import validateSession from '@src/middleware/validators/validateSession'; - -const IssuesUpdate = Router({ mergeParams: true }); - -async function checkArguments( - req: RequestWithSession, - res: Response, - roadmapOwnerCanEdit = false, -): Promise< - | { - session: RequestWithSession['session']; - issueId: bigint; - roadmapId: bigint; - issue: Issue; - roadmap: Roadmap; - db: Database; - } - | undefined -> { - // get data from body and session - const session = req.session; - const issueId = BigInt(req.params.issueId || -1); - const roadmapId = BigInt(req.params.roadmapId || -1); - - // check if session, issueId and roadmapId are valid - if (!session || issueId < 0 || roadmapId < 0) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Session, issueId or roadmapId is invalid.' }); - return; - } - - // get database connection - const db = new Database(); - - // check if issue exists - const issue = await db.get('issues', issueId); - - if (!issue) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue does not exist.' }); - return; - } - - // check if issue belongs to roadmap - if (issue.roadmapId !== roadmapId) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue does not belong to roadmap.' }); - return; - } - - // check if roadmap exists - const roadmap = await db.get('roadmaps', roadmapId); - - if (!roadmap) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - return; - } - // check if user is allowed to update issue - if ( - issue.userId !== session.userId && - (roadmapOwnerCanEdit ? roadmap.userId !== session.userId : true) - ) { - res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User is not allowed to update issue.' }); - return; - } - - return { - session, - issueId, - roadmapId, - issue, - roadmap, - db, - }; -} - -async function statusChangeIssue( - req: RequestWithSession, - res: Response, - open: boolean, -) { - // check if arguments are valid - const args = await checkArguments(req, res, true); - - if (!args) return; - - const { issueId, issue, db } = args; - - // update issue - issue.set({ - open, - updatedAt: new Date(), - }); - - // save issue to database - const success = await db.update('issues', issueId, issue); - - // check if id is valid - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); -} - -IssuesUpdate.post( - Paths.Roadmaps.Issues.Update.Title, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const issueId = BigInt(req.params.issueId || -1); - const roadmapId = BigInt(req.params.roadmapId || -1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const title = req.body?.title as string; - - // check if title is valid - if (!title) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Title is missing.' }); - } - // check if session, issueId and roadmapId are valid - if (!session || issueId < 0 || roadmapId < 0) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Session, issueId or roadmapId is invalid.' }); - } - - // get database connection - const db = new Database(); - - // check if issue exists - const issue = await db.get('issues', issueId); - - if (!issue) { - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue does not exist.' }); - } - - // check if issue belongs to roadmap - if (issue.roadmapId !== roadmapId) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue does not belong to roadmap.' }); - } - - // check if user is allowed to update issue - if (issue.userId !== session.userId) { - return res - .status(HttpStatusCodes.UNAUTHORIZED) - .json({ error: 'User is not allowed to update issue.' }); - } - - // update issue - issue.set({ - title, - updatedAt: new Date(), - }); - - // save issue to database - const success = await db.update('issues', issueId, issue); - - // check if id is valid - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -IssuesUpdate.post( - Paths.Roadmaps.Issues.Update.Content, - async (req: RequestWithSession, res) => { - // get data from body and session - const session = req.session; - const issueId = BigInt(req.params.issueId || -1); - const roadmapId = BigInt(req.params.roadmapId || -1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const content = req.body?.content as string; - - // check if content is valid - if (!content) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Content is missing.' }); - } - // check if session, issueId and roadmapId are valid - if (!session || issueId < 0 || roadmapId < 0) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Session, issueId or roadmapId is invalid.' }); - } - - // get database connection - const db = new Database(); - - // check if issue exists - const issue = await db.get('issues', issueId); - - if (!issue) { - return res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Issue does not exist.' }); - } - - // check if issue belongs to roadmap - if (issue.roadmapId !== roadmapId) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Issue does not belong to roadmap.' }); - } - - // check if user is allowed to update issue - if (issue.userId !== session.userId) { - return res - .status(HttpStatusCodes.UNAUTHORIZED) - .json({ error: 'User is not allowed to update issue.' }); - } - - // update issue - issue.set({ - content, - updatedAt: new Date(), - }); - - // save issue to database - const success = await db.update('issues', issueId, issue); - - // check if id is valid - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Issue could not be saved to database.' }); - - // return success - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, -); - -IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, validateSession); -IssuesUpdate.get(Paths.Roadmaps.Issues.Update.Status, (req, res) => - statusChangeIssue(req, res, true), -); - -IssuesUpdate.delete(Paths.Roadmaps.Issues.Update.Status, validateSession); -IssuesUpdate.delete(Paths.Roadmaps.Issues.Update.Status, (req, res) => - statusChangeIssue(req, res, false), -); - -export default IssuesUpdate; diff --git a/src/sql/setup.sql b/src/sql/setup.sql index 0b46341..e9cb410 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -52,57 +52,6 @@ create table if not exists roadmaps on delete cascade ); -create table if not exists issues -( - id bigint auto_increment - primary key, - roadmapId bigint not null, - userId bigint not null, - open tinyint(1) default 1 not null, - title varchar(255) not null, - content text null, - createdAt timestamp default current_timestamp() null, - updatedAt timestamp default current_timestamp() null on update current_timestamp(), - constraint issues_roadmapId_fk - foreign key (roadmapId) references roadmaps (id) - on delete cascade, - constraint issues_userId_fk - foreign key (userId) references users (id) - on delete cascade -); - -create table if not exists issueComments -( - id bigint auto_increment - primary key, - issueId bigint not null, - userId bigint not null, - content text not null, - createdAt timestamp default current_timestamp() not null, - updatedAt timestamp default current_timestamp() not null on update current_timestamp(), - constraint issueComments_issuesId_fk - foreign key (issueId) references issues (id) - on delete cascade, - constraint issueComments_usersId_fk - foreign key (userId) references users (id) - on delete cascade -); - -create index if not exists issueComments_issueId_createdAt_index - on issueComments (issueId, createdAt); - -create index if not exists issueComments_userid_index - on issueComments (userId); - -create index if not exists issues_roadmapId_createdAt_index - on issues (roadmapId asc, createdAt desc); - -create index if not exists issues_title_index - on issues (title); - -create index if not exists issues_userId_index - on issues (userId); - create table if not exists roadmapLikes ( id bigint auto_increment diff --git a/src/types/models/Issue.ts b/src/types/models/Issue.ts deleted file mode 100644 index bb283d0..0000000 --- a/src/types/models/Issue.ts +++ /dev/null @@ -1,147 +0,0 @@ -// Interface for full Issue object -export interface IIssue { - readonly id: bigint; - readonly roadmapId: bigint; - readonly userId: bigint; - readonly open: boolean; - readonly title: string; - readonly content: string | null; - readonly createdAt: Date; - readonly updatedAt: Date; -} - -// Interface for constructing an Issue -interface IIssueConstructor { - readonly id?: bigint; - readonly roadmapId: bigint; - readonly userId: bigint; - readonly open: boolean; - readonly title: string; - readonly content?: string | null; - readonly createdAt?: Date; - readonly updatedAt?: Date; -} - -// Interface for modifying an Issue -interface IIssueModifications { - readonly id?: bigint; - readonly roadmapId?: bigint; - readonly userId?: bigint; - readonly open?: boolean; - readonly title?: string; - readonly content?: string | null; - readonly createdAt?: Date; - readonly updatedAt?: Date; -} - -// Class -export class Issue implements IIssue { - private _id: bigint; - private _roadmapId: bigint; - private _userId: bigint; - private _open: boolean; - private _title: string; - private _content: string | null; - private _createdAt: Date; - private _updatedAt: Date; - - public constructor({ - id = -1n, - roadmapId, - userId, - open, - title, - content = null, - createdAt = new Date(), - updatedAt = new Date(), - }: IIssueConstructor) { - this._id = id; - this._roadmapId = roadmapId; - this._userId = userId; - this._open = open; - this._title = title; - this._content = content; - this._createdAt = createdAt; - this._updatedAt = updatedAt; - } - - // Method to modify the properties - public set({ - id, - roadmapId, - userId, - open, - title, - content, - createdAt, - updatedAt, - }: IIssueModifications): void { - if (id !== undefined) this._id = id; - if (roadmapId !== undefined) this._roadmapId = roadmapId; - if (userId !== undefined) this._userId = userId; - if (open !== undefined) this._open = open; - if (title !== undefined) this._title = title; - if (content !== undefined) this._content = content; - if (createdAt !== undefined) this._createdAt = createdAt; - if (updatedAt !== undefined) this._updatedAt = updatedAt; - } - - public get id(): bigint { - return this._id; - } - - public get roadmapId(): bigint { - return this._roadmapId; - } - - public get userId(): bigint { - return this._userId; - } - - public get open(): boolean { - return this._open; - } - - public get title(): string { - return this._title; - } - - public get content(): string | null { - return this._content; - } - - public get createdAt(): Date { - return this._createdAt; - } - - public get updatedAt(): Date { - return this._updatedAt; - } - - // Static method to check if an object is of type IIssue - public static isIssue(obj: unknown): obj is IIssue { - return ( - typeof obj === 'object' && - obj !== null && - 'roadmapId' in obj && - 'userId' in obj && - 'open' in obj && - 'title' in obj && - 'updatedAt' in obj - ); - } - - // toObject method - public toObject(): IIssue { - return { - id: this._id, - roadmapId: this._roadmapId, - userId: this._userId, - open: this._open, - title: this._title, - content: this._content, - createdAt: this._createdAt, - updatedAt: this._updatedAt, - }; - } -} diff --git a/src/types/models/IssueComment.ts b/src/types/models/IssueComment.ts deleted file mode 100644 index 6576e8d..0000000 --- a/src/types/models/IssueComment.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Interface for full IssueComment object -export interface IIssueComment { - readonly id: bigint; - readonly issueId: bigint; - readonly userId: bigint; - readonly content: string; - readonly createdAt: Date; - readonly updatedAt: Date; -} - -// Interface for constructing an IssueComment -interface IIssueCommentConstructor { - readonly id?: bigint; - readonly issueId: bigint; - readonly userId: bigint; - readonly content: string; - readonly createdAt?: Date; - readonly updatedAt?: Date; -} - -// Interface for modifying an IssueComment -interface IIssueCommentModifications { - readonly id?: bigint; - readonly issueId?: bigint; - readonly userId?: bigint; - readonly content?: string; - readonly createdAt?: Date; - readonly updatedAt?: Date; -} - -// Class -export class IssueComment implements IIssueComment { - private _id: bigint; - private _issueId: bigint; - private _userId: bigint; - private _content: string; - private _createdAt: Date; - private _updatedAt: Date; - - public constructor({ - id = -1n, - issueId, - userId, - content, - createdAt = new Date(), - updatedAt = new Date(), - }: IIssueCommentConstructor) { - this._id = id; - this._issueId = issueId; - this._userId = userId; - this._content = content; - this._createdAt = createdAt; - this._updatedAt = updatedAt; - } - - // Method to modify the properties - public set({ - id, - issueId, - userId, - content, - createdAt, - updatedAt, - }: IIssueCommentModifications): void { - if (id !== undefined) this._id = id; - if (issueId !== undefined) this._issueId = issueId; - if (userId !== undefined) this._userId = userId; - if (content !== undefined) this._content = content; - if (createdAt !== undefined) this._createdAt = createdAt; - if (updatedAt !== undefined) this._updatedAt = updatedAt; - } - - public get id(): bigint { - return this._id; - } - - public get issueId(): bigint { - return this._issueId; - } - - public get userId(): bigint { - return this._userId; - } - - public get content(): string { - return this._content; - } - - public get createdAt(): Date { - return this._createdAt; - } - - public get updatedAt(): Date { - return this._updatedAt; - } - - // Static method to check if an object is of type IIssueComment - public static isIssueComment(obj: unknown): obj is IIssueComment { - return ( - typeof obj === 'object' && - obj !== null && - 'issueId' in obj && - 'userId' in obj && - 'content' in obj && - 'createdAt' in obj && - 'updatedAt' in obj - ); - } - - // toObject method - public toObject(): IIssueComment { - return { - id: this._id, - issueId: this._issueId, - userId: this._userId, - content: this._content, - createdAt: this._createdAt, - updatedAt: this._updatedAt, - }; - } -} From 783a6f5e828bc627bdb098d739ff6630458f7fa4 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 19:50:02 +0300 Subject: [PATCH 079/118] Fix default isPublic value in roadmapController The previous code was allowing user input to affect the value of isPublic in the Roadmap controller. This commit fixes this by ensuring that isPublic is always true, as currently, there shouldn't be any non-public roadmaps. The feature for users to modify the isPublic status will be introduced in the future. --- src/controllers/roadmapController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index c784813..4225440 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -38,7 +38,9 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) topic = undefined; - if (isPublic !== true && isPublic !== false) isPublic = true; + // isPublic can't be modified by the user yet + // if (isPublic !== true && isPublic !== false) isPublic = true; + isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false; const roadmap = new Roadmap({ From b9de9170e0941cff39a826d3107528926f33988b Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:03:58 +0300 Subject: [PATCH 080/118] Reorganize Database classes into a separate folder This commit moves the existing Database classes (DatabaseDriver and ExploreDB) into a dedicated 'Database' folder. The import declarations referencing these classes are also updated accordingly across all files. This decision was made to improve code organization and clarity, as having a dedicated folder for Database related classes makes it easier to locate and manage these classes. --- spec/tests/routes/auth.spec.ts | 2 +- spec/tests/utils/database.spec.ts | 2 +- spec/utils/createUser.ts | 2 +- src/controllers/authController.ts | 2 +- src/controllers/exploreController.ts | 5 ++-- src/controllers/roadmapController.ts | 2 +- src/helpers/databaseManagement.ts | 2 +- src/middleware/session.ts | 3 +-- src/middleware/validators/validateSession.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsGet.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 2 +- src/util/{ => Database}/DatabaseDriver.ts | 4 ++-- src/util/{ => Database}/ExploreDB.ts | 25 ++++---------------- src/util/sessionManager.ts | 2 +- 14 files changed, 20 insertions(+), 37 deletions(-) rename src/util/{ => Database}/DatabaseDriver.ts (99%) rename src/util/{ => Database}/ExploreDB.ts (76%) diff --git a/spec/tests/routes/auth.spec.ts b/spec/tests/routes/auth.spec.ts index 4384c86..9c533c4 100644 --- a/spec/tests/routes/auth.spec.ts +++ b/spec/tests/routes/auth.spec.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; import { User } from '@src/types/models/User'; -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; describe('Authentification Tests', () => { diff --git a/spec/tests/utils/database.spec.ts b/spec/tests/utils/database.spec.ts index 4102364..41ef876 100644 --- a/spec/tests/utils/database.spec.ts +++ b/spec/tests/utils/database.spec.ts @@ -1,4 +1,4 @@ -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import { IUser, User } from '@src/types/models/User'; import { randomString } from '@spec/utils/randomString'; diff --git a/spec/utils/createUser.ts b/spec/utils/createUser.ts index 65a2449..6ff54ac 100644 --- a/spec/utils/createUser.ts +++ b/spec/utils/createUser.ts @@ -2,7 +2,7 @@ import { IUser, User } from '@src/types/models/User'; import { randomString } from '@spec/utils/randomString'; import request from 'supertest'; import app from '@src/server'; -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import { CreatedUser } from '@spec/types/tests/CreatedUser'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; import { Response } from '@spec/types/supertest'; diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 2c76cf7..7c71278 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,6 +1,6 @@ import { RequestWithBody } from '@src/middleware/validators/validateBody'; import { Response } from 'express'; -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; import { User } from '@src/types/models/User'; import axios, { HttpStatusCode } from 'axios'; import { comparePassword, saltPassword } from '@src/util/LoginUtil'; diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 4a1c6e8..4bdd0ac 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -2,12 +2,13 @@ import { RequestWithSearchParameters, } from '@src/middleware/validators/validateSearchParameters'; import { Response } from 'express'; -import { ExploreDB, SearchRoadmap } from '@src/util/ExploreDB'; +import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; +import { ResRoadmap } from '@src/types/response/ResRoadmap'; function responseSearchRoadmaps( res: Response, - roadmaps: SearchRoadmap[], + roadmaps: ResRoadmap[], ): unknown { return res.status(HttpStatusCodes.OK).json({ success: true, diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 4225440..dc1a2d9 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -1,7 +1,7 @@ import { RequestWithSession } from '@src/middleware/session'; import { Response } from 'express'; import { responseServerError } from '@src/helpers/responses/generalResponses'; -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { responseNotAllowed, diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 7c0e747..564761b 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -1,4 +1,4 @@ -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; import { IUserInfo, UserInfo } from '@src/types/models/UserInfo'; import { IUser, User } from '@src/types/models/User'; import { Roadmap } from '@src/types/models/Roadmap'; diff --git a/src/middleware/session.ts b/src/middleware/session.ts index ee95b4c..d1f3a3e 100644 --- a/src/middleware/session.ts +++ b/src/middleware/session.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; import EnvVars from '@src/constants/EnvVars'; import { NodeEnvs } from '@src/constants/misc'; diff --git a/src/middleware/validators/validateSession.ts b/src/middleware/validators/validateSession.ts index ed70f00..6c5e8d1 100644 --- a/src/middleware/validators/validateSession.ts +++ b/src/middleware/validators/validateSession.ts @@ -1,7 +1,7 @@ import { NextFunction, Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { RequestWithSession } from '@src/middleware/session'; -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; function invalidSession(res: Response): void { res.status(HttpStatusCodes.UNAUTHORIZED).json({ diff --git a/src/routes/roadmapsRoutes/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts index 4bc2cab..46adb9e 100644 --- a/src/routes/roadmapsRoutes/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -2,7 +2,7 @@ import { Response, Router } from 'express'; import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import { Roadmap } from '@src/types/models/Roadmap'; import axios from 'axios'; import EnvVars from '@src/constants/EnvVars'; diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index aecde05..8cbfe42 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -2,7 +2,7 @@ import { Response, Router } from 'express'; import Paths from '@src/constants/Paths'; import { RequestWithSession } from '@src/middleware/session'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/DatabaseDriver'; +import Database from '@src/util/Database/DatabaseDriver'; import { Roadmap } from '@src/types/models/Roadmap'; import validateSession from '@src/middleware/validators/validateSession'; diff --git a/src/util/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts similarity index 99% rename from src/util/DatabaseDriver.ts rename to src/util/Database/DatabaseDriver.ts index 04f10cd..be4419c 100644 --- a/src/util/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -90,7 +90,7 @@ interface CountQueryPacket extends RowDataPacket { result: bigint; } -interface ResultSetHeader { +export interface ResultSetHeader { fieldCount: number; affectedRows: number; insertId: number; @@ -421,7 +421,7 @@ class Database { protected async _setup() { // get setup.sql file let setupSql = fs.readFileSync( - path.join(__dirname, '..', 'sql', 'setup.sql'), + path.join(__dirname, '..', '..', 'sql', 'setup.sql'), 'utf8', ); diff --git a/src/util/ExploreDB.ts b/src/util/Database/ExploreDB.ts similarity index 76% rename from src/util/ExploreDB.ts rename to src/util/Database/ExploreDB.ts index 12bc299..3932b46 100644 --- a/src/util/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -1,30 +1,13 @@ -import Database, { DatabaseConfig } from '@src/util/DatabaseDriver'; +import Database, { DatabaseConfig } from '@src/util/Database/DatabaseDriver'; import EnvVars from '@src/constants/EnvVars'; import { SearchParameters, } from '@src/middleware/validators/validateSearchParameters'; -import { RoadmapTopic } from '@src/types/models/Roadmap'; +import { ResRoadmap } from '@src/types/response/ResRoadmap'; // database credentials const { DBCred } = EnvVars; -export interface SearchRoadmap{ - id: bigint; - name: string; - description: string; - topic: RoadmapTopic; - isFeatured: boolean; - isPublic: boolean; - isDraft: boolean; - userAvatar: string | null; - userName: string; - - likeCount: number; - viewCount: number; - - isLiked: number; -} - class ExploreDB extends Database { public constructor(config: DatabaseConfig = DBCred as DatabaseConfig) { super(config); @@ -36,7 +19,7 @@ class ExploreDB extends Database { limit, topic, order, - }: SearchParameters, userid?: bigint): Promise { + }: SearchParameters, userid?: bigint): Promise { if(!search || !page || !limit || !topic || !order) return []; const query = ` SELECT @@ -84,7 +67,7 @@ class ExploreDB extends Database { const result = await this.getQuery(query, params); if (result === null) return []; - return result as unknown as SearchRoadmap[]; + return result as unknown as ResRoadmap[]; } } diff --git a/src/util/sessionManager.ts b/src/util/sessionManager.ts index 19bb4eb..4448b49 100644 --- a/src/util/sessionManager.ts +++ b/src/util/sessionManager.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'crypto'; -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; import { Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import EnvVars from '@src/constants/EnvVars'; From b9e5c55b9892275cece91e204c5856678008bfc3 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:05:02 +0300 Subject: [PATCH 081/118] Add "Views" utility with roadmap view & impression functions Added a new "Views" utility in util folder which includes two new functions: "addRoadmapView" and "addRoadmapImpression". These functions assist in tracking views and impressions of each roadmap. This would allow for a more detailed statistical analysis of user interaction with the different roadmaps, fostering a better understanding of user behavior. --- src/util/Views.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/util/Views.ts diff --git a/src/util/Views.ts b/src/util/Views.ts new file mode 100644 index 0000000..b95b51b --- /dev/null +++ b/src/util/Views.ts @@ -0,0 +1,35 @@ +import DatabaseDriver, { + ResultSetHeader, +} from '@src/util/Database/DatabaseDriver'; +import { RoadmapView } from '@src/types/models/RoadmapView'; + +export async function addRoadmapView( + db: DatabaseDriver, + roadmapId: bigint, + userId?: bigint, +): Promise { + if (!userId) userId = -1n; + return ( + (await db.insert( + 'roadmapViews', + new RoadmapView({ + userId, + roadmapId, + full: true, + }), + )) >= 0 + ); +} + +export async function addRoadmapImpression( + db: DatabaseDriver, + roadmapId: bigint[], + userId?: bigint, +): Promise { + if (!userId) userId = -1n; + const values = roadmapId.map((id) => `(${id}, ${userId}, 0)`).join(', '); + return ((await db._query(` + INSERT INTO roadmapViews (roadmapId, userId, full) + VALUES ${values} + `)) as ResultSetHeader).affectedRows >= 0; +} \ No newline at end of file From 150e11a24470342007c46856f83d6f96aebc08e8 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:09:17 +0300 Subject: [PATCH 082/118] Refactor ResRoadmap structure and update usersController The ResRoadmap type and class were updated to include the properties for likeCount, viewCount and isLiked for improved user engagement tracking. The property 'data' has been removed. Updated "usersController.ts" to fetch and populate the newly added fields, and call the 'addRoadmapImpression' function to track views for each roadmap. --- src/controllers/usersController.ts | 36 ++++++++++++++++++++++++++++-- src/types/response/ResRoadmap.ts | 34 ++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index e43ac77..ce8f75e 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -1,6 +1,6 @@ import { Response } from 'express'; import { RequestWithSession } from '@src/middleware/session'; -import DatabaseDriver from '@src/util/DatabaseDriver'; +import DatabaseDriver from '@src/util/Database/DatabaseDriver'; import { deleteUser, followUser, @@ -33,6 +33,8 @@ import { responseUserNoRoadmaps, responseUserRoadmaps, } from '@src/helpers/responses/roadmapResponses'; +import { addRoadmapImpression } from '@src/util/Views'; +import logger from 'jet-logger'; /* ! Main route controllers @@ -127,10 +129,40 @@ export async function userGetRoadmaps( // check if user exists if (!roadmaps) return responseUserNoRoadmaps(res); + const likes: bigint[] = []; + const views: bigint[] = []; + const isLiked: boolean[] = []; + + for (const roadmap of roadmaps) { + likes.push(await db.countWhere('roadmapLikes', 'roadmapId', roadmap.id)); + views.push(await db.countWhere('roadmapViews', 'roadmapId', roadmap.id)); + isLiked.push( + !!( + await db.countWhere( + 'roadmapLikes', + 'roadmapId', + roadmap.id, + 'userId', + req.issuerUserId, + ) + ), + ); + } + + // add impressions to roadmaps + addRoadmapImpression( + db, + roadmaps.map((roadmap) => roadmap.id), + req.issuerUserId, + ).catch((e) => logger.err(e)); + // send user json return responseUserRoadmaps( res, - roadmaps.map((roadmap) => new ResRoadmap(roadmap, user)), + roadmaps.map( + (roadmap, i) => + new ResRoadmap(roadmap, user, likes[i], views[i], isLiked[i]), + ), ); } diff --git a/src/types/response/ResRoadmap.ts b/src/types/response/ResRoadmap.ts index 0ee1c01..de0fa7d 100644 --- a/src/types/response/ResRoadmap.ts +++ b/src/types/response/ResRoadmap.ts @@ -8,7 +8,6 @@ export interface IResRoadmap { readonly topic: RoadmapTopic; readonly isPublic: boolean; readonly isDraft: boolean; - readonly data: string; readonly createdAt: Date; readonly updatedAt: Date; @@ -16,6 +15,13 @@ export interface IResRoadmap { readonly userId: bigint; readonly userAvatar: string | null; readonly userName: string; + + // stats + readonly likeCount: bigint; + readonly viewCount: bigint; + + // user stats + readonly isLiked: boolean; } export class ResRoadmap implements IResRoadmap { @@ -26,7 +32,6 @@ export class ResRoadmap implements IResRoadmap { public readonly isFeatured: boolean; public readonly isPublic: boolean; public readonly isDraft: boolean; - public readonly data: string; public readonly createdAt: Date; public readonly updatedAt: Date; @@ -34,6 +39,11 @@ export class ResRoadmap implements IResRoadmap { public readonly userAvatar: string | null; public readonly userName: string; + public readonly likeCount: bigint; + public readonly viewCount: bigint; + + public readonly isLiked: boolean; + public constructor( { id, @@ -44,11 +54,13 @@ export class ResRoadmap implements IResRoadmap { isFeatured, isPublic, isDraft, - data, createdAt, updatedAt, }: IRoadmap, { avatar: userAvatar, name: userName }: IUser, + likeCount: bigint, + viewCount: bigint, + isLiked: boolean, ) { this.id = id; this.name = name; @@ -57,12 +69,16 @@ export class ResRoadmap implements IResRoadmap { this.isFeatured = isFeatured; this.isPublic = isPublic; this.isDraft = isDraft; - this.data = data; this.createdAt = createdAt; this.updatedAt = updatedAt; this.userId = userId; this.userAvatar = userAvatar; this.userName = userName; + + this.likeCount = likeCount; + this.viewCount = viewCount; + + this.isLiked = isLiked; } public static isRoadmap(obj: unknown): obj is IResRoadmap { @@ -73,13 +89,17 @@ export class ResRoadmap implements IResRoadmap { 'name' in obj && 'description' in obj && 'topic' in obj && - 'userId' in obj && 'isFeatured' in obj && 'isPublic' in obj && 'isDraft' in obj && - 'data' in obj && 'createdAt' in obj && - 'updatedAt' in obj + 'updatedAt' in obj && + 'userId' in obj && + 'userAvatar' in obj && + 'userName' in obj && + 'likeCount' in obj && + 'viewCount' in obj && + 'isLiked' in obj ); } } From 62b5b9cae8cc1d2254cbaa3fc6af370fcd760aac Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:38:46 +0300 Subject: [PATCH 083/118] Update the way roadmaps are retrieved and their responses This commit introduces significant updates to how roadmaps are retrieved and returned. It specifically adds a more detailed response for individual roadmap retrieval including all related information such as likes and views count. It has also refactored the 'isLiked' property across various models from boolean to bigint to allow more detailed user interaction stats. Most importantly, the logic of retrieval has been moved to the roadmapController from routes to ensure better readability and code organization. A new file ResFullRoadmap.ts is introduced to handle the detailed full roadmap response. --- src/controllers/roadmapController.ts | 56 +++++- src/controllers/usersController.ts | 17 +- src/helpers/databaseManagement.ts | 2 +- src/helpers/responses/roadmapResponses.ts | 12 ++ src/routes/roadmapsRoutes/RoadmapsGet.ts | 197 +--------------------- src/types/response/ResFullRoadmap.ts | 110 ++++++++++++ src/types/response/ResRoadmap.ts | 6 +- src/util/Database/DatabaseDriver.ts | 46 +++++ 8 files changed, 235 insertions(+), 211 deletions(-) create mode 100644 src/types/response/ResFullRoadmap.ts diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index dc1a2d9..0ec78f8 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -5,6 +5,7 @@ import Database from '@src/util/Database/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { responseNotAllowed, + responseRoadmap, responseRoadmapAlreadyLiked, responseRoadmapCreated, responseRoadmapDeleted, @@ -13,13 +14,18 @@ import { responseRoadmapRated, } from '@src/helpers/responses/roadmapResponses'; import { - getRoadmap, - getRoadmapLike, insertRoadmap, + getRoadmapData, + getRoadmapLike, + insertRoadmap, insertRoadmapLike, updateRoadmapLike, } from '@src/helpers/databaseManagement'; import { RequestWithBody } from '@src/middleware/validators/validateBody'; import { Roadmap, RoadmapTopic } from '@src/types/models/Roadmap'; +import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; +import { IUser } from '@src/types/models/User'; +import { addRoadmapView } from '@src/util/Views'; +import logger from 'jet-logger'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware @@ -49,7 +55,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { topic: topic as RoadmapTopic | undefined, userId, isPublic: isPublic as boolean, - isDraft: isDraft as boolean , + isDraft: isDraft as boolean, data: data as string, }); @@ -60,6 +66,48 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { return responseServerError(res); } +export async function getRoadmap(req: RequestWithSession, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + if (!roadmap) return responseRoadmapNotFound(res); + + const user = await db.get('users', roadmap.userId); + if (!user) return responseServerError(res); + const likeCount = await db.countWhere( + 'roadmapLikes', + 'roadmapId', + roadmap.id, + ); + const viewCount = await db.countWhere( + 'roadmapViews', + 'roadmapId', + roadmap.id, + ); + const isLiked = await db.sumWhere( + 'roadmapLikes', + 'roadmapId', + roadmap.id, + 'userId', + userId, + ); + + if (!roadmap.isPublic && roadmap.userId !== userId) + return responseNotAllowed(res); + + addRoadmapView(db, roadmap.id, userId).catch((e) => logger.err(e)); + + return responseRoadmap( + res, + new ResFullRoadmap(roadmap, user, likeCount, viewCount, isLiked), + ); +} + export async function deleteRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; @@ -69,7 +117,7 @@ export async function deleteRoadmap(req: RequestWithSession, res: Response) { const db = new Database(); - const roadmap = await getRoadmap(db, BigInt(roadmapId)); + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); if (!roadmap) return responseRoadmapNotFound(res); if (roadmap.userId !== userId) return responseNotAllowed(res); diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index ce8f75e..72f4db2 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -131,20 +131,19 @@ export async function userGetRoadmaps( const likes: bigint[] = []; const views: bigint[] = []; - const isLiked: boolean[] = []; + const isLiked: bigint[] = []; for (const roadmap of roadmaps) { likes.push(await db.countWhere('roadmapLikes', 'roadmapId', roadmap.id)); views.push(await db.countWhere('roadmapViews', 'roadmapId', roadmap.id)); isLiked.push( - !!( - await db.countWhere( - 'roadmapLikes', - 'roadmapId', - roadmap.id, - 'userId', - req.issuerUserId, - ) + await db.sumWhere( + 'roadmapLikes', + 'value', + 'roadmapId', + roadmap.id, + 'userId', + req.issuerUserId, ), ); } diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 564761b..fc32e9f 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -202,7 +202,7 @@ export async function updateUserInfo( return await db.update('userInfo', userId, userInfo); } -export async function getRoadmap( +export async function getRoadmapData( db: DatabaseDriver, roadmapId: bigint, ): Promise { diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index 967815e..16f597e 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -2,6 +2,18 @@ import { Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import JSONStringify from '@src/util/JSONStringify'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; +import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; + +export function responseRoadmap(res: Response, roadmap: ResFullRoadmap): void { + res + .status(HttpStatusCodes.OK) + .contentType('application/json') + .send(JSONStringify({ + data: roadmap, + message: 'Roadmap found', + success: true, + })); +} export function responseRoadmapNotFound(res: Response): void { res.status(HttpStatusCodes.NOT_FOUND).json({ diff --git a/src/routes/roadmapsRoutes/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts index 46adb9e..120fb6b 100644 --- a/src/routes/roadmapsRoutes/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -1,203 +1,12 @@ -import { Response, Router } from 'express'; +import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { RequestWithSession } from '@src/middleware/session'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/Database/DatabaseDriver'; -import { Roadmap } from '@src/types/models/Roadmap'; -import axios from 'axios'; -import EnvVars from '@src/constants/EnvVars'; -import logger from 'jet-logger'; -import { IUser } from '@src/types/models/User'; -import { RoadmapView } from '@src/types/models/RoadmapView'; +import { getRoadmap } from '@src/controllers/roadmapController'; const RoadmapsGet = Router({ mergeParams: true }); -async function checkIfRoadmapExists( - req: RequestWithSession, - res: Response, -): Promise< - | { - id: bigint; - roadmap: Roadmap; - issueCount: bigint; - likes: bigint; - isLiked: boolean; - } - | undefined -> { - const id = req.params.roadmapId; - - if (!id) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - return; - } - - // get database connection - const db = new Database(); - - // get roadmap from database - const roadmap = await db.get('roadmaps', BigInt(id)); - const issueCount = await db.countWhere('issues', 'roadmapId', id); - - // check if roadmap is valid - if (!roadmap) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - - return; - } - - // get likes where roadmapId = id - const likes = await new Database().countWhere( - 'roadmapLikes', - 'roadmapId', - id, - ); - - let isLiked = false; - - if (req.session) { - const liked = await new Database().getAllWhere<{ - roadmapId: bigint; - userId: bigint; - }>('roadmapLikes', 'userId', req.session.userId); - - if (liked) { - if (liked.some((like) => like.roadmapId === BigInt(id))) { - isLiked = true; - } - } - } - - return { id: BigInt(id), roadmap, issueCount, likes, isLiked }; -} - -export async function addView( - userId: bigint, - roadmapId: bigint, - full: boolean, -): Promise { - // get database connection - const db = new Database(); - - // get roadmap from database - const roadmap = await db.get('roadmaps', roadmapId); - - // check if roadmap is valid - if (!roadmap) return; - - const view = new RoadmapView({ userId, roadmapId, full }); - - await db.insert('roadmapViews', view); -} - RoadmapsGet.get( Paths.Roadmaps.Get.Roadmap, - async (req: RequestWithSession, res) => { - //get data from params - const data = await checkIfRoadmapExists(req, res); - - if (!data) return; - - const { roadmap, issueCount, likes, isLiked } = data; - - // add view - await addView(req.session?.userId || BigInt(-1), roadmap.id, true); - - // return roadmap - return res.status(HttpStatusCodes.OK).json({ - id: roadmap.id.toString(), - name: roadmap.name, - description: roadmap.description, - ownerId: roadmap.userId.toString(), - issueCount: issueCount.toString(), - likes: likes.toString(), - isLiked, - createdAt: roadmap.createdAt, - updatedAt: roadmap.updatedAt, - isPublic: roadmap.isPublic, - data: roadmap.data, - }); - }, -); - -RoadmapsGet.get( - Paths.Roadmaps.Get.MiniRoadmap, - async (req: RequestWithSession, res) => { - // get id from params - const data = await checkIfRoadmapExists(req, res); - - if (!data) return; - - let user = await new Database().get('users', data.roadmap.userId); - if (!user) { - user = { id: -1n } as IUser; - } - - const { roadmap, likes, isLiked } = data; - - // add view - await addView(req.session?.userId || BigInt(-1), roadmap.id, false); - - // return roadmap - return res.status(HttpStatusCodes.OK).json({ - id: roadmap.id.toString(), - name: roadmap.name, - description: roadmap.description, - likes: likes.toString(), - isLiked, - ownerName: user.name, - ownerId: roadmap.userId.toString(), - }); - }, -); - -RoadmapsGet.get( - Paths.Roadmaps.Get.Owner, - async (req: RequestWithSession, res) => { - //get data from params - const data = await checkIfRoadmapExists(req, res); - - if (!data) return; - - const { roadmap } = data; - - // fetch /api/users/:id - axios - .get(`http://localhost:${EnvVars.Port}/api/users/${roadmap.userId}`) - .then((response) => { - res.status(response.status).json(response.data); - }) - .catch((error) => { - logger.err(error); - res.status(500).send('An error occurred'); - }); - }, -); - -RoadmapsGet.get( - Paths.Roadmaps.Get.OwnerMini, - async (req: RequestWithSession, res) => { - //get data from params - const data = await checkIfRoadmapExists(req, res); - - if (!data) return; - - const { roadmap } = data; - - // fetch /api-wrapper/users/:id - const user = await axios.get( - `http://localhost:${EnvVars.Port}/api/users/${roadmap.userId}/mini`, - ); - - // ? might need to check if json needs to be parsed - - // return roadmap - return res.status(user.status).json(user.data); - }, + getRoadmap, ); export default RoadmapsGet; diff --git a/src/types/response/ResFullRoadmap.ts b/src/types/response/ResFullRoadmap.ts new file mode 100644 index 0000000..f3a2ae3 --- /dev/null +++ b/src/types/response/ResFullRoadmap.ts @@ -0,0 +1,110 @@ +import { IRoadmap, RoadmapTopic } from '@src/types/models/Roadmap'; +import { IUser } from '@src/types/models/User'; + +export interface IResFullRoadmap { + readonly id: bigint; + readonly name: string; + readonly description: string; + readonly topic: RoadmapTopic; + readonly data: string; + readonly isPublic: boolean; + readonly isDraft: boolean; + readonly createdAt: Date; + readonly updatedAt: Date; + + // user + readonly userId: bigint; + readonly userAvatar: string | null; + readonly userName: string; + + // stats + readonly likeCount: bigint; + readonly viewCount: bigint; + + // user stats + readonly isLiked: bigint; +} + +export class ResFullRoadmap implements IResFullRoadmap { + public readonly id: bigint; + public readonly name: string; + public readonly description: string; + public readonly topic: RoadmapTopic; + public readonly data: string; + public readonly isFeatured: boolean; + public readonly isPublic: boolean; + public readonly isDraft: boolean; + public readonly createdAt: Date; + public readonly updatedAt: Date; + + public readonly userId: bigint; + public readonly userAvatar: string | null; + public readonly userName: string; + + public readonly likeCount: bigint; + public readonly viewCount: bigint; + + public readonly isLiked: bigint; + + public constructor( + { + id, + name, + description, + topic, + data, + userId, + isFeatured, + isPublic, + isDraft, + createdAt, + updatedAt, + }: IRoadmap, + { avatar: userAvatar, name: userName }: IUser, + likeCount: bigint, + viewCount: bigint, + isLiked: bigint, + ) { + this.id = id; + this.name = name; + this.description = description; + this.topic = topic; + this.data = data; + this.isFeatured = isFeatured; + this.isPublic = isPublic; + this.isDraft = isDraft; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.userId = userId; + this.userAvatar = userAvatar; + this.userName = userName; + + this.likeCount = likeCount; + this.viewCount = viewCount; + + this.isLiked = isLiked; + } + + public static isRoadmap(obj: unknown): obj is IResFullRoadmap { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + 'name' in obj && + 'description' in obj && + 'topic' in obj && + 'data' in obj && + 'isFeatured' in obj && + 'isPublic' in obj && + 'isDraft' in obj && + 'createdAt' in obj && + 'updatedAt' in obj && + 'userId' in obj && + 'userAvatar' in obj && + 'userName' in obj && + 'likeCount' in obj && + 'viewCount' in obj && + 'isLiked' in obj + ); + } +} diff --git a/src/types/response/ResRoadmap.ts b/src/types/response/ResRoadmap.ts index de0fa7d..184d7b0 100644 --- a/src/types/response/ResRoadmap.ts +++ b/src/types/response/ResRoadmap.ts @@ -21,7 +21,7 @@ export interface IResRoadmap { readonly viewCount: bigint; // user stats - readonly isLiked: boolean; + readonly isLiked: bigint; } export class ResRoadmap implements IResRoadmap { @@ -42,7 +42,7 @@ export class ResRoadmap implements IResRoadmap { public readonly likeCount: bigint; public readonly viewCount: bigint; - public readonly isLiked: boolean; + public readonly isLiked: bigint; public constructor( { @@ -60,7 +60,7 @@ export class ResRoadmap implements IResRoadmap { { avatar: userAvatar, name: userName }: IUser, likeCount: bigint, viewCount: bigint, - isLiked: boolean, + isLiked: bigint, ) { this.id = id; this.name = name; diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index be4419c..e722838 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -371,6 +371,52 @@ class Database { return parseResult(result) as T[] | null; } + public async sum(table: string, column: string): Promise { + const sql = `SELECT SUM(${column}) + FROM ${table}`; + const result = await this._query(sql); + + return (result as CountDataPacket[])[0][`SUM(${column})`] as bigint || 0n; + } + + public async sumQuery(sql: string, params?: unknown[]): Promise { + const result = await this._query(sql, params); + return (result as CountQueryPacket[])[0]['result'] || 0n; + } + + public async sumWhere( + table: string, + column: string, + ...values: unknown[] + ): Promise { + return await this._sumWhere(table, column, false, ...values); + } + + public async sumWhereLike( + table: string, + column: string, + ...values: unknown[] + ): Promise { + return await this._sumWhere(table, column, true, ...values); + } + + protected async _sumWhere( + table: string, + column: string, + like: boolean, + ...values: unknown[] + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return 0n; + + const sql = `SELECT SUM(${column}) + FROM ${table} + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); + + return (result as CountDataPacket[])[0][`SUM(${column})`] as bigint || 0n; + } + public async countQuery(sql: string, params?: unknown[]): Promise { const result = await this._query(sql, params); return (result as CountQueryPacket[])[0]['result'] || 0n; From 1dc8d362577c8e288022bf7bdaf56cb33a73f4c7 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:54:40 +0300 Subject: [PATCH 084/118] Add roadmap update features and refactor controllers Added new methods to handle different roadmap update operations in roadmap controllers and modified routes to support these new operations. This implementation simplifies and organizes the code, improving maintainability. The update operations allow changes in the roadmap's name, description, data, topic, and draft status. --- src/controllers/roadmapController.ts | 181 ++++++++++++- src/helpers/responses/roadmapResponses.ts | 7 + src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 265 +++----------------- 3 files changed, 223 insertions(+), 230 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 0ec78f8..75fac67 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -1,6 +1,9 @@ import { RequestWithSession } from '@src/middleware/session'; import { Response } from 'express'; -import { responseServerError } from '@src/helpers/responses/generalResponses'; +import { + responseInvalidBody, + responseServerError, +} from '@src/helpers/responses/generalResponses'; import Database from '@src/util/Database/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { @@ -12,6 +15,7 @@ import { responseRoadmapNotFound, responseRoadmapNotRated, responseRoadmapRated, + responseRoadmapUpdated, } from '@src/helpers/responses/roadmapResponses'; import { getRoadmapData, @@ -108,6 +112,181 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { ); } +export async function updateAllRoadmap(req: RequestWithBody, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { name, description, data, topic, isDraft } = req.body; + + if (!name || !description || !data || !topic || !isDraft) + return responseServerError(res); + + if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) + return responseInvalidBody(res); + + roadmap.set({ + name: name as string, + description: description as string, + data: data as string, + topic: topic as RoadmapTopic, + isDraft: isDraft as boolean, + }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + +export async function updateNameRoadmap(req: RequestWithBody, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { name } = req.body; + + if (!name) return responseServerError(res); + + roadmap.set({ name: name as string }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + +export async function updateDescriptionRoadmap( + req: RequestWithBody, + res: Response, +) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { description } = req.body; + + if (!description) return responseServerError(res); + + roadmap.set({ description: description as string }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + +export async function updateDataRoadmap(req: RequestWithBody, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { data } = req.body; + + if (!data) return responseServerError(res); + + roadmap.set({ data: data as string }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + +export async function updateTopicRoadmap(req: RequestWithBody, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { topic } = req.body; + + if (!topic) return responseServerError(res); + + if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) + return responseInvalidBody(res); + + roadmap.set({ topic: topic as RoadmapTopic }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + +export async function updateIsDraftRoadmap( + req: RequestWithBody, + res: Response, +) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { isDraft } = req.body; + + if (!isDraft) return responseServerError(res); + + roadmap.set({ isDraft: !!isDraft }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + export async function deleteRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index 16f597e..b27ad8f 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -15,6 +15,13 @@ export function responseRoadmap(res: Response, roadmap: ResFullRoadmap): void { })); } +export function responseRoadmapUpdated(res: Response): void { + res.status(HttpStatusCodes.OK).json({ + message: 'Roadmap updated', + success: true, + }); +} + export function responseRoadmapNotFound(res: Response): void { res.status(HttpStatusCodes.NOT_FOUND).json({ message: 'Roadmap not found', diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index 8cbfe42..6e2bec7 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -1,251 +1,58 @@ -import { Response, Router } from 'express'; +import { Router } from 'express'; import Paths from '@src/constants/Paths'; -import { RequestWithSession } from '@src/middleware/session'; -import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import Database from '@src/util/Database/DatabaseDriver'; -import { Roadmap } from '@src/types/models/Roadmap'; import validateSession from '@src/middleware/validators/validateSession'; +import validateBody from '@src/middleware/validators/validateBody'; +import { + updateAllRoadmap, + updateDataRoadmap, + updateDescriptionRoadmap, + updateIsDraftRoadmap, + updateNameRoadmap, + updateTopicRoadmap, +} from '@src/controllers/roadmapController'; const RoadmapsUpdate = Router({ mergeParams: true }); -async function isRoadmapValid( - req: RequestWithSession, - res: Response, -): Promise< - | { - id: bigint; - roadmap: Roadmap; - } - | undefined -> { - // get data from request - const id = req?.params?.roadmapId; - - if (!id) { - res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap id is missing.' }); - } - - const db = new Database(); - - // get roadmap from database - const roadmap = await db.get('roadmaps', BigInt(id)); - - // check if the roadmap is valid - if (!roadmap) { - res - .status(HttpStatusCodes.NOT_FOUND) - .json({ error: 'Roadmap does not exist.' }); - return; - } - - // check if the user is the owner of the roadmap - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (roadmap.ownerId !== req.session?.userId) { - res - .status(HttpStatusCodes.FORBIDDEN) - .json({ error: 'User is not the owner of the roadmap.' }); - - return; - } - - return { id: BigInt(id), roadmap }; -} - -RoadmapsUpdate.post('*', validateSession); +RoadmapsUpdate.post( + Paths.Roadmaps.Update.All, + validateSession, + validateBody('name', 'description', 'data', 'topic', 'isDraft'), + updateAllRoadmap, +); RoadmapsUpdate.post( Paths.Roadmaps.Update.Name, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const title = req.body?.title as string; - if (!title) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap title is missing.' }); - - // check if the roadmap is valid - const data = await isRoadmapValid(req, res); - if (!data) return; - const { roadmap } = data; - - // get database connection - const db = new Database(); - - // update roadmap - roadmap.set({ - name: title, - updatedAt: new Date(), - }); - const success = await db.update('roadmaps', roadmap.id, roadmap); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); - - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + validateSession, + validateBody('name'), + updateNameRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Description, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const description = req.body?.description as string; - if (!description) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap description is missing.' }); - - // check if the roadmap is valid - const data = await isRoadmapValid(req, res); - if (!data) return; - const { roadmap } = data; - - // get database connection - const db = new Database(); - - // update roadmap - roadmap.set({ - description, - updatedAt: new Date(), - }); - const success = await db.update('roadmaps', roadmap.id, roadmap); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); - - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + validateSession, + validateBody('description'), + updateDescriptionRoadmap, ); RoadmapsUpdate.post( - Paths.Roadmaps.Update.Visibility, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const visibilityData = req.body?.visibility; - let visibility: boolean; - try { - visibility = Boolean(visibilityData); - } catch (e) { - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap visibility is invalid.' }); - } - - if (visibility === undefined) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap visibility is missing.' }); - - // check if the roadmap is valid - const data = await isRoadmapValid(req, res); - if (!data) return; - const { roadmap } = data; - - // get database connection - const db = new Database(); - - // update roadmap - roadmap.set({ - isPublic: visibility, - updatedAt: new Date(), - }); - const success = await db.update('roadmaps', roadmap.id, roadmap); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); - - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, + Paths.Roadmaps.Update.Data, + validateSession, + validateBody('data'), + updateDataRoadmap, ); -// RoadmapsUpdate.post( -// Paths.Roadmaps.Update.Owner, -// async (req: RequestWithSession, res) => { -// eslint-disable-next-line max-len -// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument -// const newOwnerId = BigInt(req?.body?.newOwnerId || -1); -// if (newOwnerId < 0) -// return res -// .status(HttpStatusCodes.BAD_REQUEST) -// .json({ error: 'Roadmap new owner id is missing.' }); -// -// // check if the roadmap is valid -// const data = await isRoadmapValid(req, res); -// if (!data) return; -// const { roadmap } = data; -// -// // get database connection -// const db = new Database(); -// -// // check if the new owner exists -// const newOwner = await db.get('users', BigInt(newOwnerId)); -// if (!newOwner) -// return res -// .status(HttpStatusCodes.NOT_FOUND) -// .json({ error: 'New owner does not exist.' }); -// -// // update roadmap -// roadmap.set({ -// userId: newOwnerId, -// updatedAt: new Date(), -// }); -// const success = await db.update('roadmaps', roadmap.id, roadmap); -// -// if (!success) -// return res -// .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) -// .json({ error: 'Roadmap could not be updated.' }); -// -// return res.status(HttpStatusCodes.OK).json({ success: true }); -// }, -// ); - RoadmapsUpdate.post( - Paths.Roadmaps.Update.Data, - async (req: RequestWithSession, res) => { - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const data = req.body?.data as string; - - if (!data) - return res - .status(HttpStatusCodes.BAD_REQUEST) - .json({ error: 'Roadmap data is missing.' }); - - // check if the roadmap is valid - const dataP = await isRoadmapValid(req, res); - if (!dataP) return; - const { roadmap } = dataP; - - // get database connection - const db = new Database(); - - // update roadmap - roadmap.set({ - data, - updatedAt: new Date(), - }); - const success = await db.update('roadmaps', roadmap.id, roadmap); - - if (!success) - return res - .status(HttpStatusCodes.INTERNAL_SERVER_ERROR) - .json({ error: 'Roadmap could not be updated.' }); + Paths.Roadmaps.Update.Topic, + validateSession, + validateBody('topic'), + updateTopicRoadmap, +); - return res.status(HttpStatusCodes.OK).json({ success: true }); - }, +RoadmapsUpdate.post( + Paths.Roadmaps.Update.Draft, + validateSession, + validateBody('isDraft'), + updateIsDraftRoadmap, ); export default RoadmapsUpdate; From e5090bd4d553d5fd1c62d0b475a90ccd705362ef Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 21:59:38 +0300 Subject: [PATCH 085/118] Fixing linting errors --- src/middleware/session.ts | 11 +++-- src/routes/usersRoutes/UsersGet.ts | 73 ------------------------------ 2 files changed, 8 insertions(+), 76 deletions(-) diff --git a/src/middleware/session.ts b/src/middleware/session.ts index d1f3a3e..d1ad116 100644 --- a/src/middleware/session.ts +++ b/src/middleware/session.ts @@ -13,6 +13,12 @@ export interface ISession { expires: Date; } +interface RequestWithCookies extends RequestWithSession { + cookies: { + [COOKIE_NAME: string]: string | undefined; + } +} + export interface RequestWithSession extends Request { session?: ISession; } @@ -45,13 +51,12 @@ async function extendSession( } export async function sessionMiddleware( - req: RequestWithSession, + req: RequestWithCookies, res: Response, next: NextFunction, ): Promise { // get token cookie - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const token = req?.cookies?.[COOKIE_NAME] as string; + const token = req.cookies?.[COOKIE_NAME] ?? ''; if (!token) { req.session = undefined; diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 3807460..30ab4d7 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -24,78 +24,5 @@ UsersGet.get(Paths.Users.Get.Follow, validateUser(), userFollow); UsersGet.delete(Paths.Users.Get.Follow, validateUser(), userUnfollow); // TODO: Following and followers lists -// UsersGet.get( -// Paths.Users.Get.UserFollowers, -// async (req: RequestWithSession, res) => { -// const userId = getUserId(req); -// -// if (userId === undefined) -// return res -// .status(HttpStatusCodes.BAD_REQUEST) -// .json({ error: 'No user specified' }); -// -// const db = new DatabaseDriver(); -// -// const followers = await db.getAllWhere( -// 'followers', -// 'userId', -// userId, -// ); -// -// res.status(HttpStatusCodes.OK).json( -// JSON.stringify( -// { -// type: 'followers', -// userId: userId.toString(), -// followers: followers, -// }, -// (_, value) => { -// if (typeof value === 'bigint') { -// return value.toString(); -// } -// // eslint-disable-next-line @typescript-eslint/no-unsafe-return -// return value; -// }, -// ), -// ); -// }, -// ); -// -// UsersGet.get( -// Paths.Users.Get.UserFollowing, -// async (req: RequestWithSession, res) => { -// const userId = getUserId(req); -// -// if (userId === undefined) -// return res -// .status(HttpStatusCodes.BAD_REQUEST) -// .json({ error: 'No user specified' }); -// -// const db = new DatabaseDriver(); -// -// const following = await db.getAllWhere( -// 'followers', -// 'followerId', -// userId, -// ); -// -// res.status(HttpStatusCodes.OK).json( -// JSON.stringify( -// { -// type: 'following', -// userId: userId.toString(), -// following: following, -// }, -// (_, value) => { -// if (typeof value === 'bigint') { -// return value.toString(); -// } -// // eslint-disable-next-line @typescript-eslint/no-unsafe-return -// return value; -// }, -// ), -// ); -// }, -// ); export default UsersGet; From 6e78dd5b184245cd267fdaa9755b08300fc9cc93 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 22:02:11 +0300 Subject: [PATCH 086/118] Version Bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8722a74..bd87438 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "navigo-learn-api", - "version": "0.0.0", + "version": "2.0.0", "description": "Navigo Learn API", "repository": "https://github.com/NavigoLearn/API.git", "author": "Navigo", From dff5a07116be0d01e71df77039a7e8a83da90f64 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 22:31:45 +0300 Subject: [PATCH 087/118] Updated README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index dbaa2aa..f3535ca 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ |:------------:|:--------------------------------------------------------------------------------------------------------:| | Production | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=prod) | | Master | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=master) | -| Refactor | ![Build Status](https://github.com/navigolearn/api/actions/workflows/test.yml/badge.svg?branch=Refactor) | ## ! This is a work in progress ! From 03c2e4a4e352ac9ffb780768f687b99fa239b1b4 Mon Sep 17 00:00:00 2001 From: sopy Date: Wed, 6 Sep 2023 23:44:45 +0300 Subject: [PATCH 088/118] Improve Roadmap Explore feature with total counts --- src/controllers/exploreController.ts | 8 +++++++- src/controllers/roadmapController.ts | 4 ++-- src/controllers/usersController.ts | 3 +++ src/helpers/responses/roadmapResponses.ts | 2 ++ src/util/Database/ExploreDB.ts | 12 +++++++++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 4bdd0ac..5c092ba 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -9,11 +9,13 @@ import { ResRoadmap } from '@src/types/response/ResRoadmap'; function responseSearchRoadmaps( res: Response, roadmaps: ResRoadmap[], + total: bigint, ): unknown { return res.status(HttpStatusCodes.OK).json({ success: true, message: `Roadmaps ${roadmaps.length ? '' : 'not '}found`, data: roadmaps, + total: total, }); } @@ -25,5 +27,9 @@ export async function searchRoadmaps( const roadmaps = await db.getRoadmaps(req, req.session?.userId); - return responseSearchRoadmaps(res, roadmaps); + return responseSearchRoadmaps( + res, + roadmaps, + roadmaps[0]?.totalRoadmaps || 0n, + ); } diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 75fac67..1d3de7e 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -8,7 +8,7 @@ import Database from '@src/util/Database/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { responseNotAllowed, - responseRoadmap, + responseRoadmap, responseRoadmapAlreadyDisliked, responseRoadmapAlreadyLiked, responseRoadmapCreated, responseRoadmapDeleted, @@ -369,7 +369,7 @@ export async function dislikeRoadmap(req: RequestWithSession, res: Response) { } if (!liked) return responseServerError(res); - if (liked.value == -1) return responseRoadmapAlreadyLiked(res); + if (liked.value == -1) return responseRoadmapAlreadyDisliked(res); liked.set({ value: -1 }); diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 72f4db2..8f3daf1 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -129,6 +129,8 @@ export async function userGetRoadmaps( // check if user exists if (!roadmaps) return responseUserNoRoadmaps(res); + const totalRoadmaps = await db.countWhere('roadmaps', 'userId', userId); + const likes: bigint[] = []; const views: bigint[] = []; const isLiked: bigint[] = []; @@ -162,6 +164,7 @@ export async function userGetRoadmaps( (roadmap, i) => new ResRoadmap(roadmap, user, likes[i], views[i], isLiked[i]), ), + totalRoadmaps, ); } diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index b27ad8f..989994b 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -67,6 +67,7 @@ export function responseUserNoRoadmaps(res: Response): void { export function responseUserRoadmaps( res: Response, roadmaps: ResRoadmap[], + total: bigint, ): void { res .status(HttpStatusCodes.OK) @@ -76,6 +77,7 @@ export function responseUserRoadmaps( data: roadmaps, message: 'Roadmaps found', success: true, + total: total, }), ); } diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 3932b46..6832325 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -8,6 +8,10 @@ import { ResRoadmap } from '@src/types/response/ResRoadmap'; // database credentials const { DBCred } = EnvVars; +export interface ResRoadmapExplore extends ResRoadmap { + totalRoadmaps: bigint; +} + class ExploreDB extends Database { public constructor(config: DatabaseConfig = DBCred as DatabaseConfig) { super(config); @@ -19,7 +23,7 @@ class ExploreDB extends Database { limit, topic, order, - }: SearchParameters, userid?: bigint): Promise { + }: SearchParameters, userid?: bigint): Promise { if(!search || !page || !limit || !topic || !order) return []; const query = ` SELECT @@ -41,7 +45,9 @@ class ExploreDB extends Database { WHERE roadmapId = r.id AND userId = ? ) - ` : '0'} AS isLiked + ` : '0'} AS isLiked, + + (SELECT COUNT(*) FROM roadmaps) AS totalRoadmaps FROM roadmaps r INNER JOIN users u ON r.userId = u.id @@ -67,7 +73,7 @@ class ExploreDB extends Database { const result = await this.getQuery(query, params); if (result === null) return []; - return result as unknown as ResRoadmap[]; + return result as unknown as ResRoadmapExplore[]; } } From 175749abd72ee73b08a523c5ead824fa17e6e506 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 14:49:39 +0300 Subject: [PATCH 089/118] Refactor search parameters and JSON response safety Replaced 'JSONStringify' with 'JSONSafety' across all responses to ensure secure JSON objects. Refactored search parameters for more efficient and accurate results. Also altered the RoadmapTopic enum from ['programming', 'math', 'design', 'other'] to ['programming', 'math', 'physics', 'biology']. 'searchParam', 'pageParam', 'limitParam', 'topicParam', 'orderParam' from req.query were fixed to proper values. Test files were aligned to these changes. Paths for Exploring endpoint to '/search/roadmaps' from '/explore/search/roadmaps' for clarity and concise routing. Updated 'package-lock.json' to version 2.0.0. Additional checks for 'userId' were performed during queries for 'isLiked' statuses. 'setup.sql' topic enum was matched to application level 'RoadmapTopic'. --- package-lock.json | 4 +- spec/tests/routes/users.spec.ts | 6 +-- src/constants/Paths.ts | 7 +-- src/controllers/exploreController.ts | 9 ++-- src/helpers/responses/roadmapResponses.ts | 8 +-- src/helpers/responses/userResponses.ts | 6 +-- .../validators/validateSearchParameters.ts | 41 +++++++++----- src/routes/ExploreRouter.ts | 2 +- src/sql/setup.sql | 20 +++---- src/types/models/Roadmap.ts | 4 +- src/util/Database/ExploreDB.ts | 53 ++++++++++++++----- src/util/{JSONStringify.ts => JSONSafety.ts} | 6 +-- 12 files changed, 105 insertions(+), 61 deletions(-) rename src/util/{JSONStringify.ts => JSONSafety.ts} (77%) diff --git a/package-lock.json b/package-lock.json index 83cbb8c..b19809d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "navigo-learn-api", - "version": "0.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "navigo-learn-api", - "version": "0.0.0", + "version": "2.0.0", "license": "BSD 3-Clause", "dependencies": { "axios": "^1.4.0", diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts index 9bc1d9d..a7524ce 100644 --- a/spec/tests/routes/users.spec.ts +++ b/spec/tests/routes/users.spec.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; import { CreatedUser } from '@spec/types/tests/CreatedUser'; -import JSONStringify from '@src/util/JSONStringify'; +import JSONSafety from '@src/util/JSONSafety'; import { ResUserProfile } from '@src/types/response/ResUserProfile'; import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; @@ -51,7 +51,7 @@ describe('Get User Tests', () => { expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); expect(body.data).toEqual( JSON.parse( - JSONStringify(new ResUserMiniProfile(user.user.toObject())), + JSONSafety(new ResUserMiniProfile(user.user.toObject())), ), ); }); @@ -68,7 +68,7 @@ describe('Get User Tests', () => { expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); expect(body.data).toEqual( JSON.parse( - JSONStringify(new ResUserMiniProfile(user.user.toObject())), + JSONSafety(new ResUserMiniProfile(user.user.toObject())), ), ); }); diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 46394d7..666af75 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -17,11 +17,8 @@ const Paths = { Logout: '/logout', }, Explore: { - Base: '/explore', - Search: { - Base: '/search', - Roadmaps: '/roadmaps', - }, + Base: '/search', + Roadmaps: '/roadmaps', }, Roadmaps: { Base: '/roadmaps', diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 5c092ba..123b050 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -5,18 +5,19 @@ import { Response } from 'express'; import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; +import JSONSafety from '@src/util/JSONSafety'; function responseSearchRoadmaps( res: Response, roadmaps: ResRoadmap[], total: bigint, ): unknown { - return res.status(HttpStatusCodes.OK).json({ + return res.status(HttpStatusCodes.OK).json(JSONSafety({ success: true, message: `Roadmaps ${roadmaps.length ? '' : 'not '}found`, data: roadmaps, total: total, - }); + })); } export async function searchRoadmaps( @@ -29,7 +30,7 @@ export async function searchRoadmaps( return responseSearchRoadmaps( res, - roadmaps, - roadmaps[0]?.totalRoadmaps || 0n, + roadmaps.result, + roadmaps.totalRoadmaps, ); } diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index 989994b..ad9d996 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -1,6 +1,6 @@ import { Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import JSONStringify from '@src/util/JSONStringify'; +import JSONSafety from '@src/util/JSONSafety'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; @@ -8,7 +8,7 @@ export function responseRoadmap(res: Response, roadmap: ResFullRoadmap): void { res .status(HttpStatusCodes.OK) .contentType('application/json') - .send(JSONStringify({ + .send(JSONSafety({ data: roadmap, message: 'Roadmap found', success: true, @@ -56,7 +56,7 @@ export function responseUserNoRoadmaps(res: Response): void { .status(HttpStatusCodes.OK) .contentType('application/json') .send( - JSONStringify({ + JSONSafety({ data: [], message: 'User has no roadmaps', success: true, @@ -73,7 +73,7 @@ export function responseUserRoadmaps( .status(HttpStatusCodes.OK) .contentType('application/json') .send( - JSONStringify({ + JSONSafety({ data: roadmaps, message: 'Roadmaps found', success: true, diff --git a/src/helpers/responses/userResponses.ts b/src/helpers/responses/userResponses.ts index 9495d06..aa5a852 100644 --- a/src/helpers/responses/userResponses.ts +++ b/src/helpers/responses/userResponses.ts @@ -2,7 +2,7 @@ import { Response } from 'express'; import { User } from '@src/types/models/User'; import { UserInfo } from '@src/types/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; -import JSONStringify from '@src/util/JSONStringify'; +import JSONSafety from '@src/util/JSONSafety'; import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; import { ResUserProfile } from '@src/types/response/ResUserProfile'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; @@ -56,7 +56,7 @@ export function responseUserProfile( .status(HttpStatusCodes.OK) .contentType('application/json') .send( - JSONStringify({ + JSONSafety({ data: new ResUserProfile(user, userInfo, userStats, isFollowing), message: 'User found', success: true, @@ -69,7 +69,7 @@ export function responseUserMiniProfile(res: Response, user: User): void { .status(HttpStatusCodes.OK) .contentType('application/json') .send( - JSONStringify({ + JSONSafety({ data: new ResUserMiniProfile(user), message: 'User found', success: true, diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts index 1dd496b..cf1e39e 100644 --- a/src/middleware/validators/validateSearchParameters.ts +++ b/src/middleware/validators/validateSearchParameters.ts @@ -25,7 +25,7 @@ export default function ( next: NextFunction, ) { // get parameters from request - const { searchParam, pageParam, limitParam, topicParam, orderParam } = + const { query: searchParam, page: pageParam, limit: limitParam, topic: topicParam, order: orderParam } = req.query; const search = (searchParam as string) || ''; const page = parseInt((pageParam as string) || '1'); @@ -35,12 +35,12 @@ export default function ( ([ RoadmapTopic.PROGRAMMING, RoadmapTopic.MATH, - RoadmapTopic.DESIGN, - RoadmapTopic.OTHER, + RoadmapTopic.PHYSICS, + RoadmapTopic.BIOLOGY, ] as RoadmapTopic[]); let order: Order; - const [by, direction] = (orderParam as string).split(':'); + const [by, direction] = (orderParam as string)?.split(':') || ['age', 'DESC']; switch (by) { case 'views': order = { @@ -69,14 +69,31 @@ export default function ( order.direction = 'ASC'; } - topic = topic.filter((t) => { - return ( - t === RoadmapTopic.PROGRAMMING || - t === RoadmapTopic.MATH || - t === RoadmapTopic.DESIGN || - t === RoadmapTopic.OTHER - ); - }); + // make sure topic is valid + if (Array.isArray(topic)) { + topic = topic.filter((t) => { + return ( + t === RoadmapTopic.PROGRAMMING || + t === RoadmapTopic.MATH || + t === RoadmapTopic.PHYSICS || + t === RoadmapTopic.BIOLOGY + ); + }); + } else { + if ( + topic !== RoadmapTopic.PROGRAMMING && + topic !== RoadmapTopic.MATH && + topic !== RoadmapTopic.PHYSICS && + topic !== RoadmapTopic.BIOLOGY + ) topic = [ + RoadmapTopic.PROGRAMMING, + RoadmapTopic.MATH, + RoadmapTopic.PHYSICS, + RoadmapTopic.BIOLOGY, + ]; + } + + console.log("search parameters", search, page, limit, topic, order); req.search = search; req.page = page; diff --git a/src/routes/ExploreRouter.ts b/src/routes/ExploreRouter.ts index 0e6d9b1..6fbd5a5 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/ExploreRouter.ts @@ -7,7 +7,7 @@ import { searchRoadmaps } from '@src/controllers/exploreController'; const ExploreRouter = Router(); ExploreRouter.get( - Paths.Explore.Search.Base + Paths.Explore.Search.Roadmaps, + Paths.Explore.Roadmaps, validateSearchParameters, searchRoadmaps, ); diff --git a/src/sql/setup.sql b/src/sql/setup.sql index e9cb410..2fb5176 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -37,16 +37,16 @@ create table if not exists roadmaps ( id bigint auto_increment primary key, - name varchar(255) not null, - description varchar(255) not null, - topic enum ('programming', 'math', 'design', 'other') not null, - userId bigint not null, - isFeatured tinyint(1) default 0 not null, - isPublic tinyint(1) default 1 not null, - isDraft tinyint(1) default 0 not null, - data longtext not null, - createdAt timestamp default current_timestamp() not null, - updatedAt timestamp default current_timestamp() not null on update current_timestamp(), + name varchar(255) not null, + description varchar(255) not null, + topic enum ('programming', 'math', 'phisics', 'biology') not null, + userId bigint not null, + isFeatured tinyint(1) default 0 not null, + isPublic tinyint(1) default 1 not null, + isDraft tinyint(1) default 0 not null, + data longtext not null, + createdAt timestamp default current_timestamp() not null, + updatedAt timestamp default current_timestamp() not null on update current_timestamp(), constraint roadmaps_userId_fk foreign key (userId) references users (id) on delete cascade diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index 285c9d8..c7adf50 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -1,8 +1,8 @@ export enum RoadmapTopic { PROGRAMMING = 'programming', MATH = 'math', - DESIGN = 'design', - OTHER = 'other', + PHYSICS = 'physics', + BIOLOGY = 'biology', } // Interface for full Roadmap object diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 6832325..1119c5f 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -8,7 +8,8 @@ import { ResRoadmap } from '@src/types/response/ResRoadmap'; // database credentials const { DBCred } = EnvVars; -export interface ResRoadmapExplore extends ResRoadmap { +export interface ResRoadmapExplore { + result: ResRoadmap[]; totalRoadmaps: bigint; } @@ -23,8 +24,9 @@ class ExploreDB extends Database { limit, topic, order, - }: SearchParameters, userid?: bigint): Promise { - if(!search || !page || !limit || !topic || !order) return []; + }: SearchParameters, userid?: bigint): Promise { + if(typeof search != 'string' || !page || !limit || !topic || !order) + return { result: [], totalRoadmaps: 0n }; const query = ` SELECT r.id as id, @@ -39,21 +41,42 @@ class ExploreDB extends Database { u.name AS userName, (SELECT COUNT(*) FROM roadmapLikes WHERE roadmapId = r.id) AS likeCount, (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, - u.avatar AS userAvatar, - u.name AS userName, - ${!!userid ? `(SELECT COUNT(*) FROM roadmapLikes; + ${!!userid ? `(SELECT value FROM roadmapLikes; WHERE roadmapId = r.id AND userId = ? ) - ` : '0'} AS isLiked, + ` : '0'} AS isLiked + FROM + roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE + r.name LIKE ? + AND r.topic IN (${Array.isArray(topic) ? + topic.map(() => '?').join(', ') : + '?'}) + AND r.isPublic = 1 + AND r.isDraft = 0 + ORDER BY + r.isFeatured DESC, ${order.by} ${order.direction} + LIMIT ?, ?; + `; - (SELECT COUNT(*) FROM roadmaps) AS totalRoadmaps + const query2 = ` + SELECT + COUNT(*) AS result, + ${!!userid ? `(SELECT value FROM roadmapLikes; + WHERE roadmapId = r.id + AND userId = ? + ) + ` : '0'} AS isLiked FROM roadmaps r INNER JOIN users u ON r.userId = u.id WHERE r.name LIKE ? - AND r.topic IN (?) + AND r.topic IN (${Array.isArray(topic) ? + topic.map(() => '?').join(', ') : + '?'}) AND r.isPublic = 1 AND r.isDraft = 0 ORDER BY @@ -67,13 +90,19 @@ class ExploreDB extends Database { params.push(userid); } params.push(`%${search}%`); - params.push(topic.map((t) => t.toString()).join(',')); + if (Array.isArray(topic)) + topic.forEach((t) => params.push(t.toString())); + else params.push(topic); params.push((page - 1) * limit); params.push(limit); const result = await this.getQuery(query, params); - if (result === null) return []; - return result as unknown as ResRoadmapExplore[]; + const result2 = await this.countQuery(query2, params); + if (result === null) return { result: [], totalRoadmaps: 0n }; + return { + result: result as unknown as ResRoadmap[], + totalRoadmaps: result2 + }; } } diff --git a/src/util/JSONStringify.ts b/src/util/JSONSafety.ts similarity index 77% rename from src/util/JSONStringify.ts rename to src/util/JSONSafety.ts index 0ccd4eb..80d0c1a 100644 --- a/src/util/JSONStringify.ts +++ b/src/util/JSONSafety.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -export default function JSONStringify(obj: unknown): string { - return JSON.stringify(obj, (key, value) => { +export default function JSONSafety(obj: unknown): unknown { + return JSON.parse(JSON.stringify(obj, (key, value) => { // if value is a bigint, convert it to a string if (typeof value === 'bigint') return value.toString(); // if value has a toObject method, call it and return the result @@ -15,5 +15,5 @@ export default function JSONStringify(obj: unknown): string { // return value as is return value; - }); + })); } From 1a03aec67932075fbc2a0257cfba516a3c6cc4c1 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 16:32:30 +0300 Subject: [PATCH 090/118] BugFix Likes/dislikes and console.log removed everywhere --- src/controllers/roadmapController.ts | 20 ++++++++++---------- src/helpers/databaseManagement.ts | 3 +++ src/helpers/responses/roadmapResponses.ts | 7 +++++++ src/routes/RoadmapsRouter.ts | 4 ++-- src/util/Database/ExploreDB.ts | 14 +++++++------- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 1d3de7e..85170c0 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -14,7 +14,7 @@ import { responseRoadmapDeleted, responseRoadmapNotFound, responseRoadmapNotRated, - responseRoadmapRated, + responseRoadmapRated, responseRoadmapUnrated, responseRoadmapUpdated, } from '@src/helpers/responses/roadmapResponses'; import { @@ -316,9 +316,9 @@ export async function likeRoadmap(req: RequestWithSession, res: Response) { const db = new Database(); - const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + const likeEntry = await getRoadmapLike(db, userId, BigInt(roadmapId)); - if (!liked) { + if (!likeEntry) { if ( (await insertRoadmapLike( db, @@ -332,12 +332,12 @@ export async function likeRoadmap(req: RequestWithSession, res: Response) { return responseRoadmapRated(res); } - if (!liked) return responseServerError(res); - if (liked.value == 1) return responseRoadmapAlreadyLiked(res); + if (!likeEntry) return responseServerError(res); + if (likeEntry.value == 1) return responseRoadmapAlreadyLiked(res); - liked.set({ value: 1 }); + likeEntry.set({ value: 1 }); - if (await updateRoadmapLike(db, liked.id, liked)) + if (await updateRoadmapLike(db, likeEntry.id, likeEntry)) return responseRoadmapRated(res); return responseServerError(res); @@ -352,7 +352,7 @@ export async function dislikeRoadmap(req: RequestWithSession, res: Response) { const db = new Database(); - const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + const liked = await getRoadmapLike(db, userId, BigInt(roadmapId)); if (!liked) { if ( @@ -391,12 +391,12 @@ export async function removeLikeRoadmap( const db = new Database(); - const liked = await getRoadmapLike(db, BigInt(roadmapId), userId); + const liked = await getRoadmapLike(db, userId, BigInt(roadmapId),); if (!liked) return responseRoadmapNotRated(res); if (await db.delete('roadmapLikes', liked.id)) - return responseRoadmapRated(res); + return responseRoadmapUnrated(res); return responseServerError(res); } diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index fc32e9f..6a65cb7 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -238,6 +238,7 @@ export async function getRoadmapLike( userId: bigint, roadmapId: bigint, ): Promise { + console.log(userId, roadmapId) const like = await db.getWhere( 'roadmapLikes', 'userId', @@ -245,7 +246,9 @@ export async function getRoadmapLike( 'roadmapId', roadmapId, ); + if (!like) return null; + return new RoadmapLike(like); } diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index ad9d996..ac80f89 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -109,3 +109,10 @@ export function responseRoadmapRated(res: Response) { success: true, }); } + +export function responseRoadmapUnrated(res: Response) { + return res.status(HttpStatusCodes.OK).json({ + message: 'Roadmap unrated', + success: true, + }); +} diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 47d1305..d0750ce 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -32,8 +32,8 @@ RoadmapsRouter.delete(Paths.Roadmaps.Delete, validateSession, deleteRoadmap); RoadmapsRouter.all(Paths.Roadmaps.Like, validateSession); RoadmapsRouter.all(Paths.Roadmaps.Dislike, validateSession); -RoadmapsRouter.post(Paths.Roadmaps.Like, likeRoadmap); -RoadmapsRouter.post(Paths.Roadmaps.Dislike, dislikeRoadmap); +RoadmapsRouter.get(Paths.Roadmaps.Like, likeRoadmap); +RoadmapsRouter.get(Paths.Roadmaps.Dislike, dislikeRoadmap); RoadmapsRouter.delete(Paths.Roadmaps.Like, removeLikeRoadmap); RoadmapsRouter.delete(Paths.Roadmaps.Dislike, removeLikeRoadmap); diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 1119c5f..226612b 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -39,9 +39,9 @@ class ExploreDB extends Database { u.id AS userId, u.avatar AS userAvatar, u.name AS userName, - (SELECT COUNT(*) FROM roadmapLikes WHERE roadmapId = r.id) AS likeCount, + (SELECT SUM(rl.value) FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, - ${!!userid ? `(SELECT value FROM roadmapLikes; + ${!!userid ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? ) @@ -50,7 +50,7 @@ class ExploreDB extends Database { roadmaps r INNER JOIN users u ON r.userId = u.id WHERE - r.name LIKE ? + r.name LIKE ? or r.description LIKE ? AND r.topic IN (${Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?'}) @@ -64,16 +64,15 @@ class ExploreDB extends Database { const query2 = ` SELECT COUNT(*) AS result, - ${!!userid ? `(SELECT value FROM roadmapLikes; + ${!!userid ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? - ) - ` : '0'} AS isLiked + )` : '0'} AS isLiked FROM roadmaps r INNER JOIN users u ON r.userId = u.id WHERE - r.name LIKE ? + r.name LIKE ? or r.description LIKE ? AND r.topic IN (${Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?'}) @@ -90,6 +89,7 @@ class ExploreDB extends Database { params.push(userid); } params.push(`%${search}%`); + params.push(`%${search}%`); if (Array.isArray(topic)) topic.forEach((t) => params.push(t.toString())); else params.push(topic); From abed53207b494b934b9dff574a60b60647df1359 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 16:37:26 +0300 Subject: [PATCH 091/118] Eslint fixing --- src/controllers/roadmapController.ts | 2 +- src/helpers/databaseManagement.ts | 1 - src/helpers/responses/roadmapResponses.ts | 20 ++++++++++--------- .../validators/validateSearchParameters.ts | 13 +++++++----- src/util/Database/ExploreDB.ts | 17 +++++++++------- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 85170c0..bbd20d3 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -391,7 +391,7 @@ export async function removeLikeRoadmap( const db = new Database(); - const liked = await getRoadmapLike(db, userId, BigInt(roadmapId),); + const liked = await getRoadmapLike(db, userId, BigInt(roadmapId)); if (!liked) return responseRoadmapNotRated(res); diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 6a65cb7..be76460 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -238,7 +238,6 @@ export async function getRoadmapLike( userId: bigint, roadmapId: bigint, ): Promise { - console.log(userId, roadmapId) const like = await db.getWhere( 'roadmapLikes', 'userId', diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index ac80f89..256cd8c 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -8,11 +8,13 @@ export function responseRoadmap(res: Response, roadmap: ResFullRoadmap): void { res .status(HttpStatusCodes.OK) .contentType('application/json') - .send(JSONSafety({ - data: roadmap, - message: 'Roadmap found', - success: true, - })); + .send( + JSONSafety({ + data: roadmap, + message: 'Roadmap found', + success: true, + }), + ); } export function responseRoadmapUpdated(res: Response): void { @@ -111,8 +113,8 @@ export function responseRoadmapRated(res: Response) { } export function responseRoadmapUnrated(res: Response) { - return res.status(HttpStatusCodes.OK).json({ - message: 'Roadmap unrated', - success: true, - }); + return res.status(HttpStatusCodes.OK).json({ + message: 'Roadmap unrated', + success: true, + }); } diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts index cf1e39e..b2d5671 100644 --- a/src/middleware/validators/validateSearchParameters.ts +++ b/src/middleware/validators/validateSearchParameters.ts @@ -25,12 +25,17 @@ export default function ( next: NextFunction, ) { // get parameters from request - const { query: searchParam, page: pageParam, limit: limitParam, topic: topicParam, order: orderParam } = - req.query; + const { + query: searchParam, + page: pageParam, + limit: limitParam, + topic: topicParam, + order: orderParam } = + req.query; const search = (searchParam as string) || ''; const page = parseInt((pageParam as string) || '1'); const limit = parseInt((limitParam as string) || '12'); - let topic = + let topic: RoadmapTopic | RoadmapTopic[] = (topicParam as RoadmapTopic[]) || ([ RoadmapTopic.PROGRAMMING, @@ -93,8 +98,6 @@ export default function ( ]; } - console.log("search parameters", search, page, limit, topic, order); - req.search = search; req.page = page; req.limit = limit; diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 226612b..3a1e1ec 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -39,7 +39,8 @@ class ExploreDB extends Database { u.id AS userId, u.avatar AS userAvatar, u.name AS userName, - (SELECT SUM(rl.value) FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, + (SELECT SUM(rl.value) + FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, ${!!userid ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id @@ -51,9 +52,11 @@ class ExploreDB extends Database { INNER JOIN users u ON r.userId = u.id WHERE r.name LIKE ? or r.description LIKE ? - AND r.topic IN (${Array.isArray(topic) ? - topic.map(() => '?').join(', ') : - '?'}) + AND r.topic IN (${ + Array.isArray(topic) ? + topic.map(() => '?').join(', ') : + '?' +}) AND r.isPublic = 1 AND r.isDraft = 0 ORDER BY @@ -74,8 +77,8 @@ class ExploreDB extends Database { WHERE r.name LIKE ? or r.description LIKE ? AND r.topic IN (${Array.isArray(topic) ? - topic.map(() => '?').join(', ') : - '?'}) + topic.map(() => '?').join(', ') : + '?'}) AND r.isPublic = 1 AND r.isDraft = 0 ORDER BY @@ -101,7 +104,7 @@ class ExploreDB extends Database { if (result === null) return { result: [], totalRoadmaps: 0n }; return { result: result as unknown as ResRoadmap[], - totalRoadmaps: result2 + totalRoadmaps: result2, }; } } From c5961d4d677bc5ac3bfe5eef1b10cab8fe3a1625 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 16:41:59 +0300 Subject: [PATCH 092/118] Test Fixed --- spec/tests/routes/users.spec.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts index a7524ce..d788faf 100644 --- a/spec/tests/routes/users.spec.ts +++ b/spec/tests/routes/users.spec.ts @@ -50,9 +50,7 @@ describe('Get User Tests', () => { expect(body.success).toBe(true); expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); expect(body.data).toEqual( - JSON.parse( - JSONSafety(new ResUserMiniProfile(user.user.toObject())), - ), + JSONSafety(new ResUserMiniProfile(user.user.toObject())), ); }); }); @@ -66,10 +64,8 @@ describe('Get User Tests', () => { expect(body.success).toBe(true); expect(body.data).toBeDefined(); expect(ResUserMiniProfile.isMiniProfile(body.data)).toBe(true); - expect(body.data).toEqual( - JSON.parse( - JSONSafety(new ResUserMiniProfile(user.user.toObject())), - ), + expect(JSONSafety(body.data)).toEqual( + JSONSafety(new ResUserMiniProfile(user.user.toObject())), ); }); }); From 2c8ea38e9dd1e2207380d2c3e31acee424a4a334 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 19:44:46 +0300 Subject: [PATCH 093/118] Fixed SQL ExploreDB.ts query and fix spelling error in setup This commit fixes a spelling mistake in setup.sql's enum declaration, changing 'phisics' to 'physics'. It also updates the SQL queries in ExploreDB.ts. The Boolean OR conditions for searching both 'name' and 'description' were grouped inside parentheses for explicit precedence and ensure the correct application of LIKE clause. --- src/sql/setup.sql | 2 +- src/util/Database/ExploreDB.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sql/setup.sql b/src/sql/setup.sql index 2fb5176..d43799b 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -39,7 +39,7 @@ create table if not exists roadmaps primary key, name varchar(255) not null, description varchar(255) not null, - topic enum ('programming', 'math', 'phisics', 'biology') not null, + topic enum ('programming', 'math', 'physics', 'biology') not null, userId bigint not null, isFeatured tinyint(1) default 0 not null, isPublic tinyint(1) default 1 not null, diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 3a1e1ec..86aa6a9 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -51,7 +51,7 @@ class ExploreDB extends Database { roadmaps r INNER JOIN users u ON r.userId = u.id WHERE - r.name LIKE ? or r.description LIKE ? + (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${ Array.isArray(topic) ? topic.map(() => '?').join(', ') : @@ -75,7 +75,7 @@ class ExploreDB extends Database { roadmaps r INNER JOIN users u ON r.userId = u.id WHERE - r.name LIKE ? or r.description LIKE ? + (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?'}) From fa1d46e3ca09e3c07d75d99a86b77578139cec45 Mon Sep 17 00:00:00 2001 From: erupturatis Date: Thu, 7 Sep 2023 20:25:24 +0300 Subject: [PATCH 094/118] (fix)[hotfix rm pagination from total roadmaps query] --- env | 2 +- src/controllers/exploreController.ts | 26 +++++----- src/util/Database/DatabaseDriver.ts | 6 ++- src/util/Database/ExploreDB.ts | 75 ++++++++++++---------------- 4 files changed, 49 insertions(+), 60 deletions(-) diff --git a/env b/env index a9f299d..fcef617 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit a9f299d52284837bade9be9f0f6b733f0b6b70bd +Subproject commit fcef6176b5f4acfd07b0531d59d344668076f003 diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 123b050..fc06098 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -1,23 +1,24 @@ -import { - RequestWithSearchParameters, -} from '@src/middleware/validators/validateSearchParameters'; +import { RequestWithSearchParameters } from '@src/middleware/validators/validateSearchParameters'; import { Response } from 'express'; import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; import JSONSafety from '@src/util/JSONSafety'; +import * as console from 'console'; function responseSearchRoadmaps( res: Response, roadmaps: ResRoadmap[], total: bigint, ): unknown { - return res.status(HttpStatusCodes.OK).json(JSONSafety({ - success: true, - message: `Roadmaps ${roadmaps.length ? '' : 'not '}found`, - data: roadmaps, - total: total, - })); + return res.status(HttpStatusCodes.OK).json( + JSONSafety({ + success: true, + message: `Roadmaps ${roadmaps.length ? '' : 'not '}found`, + data: roadmaps, + total: total, + }), + ); } export async function searchRoadmaps( @@ -27,10 +28,7 @@ export async function searchRoadmaps( const db = new ExploreDB(); const roadmaps = await db.getRoadmaps(req, req.session?.userId); + console.log(roadmaps); - return responseSearchRoadmaps( - res, - roadmaps.result, - roadmaps.totalRoadmaps, - ); + return responseSearchRoadmaps(res, roadmaps.result, roadmaps.totalRoadmaps); } diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index e722838..5841d44 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -5,6 +5,7 @@ import path from 'path'; import logger from 'jet-logger'; import { User } from '@src/types/models/User'; import { GenericModelClass } from '@src/types/models/GenericModelClass'; +import * as console from 'console'; // database credentials const { DBCred } = EnvVars; @@ -376,7 +377,7 @@ class Database { FROM ${table}`; const result = await this._query(sql); - return (result as CountDataPacket[])[0][`SUM(${column})`] as bigint || 0n; + return ((result as CountDataPacket[])[0][`SUM(${column})`] as bigint) || 0n; } public async sumQuery(sql: string, params?: unknown[]): Promise { @@ -414,11 +415,12 @@ class Database { WHERE ${queryBuilderResult.keyString}`; const result = await this._query(sql, queryBuilderResult.params); - return (result as CountDataPacket[])[0][`SUM(${column})`] as bigint || 0n; + return ((result as CountDataPacket[])[0][`SUM(${column})`] as bigint) || 0n; } public async countQuery(sql: string, params?: unknown[]): Promise { const result = await this._query(sql, params); + console.log(result); return (result as CountQueryPacket[])[0]['result'] || 0n; } diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 86aa6a9..cb6d739 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -1,9 +1,8 @@ import Database, { DatabaseConfig } from '@src/util/Database/DatabaseDriver'; import EnvVars from '@src/constants/EnvVars'; -import { - SearchParameters, -} from '@src/middleware/validators/validateSearchParameters'; +import { SearchParameters } from '@src/middleware/validators/validateSearchParameters'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; +import * as console from 'console'; // database credentials const { DBCred } = EnvVars; @@ -18,14 +17,11 @@ class ExploreDB extends Database { super(config); } - public async getRoadmaps({ - search, - page, - limit, - topic, - order, - }: SearchParameters, userid?: bigint): Promise { - if(typeof search != 'string' || !page || !limit || !topic || !order) + public async getRoadmaps( + { search, page, limit, topic, order }: SearchParameters, + userid?: bigint, + ): Promise { + if (typeof search != 'string' || !page || !limit || !topic || !order) return { result: [], totalRoadmaps: 0n }; const query = ` SELECT @@ -42,65 +38,58 @@ class ExploreDB extends Database { (SELECT SUM(rl.value) FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, - ${!!userid ? `(SELECT value FROM roadmapLikes + ${ + !!userid + ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? ) - ` : '0'} AS isLiked + ` + : '0' + } AS isLiked FROM roadmaps r INNER JOIN users u ON r.userId = u.id WHERE (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${ - Array.isArray(topic) ? - topic.map(() => '?').join(', ') : - '?' -}) + Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' + }) AND r.isPublic = 1 AND r.isDraft = 0 ORDER BY r.isFeatured DESC, ${order.by} ${order.direction} LIMIT ?, ?; `; - const query2 = ` - SELECT - COUNT(*) AS result, - ${!!userid ? `(SELECT value FROM roadmapLikes - WHERE roadmapId = r.id - AND userId = ? - )` : '0'} AS isLiked - FROM - roadmaps r - INNER JOIN users u ON r.userId = u.id - WHERE - (r.name LIKE ? OR r.description LIKE ?) - AND r.topic IN (${Array.isArray(topic) ? - topic.map(() => '?').join(', ') : - '?'}) - AND r.isPublic = 1 - AND r.isDraft = 0 - ORDER BY - r.isFeatured DESC, ${order.by} ${order.direction} - LIMIT ?, ?; - `; - + SELECT + COUNT(*) AS result + FROM + roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE + (r.name LIKE ? OR r.description LIKE ?) + AND r.topic IN (${ + Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' + }) + AND r.isPublic = 1 + AND r.isDraft = 0; + `; const params = []; - + if (!!userid) { params.push(userid); } params.push(`%${search}%`); params.push(`%${search}%`); - if (Array.isArray(topic)) - topic.forEach((t) => params.push(t.toString())); + if (Array.isArray(topic)) topic.forEach((t) => params.push(t.toString())); else params.push(topic); params.push((page - 1) * limit); params.push(limit); const result = await this.getQuery(query, params); const result2 = await this.countQuery(query2, params); + if (result === null) return { result: [], totalRoadmaps: 0n }; return { result: result as unknown as ResRoadmap[], @@ -109,4 +98,4 @@ class ExploreDB extends Database { } } -export { ExploreDB }; \ No newline at end of file +export { ExploreDB }; From 9407cb93745ab46234186c70a4e2520b548f9b2f Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 22:13:08 +0300 Subject: [PATCH 095/118] Remove console log and adjust data processing in ExploreDB --- src/controllers/exploreController.ts | 2 -- src/util/Database/ExploreDB.ts | 34 ++++++++++++++++------------ src/util/JSONSafety.ts | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index fc06098..199a6c0 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -4,7 +4,6 @@ import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; import JSONSafety from '@src/util/JSONSafety'; -import * as console from 'console'; function responseSearchRoadmaps( res: Response, @@ -28,7 +27,6 @@ export async function searchRoadmaps( const db = new ExploreDB(); const roadmaps = await db.getRoadmaps(req, req.session?.userId); - console.log(roadmaps); return responseSearchRoadmaps(res, roadmaps.result, roadmaps.totalRoadmaps); } diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index cb6d739..bd8b9c3 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -2,7 +2,6 @@ import Database, { DatabaseConfig } from '@src/util/Database/DatabaseDriver'; import EnvVars from '@src/constants/EnvVars'; import { SearchParameters } from '@src/middleware/validators/validateSearchParameters'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; -import * as console from 'console'; // database credentials const { DBCred } = EnvVars; @@ -62,18 +61,25 @@ class ExploreDB extends Database { LIMIT ?, ?; `; const query2 = ` - SELECT - COUNT(*) AS result - FROM - roadmaps r - INNER JOIN users u ON r.userId = u.id - WHERE - (r.name LIKE ? OR r.description LIKE ?) - AND r.topic IN (${ - Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' - }) - AND r.isPublic = 1 - AND r.isDraft = 0; + SELECT + count(*) AS result, + ${ + !!userid + ? `(SELECT value FROM roadmapLikes + WHERE roadmapId = r.id + AND userId = ? + )` : '0' + } AS isLiked + FROM + roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE + (r.name LIKE ? OR r.description LIKE ?) + AND r.topic IN (${ + Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' + }) + AND r.isPublic = 1 + AND r.isDraft = 0; `; const params = []; @@ -88,7 +94,7 @@ class ExploreDB extends Database { params.push(limit); const result = await this.getQuery(query, params); - const result2 = await this.countQuery(query2, params); + const result2 = await this.countQuery(query2, params.slice(0, -2)); if (result === null) return { result: [], totalRoadmaps: 0n }; return { diff --git a/src/util/JSONSafety.ts b/src/util/JSONSafety.ts index 80d0c1a..e6c2eb2 100644 --- a/src/util/JSONSafety.ts +++ b/src/util/JSONSafety.ts @@ -2,7 +2,7 @@ export default function JSONSafety(obj: unknown): unknown { return JSON.parse(JSON.stringify(obj, (key, value) => { // if value is a bigint, convert it to a string - if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'bigint') return Number(value); // if value has a toObject method, call it and return the result else if ( typeof value === 'object' && From 52acb5358c9c40ec188088c3895d49d268169a07 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 23:04:58 +0300 Subject: [PATCH 096/118] Improve data validation and refactor helper functions Added and utilized an isEmptyObject helper function in the misc.ts file (was JSONSafety.ts) to better handle data validation in authController.ts. Renamed JSONSafety.ts to misc.ts as it now houses more than JSON related functionality. Used the new helper function to enhance null and empty object validation in several checks. Also, updated data update mechanism in databaseManagement.ts file to leverage newly implemented updateWhere function within DatabaseDriver.ts to offer more flexibility. --- spec/tests/routes/users.spec.ts | 2 +- src/controllers/authController.ts | 17 ++++++++----- src/controllers/exploreController.ts | 2 +- src/helpers/databaseManagement.ts | 2 +- src/helpers/responses/roadmapResponses.ts | 2 +- src/helpers/responses/userResponses.ts | 2 +- src/util/Database/DatabaseDriver.ts | 30 +++++++++++++++++++++++ src/util/{JSONSafety.ts => misc.ts} | 6 ++++- 8 files changed, 51 insertions(+), 12 deletions(-) rename src/util/{JSONSafety.ts => misc.ts} (71%) diff --git a/spec/tests/routes/users.spec.ts b/spec/tests/routes/users.spec.ts index d788faf..744258b 100644 --- a/spec/tests/routes/users.spec.ts +++ b/spec/tests/routes/users.spec.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import app from '@src/server'; import httpStatusCodes from '@src/constants/HttpStatusCodes'; import { CreatedUser } from '@spec/types/tests/CreatedUser'; -import JSONSafety from '@src/util/JSONSafety'; +import { JSONSafety } from '@src/util/misc'; import { ResUserProfile } from '@src/types/response/ResUserProfile'; import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 7c71278..9edfa8d 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -37,6 +37,7 @@ import { responseLogoutSuccessful, responsePasswordChanged, } from '@src/helpers/responses/authResponses'; +import { isEmptyObject } from '@src/util/misc'; /* * Interfaces @@ -214,7 +215,7 @@ export async function authGoogleCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return responseServerError(res); + if (isEmptyObject(response.data)) return responseServerError(res); // get access token from response const data = response.data as { access_token?: string }; @@ -319,7 +320,8 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return responseServerError(res); + if (isEmptyObject(response.data)) + return responseServerError(res); // get access token from response const data = response.data as { access_token?: string }; @@ -336,11 +338,13 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (!response.data) return responseServerError(res); + if (isEmptyObject(response.data)) + return responseServerError(res); // get user data const userData = response.data as GitHubUserData; - if (!userData) return responseServerError(res); + if (isEmptyObject(userData) && !userData) + return responseServerError(res); // get email from github response = await axios.get('https://api.github.com/user/emails', { @@ -365,7 +369,8 @@ export async function authGitHubCallback( userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; // check if email is valid - if (userData.email == '') return responseServerError(res); + if (userData.email === '') + return responseServerError(res); // get database const db = new DatabaseDriver(); @@ -373,7 +378,7 @@ export async function authGitHubCallback( // check if user exists let user = await getUserByEmail(db, userData.email); - if (!user) { + if (user === null && !isEmptyObject(user)) { // create user user = new User({ name: userData.name || userData.login, diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index 123b050..4558ebd 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -5,7 +5,7 @@ import { Response } from 'express'; import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; -import JSONSafety from '@src/util/JSONSafety'; +import { JSONSafety } from '@src/util/misc'; function responseSearchRoadmaps( res: Response, diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index be76460..3f8b90d 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -199,7 +199,7 @@ export async function updateUserInfo( userId: bigint, userInfo: UserInfo, ): Promise { - return await db.update('userInfo', userId, userInfo); + return await db.updateWhere('userInfo', userInfo, 'userId', userId); } export async function getRoadmapData( diff --git a/src/helpers/responses/roadmapResponses.ts b/src/helpers/responses/roadmapResponses.ts index 256cd8c..60b4aa2 100644 --- a/src/helpers/responses/roadmapResponses.ts +++ b/src/helpers/responses/roadmapResponses.ts @@ -1,6 +1,6 @@ import { Response } from 'express'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; -import JSONSafety from '@src/util/JSONSafety'; +import { JSONSafety } from '@src/util/misc'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; diff --git a/src/helpers/responses/userResponses.ts b/src/helpers/responses/userResponses.ts index aa5a852..1e7bbad 100644 --- a/src/helpers/responses/userResponses.ts +++ b/src/helpers/responses/userResponses.ts @@ -2,7 +2,7 @@ import { Response } from 'express'; import { User } from '@src/types/models/User'; import { UserInfo } from '@src/types/models/UserInfo'; import { UserStats } from '@src/helpers/databaseManagement'; -import JSONSafety from '@src/util/JSONSafety'; +import { JSONSafety } from '@src/util/misc'; import { ResUserMiniProfile } from '@src/types/response/ResUserMiniProfile'; import { ResUserProfile } from '@src/types/response/ResUserProfile'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index e722838..2b4dafc 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -220,6 +220,36 @@ class Database { return affectedRows > 0; } + public async updateWhere( + table: string, + data: object | Record, + ...values: unknown[] + ): Promise { + const { keys, values: dataValues } = processData(data); + + const queryBuilderResult = this._buildWhereQuery(false, ...values); + if (!queryBuilderResult) return false; + + // create sql query - update table set key = ?, key = ? where id = ? + // ? for values to be replaced by params + const sqlKeys = keys.map((key) => `${key} = ?`).join(','); + const sql = `UPDATE ${table} + SET ${sqlKeys} + WHERE ${queryBuilderResult.keyString}`; + const params = [...dataValues, ...queryBuilderResult.params]; + + // execute query + const result = (await this._query(sql, params)) as ResultSetHeader; + + let affectedRows = -1; + if (result) { + affectedRows = result.affectedRows || -1; + } + + // return true if affected rows > 0 else false + return affectedRows > 0; + } + public async delete(table: string, id: bigint): Promise { const sql = `DELETE FROM ${table} diff --git a/src/util/JSONSafety.ts b/src/util/misc.ts similarity index 71% rename from src/util/JSONSafety.ts rename to src/util/misc.ts index 80d0c1a..6fa5a3b 100644 --- a/src/util/JSONSafety.ts +++ b/src/util/misc.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -export default function JSONSafety(obj: unknown): unknown { +export function JSONSafety(obj: unknown): unknown { return JSON.parse(JSON.stringify(obj, (key, value) => { // if value is a bigint, convert it to a string if (typeof value === 'bigint') return value.toString(); @@ -17,3 +17,7 @@ export default function JSONSafety(obj: unknown): unknown { return value; })); } + +export function isEmptyObject(obj: any): obj is Record { + return obj && typeof obj === 'object' && Object.keys(obj).length === 0 +} \ No newline at end of file From 5aece6fa123c7cb6d96eddd7e2b9acb1c8d1b476 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 23:06:19 +0300 Subject: [PATCH 097/118] Eslint fix --- src/controllers/exploreController.ts | 4 +++- src/util/Database/ExploreDB.ts | 26 ++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/controllers/exploreController.ts b/src/controllers/exploreController.ts index fbcde02..34bf966 100644 --- a/src/controllers/exploreController.ts +++ b/src/controllers/exploreController.ts @@ -1,4 +1,6 @@ -import { RequestWithSearchParameters } from '@src/middleware/validators/validateSearchParameters'; +import { + RequestWithSearchParameters, +} from '@src/middleware/validators/validateSearchParameters'; import { Response } from 'express'; import { ExploreDB } from '@src/util/Database/ExploreDB'; import HttpStatusCodes from '@src/constants/HttpStatusCodes'; diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index bd8b9c3..406a0fc 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -1,6 +1,8 @@ import Database, { DatabaseConfig } from '@src/util/Database/DatabaseDriver'; import EnvVars from '@src/constants/EnvVars'; -import { SearchParameters } from '@src/middleware/validators/validateSearchParameters'; +import { + SearchParameters, +} from '@src/middleware/validators/validateSearchParameters'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; // database credentials @@ -38,22 +40,22 @@ class ExploreDB extends Database { FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, ${ - !!userid - ? `(SELECT value FROM roadmapLikes + !!userid + ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? ) ` - : '0' - } AS isLiked + : '0' +} AS isLiked FROM roadmaps r INNER JOIN users u ON r.userId = u.id WHERE (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${ - Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' - }) + Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' +}) AND r.isPublic = 1 AND r.isDraft = 0 ORDER BY @@ -64,20 +66,20 @@ class ExploreDB extends Database { SELECT count(*) AS result, ${ - !!userid - ? `(SELECT value FROM roadmapLikes + !!userid + ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? )` : '0' - } AS isLiked +} AS isLiked FROM roadmaps r INNER JOIN users u ON r.userId = u.id WHERE (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${ - Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' - }) + Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' +}) AND r.isPublic = 1 AND r.isDraft = 0; `; From a4f55962bda53c4d4205a8664deee8de83f31f92 Mon Sep 17 00:00:00 2001 From: sopy Date: Thu, 7 Sep 2023 23:14:19 +0300 Subject: [PATCH 098/118] Set order rules --- src/middleware/validators/validateSearchParameters.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts index b2d5671..d49e215 100644 --- a/src/middleware/validators/validateSearchParameters.ts +++ b/src/middleware/validators/validateSearchParameters.ts @@ -46,22 +46,22 @@ export default function ( let order: Order; const [by, direction] = (orderParam as string)?.split(':') || ['age', 'DESC']; - switch (by) { + switch (by.toLowerCase()) { case 'views': order = { - by: 'views', + by: 'viewCount', direction: 'DESC', }; break; case 'likes': order = { - by: 'likes', + by: 'likeCount', direction: 'DESC', }; break; - case 'age': + case 'new': default: order = { by: 'r.createdAt', From 3df448464a4f4e2252087f0f353544d97c9e0b79 Mon Sep 17 00:00:00 2001 From: sopy Date: Fri, 8 Sep 2023 00:01:22 +0300 Subject: [PATCH 099/118] Figured out negative numbers sorting corectly --- .../validators/validateSearchParameters.ts | 10 +++++----- src/util/Database/ExploreDB.ts | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts index d49e215..f59839a 100644 --- a/src/middleware/validators/validateSearchParameters.ts +++ b/src/middleware/validators/validateSearchParameters.ts @@ -4,7 +4,7 @@ import { RoadmapTopic } from '@src/types/models/Roadmap'; interface Order { by: string; - direction?: 'ASC' | 'DESC'; + direction: 'ASC' | 'DESC'; } export interface SearchParameters { @@ -30,7 +30,7 @@ export default function ( page: pageParam, limit: limitParam, topic: topicParam, - order: orderParam } = + sortBy: orderParam } = req.query; const search = (searchParam as string) || ''; const page = parseInt((pageParam as string) || '1'); @@ -49,14 +49,14 @@ export default function ( switch (by.toLowerCase()) { case 'views': order = { - by: 'viewCount', + by: 't.viewCount', direction: 'DESC', }; break; case 'likes': order = { - by: 'likeCount', + by: 't.likeCount', direction: 'DESC', }; break; @@ -64,7 +64,7 @@ export default function ( case 'new': default: order = { - by: 'r.createdAt', + by: 't.createdAt', direction: 'DESC', }; break; diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 406a0fc..41a68c1 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -25,6 +25,8 @@ class ExploreDB extends Database { if (typeof search != 'string' || !page || !limit || !topic || !order) return { result: [], totalRoadmaps: 0n }; const query = ` + SELECT * + FROM ( SELECT r.id as id, r.name AS name, @@ -33,6 +35,8 @@ class ExploreDB extends Database { r.isFeatured AS isFeatured, r.isPublic AS isPublic, r.isDraft AS isDraft, + r.createdAt AS createdAt, + r.updatedAt AS updatedAt, u.id AS userId, u.avatar AS userAvatar, u.name AS userName, @@ -58,9 +62,16 @@ class ExploreDB extends Database { }) AND r.isPublic = 1 AND r.isDraft = 0 - ORDER BY - r.isFeatured DESC, ${order.by} ${order.direction} - LIMIT ?, ?; + ) as t + ORDER BY + t.isFeatured DESC, ${order.by === 't.likeCount' ? + `CASE + WHEN t.likeCount < 0 THEN 3 + WHEN t.likeCount = 0 THEN 2 + ELSE 1 + END,` : ''} ${order.by} ${order.direction} + LIMIT ?, ? + ; `; const query2 = ` SELECT From a71b3ab83884ef05253204897c35ecb665086438 Mon Sep 17 00:00:00 2001 From: erupturatis Date: Fri, 8 Sep 2023 11:59:43 +0300 Subject: [PATCH 100/118] (fix)[making isPublic modifiable] --- src/controllers/roadmapController.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index bbd20d3..ba37f68 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -8,13 +8,15 @@ import Database from '@src/util/Database/DatabaseDriver'; import { RoadmapLike } from '@src/types/models/RoadmapLike'; import { responseNotAllowed, - responseRoadmap, responseRoadmapAlreadyDisliked, + responseRoadmap, + responseRoadmapAlreadyDisliked, responseRoadmapAlreadyLiked, responseRoadmapCreated, responseRoadmapDeleted, responseRoadmapNotFound, responseRoadmapNotRated, - responseRoadmapRated, responseRoadmapUnrated, + responseRoadmapRated, + responseRoadmapUnrated, responseRoadmapUpdated, } from '@src/helpers/responses/roadmapResponses'; import { @@ -49,8 +51,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { topic = undefined; // isPublic can't be modified by the user yet - // if (isPublic !== true && isPublic !== false) isPublic = true; - isPublic = true; + if (isPublic !== true && isPublic !== false) isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false; const roadmap = new Roadmap({ From a54dbe93c8c501b0f4663545ff7b8d9d28d29e42 Mon Sep 17 00:00:00 2001 From: erupturatis Date: Fri, 8 Sep 2023 23:42:31 +0300 Subject: [PATCH 101/118] (fix)[integer responses instead of booleans] --- src/controllers/roadmapController.ts | 9 ++++++--- src/middleware/validators/validateBody.ts | 2 +- src/routes/RoadmapsRouter.ts | 2 +- src/util/data-alteration/AlterResponse.ts | 9 +++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/util/data-alteration/AlterResponse.ts diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index ba37f68..eeb71d0 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -32,6 +32,8 @@ import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; import { IUser } from '@src/types/models/User'; import { addRoadmapView } from '@src/util/Views'; import logger from 'jet-logger'; +import * as console from 'console'; +import { alterResponseToBooleans } from '@src/util/data-alteration/AlterResponse'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware @@ -50,7 +52,6 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) topic = undefined; - // isPublic can't be modified by the user yet if (isPublic !== true && isPublic !== false) isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false; @@ -107,10 +108,12 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { addRoadmapView(db, roadmap.id, userId).catch((e) => logger.err(e)); - return responseRoadmap( - res, + const roadmapResponsePayload: ResFullRoadmap = alterResponseToBooleans( new ResFullRoadmap(roadmap, user, likeCount, viewCount, isLiked), + ['isFeatured', 'isPublic', 'isDraft'], ); + + return responseRoadmap(res, roadmapResponsePayload); } export async function updateAllRoadmap(req: RequestWithBody, res: Response) { diff --git a/src/middleware/validators/validateBody.ts b/src/middleware/validators/validateBody.ts index 41d40d6..1faa551 100644 --- a/src/middleware/validators/validateBody.ts +++ b/src/middleware/validators/validateBody.ts @@ -23,7 +23,7 @@ export default function ( } for (const item of requiredFields) { - if (!body[item]) { + if (!body[item] === null || !body[item] === undefined) { return res .status(HttpStatusCode.BadRequest) .json({ error: `Missing required field: ${item}` }); diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index d0750ce..91f8247 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -17,7 +17,7 @@ const RoadmapsRouter = Router(); RoadmapsRouter.post( Paths.Roadmaps.Create, validateSession, - validateBody('name', 'description', 'data'), + validateBody('name', 'description', 'data', 'isPublic', 'isDraft'), createRoadmap, ); diff --git a/src/util/data-alteration/AlterResponse.ts b/src/util/data-alteration/AlterResponse.ts new file mode 100644 index 0000000..c19caf8 --- /dev/null +++ b/src/util/data-alteration/AlterResponse.ts @@ -0,0 +1,9 @@ +export function alterResponseToBooleans(payload: T, keys: string[]) { + const alteredPayload = { ...payload }; + keys.forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + alteredPayload[key] = !!alteredPayload[key]; + }); + return alteredPayload; +} From d3885caa3b61428b697e040cad2a5afec2ec141a Mon Sep 17 00:00:00 2001 From: erupturatis Date: Sat, 9 Sep 2023 14:29:02 +0300 Subject: [PATCH 102/118] (working)[misc data update] --- src/constants/Paths.ts | 1 + src/controllers/roadmapController.ts | 6 +++--- src/routes/RoadmapsRouter.ts | 2 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 1 + src/sql/setup.sql | 1 + src/types/models/Roadmap.ts | 11 +++++++++++ src/types/response/ResFullRoadmap.ts | 4 ++++ 7 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 666af75..035396d 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -39,6 +39,7 @@ const Paths = { Visibility: '/visibility', Draft: '/draft', Data: '/data', + MiscData: '/misc-data', // used for different roadmap wide data like roadmap theme }, Delete: '/:roadmapId([0-9]+)', Like: '/:roadmapId([0-9]+)/like', diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index eeb71d0..1512402 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -32,12 +32,11 @@ import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; import { IUser } from '@src/types/models/User'; import { addRoadmapView } from '@src/util/Views'; import logger from 'jet-logger'; -import * as console from 'console'; import { alterResponseToBooleans } from '@src/util/data-alteration/AlterResponse'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware - const { name, description, data } = req.body; + const { name, description, data, miscData } = req.body; // non guaranteed to exist by middleware of type Roadmap let { topic, isPublic, isDraft } = req.body; @@ -52,7 +51,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) topic = undefined; - if (isPublic !== true && isPublic !== false) isPublic = true; + isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false; const roadmap = new Roadmap({ @@ -63,6 +62,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { isPublic: isPublic as boolean, isDraft: isDraft as boolean, data: data as string, + miscData: miscData as string, }); const id = await insertRoadmap(db, roadmap); diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 91f8247..488ab3a 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -17,7 +17,7 @@ const RoadmapsRouter = Router(); RoadmapsRouter.post( Paths.Roadmaps.Create, validateSession, - validateBody('name', 'description', 'data', 'isPublic', 'isDraft'), + validateBody('name', 'description', 'data', 'isPublic', 'isDraft', 'miscData'), createRoadmap, ); diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index 6e2bec7..cf8b3c6 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -55,4 +55,5 @@ RoadmapsUpdate.post( updateIsDraftRoadmap, ); + export default RoadmapsUpdate; diff --git a/src/sql/setup.sql b/src/sql/setup.sql index d43799b..94b22fb 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -45,6 +45,7 @@ create table if not exists roadmaps isPublic tinyint(1) default 1 not null, isDraft tinyint(1) default 0 not null, data longtext not null, + miscData longtext not null, createdAt timestamp default current_timestamp() not null, updatedAt timestamp default current_timestamp() not null on update current_timestamp(), constraint roadmaps_userId_fk diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index c7adf50..cc4e661 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -16,6 +16,7 @@ export interface IRoadmap { readonly isPublic: boolean; readonly isDraft: boolean; readonly data: string; + readonly miscData: string; readonly createdAt: Date; readonly updatedAt: Date; } @@ -27,6 +28,7 @@ interface IRoadmapConstructor { readonly description: string; readonly topic?: RoadmapTopic; readonly userId: bigint; + readonly miscData: string; readonly isFeatured?: boolean; readonly isPublic?: boolean; readonly isDraft?: boolean; @@ -46,6 +48,7 @@ interface IRoadmapModifications { readonly isPublic?: boolean; readonly isDraft?: boolean; readonly data?: string; + readonly miscData?: string; readonly createdAt?: Date; readonly updatedAt?: Date; } @@ -61,6 +64,7 @@ export class Roadmap implements IRoadmap { private _isPublic: boolean; private _isDraft: boolean; private _data: string; + private _miscData: string; private _createdAt: Date; private _updatedAt: Date; @@ -74,6 +78,7 @@ export class Roadmap implements IRoadmap { isPublic = true, isDraft = false, data, + miscData, createdAt = new Date(), updatedAt = new Date(), }: IRoadmapConstructor) { @@ -86,6 +91,7 @@ export class Roadmap implements IRoadmap { this._isPublic = isPublic; this._isDraft = isDraft; this._data = data; + this._miscData = miscData; this._createdAt = createdAt; this._updatedAt = updatedAt; } @@ -151,6 +157,10 @@ export class Roadmap implements IRoadmap { return this._data; } + public get miscData(): string { + return this._miscData; + } + public get createdAt(): Date { return this._createdAt; } @@ -187,6 +197,7 @@ export class Roadmap implements IRoadmap { isPublic: this._isPublic, isDraft: this._isDraft, data: this._data, + miscData: this._miscData, createdAt: this._createdAt, updatedAt: this._updatedAt, }; diff --git a/src/types/response/ResFullRoadmap.ts b/src/types/response/ResFullRoadmap.ts index f3a2ae3..24b70f3 100644 --- a/src/types/response/ResFullRoadmap.ts +++ b/src/types/response/ResFullRoadmap.ts @@ -31,6 +31,7 @@ export class ResFullRoadmap implements IResFullRoadmap { public readonly description: string; public readonly topic: RoadmapTopic; public readonly data: string; + public readonly miscData: string; public readonly isFeatured: boolean; public readonly isPublic: boolean; public readonly isDraft: boolean; @@ -53,6 +54,7 @@ export class ResFullRoadmap implements IResFullRoadmap { description, topic, data, + miscData, userId, isFeatured, isPublic, @@ -70,6 +72,7 @@ export class ResFullRoadmap implements IResFullRoadmap { this.description = description; this.topic = topic; this.data = data; + this.miscData = miscData; this.isFeatured = isFeatured; this.isPublic = isPublic; this.isDraft = isDraft; @@ -94,6 +97,7 @@ export class ResFullRoadmap implements IResFullRoadmap { 'description' in obj && 'topic' in obj && 'data' in obj && + 'miscData' in obj && 'isFeatured' in obj && 'isPublic' in obj && 'isDraft' in obj && From cf0b41b32de14d014a324a8c094d5d0525ed9811 Mon Sep 17 00:00:00 2001 From: erupturatis Date: Sat, 9 Sep 2023 18:36:57 +0300 Subject: [PATCH 103/118] (working)[added misc data] --- src/constants/Paths.ts | 1 + src/controllers/roadmapController.ts | 80 +++++++++++++++++++++ src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 20 +++++- src/types/models/Roadmap.ts | 2 + src/util/Database/DatabaseDriver.ts | 2 + 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 035396d..9d07346 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -33,6 +33,7 @@ const Paths = { Update: { Base: '/:roadmapId([0-9]+)', All: '/', + About: '/about', Name: '/title', Description: '/description', Topic: '/topic', diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 1512402..2f5ff8b 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -33,6 +33,7 @@ import { IUser } from '@src/types/models/User'; import { addRoadmapView } from '@src/util/Views'; import logger from 'jet-logger'; import { alterResponseToBooleans } from '@src/util/data-alteration/AlterResponse'; +import * as console from 'console'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware @@ -76,6 +77,7 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; + console.log('WAIdehAIOE',roadmapId, userId, req.params ); if (!roadmapId) return responseServerError(res); const db = new Database(); @@ -83,6 +85,7 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { const roadmap = await getRoadmapData(db, BigInt(roadmapId)); if (!roadmap) return responseRoadmapNotFound(res); + const user = await db.get('users', roadmap.userId); if (!user) return responseServerError(res); const likeCount = await db.countWhere( @@ -108,6 +111,7 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { addRoadmapView(db, roadmap.id, userId).catch((e) => logger.err(e)); + const roadmapResponsePayload: ResFullRoadmap = alterResponseToBooleans( new ResFullRoadmap(roadmap, user, likeCount, viewCount, isLiked), ['isFeatured', 'isPublic', 'isDraft'], @@ -116,6 +120,42 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { return responseRoadmap(res, roadmapResponsePayload); } +export async function updateAboutRoadmap(req: RequestWithBody, res: Response) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { name, description, topic, miscData} = req.body; + + if (!name || !description || !miscData || !topic ) + return responseServerError(res); + + if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) + return responseInvalidBody(res); + + roadmap.set({ + name: name as string, + description: description as string, + topic: topic as RoadmapTopic, + miscData: miscData as string, + }); + + if (await db.update('roadmaps', roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + + export async function updateAllRoadmap(req: RequestWithBody, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; @@ -135,6 +175,7 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) { if (!name || !description || !data || !topic || !isDraft) return responseServerError(res); + if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) return responseInvalidBody(res); @@ -262,6 +303,45 @@ export async function updateTopicRoadmap(req: RequestWithBody, res: Response) { return responseServerError(res); } +export async function updateMiscDataRoadmap( + req: RequestWithBody, + res: Response, +) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + if (roadmap.userId !== userId) return responseNotAllowed(res); + + const { miscData } = req.body; + + if (!miscData) return responseServerError(res); + + console.log(typeof miscData, miscData); + roadmap.set({ miscData: miscData as string}); + console.log('NEW ROADMAP BEFORE SAVE', atob(roadmap.miscData) , miscData); + + if (await db.update('roadmaps', roadmap.id, roadmap)){ + console.log('WORKED'); + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + if(roadmap){ + console.log(miscData); + console.log('DIAWHEA',roadmap.name, roadmap.miscData, atob(roadmap.miscData)); + } + + return responseRoadmapUpdated(res); + } + + return responseServerError(res); +} + export async function updateIsDraftRoadmap( req: RequestWithBody, res: Response, diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index cf8b3c6..af5f438 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -3,10 +3,11 @@ import Paths from '@src/constants/Paths'; import validateSession from '@src/middleware/validators/validateSession'; import validateBody from '@src/middleware/validators/validateBody'; import { + updateAboutRoadmap, updateAllRoadmap, updateDataRoadmap, updateDescriptionRoadmap, - updateIsDraftRoadmap, + updateIsDraftRoadmap, updateMiscDataRoadmap, updateNameRoadmap, updateTopicRoadmap, } from '@src/controllers/roadmapController'; @@ -56,4 +57,21 @@ RoadmapsUpdate.post( ); +RoadmapsUpdate.post( + Paths.Roadmaps.Update.MiscData, + validateSession, + validateBody('miscData'), + updateMiscDataRoadmap, +); + +RoadmapsUpdate.post( + Paths.Roadmaps.Update.About, + validateSession, + validateBody('name', 'description', 'topic', 'miscData'), + updateAboutRoadmap, +); + + + + export default RoadmapsUpdate; diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index cc4e661..f364eaf 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -106,6 +106,7 @@ export class Roadmap implements IRoadmap { isPublic, isDraft, data, + miscData, createdAt, updatedAt, }: IRoadmapModifications): void { @@ -117,6 +118,7 @@ export class Roadmap implements IRoadmap { if (isPublic !== undefined) this._isPublic = isPublic; if (isDraft !== undefined) this._isDraft = isDraft; if (data !== undefined) this._data = data; + if (miscData !== undefined) this._miscData = miscData; if (createdAt !== undefined) this._createdAt = createdAt; if (updatedAt !== undefined) this._updatedAt = updatedAt; } diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index 590f5a2..5625f2c 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -201,6 +201,8 @@ class Database { discardId = true, ): Promise { const { keys, values } = processData(data, discardId); + // console.log('keys', keys); + // console.log('values', values); // create sql query - update table set key = ?, key = ? where id = ? // ? for values to be replaced by params From 414617461e1485d05bf1f9a01a00536b557b1e01 Mon Sep 17 00:00:00 2001 From: erupturatis Date: Sat, 9 Sep 2023 23:52:57 +0300 Subject: [PATCH 104/118] (working)[misc work and fix in /draft] --- src/controllers/roadmapController.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 2f5ff8b..a244733 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -324,18 +324,10 @@ export async function updateMiscDataRoadmap( if (!miscData) return responseServerError(res); - console.log(typeof miscData, miscData); roadmap.set({ miscData: miscData as string}); - console.log('NEW ROADMAP BEFORE SAVE', atob(roadmap.miscData) , miscData); if (await db.update('roadmaps', roadmap.id, roadmap)){ - console.log('WORKED'); const roadmap = await getRoadmapData(db, BigInt(roadmapId)); - if(roadmap){ - console.log(miscData); - console.log('DIAWHEA',roadmap.name, roadmap.miscData, atob(roadmap.miscData)); - } - return responseRoadmapUpdated(res); } @@ -361,7 +353,7 @@ export async function updateIsDraftRoadmap( const { isDraft } = req.body; - if (!isDraft) return responseServerError(res); + if (isDraft === null || isDraft === undefined) return responseServerError(res); roadmap.set({ isDraft: !!isDraft }); From 7bda3b4c2f105e2509991efc6bba88108ad14e29 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 17:14:12 +0300 Subject: [PATCH 105/118] "Refactor code for better readability and performance" This commit introduces multiple refactoring changes to improve the code's readability and performance. - Removed AlterResponse.ts file. The function in this file wasn't used anywhere else in the codebase. - Renamed ExploreRouter.ts to SearchRouter.ts. This aligns more with the functionality that the router provides. - Updated bcrypt dependency from 5.1.0 to 5.1.1 in package.json. - Updated multiple types to allow 'bigint' in addition to 'boolean' for properties 'isFeatured', 'isPublic', 'isDraft'. - Changes made to usersRoutes/UsersGet.ts and roadmapsRoutes/RoadmapsUpdate.ts file for better input validation and improved code order. - Refactored 'console.log' calls in roadmapController.ts. Removed unnecessary console.log and other redundant lines. - Changed database queries to use 'sumWhere' instead of 'countWhere' in usersController.ts and roadmapController.ts to improve performance. - Reordered endpoints in RoadmapsUpdate.ts for better readability. User now needs to be validated with a session before following/unfollowing a user. - In Paths.ts, renamed 'Explore' to 'Search' to align with the renamed router and 'title' to 'name' for better accuracy. Also removed MiniRoadmap, Owner, OwnerMini and Issues that were no longer in use - Other misc changes done automatically by Webstorm's "Refactor code" --- env | 2 +- package-lock.json | 28 +-- package.json | 2 +- src/constants/EnvVars.ts | 1 - src/constants/Paths.ts | 30 +-- src/controllers/authController.ts | 12 +- src/controllers/roadmapController.ts | 64 +++--- src/controllers/usersController.ts | 4 +- src/helpers/databaseManagement.ts | 2 +- src/index.ts | 24 +-- src/middleware/session.ts | 6 +- .../validators/validateSearchParameters.ts | 17 +- src/pre-start.ts | 70 +++---- src/routes/RoadmapsRouter.ts | 9 +- .../{ExploreRouter.ts => SearchRouter.ts} | 8 +- src/routes/entry.ts | 10 +- src/routes/roadmapsRoutes/RoadmapsGet.ts | 5 +- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 21 +- src/routes/usersRoutes/UsersGet.ts | 15 +- src/routes/usersRoutes/UsersUpdate.ts | 5 +- src/sql/metrics.sql | 2 +- src/types/models/Follower.ts | 37 ++-- src/types/models/Roadmap.ts | 105 +++++----- src/types/models/RoadmapLike.ts | 44 +++-- src/types/models/RoadmapView.ts | 50 ++--- src/types/models/Session.ts | 27 +-- src/types/models/User.ts | 72 ++++--- src/types/models/UserInfo.ts | 51 ++--- src/types/response/ResFullRoadmap.ts | 2 +- src/util/Database/DatabaseDriver.ts | 186 +++++++++--------- src/util/Database/ExploreDB.ts | 90 +++++---- src/util/EmailUtil.ts | 10 +- src/util/LoginUtil.ts | 7 +- src/util/Views.ts | 10 +- src/util/data-alteration/AlterResponse.ts | 9 - src/util/misc.ts | 36 ++-- 36 files changed, 545 insertions(+), 528 deletions(-) rename src/routes/{ExploreRouter.ts => SearchRouter.ts} (73%) delete mode 100644 src/util/data-alteration/AlterResponse.ts diff --git a/env b/env index fcef617..a9f299d 160000 --- a/env +++ b/env @@ -1 +1 @@ -Subproject commit fcef6176b5f4acfd07b0531d59d344668076f003 +Subproject commit a9f299d52284837bade9be9f0f6b733f0b6b70bd diff --git a/package-lock.json b/package-lock.json index b19809d..d738128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD 3-Clause", "dependencies": { "axios": "^1.4.0", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -947,12 +947,12 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/bcrypt": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", - "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", "hasInstallScript": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.10", + "@mapbox/node-pre-gyp": "^1.0.11", "node-addon-api": "^5.0.0" }, "engines": { @@ -1427,9 +1427,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", "engines": { "node": ">=8" } @@ -3063,9 +3063,9 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3925,9 +3925,9 @@ } }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/package.json b/package.json index bd87438..264717a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "axios": "^1.4.0", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", "express": "^4.18.2", diff --git a/src/constants/EnvVars.ts b/src/constants/EnvVars.ts index f304bb0..fd6f64d 100644 --- a/src/constants/EnvVars.ts +++ b/src/constants/EnvVars.ts @@ -4,7 +4,6 @@ * Environments variables declared here. */ - import { NodeEnvs } from '@src/constants/misc'; interface IEnvVars { diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index 9d07346..c20190a 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -16,7 +16,7 @@ const Paths = { GithubCallback: '/github-callback', Logout: '/logout', }, - Explore: { + Search: { Base: '/search', Roadmaps: '/roadmaps', }, @@ -26,46 +26,22 @@ const Paths = { Get: { Base: '/:roadmapId([0-9]+)?', Roadmap: '/', - MiniRoadmap: '/mini', - Owner: '/owner', - OwnerMini: '/owner/mini', }, Update: { Base: '/:roadmapId([0-9]+)', All: '/', About: '/about', - Name: '/title', + Name: '/name', Description: '/description', Topic: '/topic', Visibility: '/visibility', Draft: '/draft', Data: '/data', - MiscData: '/misc-data', // used for different roadmap wide data like roadmap theme + MiscData: '/misc-data', // used for roadmap wide data like roadmap theme }, Delete: '/:roadmapId([0-9]+)', Like: '/:roadmapId([0-9]+)/like', Dislike: '/:roadmapId([0-9]+)/dislike', - Issues: { - Base: '/:roadmapId([0-9]+)/issues', - Create: '/create', - Get: '/:issueId([0-9]+)', - GetAll: '/', - Update: { - Base: '/:issueId([0-9]+)', - All: '/', - Title: '/title', - Content: '/content', - Status: '/status', - }, - Delete: '/:issueId', - Comments: { - Base: '/:issueId/comments', - Create: '/create', - Get: '/:commentId?', - Update: '/:commentId', - Delete: '/:commentId', - }, - }, }, Users: { Base: '/users', diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 9edfa8d..e929fbd 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -320,8 +320,7 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (isEmptyObject(response.data)) - return responseServerError(res); + if (isEmptyObject(response.data)) return responseServerError(res); // get access token from response const data = response.data as { access_token?: string }; @@ -338,13 +337,11 @@ export async function authGitHubCallback( // check if response is valid if (response.status !== 200) return _handleNotOkay(res, response.status); - if (isEmptyObject(response.data)) - return responseServerError(res); + if (isEmptyObject(response.data)) return responseServerError(res); // get user data const userData = response.data as GitHubUserData; - if (isEmptyObject(userData) && !userData) - return responseServerError(res); + if (isEmptyObject(userData) && !userData) return responseServerError(res); // get email from github response = await axios.get('https://api.github.com/user/emails', { @@ -369,8 +366,7 @@ export async function authGitHubCallback( userData.email = emails.find((e) => e.primary && e.verified)?.email ?? ''; // check if email is valid - if (userData.email === '') - return responseServerError(res); + if (userData.email === '') return responseServerError(res); // get database const db = new DatabaseDriver(); diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index a244733..c647f8f 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -20,20 +20,21 @@ import { responseRoadmapUpdated, } from '@src/helpers/responses/roadmapResponses'; import { + deleteDBRoadmap, + deleteRoadmapLike, getRoadmapData, getRoadmapLike, + getUser, insertRoadmap, insertRoadmapLike, + updateRoadmap, updateRoadmapLike, } from '@src/helpers/databaseManagement'; import { RequestWithBody } from '@src/middleware/validators/validateBody'; import { Roadmap, RoadmapTopic } from '@src/types/models/Roadmap'; import { ResFullRoadmap } from '@src/types/response/ResFullRoadmap'; -import { IUser } from '@src/types/models/User'; import { addRoadmapView } from '@src/util/Views'; import logger from 'jet-logger'; -import { alterResponseToBooleans } from '@src/util/data-alteration/AlterResponse'; -import * as console from 'console'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware @@ -77,7 +78,6 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; - console.log('WAIdehAIOE',roadmapId, userId, req.params ); if (!roadmapId) return responseServerError(res); const db = new Database(); @@ -85,11 +85,13 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { const roadmap = await getRoadmapData(db, BigInt(roadmapId)); if (!roadmap) return responseRoadmapNotFound(res); + const user = await getUser(db, roadmap.userId); - const user = await db.get('users', roadmap.userId); - if (!user) return responseServerError(res); - const likeCount = await db.countWhere( + if (user === null) return responseServerError(res); + + const likeCount = await db.sumWhere( 'roadmapLikes', + 'value', 'roadmapId', roadmap.id, ); @@ -100,6 +102,7 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { ); const isLiked = await db.sumWhere( 'roadmapLikes', + 'value', 'roadmapId', roadmap.id, 'userId', @@ -111,13 +114,10 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { addRoadmapView(db, roadmap.id, userId).catch((e) => logger.err(e)); - - const roadmapResponsePayload: ResFullRoadmap = alterResponseToBooleans( + return responseRoadmap( + res, new ResFullRoadmap(roadmap, user, likeCount, viewCount, isLiked), - ['isFeatured', 'isPublic', 'isDraft'], ); - - return responseRoadmap(res, roadmapResponsePayload); } export async function updateAboutRoadmap(req: RequestWithBody, res: Response) { @@ -134,9 +134,9 @@ export async function updateAboutRoadmap(req: RequestWithBody, res: Response) { if (!roadmap) return responseRoadmapNotFound(res); if (roadmap.userId !== userId) return responseNotAllowed(res); - const { name, description, topic, miscData} = req.body; + const { name, description, topic, miscData } = req.body; - if (!name || !description || !miscData || !topic ) + if (!name || !description || !miscData || !topic) return responseServerError(res); if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) @@ -149,13 +149,12 @@ export async function updateAboutRoadmap(req: RequestWithBody, res: Response) { miscData: miscData as string, }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); } - export async function updateAllRoadmap(req: RequestWithBody, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; @@ -175,7 +174,6 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) { if (!name || !description || !data || !topic || !isDraft) return responseServerError(res); - if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) return responseInvalidBody(res); @@ -184,10 +182,10 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) { description: description as string, data: data as string, topic: topic as RoadmapTopic, - isDraft: isDraft as boolean, + isDraft: Boolean(isDraft), }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -213,7 +211,7 @@ export async function updateNameRoadmap(req: RequestWithBody, res: Response) { roadmap.set({ name: name as string }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -242,7 +240,7 @@ export async function updateDescriptionRoadmap( roadmap.set({ description: description as string }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -268,7 +266,7 @@ export async function updateDataRoadmap(req: RequestWithBody, res: Response) { roadmap.set({ data: data as string }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -297,7 +295,7 @@ export async function updateTopicRoadmap(req: RequestWithBody, res: Response) { roadmap.set({ topic: topic as RoadmapTopic }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -320,16 +318,14 @@ export async function updateMiscDataRoadmap( if (!roadmap) return responseRoadmapNotFound(res); if (roadmap.userId !== userId) return responseNotAllowed(res); - const { miscData } = req.body; + const { miscData } = req.body; if (!miscData) return responseServerError(res); - roadmap.set({ miscData: miscData as string}); + roadmap.set({ miscData: miscData as string }); - if (await db.update('roadmaps', roadmap.id, roadmap)){ - const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); - } return responseServerError(res); } @@ -353,11 +349,12 @@ export async function updateIsDraftRoadmap( const { isDraft } = req.body; - if (isDraft === null || isDraft === undefined) return responseServerError(res); + if (isDraft === null || isDraft === undefined) + return responseInvalidBody(res); - roadmap.set({ isDraft: !!isDraft }); + roadmap.set({ isDraft: Boolean(isDraft) }); - if (await db.update('roadmaps', roadmap.id, roadmap)) + if (await updateRoadmap(db, roadmap.id, roadmap)) return responseRoadmapUpdated(res); return responseServerError(res); @@ -377,7 +374,7 @@ export async function deleteRoadmap(req: RequestWithSession, res: Response) { if (!roadmap) return responseRoadmapNotFound(res); if (roadmap.userId !== userId) return responseNotAllowed(res); - if (await db.delete('roadmaps', BigInt(roadmapId))) + if (await deleteDBRoadmap(db, BigInt(roadmapId))) return responseRoadmapDeleted(res); return responseServerError(res); @@ -471,8 +468,7 @@ export async function removeLikeRoadmap( if (!liked) return responseRoadmapNotRated(res); - if (await db.delete('roadmapLikes', liked.id)) - return responseRoadmapUnrated(res); + if (await deleteRoadmapLike(db, liked)) return responseRoadmapUnrated(res); return responseServerError(res); } diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 8f3daf1..93a8bc7 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -136,7 +136,9 @@ export async function userGetRoadmaps( const isLiked: bigint[] = []; for (const roadmap of roadmaps) { - likes.push(await db.countWhere('roadmapLikes', 'roadmapId', roadmap.id)); + likes.push( + await db.sumWhere('roadmapLikes', 'value', 'roadmapId', roadmap.id), + ); views.push(await db.countWhere('roadmapViews', 'roadmapId', roadmap.id)); isLiked.push( await db.sumWhere( diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index 3f8b90d..a26e70c 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -226,7 +226,7 @@ export async function updateRoadmap( return await db.update('roadmaps', roadmapId, roadmap); } -export async function deleteRoadmap( +export async function deleteDBRoadmap( db: DatabaseDriver, roadmapId: bigint, ): Promise { diff --git a/src/index.ts b/src/index.ts index 642c934..252e668 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ -import './pre-start'; // Must be the first import -import logger from 'jet-logger'; - -import EnvVars from '@src/constants/EnvVars'; -import server from './server'; - -// **** Run **** // - -const SERVER_START_MSG = ('Express server started on port: ' + - EnvVars.Port.toString()); - -server.listen(EnvVars.Port, () => logger.info(SERVER_START_MSG)); +import './pre-start'; // Must be the first import +import logger from 'jet-logger'; + +import EnvVars from '@src/constants/EnvVars'; +import server from './server'; + +// **** Run **** // + +const SERVER_START_MSG = + 'Express server started on port: ' + EnvVars.Port.toString(); + +server.listen(EnvVars.Port, () => logger.info(SERVER_START_MSG)); diff --git a/src/middleware/session.ts b/src/middleware/session.ts index d1ad116..5038610 100644 --- a/src/middleware/session.ts +++ b/src/middleware/session.ts @@ -14,9 +14,9 @@ export interface ISession { } interface RequestWithCookies extends RequestWithSession { - cookies: { - [COOKIE_NAME: string]: string | undefined; - } + cookies: { + [COOKIE_NAME: string]: string | undefined; + }; } export interface RequestWithSession extends Request { diff --git a/src/middleware/validators/validateSearchParameters.ts b/src/middleware/validators/validateSearchParameters.ts index f59839a..38a5eab 100644 --- a/src/middleware/validators/validateSearchParameters.ts +++ b/src/middleware/validators/validateSearchParameters.ts @@ -30,8 +30,8 @@ export default function ( page: pageParam, limit: limitParam, topic: topicParam, - sortBy: orderParam } = - req.query; + sortBy: orderParam, + } = req.query; const search = (searchParam as string) || ''; const page = parseInt((pageParam as string) || '1'); const limit = parseInt((limitParam as string) || '12'); @@ -90,12 +90,13 @@ export default function ( topic !== RoadmapTopic.MATH && topic !== RoadmapTopic.PHYSICS && topic !== RoadmapTopic.BIOLOGY - ) topic = [ - RoadmapTopic.PROGRAMMING, - RoadmapTopic.MATH, - RoadmapTopic.PHYSICS, - RoadmapTopic.BIOLOGY, - ]; + ) + topic = [ + RoadmapTopic.PROGRAMMING, + RoadmapTopic.MATH, + RoadmapTopic.PHYSICS, + RoadmapTopic.BIOLOGY, + ]; } req.search = search; diff --git a/src/pre-start.ts b/src/pre-start.ts index 6ddfaa9..4d75ccd 100644 --- a/src/pre-start.ts +++ b/src/pre-start.ts @@ -1,35 +1,35 @@ -/** - * Pre-start is where we want to place things that must run BEFORE the express - * server is started. This is useful for environment variables, command-line - * arguments, and cron-jobs. - */ - -// NOTE: DO NOT IMPORT ANY SOURCE CODE HERE -import path from 'path'; -import dotenv from 'dotenv'; -import { parse } from 'ts-command-line-args'; - -// **** Types **** // - -interface IArgs { - env: string; -} - -// **** Setup **** // - -// Command line arguments -const args = parse({ - env: { - type: String, - defaultValue: 'development', - alias: 'e', - }, -}); - -// Set the env file -const result2 = dotenv.config({ - path: path.join(__dirname, `../env/${args.env}.env`), -}); -if (result2.error) { - throw result2.error; -} +/** + * Pre-start is where we want to place things that must run BEFORE the express + * server is started. This is useful for environment variables, command-line + * arguments, and cron-jobs. + */ + +// NOTE: DO NOT IMPORT ANY SOURCE CODE HERE +import path from 'path'; +import dotenv from 'dotenv'; +import { parse } from 'ts-command-line-args'; + +// **** Types **** // + +interface IArgs { + env: string; +} + +// **** Setup **** // + +// Command line arguments +const args = parse({ + env: { + type: String, + defaultValue: 'development', + alias: 'e', + }, +}); + +// Set the env file +const result2 = dotenv.config({ + path: path.join(__dirname, `../env/${args.env}.env`), +}); +if (result2.error) { + throw result2.error; +} diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 488ab3a..70918b0 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -17,7 +17,14 @@ const RoadmapsRouter = Router(); RoadmapsRouter.post( Paths.Roadmaps.Create, validateSession, - validateBody('name', 'description', 'data', 'isPublic', 'isDraft', 'miscData'), + validateBody( + 'name', + 'description', + 'data', + 'isPublic', + 'isDraft', + 'miscData', + ), createRoadmap, ); diff --git a/src/routes/ExploreRouter.ts b/src/routes/SearchRouter.ts similarity index 73% rename from src/routes/ExploreRouter.ts rename to src/routes/SearchRouter.ts index 6fbd5a5..deec899 100644 --- a/src/routes/ExploreRouter.ts +++ b/src/routes/SearchRouter.ts @@ -4,12 +4,12 @@ import validateSearchParameters from '@src/middleware/validators/validateSearchParameters'; import { searchRoadmaps } from '@src/controllers/exploreController'; -const ExploreRouter = Router(); +const SearchRouter = Router(); -ExploreRouter.get( - Paths.Explore.Roadmaps, +SearchRouter.get( + Paths.Search.Roadmaps, validateSearchParameters, searchRoadmaps, ); -export default ExploreRouter; +export default SearchRouter; diff --git a/src/routes/entry.ts b/src/routes/entry.ts index a096772..e40fda9 100644 --- a/src/routes/entry.ts +++ b/src/routes/entry.ts @@ -3,18 +3,18 @@ import Paths from '@src/constants/Paths'; import AuthRouter from '@src/routes/AuthRouter'; import RoadmapsRouter from '@src/routes/RoadmapsRouter'; import UsersRouter from '@src/routes/UsersRouter'; -import ExploreRouter from '@src/routes/ExploreRouter'; +import SearchRouter from '@src/routes/SearchRouter'; const BaseRouter = Router(); // Import all routes at base path -const { Auth, Explore, Roadmaps, Users } = Paths; +const { Auth, Search, Roadmaps, Users } = Paths; // Auth routes BaseRouter.use(Auth.Base, AuthRouter); -// Explore routes -BaseRouter.use(Explore.Base, ExploreRouter); +// Search routes +BaseRouter.use(Search.Base, SearchRouter); // Roadmaps routes BaseRouter.use(Roadmaps.Base, RoadmapsRouter); @@ -22,4 +22,4 @@ BaseRouter.use(Roadmaps.Base, RoadmapsRouter); // Users routes BaseRouter.use(Users.Base, UsersRouter); -export default BaseRouter; \ No newline at end of file +export default BaseRouter; diff --git a/src/routes/roadmapsRoutes/RoadmapsGet.ts b/src/routes/roadmapsRoutes/RoadmapsGet.ts index 120fb6b..e758804 100644 --- a/src/routes/roadmapsRoutes/RoadmapsGet.ts +++ b/src/routes/roadmapsRoutes/RoadmapsGet.ts @@ -4,9 +4,6 @@ import { getRoadmap } from '@src/controllers/roadmapController'; const RoadmapsGet = Router({ mergeParams: true }); -RoadmapsGet.get( - Paths.Roadmaps.Get.Roadmap, - getRoadmap, -); +RoadmapsGet.get(Paths.Roadmaps.Get.Roadmap, getRoadmap); export default RoadmapsGet; diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index af5f438..3d62bdd 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -7,7 +7,8 @@ import { updateAllRoadmap, updateDataRoadmap, updateDescriptionRoadmap, - updateIsDraftRoadmap, updateMiscDataRoadmap, + updateIsDraftRoadmap, + updateMiscDataRoadmap, updateNameRoadmap, updateTopicRoadmap, } from '@src/controllers/roadmapController'; @@ -21,6 +22,13 @@ RoadmapsUpdate.post( updateAllRoadmap, ); +RoadmapsUpdate.post( + Paths.Roadmaps.Update.About, + validateSession, + validateBody('name', 'description', 'topic', 'miscData'), + updateAboutRoadmap, +); + RoadmapsUpdate.post( Paths.Roadmaps.Update.Name, validateSession, @@ -56,7 +64,6 @@ RoadmapsUpdate.post( updateIsDraftRoadmap, ); - RoadmapsUpdate.post( Paths.Roadmaps.Update.MiscData, validateSession, @@ -64,14 +71,4 @@ RoadmapsUpdate.post( updateMiscDataRoadmap, ); -RoadmapsUpdate.post( - Paths.Roadmaps.Update.About, - validateSession, - validateBody('name', 'description', 'topic', 'miscData'), - updateAboutRoadmap, -); - - - - export default RoadmapsUpdate; diff --git a/src/routes/usersRoutes/UsersGet.ts b/src/routes/usersRoutes/UsersGet.ts index 30ab4d7..562a15f 100644 --- a/src/routes/usersRoutes/UsersGet.ts +++ b/src/routes/usersRoutes/UsersGet.ts @@ -8,6 +8,7 @@ import { usersGetProfile, userUnfollow, } from '@src/controllers/usersController'; +import validateSession from '@src/middleware/validators/validateSession'; // ! What would I do without StackOverflow? // ! https://stackoverflow.com/a/60848873 @@ -19,9 +20,19 @@ UsersGet.get(Paths.Users.Get.MiniProfile, validateUser(), usersGetMiniProfile); UsersGet.get(Paths.Users.Get.UserRoadmaps, validateUser(), userGetRoadmaps); -UsersGet.get(Paths.Users.Get.Follow, validateUser(), userFollow); +UsersGet.get( + Paths.Users.Get.Follow, + validateSession, + validateUser(), + userFollow, +); -UsersGet.delete(Paths.Users.Get.Follow, validateUser(), userUnfollow); +UsersGet.delete( + Paths.Users.Get.Follow, + validateSession, + validateUser(), + userUnfollow, +); // TODO: Following and followers lists diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index 449239d..d775b13 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -9,7 +9,9 @@ import validateBody from '@src/middleware/validators/validateBody'; import { usersPostProfile, usersPostProfileGithubUrl, - usersPostProfileName, usersPostProfileQuote, usersPostProfileWebsiteUrl, + usersPostProfileName, + usersPostProfileQuote, + usersPostProfileWebsiteUrl, } from '@src/controllers/usersController'; const UsersUpdate = Router({ mergeParams: true }); @@ -40,7 +42,6 @@ UsersUpdate.post( usersPostProfileWebsiteUrl, ); - UsersUpdate.post( Paths.Users.Update.Quote, validateBody('quote'), diff --git a/src/sql/metrics.sql b/src/sql/metrics.sql index 44dc541..24cb1c9 100644 --- a/src/sql/metrics.sql +++ b/src/sql/metrics.sql @@ -20,7 +20,7 @@ select # get count of users that created a roadmap in the last 7 days (select - count(distinct ownerId) + count(distinct userId) from roadmaps where createdAt >= CURRENT_TIMESTAMP - interval 7 day) as creators, diff --git a/src/types/models/Follower.ts b/src/types/models/Follower.ts index 53d34d6..8176970 100644 --- a/src/types/models/Follower.ts +++ b/src/types/models/Follower.ts @@ -24,11 +24,6 @@ interface IFollowerModifications { // Class export class Follower implements IFollower { - private _id: bigint; - private _followerId: bigint; - private _userId: bigint; - private _createdAt: Date; - public constructor({ id = -1n, followerId, @@ -41,31 +36,26 @@ export class Follower implements IFollower { this._createdAt = createdAt; } - // Method to modify the properties - public set({ - id, - followerId, - userId, - createdAt, - }: IFollowerModifications): void { - if (id !== undefined) this._id = id; - if (followerId !== undefined) this._followerId = followerId; - if (userId !== undefined) this._userId = userId; - if (createdAt !== undefined) this._createdAt = createdAt; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _followerId: bigint; + public get followerId(): bigint { return this._followerId; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _createdAt: Date; + public get createdAt(): Date { return this._createdAt; } @@ -81,6 +71,19 @@ export class Follower implements IFollower { ); } + // Method to modify the properties + public set({ + id, + followerId, + userId, + createdAt, + }: IFollowerModifications): void { + if (id !== undefined) this._id = id; + if (followerId !== undefined) this._followerId = followerId; + if (userId !== undefined) this._userId = userId; + if (createdAt !== undefined) this._createdAt = createdAt; + } + // toObject method public toObject(): IFollower { return { diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index f364eaf..c416909 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -29,9 +29,9 @@ interface IRoadmapConstructor { readonly topic?: RoadmapTopic; readonly userId: bigint; readonly miscData: string; - readonly isFeatured?: boolean; - readonly isPublic?: boolean; - readonly isDraft?: boolean; + readonly isFeatured?: boolean | bigint; + readonly isPublic?: boolean | bigint; + readonly isDraft?: boolean | bigint; readonly data: string; readonly createdAt?: Date; readonly updatedAt?: Date; @@ -45,8 +45,8 @@ interface IRoadmapModifications { readonly topic?: RoadmapTopic; readonly userId?: bigint; // isFeatured is not modifiable - readonly isPublic?: boolean; - readonly isDraft?: boolean; + readonly isPublic?: boolean | bigint; + readonly isDraft?: boolean | bigint; readonly data?: string; readonly miscData?: string; readonly createdAt?: Date; @@ -55,19 +55,6 @@ interface IRoadmapModifications { // Class export class Roadmap implements IRoadmap { - private _id: bigint; - private _name: string; - private _description: string; - private _topic: RoadmapTopic; - private _userId: bigint; - private _isFeatured: boolean; - private _isPublic: boolean; - private _isDraft: boolean; - private _data: string; - private _miscData: string; - private _createdAt: Date; - private _updatedAt: Date; - public constructor({ id = 0n, name, @@ -87,86 +74,83 @@ export class Roadmap implements IRoadmap { this._description = description; this._topic = topic; this._userId = userId; - this._isFeatured = isFeatured; - this._isPublic = isPublic; - this._isDraft = isDraft; + this._isFeatured = !!isFeatured; + this._isPublic = !!isPublic; + this._isDraft = !!isDraft; this._data = data; this._miscData = miscData; this._createdAt = createdAt; this._updatedAt = updatedAt; } - // Method to modify the properties - public set({ - id, - name, - description, - topic, - userId, - isPublic, - isDraft, - data, - miscData, - createdAt, - updatedAt, - }: IRoadmapModifications): void { - if (id !== undefined) this._id = id; - if (name !== undefined) this._name = name; - if (description !== undefined) this._description = description; - if (topic !== undefined) this._topic = topic; - if (userId !== undefined) this._userId = userId; - if (isPublic !== undefined) this._isPublic = isPublic; - if (isDraft !== undefined) this._isDraft = isDraft; - if (data !== undefined) this._data = data; - if (miscData !== undefined) this._miscData = miscData; - if (createdAt !== undefined) this._createdAt = createdAt; - if (updatedAt !== undefined) this._updatedAt = updatedAt; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _name: string; + public get name(): string { return this._name; } + private _description: string; + public get description(): string { return this._description; } + private _topic: RoadmapTopic; + public get topic(): RoadmapTopic { return this._topic; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _isFeatured: boolean; + public get isFeatured(): boolean { return this._isFeatured; } + private _isPublic: boolean; + public get isPublic(): boolean { return this._isPublic; } + private _isDraft: boolean; + public get isDraft(): boolean { return this._isDraft; } + private _data: string; + public get data(): string { return this._data; } + private _miscData: string; + public get miscData(): string { return this._miscData; } + private _createdAt: Date; + public get createdAt(): Date { return this._createdAt; } + private _updatedAt: Date; + public get updatedAt(): Date { return this._updatedAt; } @@ -187,6 +171,33 @@ export class Roadmap implements IRoadmap { ); } + // Method to modify the properties + public set({ + id, + name, + description, + topic, + userId, + isPublic, + isDraft, + data, + miscData, + createdAt, + updatedAt, + }: IRoadmapModifications): void { + if (id !== undefined) this._id = id; + if (name !== undefined) this._name = name; + if (description !== undefined) this._description = description; + if (topic !== undefined) this._topic = topic; + if (userId !== undefined) this._userId = userId; + if (isPublic !== undefined) this._isPublic = !!isPublic; + if (isDraft !== undefined) this._isDraft = !!isDraft; + if (data !== undefined) this._data = data; + if (miscData !== undefined) this._miscData = miscData; + if (createdAt !== undefined) this._createdAt = createdAt; + if (updatedAt !== undefined) this._updatedAt = updatedAt; + } + // toObject method public toObject(): IRoadmap { return { diff --git a/src/types/models/RoadmapLike.ts b/src/types/models/RoadmapLike.ts index 6014b76..b294d48 100644 --- a/src/types/models/RoadmapLike.ts +++ b/src/types/models/RoadmapLike.ts @@ -27,12 +27,6 @@ interface IRoadmapLikeModifications { // Class export class RoadmapLike implements IRoadmapLike { - private _id: bigint; - private _roadmapId: bigint; - private _userId: bigint; - private _value: number; - private _createdAt: Date; - public constructor({ id = -1n, roadmapId, @@ -47,37 +41,32 @@ export class RoadmapLike implements IRoadmapLike { this._createdAt = createdAt; } - // Method to modify the properties - public set({ - id, - roadmapId, - userId, - value, - createdAt, - }: IRoadmapLikeModifications): void { - if (id !== undefined) this._id = id; - if (roadmapId !== undefined) this._roadmapId = roadmapId; - if (userId !== undefined) this._userId = userId; - if (value !== undefined) this._value = value; - if (createdAt !== undefined) this._createdAt = createdAt; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _roadmapId: bigint; + public get roadmapId(): bigint { return this._roadmapId; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _value: number; + public get value(): number { return this._value; } + private _createdAt: Date; + public get createdAt(): Date { return this._createdAt; } @@ -92,6 +81,21 @@ export class RoadmapLike implements IRoadmapLike { ); } + // Method to modify the properties + public set({ + id, + roadmapId, + userId, + value, + createdAt, + }: IRoadmapLikeModifications): void { + if (id !== undefined) this._id = id; + if (roadmapId !== undefined) this._roadmapId = roadmapId; + if (userId !== undefined) this._userId = userId; + if (value !== undefined) this._value = value; + if (createdAt !== undefined) this._createdAt = createdAt; + } + // toObject method public toObject(): IRoadmapLike { return { diff --git a/src/types/models/RoadmapView.ts b/src/types/models/RoadmapView.ts index aea902f..3fa996d 100644 --- a/src/types/models/RoadmapView.ts +++ b/src/types/models/RoadmapView.ts @@ -12,7 +12,7 @@ interface IRoadmapViewConstructor { readonly id?: bigint; readonly userId?: bigint; readonly roadmapId: bigint; - readonly full?: boolean; + readonly full?: boolean | bigint; readonly createdAt?: Date; } @@ -21,18 +21,12 @@ interface IRoadmapViewModifications { readonly id?: bigint; readonly userId?: bigint; readonly roadmapId?: bigint; - readonly full?: boolean; + readonly full?: boolean | bigint; readonly createdAt?: Date; } // Class export class RoadmapView implements IRoadmapView { - private _id: bigint; - private _userId: bigint; - private _roadmapId: bigint; - private _full: boolean; - private _createdAt: Date; - public constructor({ id = -1n, userId = -1n, @@ -43,41 +37,36 @@ export class RoadmapView implements IRoadmapView { this._id = id; this._userId = userId; this._roadmapId = roadmapId; - this._full = full; + this._full = !!full; this._createdAt = createdAt; } - // Method to modify the properties - public set({ - id, - userId, - roadmapId, - full, - createdAt, - }: IRoadmapViewModifications): void { - if (id !== undefined) this._id = id; - if (userId !== undefined) this._userId = userId; - if (roadmapId !== undefined) this._roadmapId = roadmapId; - if (full !== undefined) this._full = full; - if (createdAt !== undefined) this._createdAt = createdAt; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _roadmapId: bigint; + public get roadmapId(): bigint { return this._roadmapId; } + private _full: boolean; + public get full(): boolean { return this._full; } + private _createdAt: Date; + public get createdAt(): Date { return this._createdAt; } @@ -94,6 +83,21 @@ export class RoadmapView implements IRoadmapView { ); } + // Method to modify the properties + public set({ + id, + userId, + roadmapId, + full, + createdAt, + }: IRoadmapViewModifications): void { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (roadmapId !== undefined) this._roadmapId = roadmapId; + if (full !== undefined) this._full = !!full; + if (createdAt !== undefined) this._createdAt = createdAt; + } + // toObject method public toObject(): IRoadmapView { return { diff --git a/src/types/models/Session.ts b/src/types/models/Session.ts index 0401be4..f4b0165 100644 --- a/src/types/models/Session.ts +++ b/src/types/models/Session.ts @@ -24,11 +24,6 @@ interface ISessionModifications { // Class export class Session implements ISession { - private _id: bigint; - private _userId: bigint; - private _token: string; - private _expires: Date; - public constructor({ id = -1n, userId, @@ -41,26 +36,26 @@ export class Session implements ISession { this._expires = expires; } - // Method to modify the properties - public set({ id, userId, token, expires }: ISessionModifications): void { - if (id !== undefined) this._id = id; - if (userId !== undefined) this._userId = userId; - if (token !== undefined) this._token = token; - if (expires !== undefined) this._expires = expires; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _token: string; + public get token(): string { return this._token; } + private _expires: Date; + public get expires(): Date { return this._expires; } @@ -76,6 +71,14 @@ export class Session implements ISession { ); } + // Method to modify the properties + public set({ id, userId, token, expires }: ISessionModifications): void { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (token !== undefined) this._token = token; + if (expires !== undefined) this._expires = expires; + } + // toObject method to convert the class instance to an object public toObject(): ISession { return { diff --git a/src/types/models/User.ts b/src/types/models/User.ts index 0a7f826..3e3f165 100644 --- a/src/types/models/User.ts +++ b/src/types/models/User.ts @@ -39,16 +39,6 @@ interface IUserModifications { // Class export class User implements IUser { - private _id: bigint; - private _avatar: string | null; - private _name: string; - private _email: string; - private _role: number | null; - private _pwdHash: string | null; - private _googleId: string | null; - private _githubId: string | null; - private _createdAt: Date; - public constructor({ id = -1n, avatar = null, @@ -71,61 +61,56 @@ export class User implements IUser { this._createdAt = createdAt; } - // Method to modify the properties - public set({ - id, - avatar, - name, - email, - role, - pwdHash, - googleId, - githubId, - createdAt, - }: IUserModifications): void { - if (id !== undefined) this._id = id; - if (avatar !== undefined) this._avatar = avatar; - if (name !== undefined) this._name = name; - if (email !== undefined) this._email = email; - if (role !== undefined) this._role = role; - if (pwdHash !== undefined) this._pwdHash = pwdHash; - if (googleId !== undefined) this._googleId = googleId; - if (githubId !== undefined) this._githubId = githubId; - if (createdAt !== undefined) this._createdAt = createdAt; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _avatar: string | null; + public get avatar(): string | null { return this._avatar; } + private _name: string; + public get name(): string { return this._name; } + private _email: string; + public get email(): string { return this._email; } + private _role: number | null; + public get role(): number | null { return this._role; } + private _pwdHash: string | null; + public get pwdHash(): string | null { return this._pwdHash; } + private _googleId: string | null; + public get googleId(): string | null { return this._googleId; } + private _githubId: string | null; + public get githubId(): string | null { return this._githubId; } + private _createdAt: Date; + public get createdAt(): Date { return this._createdAt; } @@ -141,6 +126,29 @@ export class User implements IUser { ); } + // Method to modify the properties + public set({ + id, + avatar, + name, + email, + role, + pwdHash, + googleId, + githubId, + createdAt, + }: IUserModifications): void { + if (id !== undefined) this._id = id; + if (avatar !== undefined) this._avatar = avatar; + if (name !== undefined) this._name = name; + if (email !== undefined) this._email = email; + if (role !== undefined) this._role = role; + if (pwdHash !== undefined) this._pwdHash = pwdHash; + if (googleId !== undefined) this._googleId = googleId; + if (githubId !== undefined) this._githubId = githubId; + if (createdAt !== undefined) this._createdAt = createdAt; + } + // toObject method public toObject(): IUser { return { diff --git a/src/types/models/UserInfo.ts b/src/types/models/UserInfo.ts index c503ff4..3b3005b 100644 --- a/src/types/models/UserInfo.ts +++ b/src/types/models/UserInfo.ts @@ -30,13 +30,6 @@ interface IUserInfoModifications { // Class export class UserInfo implements IUserInfo { - private _id: bigint; - private _userId: bigint; - private _bio: string | null; - private _quote: string | null; - private _websiteUrl: string | null; - private _githubUrl: string | null; - public constructor({ id = -1n, userId, @@ -53,43 +46,38 @@ export class UserInfo implements IUserInfo { this._githubUrl = githubUrl; } - // Method to modify the properties - public set({ - id, - userId, - bio, - quote, - websiteUrl, - githubUrl, - }: IUserInfoModifications) { - if (id !== undefined) this._id = id; - if (userId !== undefined) this._userId = userId; - if (bio !== undefined) this._bio = bio; - if (quote !== undefined) this._quote = quote; - if (websiteUrl !== undefined) this._websiteUrl = websiteUrl; - if (githubUrl !== undefined) this._githubUrl = githubUrl; - } + private _id: bigint; public get id(): bigint { return this._id; } + private _userId: bigint; + public get userId(): bigint { return this._userId; } + private _bio: string | null; + public get bio(): string | null { return this._bio; } + private _quote: string | null; + public get quote(): string | null { return this._quote; } + private _websiteUrl: string | null; + public get websiteUrl(): string | null { return this._websiteUrl; } + private _githubUrl: string | null; + public get githubUrl(): string | null { return this._githubUrl; } @@ -99,6 +87,23 @@ export class UserInfo implements IUserInfo { return typeof obj === 'object' && obj !== null && 'userId' in obj; } + // Method to modify the properties + public set({ + id, + userId, + bio, + quote, + websiteUrl, + githubUrl, + }: IUserInfoModifications) { + if (id !== undefined) this._id = id; + if (userId !== undefined) this._userId = userId; + if (bio !== undefined) this._bio = bio; + if (quote !== undefined) this._quote = quote; + if (websiteUrl !== undefined) this._websiteUrl = websiteUrl; + if (githubUrl !== undefined) this._githubUrl = githubUrl; + } + // toObject method public toObject(): IUserInfo { return { diff --git a/src/types/response/ResFullRoadmap.ts b/src/types/response/ResFullRoadmap.ts index 24b70f3..6b5b4d2 100644 --- a/src/types/response/ResFullRoadmap.ts +++ b/src/types/response/ResFullRoadmap.ts @@ -96,7 +96,7 @@ export class ResFullRoadmap implements IResFullRoadmap { 'name' in obj && 'description' in obj && 'topic' in obj && - 'data' in obj && + 'data' in obj && 'miscData' in obj && 'isFeatured' in obj && 'isPublic' in obj && diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index 5625f2c..1944614 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -281,35 +281,6 @@ class Database { return result.affectedRows > 0; } - private _buildWhereQuery = ( - like: boolean, - ...values: unknown[] - ): { keyString: string; params: unknown[] } | null => { - let keyString = ''; - let params: unknown[] = []; - - for (let i = 0; i < values.length - 1; i += 2) { - const key = values[i] as string; - - if (Array.isArray(values[i + 1])) { - const arrayParams = values[i + 1] as unknown[]; - if ((values[i + 1] as unknown[]).length === 0) continue; - const subKeyString = arrayParams - .map(() => `${key} ${like ? 'LIKE' : '='} ?`) - .join(' OR '); - keyString += i > 0 ? ' AND ' : ''; - keyString += `(${subKeyString})`; - params = [...params, ...arrayParams]; - } else { - if (i > 0) keyString += ' AND '; - keyString += `${key} ${like ? 'LIKE' : '='} ?`; - params = [...params, values[i + 1]]; - } - } - - return { keyString, params }; - }; - public async getQuery( sql: string, params?: unknown[], @@ -345,22 +316,6 @@ class Database { return this._getWhere(table, true, ...values); } - protected async _getWhere( - table: string, - like: boolean, - ...values: unknown[] - ): Promise { - const queryBuilderResult = this._buildWhereQuery(like, ...values); - if (!queryBuilderResult) return null; - - const sql = `SELECT * - FROM ${table} - WHERE ${queryBuilderResult.keyString}`; - const result = await this._query(sql, queryBuilderResult.params); - - return getFirstResult(result); - } - public async getAll(table: string): Promise { // create sql query - select * from table const sql = `SELECT * @@ -388,22 +343,6 @@ class Database { return this._getAllWhere(table, true, ...values); } - protected async _getAllWhere( - table: string, - like: boolean, - ...values: unknown[] - ): Promise { - const queryBuilderResult = this._buildWhereQuery(like, ...values); - if (!queryBuilderResult) return null; - - const sql = `SELECT * - FROM ${table} - WHERE ${queryBuilderResult.keyString}`; - const result = await this._query(sql, queryBuilderResult.params); - - return parseResult(result) as T[] | null; - } - public async sum(table: string, column: string): Promise { const sql = `SELECT SUM(${column}) FROM ${table}`; @@ -433,23 +372,6 @@ class Database { return await this._sumWhere(table, column, true, ...values); } - protected async _sumWhere( - table: string, - column: string, - like: boolean, - ...values: unknown[] - ): Promise { - const queryBuilderResult = this._buildWhereQuery(like, ...values); - if (!queryBuilderResult) return 0n; - - const sql = `SELECT SUM(${column}) - FROM ${table} - WHERE ${queryBuilderResult.keyString}`; - const result = await this._query(sql, queryBuilderResult.params); - - return ((result as CountDataPacket[])[0][`SUM(${column})`] as bigint) || 0n; - } - public async countQuery(sql: string, params?: unknown[]): Promise { const result = await this._query(sql, params); console.log(result); @@ -482,6 +404,73 @@ class Database { return await this._countWhere(table, true, ...values); } + public async _query( + sql: string, + params?: unknown[], + ): Promise { + while (!Database.isSetup) await new Promise((r) => setTimeout(r, 100)); + if (!sql) return Promise.reject(new Error('No SQL query')); + + // get connection from pool + const conn = await Database.pool.getConnection(); + try { + // execute query and return result + return await conn.query(sql, params); + } finally { + // release connection + await conn.release(); + } + } + + protected async _getWhere( + table: string, + like: boolean, + ...values: unknown[] + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return null; + + const sql = `SELECT * + FROM ${table} + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); + + return getFirstResult(result); + } + + protected async _getAllWhere( + table: string, + like: boolean, + ...values: unknown[] + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return null; + + const sql = `SELECT * + FROM ${table} + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); + + return parseResult(result) as T[] | null; + } + + protected async _sumWhere( + table: string, + column: string, + like: boolean, + ...values: unknown[] + ): Promise { + const queryBuilderResult = this._buildWhereQuery(like, ...values); + if (!queryBuilderResult) return 0n; + + const sql = `SELECT SUM(${column}) + FROM ${table} + WHERE ${queryBuilderResult.keyString}`; + const result = await this._query(sql, queryBuilderResult.params); + + return ((result as CountDataPacket[])[0][`SUM(${column})`] as bigint) || 0n; + } + protected async _countWhere( table: string, like: boolean, @@ -544,23 +533,34 @@ class Database { } else await this.insert('users', user, false); } - public async _query( - sql: string, - params?: unknown[], - ): Promise { - while (!Database.isSetup) await new Promise((r) => setTimeout(r, 100)); - if (!sql) return Promise.reject(new Error('No SQL query')); + private _buildWhereQuery = ( + like: boolean, + ...values: unknown[] + ): { keyString: string; params: unknown[] } | null => { + let keyString = ''; + let params: unknown[] = []; - // get connection from pool - const conn = await Database.pool.getConnection(); - try { - // execute query and return result - return await conn.query(sql, params); - } finally { - // release connection - await conn.release(); + for (let i = 0; i < values.length - 1; i += 2) { + const key = values[i] as string; + + if (Array.isArray(values[i + 1])) { + const arrayParams = values[i + 1] as unknown[]; + if ((values[i + 1] as unknown[]).length === 0) continue; + const subKeyString = arrayParams + .map(() => `${key} ${like ? 'LIKE' : '='} ?`) + .join(' OR '); + keyString += i > 0 ? ' AND ' : ''; + keyString += `(${subKeyString})`; + params = [...params, ...arrayParams]; + } else { + if (i > 0) keyString += ' AND '; + keyString += `${key} ${like ? 'LIKE' : '='} ?`; + params = [...params, values[i + 1]]; + } } - } + + return { keyString, params }; + }; } export default Database; diff --git a/src/util/Database/ExploreDB.ts b/src/util/Database/ExploreDB.ts index 41a68c1..9e8505e 100644 --- a/src/util/Database/ExploreDB.ts +++ b/src/util/Database/ExploreDB.ts @@ -25,25 +25,26 @@ class ExploreDB extends Database { if (typeof search != 'string' || !page || !limit || !topic || !order) return { result: [], totalRoadmaps: 0n }; const query = ` - SELECT * - FROM ( - SELECT - r.id as id, - r.name AS name, - r.description AS description, - r.topic AS topic, - r.isFeatured AS isFeatured, - r.isPublic AS isPublic, - r.isDraft AS isDraft, - r.createdAt AS createdAt, - r.updatedAt AS updatedAt, - u.id AS userId, - u.avatar AS userAvatar, - u.name AS userName, - (SELECT SUM(rl.value) - FROM roadmapLikes rl WHERE roadmapId = r.id) AS likeCount, - (SELECT COUNT(*) FROM roadmapViews WHERE roadmapId = r.id) AS viewCount, - ${ + SELECT * + FROM (SELECT r.id as id, + r.name AS name, + r.description AS description, + r.topic AS topic, + r.isFeatured AS isFeatured, + r.isPublic AS isPublic, + r.isDraft AS isDraft, + r.createdAt AS createdAt, + r.updatedAt AS updatedAt, + u.id AS userId, + u.avatar AS userAvatar, + u.name AS userName, + (SELECT SUM(rl.value) + FROM roadmapLikes rl + WHERE roadmapId = r.id) AS likeCount, + (SELECT COUNT(*) + FROM roadmapViews + WHERE roadmapId = r.id) AS viewCount, + ${ !!userid ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id @@ -51,49 +52,46 @@ class ExploreDB extends Database { ) ` : '0' -} AS isLiked - FROM - roadmaps r - INNER JOIN users u ON r.userId = u.id - WHERE - (r.name LIKE ? OR r.description LIKE ?) - AND r.topic IN (${ +} AS isLiked + FROM roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE (r.name LIKE ? OR r.description LIKE ?) + AND r.topic IN (${ Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' }) - AND r.isPublic = 1 - AND r.isDraft = 0 - ) as t - ORDER BY - t.isFeatured DESC, ${order.by === 't.likeCount' ? - `CASE + AND r.isPublic = 1 + AND r.isDraft = 0) as t + ORDER BY t.isFeatured DESC, ${ + order.by === 't.likeCount' + ? `CASE WHEN t.likeCount < 0 THEN 3 WHEN t.likeCount = 0 THEN 2 ELSE 1 - END,` : ''} ${order.by} ${order.direction} - LIMIT ?, ? - ; + END,` + : '' +} ${order.by} ${order.direction} + LIMIT ?, ? + ; `; const query2 = ` - SELECT - count(*) AS result, - ${ + SELECT count(*) AS result, + ${ !!userid ? `(SELECT value FROM roadmapLikes WHERE roadmapId = r.id AND userId = ? - )` : '0' -} AS isLiked - FROM - roadmaps r - INNER JOIN users u ON r.userId = u.id - WHERE - (r.name LIKE ? OR r.description LIKE ?) + )` + : '0' +} AS isLiked + FROM roadmaps r + INNER JOIN users u ON r.userId = u.id + WHERE (r.name LIKE ? OR r.description LIKE ?) AND r.topic IN (${ Array.isArray(topic) ? topic.map(() => '?').join(', ') : '?' }) AND r.isPublic = 1 AND r.isDraft = 0; - `; + `; const params = []; if (!!userid) { diff --git a/src/util/EmailUtil.ts b/src/util/EmailUtil.ts index 9b20caf..905713e 100644 --- a/src/util/EmailUtil.ts +++ b/src/util/EmailUtil.ts @@ -1,6 +1,10 @@ export function checkEmail(email: string): boolean { // check if email is valid // https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1 - return !!email.match(RegExp('^[\\w!#$%&\'*+/=?^`{|}~.-]+' + - '@(?!-)[A-Za-z0-9-]+([-.][a-z0-9]+)*\\.[A-Za-z]{2,63}$')); -} \ No newline at end of file + return !!email.match( + RegExp( + '^[\\w!#$%&\'*+/=?^`{|}~.-]+' + + '@(?!-)[A-Za-z0-9-]+([-.][a-z0-9]+)*\\.[A-Za-z]{2,63}$', + ), + ); +} diff --git a/src/util/LoginUtil.ts b/src/util/LoginUtil.ts index 57592a2..e6fd4b2 100644 --- a/src/util/LoginUtil.ts +++ b/src/util/LoginUtil.ts @@ -13,12 +13,9 @@ function saltPassword(password: string): string { } function comparePassword(password: string, hash: string): boolean { - const [ salt, hashFromDb ] = hash.split(':'); + const [salt, hashFromDb] = hash.split(':'); const hashToCompare = bcrypt.hashSync(password, salt); return hashToCompare === hashFromDb; } -export { - saltPassword, - comparePassword, -}; +export { saltPassword, comparePassword }; diff --git a/src/util/Views.ts b/src/util/Views.ts index b95b51b..7a4015f 100644 --- a/src/util/Views.ts +++ b/src/util/Views.ts @@ -28,8 +28,12 @@ export async function addRoadmapImpression( ): Promise { if (!userId) userId = -1n; const values = roadmapId.map((id) => `(${id}, ${userId}, 0)`).join(', '); - return ((await db._query(` + return ( + ( + (await db._query(` INSERT INTO roadmapViews (roadmapId, userId, full) VALUES ${values} - `)) as ResultSetHeader).affectedRows >= 0; -} \ No newline at end of file + `)) as ResultSetHeader + ).affectedRows >= 0 + ); +} diff --git a/src/util/data-alteration/AlterResponse.ts b/src/util/data-alteration/AlterResponse.ts deleted file mode 100644 index c19caf8..0000000 --- a/src/util/data-alteration/AlterResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function alterResponseToBooleans(payload: T, keys: string[]) { - const alteredPayload = { ...payload }; - keys.forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - alteredPayload[key] = !!alteredPayload[key]; - }); - return alteredPayload; -} diff --git a/src/util/misc.ts b/src/util/misc.ts index 18afc60..908c7ed 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -1,23 +1,25 @@ /* eslint-disable */ export function JSONSafety(obj: unknown): unknown { - return JSON.parse(JSON.stringify(obj, (key, value) => { - // if value is a bigint, convert it to a string - if (typeof value === 'bigint') return Number(value); - // if value has a toObject method, call it and return the result - else if ( - typeof value === 'object' && - value !== null && - 'toObject' in value && - typeof value.toObject === 'function' - ) { - return value.toObject(); - } + return JSON.parse( + JSON.stringify(obj, (key, value) => { + // if value is a bigint, convert it to a string + if (typeof value === 'bigint') return Number(value); + // if value has a toObject method, call it and return the result + else if ( + typeof value === 'object' && + value !== null && + 'toObject' in value && + typeof value.toObject === 'function' + ) { + return value.toObject(); + } - // return value as is - return value; - })); + // return value as is + return value; + }), + ); } export function isEmptyObject(obj: any): obj is Record { - return obj && typeof obj === 'object' && Object.keys(obj).length === 0 -} \ No newline at end of file + return obj && typeof obj === 'object' && Object.keys(obj).length === 0; +} From c9a940a76a01ab1ec78c58beb8c60593f14f7778 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 17:24:55 +0300 Subject: [PATCH 106/118] Updated test.yml - use the right secret --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f011994..2374eb1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 with: submodules: 'recursive' - token: ${{ secrets.envAccessToken }} + token: ${{ secrets.PAT }} - uses: actions/setup-node@v1 with: node-version: '18.x' From b512dfff64f7008021489d0f35ed8502151f0a69 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 22:38:43 +0300 Subject: [PATCH 107/118] Fixing isLiked on full roadmap --- src/controllers/roadmapController.ts | 19 +++++++++++-------- src/util/Database/DatabaseDriver.ts | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index c647f8f..3553a1a 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -100,14 +100,17 @@ export async function getRoadmap(req: RequestWithSession, res: Response) { 'roadmapId', roadmap.id, ); - const isLiked = await db.sumWhere( - 'roadmapLikes', - 'value', - 'roadmapId', - roadmap.id, - 'userId', - userId, - ); + const isLiked = + userId !== undefined && userId !== null ? + await db.sumWhere( + 'roadmapLikes', + 'value', + 'roadmapId', + roadmap.id, + 'userId', + userId, + ) + : 0n; if (!roadmap.isPublic && roadmap.userId !== userId) return responseNotAllowed(res); diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index 1944614..9096ff4 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -5,7 +5,6 @@ import path from 'path'; import logger from 'jet-logger'; import { User } from '@src/types/models/User'; import { GenericModelClass } from '@src/types/models/GenericModelClass'; -import * as console from 'console'; // database credentials const { DBCred } = EnvVars; @@ -374,7 +373,6 @@ class Database { public async countQuery(sql: string, params?: unknown[]): Promise { const result = await this._query(sql, params); - console.log(result); return (result as CountQueryPacket[])[0]['result'] || 0n; } @@ -468,7 +466,9 @@ class Database { WHERE ${queryBuilderResult.keyString}`; const result = await this._query(sql, queryBuilderResult.params); - return ((result as CountDataPacket[])[0][`SUM(${column})`] as bigint) || 0n; + return BigInt( + (result as CountDataPacket[])[0][`SUM(${column})`] as number, + ) || 0n; } protected async _countWhere( From dbeb8065f3ace7a3930cdc551f1b929fb992f090 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 22:55:11 +0300 Subject: [PATCH 108/118] Fixing _sumWhere --- src/util/Database/DatabaseDriver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index 9096ff4..2ba81b7 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -467,8 +467,8 @@ class Database { const result = await this._query(sql, queryBuilderResult.params); return BigInt( - (result as CountDataPacket[])[0][`SUM(${column})`] as number, - ) || 0n; + (result as CountDataPacket[])[0][`SUM(${column})`] as number || 0, + ); } protected async _countWhere( From b1f0f5ea4fd79c2757791ff9dd7f8ebed6162fd7 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 22:57:47 +0300 Subject: [PATCH 109/118] Fix validateBody.ts --- src/middleware/validators/validateBody.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/validators/validateBody.ts b/src/middleware/validators/validateBody.ts index 1faa551..2278091 100644 --- a/src/middleware/validators/validateBody.ts +++ b/src/middleware/validators/validateBody.ts @@ -23,7 +23,7 @@ export default function ( } for (const item of requiredFields) { - if (!body[item] === null || !body[item] === undefined) { + if (body[item] === null || body[item] === undefined) { return res .status(HttpStatusCode.BadRequest) .json({ error: `Missing required field: ${item}` }); From 2d0fcbc6ba49f2b781b9c8afd8ef4b30e23eca28 Mon Sep 17 00:00:00 2001 From: sopy Date: Mon, 11 Sep 2023 23:30:08 +0300 Subject: [PATCH 110/118] Fix profile --- src/constants/Paths.ts | 1 + src/controllers/usersController.ts | 36 ++++++++++++++++++++------- src/routes/usersRoutes/UsersUpdate.ts | 6 ++--- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index c20190a..c23bedc 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -54,6 +54,7 @@ const Paths = { }, Update: { Base: '/', + All: '/', ProfilePicture: '/profile-picture', Bio: '/bio', Quote: '/quote', diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 93a8bc7..4c78b62 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -17,7 +17,9 @@ import { RequestWithTargetUserId, } from '@src/middleware/validators/validateUser'; import { ResRoadmap } from '@src/types/response/ResRoadmap'; -import { responseServerError } from '@src/helpers/responses/generalResponses'; +import { + responseServerError, +} from '@src/helpers/responses/generalResponses'; import { responseAlreadyFollowing, responseCantFollowYourself, @@ -35,6 +37,8 @@ import { } from '@src/helpers/responses/roadmapResponses'; import { addRoadmapImpression } from '@src/util/Views'; import logger from 'jet-logger'; +import * as console from 'console'; +import { RequestWithBody } from '@src/middleware/validators/validateBody'; /* ! Main route controllers @@ -234,13 +238,27 @@ export async function userUnfollow( ! UsersPost route controllers */ export async function usersPostProfile( - req: RequestWithSession, + req: RequestWithBody, res: Response, ): Promise { // get variables - const { name, githubUrl, websiteUrl, quote } = req.body as { - [key: string]: string; - }; + const { name, githubUrl, websiteUrl, bio } = req.body; + + if ( + name === undefined || + name === null || + githubUrl === undefined || + githubUrl === null || + websiteUrl === undefined || + websiteUrl === null || + bio === undefined || + bio === null + ) + return responseServerError(res); + + console.log(req.body); + + console.log(Object.keys(req)); // get database const db = new DatabaseDriver(); @@ -258,13 +276,13 @@ export async function usersPostProfile( if (!user || !userInfo) return responseServerError(res); user.set({ - name, + name: name as string, }); userInfo.set({ - githubUrl, - websiteUrl, - quote, + githubUrl: githubUrl as string, + websiteUrl: githubUrl as string, + bio: bio as string, }); // save user to database diff --git a/src/routes/usersRoutes/UsersUpdate.ts b/src/routes/usersRoutes/UsersUpdate.ts index d775b13..6abde26 100644 --- a/src/routes/usersRoutes/UsersUpdate.ts +++ b/src/routes/usersRoutes/UsersUpdate.ts @@ -14,13 +14,13 @@ import { usersPostProfileWebsiteUrl, } from '@src/controllers/usersController'; -const UsersUpdate = Router({ mergeParams: true }); +const UsersUpdate = Router(); UsersUpdate.post('*', validateSession); // ! Global middleware for file UsersUpdate.post( - Paths.Users.Update.Base, - validateBody('name', 'githubUrl', 'websiteUrl', 'quote'), + Paths.Users.Update.All, + validateBody('name', 'githubUrl', 'websiteUrl', 'bio'), usersPostProfile, ); From b2b47005e67dcc0b4535aff3ef996735022fe40f Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 00:25:32 +0300 Subject: [PATCH 111/118] Optimize session middleware handling --- src/middleware/session.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/middleware/session.ts b/src/middleware/session.ts index 5038610..4fceb77 100644 --- a/src/middleware/session.ts +++ b/src/middleware/session.ts @@ -85,19 +85,11 @@ export async function sessionMiddleware( }); req.session = undefined; - - next(); - - return; } else { await extendSession(db, req, res); } } else { req.session = undefined; - } - - // if session still doesn't exist, delete cookie - if (!req.session) { res.cookie('token', '', { httpOnly: false, secure: EnvVars.NodeEnv === NodeEnvs.Production, @@ -105,6 +97,5 @@ export async function sessionMiddleware( sameSite: 'strict', }); } - next(); } From 8b47297126ece22945ae11c43466a651bca3ee4d Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 00:25:58 +0300 Subject: [PATCH 112/118] Fix DB queries for user statistics --- src/helpers/databaseManagement.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/helpers/databaseManagement.ts b/src/helpers/databaseManagement.ts index a26e70c..3d7b3ec 100644 --- a/src/helpers/databaseManagement.ts +++ b/src/helpers/databaseManagement.ts @@ -69,17 +69,7 @@ export async function getUserStats( userId: bigint, ): Promise { const roadmapsCount = await db.countWhere('roadmaps', 'userId', userId); - const roadmapsViews = await db.countQuery( - ` - SELECT SUM(rl.value) AS 'result' - FROM users u - LEFT JOIN roadmaps r ON u.id = r.userId - LEFT JOIN roadmapLikes rl ON r.id = rl.roadmapId - WHERE u.id = ?; - `, - [userId], - ); - const roadmapsLikes = await db.countQuery( + const roadmapsViews = await db.sumQuery( ` SELECT COUNT(rv.userId) AS 'result' FROM roadmaps r @@ -89,6 +79,16 @@ export async function getUserStats( `, [userId], ); + const roadmapsLikes = await db.sumQuery( + ` + SELECT SUM(rl.value) AS 'result' + FROM users u + LEFT JOIN roadmaps r ON u.id = r.userId + LEFT JOIN roadmapLikes rl ON r.id = rl.roadmapId + WHERE u.id = ?; + `, + [userId], + ); const followerCount = await db.countWhere('followers', 'userId', userId); const followingCount = await db.countWhere('followers', 'followerId', userId); From ef9a236d12280696e99058325a08325d97d2fdc4 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 07:16:38 +0300 Subject: [PATCH 113/118] Getting rid of useless console logs --- src/controllers/usersController.ts | 5 ----- src/util/Database/DatabaseDriver.ts | 2 -- 2 files changed, 7 deletions(-) diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 4c78b62..569ef6e 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -37,7 +37,6 @@ import { } from '@src/helpers/responses/roadmapResponses'; import { addRoadmapImpression } from '@src/util/Views'; import logger from 'jet-logger'; -import * as console from 'console'; import { RequestWithBody } from '@src/middleware/validators/validateBody'; /* @@ -256,10 +255,6 @@ export async function usersPostProfile( ) return responseServerError(res); - console.log(req.body); - - console.log(Object.keys(req)); - // get database const db = new DatabaseDriver(); diff --git a/src/util/Database/DatabaseDriver.ts b/src/util/Database/DatabaseDriver.ts index 2ba81b7..0ebecf9 100644 --- a/src/util/Database/DatabaseDriver.ts +++ b/src/util/Database/DatabaseDriver.ts @@ -200,8 +200,6 @@ class Database { discardId = true, ): Promise { const { keys, values } = processData(data, discardId); - // console.log('keys', keys); - // console.log('values', values); // create sql query - update table set key = ?, key = ? where id = ? // ? for values to be replaced by params From 45d9da5743ac3fc05fbd1a7bb45f0f8eda9dd63e Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 13:49:57 +0300 Subject: [PATCH 114/118] Add version field to Roadmap model A 'version' field has been added to the 'Roadmap' model and setup.sql script. This field can be used to track the version number of each roadmap. A RegEx check has been added to make sure that the version follows the correct format (e.g. '1.0.0'). By default, the version is set to be '0.0.0'. This change will be beneficial for future roadmap iterations and version controls to ensure backwards compatibility. --- src/sql/setup.sql | 8 +++++--- src/types/models/Roadmap.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/sql/setup.sql b/src/sql/setup.sql index 94b22fb..a113c4d 100644 --- a/src/sql/setup.sql +++ b/src/sql/setup.sql @@ -41,11 +41,13 @@ create table if not exists roadmaps description varchar(255) not null, topic enum ('programming', 'math', 'physics', 'biology') not null, userId bigint not null, - isFeatured tinyint(1) default 0 not null, - isPublic tinyint(1) default 1 not null, - isDraft tinyint(1) default 0 not null, + isFeatured tinyint(1) default 0 not null, + isPublic tinyint(1) default 1 not null, + isDraft tinyint(1) default 0 not null, data longtext not null, miscData longtext not null, + version varchar(255) default '0.0.0' not null + check (version regexp '^[0-9]+\\.[0-9]+\\.[0-9]+$'), createdAt timestamp default current_timestamp() not null, updatedAt timestamp default current_timestamp() not null on update current_timestamp(), constraint roadmaps_userId_fk diff --git a/src/types/models/Roadmap.ts b/src/types/models/Roadmap.ts index c416909..925641f 100644 --- a/src/types/models/Roadmap.ts +++ b/src/types/models/Roadmap.ts @@ -17,6 +17,7 @@ export interface IRoadmap { readonly isDraft: boolean; readonly data: string; readonly miscData: string; + readonly version: string; readonly createdAt: Date; readonly updatedAt: Date; } @@ -33,6 +34,7 @@ interface IRoadmapConstructor { readonly isPublic?: boolean | bigint; readonly isDraft?: boolean | bigint; readonly data: string; + readonly version: string; readonly createdAt?: Date; readonly updatedAt?: Date; } @@ -49,6 +51,7 @@ interface IRoadmapModifications { readonly isDraft?: boolean | bigint; readonly data?: string; readonly miscData?: string; + readonly version?: string; readonly createdAt?: Date; readonly updatedAt?: Date; } @@ -66,6 +69,7 @@ export class Roadmap implements IRoadmap { isDraft = false, data, miscData, + version = '0.0.0', createdAt = new Date(), updatedAt = new Date(), }: IRoadmapConstructor) { @@ -81,6 +85,11 @@ export class Roadmap implements IRoadmap { this._miscData = miscData; this._createdAt = createdAt; this._updatedAt = updatedAt; + + if (!version.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)) + version = '0.0.0'; + + this._version = version; } private _id: bigint; @@ -143,6 +152,12 @@ export class Roadmap implements IRoadmap { return this._miscData; } + private _version: string; + + public get version(): string { + return this._version; + } + private _createdAt: Date; public get createdAt(): Date { @@ -182,6 +197,7 @@ export class Roadmap implements IRoadmap { isDraft, data, miscData, + version, createdAt, updatedAt, }: IRoadmapModifications): void { @@ -196,6 +212,9 @@ export class Roadmap implements IRoadmap { if (miscData !== undefined) this._miscData = miscData; if (createdAt !== undefined) this._createdAt = createdAt; if (updatedAt !== undefined) this._updatedAt = updatedAt; + + if (version !== undefined && version.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)) + this._version = version; } // toObject method @@ -211,6 +230,7 @@ export class Roadmap implements IRoadmap { isDraft: this._isDraft, data: this._data, miscData: this._miscData, + version: this._version, createdAt: this._createdAt, updatedAt: this._updatedAt, }; From 59c4af91b8b542ab30bfb07456fc61d67c788eaf Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 14:02:10 +0300 Subject: [PATCH 115/118] Add version handling to roadmaps paths The changes herein introduce a 'version' parameter to roadmaps in the application. Slight tweaks in other files were made to accommodate this new parameter. --- src/constants/Paths.ts | 1 + src/controllers/roadmapController.ts | 42 ++++++++++++++++++--- src/middleware/validators/validateBody.ts | 2 +- src/routes/RoadmapsRouter.ts | 1 + src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 11 +++++- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/constants/Paths.ts b/src/constants/Paths.ts index c23bedc..f3f65fc 100644 --- a/src/constants/Paths.ts +++ b/src/constants/Paths.ts @@ -38,6 +38,7 @@ const Paths = { Draft: '/draft', Data: '/data', MiscData: '/misc-data', // used for roadmap wide data like roadmap theme + Version: '/version', }, Delete: '/:roadmapId([0-9]+)', Like: '/:roadmapId([0-9]+)/like', diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 3553a1a..9d26e9c 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -38,7 +38,7 @@ import logger from 'jet-logger'; export async function createRoadmap(req: RequestWithBody, res: Response) { // guaranteed to exist by middleware - const { name, description, data, miscData } = req.body; + const { name, description, data, miscData, version } = req.body; // non guaranteed to exist by middleware of type Roadmap let { topic, isPublic, isDraft } = req.body; @@ -51,7 +51,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { const db = new Database(); if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) - topic = undefined; + topic = null; isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false; @@ -62,9 +62,10 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { topic: topic as RoadmapTopic | undefined, userId, isPublic: isPublic as boolean, - isDraft: isDraft as boolean, + isDraft: isDraft , data: data as string, miscData: miscData as string, + version: version as string, }); const id = await insertRoadmap(db, roadmap); @@ -172,9 +173,9 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) { if (!roadmap) return responseRoadmapNotFound(res); if (roadmap.userId !== userId) return responseNotAllowed(res); - const { name, description, data, topic, isDraft } = req.body; + const { name, description, data, topic, miscData, isDraft } = req.body; - if (!name || !description || !data || !topic || !isDraft) + if (!name || !description || !data || !topic || !miscData || !isDraft) return responseServerError(res); if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) @@ -185,6 +186,7 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) { description: description as string, data: data as string, topic: topic as RoadmapTopic, + miscData: miscData as string, isDraft: Boolean(isDraft), }); @@ -363,6 +365,36 @@ export async function updateIsDraftRoadmap( return responseServerError(res); } +export async function updateVersionRoadmap( + req: RequestWithBody, + res: Response, +) { + const roadmapId = req.params.roadmapId; + const userId = req.session?.userId; + + if (!roadmapId) return responseServerError(res); + if (!userId) return responseServerError(res); + + const { version } = req.body; + + if (!version) return responseServerError(res); + + const db = new Database(); + + const roadmap = await getRoadmapData(db, BigInt(roadmapId)); + + if (!roadmap) return responseRoadmapNotFound(res); + + if (roadmap.userId !== userId) return responseNotAllowed(res); + + roadmap.set({ version: version as string }); + + if (await updateRoadmap(db, roadmap.id, roadmap)) + return responseRoadmapUpdated(res); + + return responseServerError(res); +} + export async function deleteRoadmap(req: RequestWithSession, res: Response) { const roadmapId = req.params.roadmapId; const userId = req.session?.userId; diff --git a/src/middleware/validators/validateBody.ts b/src/middleware/validators/validateBody.ts index 2278091..3155dc8 100644 --- a/src/middleware/validators/validateBody.ts +++ b/src/middleware/validators/validateBody.ts @@ -3,7 +3,7 @@ import { NextFunction, Response } from 'express'; import { HttpStatusCode } from 'axios'; interface IBody { - [key: string]: unknown; + [key: string]: string | number | boolean | object | null; } export interface RequestWithBody extends RequestWithSession { diff --git a/src/routes/RoadmapsRouter.ts b/src/routes/RoadmapsRouter.ts index 70918b0..f8fe701 100644 --- a/src/routes/RoadmapsRouter.ts +++ b/src/routes/RoadmapsRouter.ts @@ -23,6 +23,7 @@ RoadmapsRouter.post( 'data', 'isPublic', 'isDraft', + 'version', 'miscData', ), createRoadmap, diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index 3d62bdd..c322923 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -10,7 +10,7 @@ import { updateIsDraftRoadmap, updateMiscDataRoadmap, updateNameRoadmap, - updateTopicRoadmap, + updateTopicRoadmap, updateVersionRoadmap, } from '@src/controllers/roadmapController'; const RoadmapsUpdate = Router({ mergeParams: true }); @@ -18,7 +18,7 @@ const RoadmapsUpdate = Router({ mergeParams: true }); RoadmapsUpdate.post( Paths.Roadmaps.Update.All, validateSession, - validateBody('name', 'description', 'data', 'topic', 'isDraft'), + validateBody('name', 'description', 'data', 'topic', 'miscData', 'isDraft'), updateAllRoadmap, ); @@ -71,4 +71,11 @@ RoadmapsUpdate.post( updateMiscDataRoadmap, ); +RoadmapsUpdate.post( + Paths.Roadmaps.Update.Version, + validateSession, + validateBody('version'), + updateVersionRoadmap, +); + export default RoadmapsUpdate; From 8ef3dc7ce6dc98471126906baefa6e6db4498481 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 14:05:34 +0300 Subject: [PATCH 116/118] Add 'version' and 'miscData' fields to ResFullRoadmap interface This commit adds 'version' and 'miscData' fields to the ResFullRoadmap interface, and modifies its constructor and type guard accordingly. The 'version' field is needed to handle various versions of the roadmap internals, and 'miscData' is necessary for storing additional miscellaneous information about the roadmap. Minor adjustments in the constructor and type guard functions are included to support these changes. --- src/types/response/ResFullRoadmap.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/types/response/ResFullRoadmap.ts b/src/types/response/ResFullRoadmap.ts index 6b5b4d2..2de7dcc 100644 --- a/src/types/response/ResFullRoadmap.ts +++ b/src/types/response/ResFullRoadmap.ts @@ -7,6 +7,7 @@ export interface IResFullRoadmap { readonly description: string; readonly topic: RoadmapTopic; readonly data: string; + readonly isFeatured: boolean; readonly isPublic: boolean; readonly isDraft: boolean; readonly createdAt: Date; @@ -23,6 +24,10 @@ export interface IResFullRoadmap { // user stats readonly isLiked: bigint; + + // misc + readonly miscData: string; + readonly version: string; } export class ResFullRoadmap implements IResFullRoadmap { @@ -31,7 +36,6 @@ export class ResFullRoadmap implements IResFullRoadmap { public readonly description: string; public readonly topic: RoadmapTopic; public readonly data: string; - public readonly miscData: string; public readonly isFeatured: boolean; public readonly isPublic: boolean; public readonly isDraft: boolean; @@ -47,6 +51,9 @@ export class ResFullRoadmap implements IResFullRoadmap { public readonly isLiked: bigint; + public readonly miscData: string; + public readonly version: string; + public constructor( { id, @@ -59,6 +66,7 @@ export class ResFullRoadmap implements IResFullRoadmap { isFeatured, isPublic, isDraft, + version, createdAt, updatedAt, }: IRoadmap, @@ -72,7 +80,6 @@ export class ResFullRoadmap implements IResFullRoadmap { this.description = description; this.topic = topic; this.data = data; - this.miscData = miscData; this.isFeatured = isFeatured; this.isPublic = isPublic; this.isDraft = isDraft; @@ -86,6 +93,9 @@ export class ResFullRoadmap implements IResFullRoadmap { this.viewCount = viewCount; this.isLiked = isLiked; + + this.miscData = miscData; + this.version = version; } public static isRoadmap(obj: unknown): obj is IResFullRoadmap { @@ -97,7 +107,6 @@ export class ResFullRoadmap implements IResFullRoadmap { 'description' in obj && 'topic' in obj && 'data' in obj && - 'miscData' in obj && 'isFeatured' in obj && 'isPublic' in obj && 'isDraft' in obj && @@ -108,7 +117,9 @@ export class ResFullRoadmap implements IResFullRoadmap { 'userName' in obj && 'likeCount' in obj && 'viewCount' in obj && - 'isLiked' in obj + 'isLiked' in obj && + 'miscData' in obj && + 'version' in obj ); } } From b3e8f66134718b217b7b18e9ed863b2d35bbe532 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 14:44:46 +0300 Subject: [PATCH 117/118] Refactor session validation in RoadmapsUpdate routes Validation of session has been moved to apply for all routes defined in src/routes/roadmapsRoutes/RoadmapsUpdate.ts as a whole instead of applying it for each route individually. This is for code optimization and to ensure consistency of session validation step across all route definitions. --- src/routes/roadmapsRoutes/RoadmapsUpdate.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts index c322923..51a76ee 100644 --- a/src/routes/roadmapsRoutes/RoadmapsUpdate.ts +++ b/src/routes/roadmapsRoutes/RoadmapsUpdate.ts @@ -15,65 +15,58 @@ import { const RoadmapsUpdate = Router({ mergeParams: true }); +RoadmapsUpdate.post('*', validateSession); + RoadmapsUpdate.post( Paths.Roadmaps.Update.All, - validateSession, validateBody('name', 'description', 'data', 'topic', 'miscData', 'isDraft'), updateAllRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.About, - validateSession, validateBody('name', 'description', 'topic', 'miscData'), updateAboutRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Name, - validateSession, validateBody('name'), updateNameRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Description, - validateSession, validateBody('description'), updateDescriptionRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Data, - validateSession, validateBody('data'), updateDataRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Topic, - validateSession, validateBody('topic'), updateTopicRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Draft, - validateSession, validateBody('isDraft'), updateIsDraftRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.MiscData, - validateSession, validateBody('miscData'), updateMiscDataRoadmap, ); RoadmapsUpdate.post( Paths.Roadmaps.Update.Version, - validateSession, validateBody('version'), updateVersionRoadmap, ); From d0911e94101f23a2157a01985dd0d91ca3b552d2 Mon Sep 17 00:00:00 2001 From: sopy Date: Tue, 12 Sep 2023 16:38:05 +0300 Subject: [PATCH 118/118] Set default topic in roadmapController Made 'PROGRAMMING' the default topic if an invalid topic is provided. This step provides a fallback mechanism which prevents route failure due to any potential input error related to the 'topic' key.. --- src/controllers/roadmapController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/roadmapController.ts b/src/controllers/roadmapController.ts index 9d26e9c..6353c1f 100644 --- a/src/controllers/roadmapController.ts +++ b/src/controllers/roadmapController.ts @@ -51,7 +51,7 @@ export async function createRoadmap(req: RequestWithBody, res: Response) { const db = new Database(); if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic)) - topic = null; + topic = RoadmapTopic.PROGRAMMING; isPublic = true; if (isDraft !== true && isDraft !== false) isDraft = false;