diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..45a93db9 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2020", + "jsx": "react", + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} diff --git a/planning/v1/PERMISSIONS.md b/planning/v1/PERMISSIONS.md new file mode 100644 index 00000000..1b63479f --- /dev/null +++ b/planning/v1/PERMISSIONS.md @@ -0,0 +1,219 @@ +# Permissions + +## User Section + +| Permission Name | Description | Admin | +| ----------------- | ------------------------------- | ----- | +| user.read.private | Can read non-public users' data | false | +| user.read.admin | Can read users' admin fields | true | +| user.write.self | Can update own account | false | +| user.write.all | Can update all user data | true | + +--- + +## Article Section + +| Permission Name | Description | Admin | +| ------------------------ | -------------------------------------------------- | ----- | +| article.read.restricted | Can read institute restricted articles | false | +| article.read.unpublished | Can read unpublished, archived or trashed articles | true | +| article.read.admin | Can read admin fields of an article | true | +| article.write.new | Can write a new article | true | +| article.write.self | Can update own articles | true | +| article.write.all | Can update all articles | true | +| article.approve.all | Can approve all articles | true | + +--- + +## Reactions Section + +| Permission Name | Description | Admin | +| -------------------- | ---------------------------- | ----- | +| reactions.write.self | Can add/remove a reaction | false | +| reactions.write.all | Can add/remove all reactions | true | + +--- + +## Comment Section + +| Permission Name | Description | Admin | +| ----------------------- | ------------------------------ | ----- | +| comment.read.public | Can read public comments | false | +| comment.read.unapproved | Can read unapproved comments | true | +| comment.write.new | Can write a new comment | false | +| comment.write.approved | Can write pre-appoved comments | false | +| comment.write.self | Can update/delete own comments | false | +| comment.write.delete | Can delete all comments | true | +| comment.approve.all | Can approve all comments | true | + +--- + +## Issue Section + +| Permission Name | Description | Admin | +| ---------------------- | --------------------------------- | ----- | +| issue.read.unpublished | Can read unpublished issues | true | +| issue.read.admin | Can read admin fields of an issue | true | +| issue.write.new | Can create a new issue | true | +| issue.write.all | Can update all issues | true | +| issue.write.delete | Can delete all issues | true | + +--- + +## Session Section + +| Permission Name | Description | Admin | +| -------------------- | --------------------------- | ----- | +| session.write.new | Can create a new session | true | +| session.write.all | Can edit all session data | true | +| session.write.delete | Can delete all session data | true | + +--- + +## Squiggle Section + +| Permission Name | Description | Admin | +| --------------------- | ------------------------ | ----- | +| squiggle.read.all | Can read all squiggles | true | +| squiggle.write.new | Can add new squiggles | true | +| squiggle.write.all | Can update all squiggles | true | +| squiggle.write.delete | Can delete all squiggles | true | + +--- + +## Poll Section + +| Permission Name | Description | Admin | +| --------------------- | ----------------------------------------- | ----- | +| poll.write.restricted | Can respond to institute restricted polls | false | +| poll.write.all | Can add/edit all polls | true | +| poll.write.delete | Can delete all polls | true | + +--- + +## Media Section + +| Permission Name | Description | Admin | +| ------------------ | ----------------------------- | ----- | +| media.write.all | Can add/update all media data | true | +| media.write.delete | Can delete all media data | true | + +--- + +## Album Section + +| Permission Name | Description | Admin | +| --------------- | ------------------------------------ | ----- | +| album.write.all | Can add/update/delete all album data | true | + +--- + +## Tag Section + +| Permission Name | Description | Admin | +| ---------------- | ----------------------------- | ----- | +| tag.read.admin | Can read admin tags | true | +| tag.write.public | Can create/update public tags | true | +| tag.write.admin | Can create/update admin tags | true | +| tag.write.delete | Can delete all tags | true | + +--- + +## Category Map Section + +| Permission Name | Description | Admin | +| ------------------ | --------------------------------------- | ----- | +| category.write.all | Can add/update/delete all category data | true | + +--- + +## Role Section + +| Permission Name | Description | Admin | +| --------------- | ----------------------------------- | ----- | +| role.write.all | Can add/update/delete all role data | true | + +--- + +## Club Section + +| Permission Name | Description | Admin | +| ----------------- | ---------------------------- | ----- | +| club.write.all | Can add/update all club data | true | +| club.write.delete | Can delete all club data | true | + +--- + +## Event Section + +| Permission Name | Description | Admin | +| --------------- | ------------------------------------ | ----- | +| event.write.all | Can add/update/delete all event data | true | + +--- + +## Company Section + +| Permission Name | Description | Admin | +| ----------------------- | ------------------------------------------ | ----- | +| company.read.public | Can read public company data | false | +| company.read.restricted | Can read institute restricted company data | false | +| company.read.private | Can read private company data | true | +| company.write.new | Can add new company data | true | +| company.write.all | Can add/update all company data | true | +| company.write.delete | Can delete all company data | true | + +--- + +## Live Section + +| Permission Name | Description | Admin | +| -------------------- | --------------------------------------- | ----- | +| live.read.public | Can read public live data | false | +| live.read.restricted | Can read institute restricted live data | false | +| live.read.private | Can read private live data | true | +| live.write.new | Can add new live data | true | +| live.write.all | Can add/update/delete all live data | true | + +--- + +## Share Internship Section + +| Permission Name | Description | Admin | +| ------------------------------- | -------------------------------------------------- | ----- | +| shareInternship.read.public | Can read public shareInternship data | false | +| shareInternship.read.restricted | Can read institute restricted shareInternship data | false | +| shareInternship.read.unapproved | Can read unapproved shareInternship data | true | +| shareInternship.write.new | Can add new shareInternship data | false | +| shareInternship.write.all | Can add/update/delete all shareInternship data | true | +| shareInternship.approve.all | Can approve all shareInternship data | true | + +--- + +## Forum Thread Section + +| Permission Name | Description | Admin | +| --------------------------- | ------------------------------------------- | ----- | +| forumThread.read.public | Can read public forum threads | false | +| forumThread.read.restricted | Can read institute restricted forum threads | false | +| forumThread.read.unapproved | Can read unapproved forum threads | true | +| forumThread.write.new | Can create a new forum thread | false | +| forumThread.write.approved | Can create pre-approved forum threads | false | +| forumThread.write.self | Can update own forum threads | false | +| forumThread.write.all | Can update all forum threads | true | +| forumThread.write.delete | Can delete all forum threads | true | +| forumThread.approve.all | Can approve all forum threads | true | + +--- + +## Forum Message Section + +| Permission Name | Description | Admin | +| ---------------------------- | -------------------------------------- | ----- | +| forumMessage.read.public | Can read public forum messages | false | +| forumMessage.read.unapproved | Can read unapproved forum messages | true | +| forumMessage.write.new | Can create a new forum message | false | +| forumMessage.write.approved | Can create pre-approved forum messages | false | +| forumMessage.write.self | Can update own forum messages | false | +| forumMessage.write.delete | Can delete all forum messages | true | +| forumMessage.approve.all | Can approve all forum messages | true | diff --git a/planning/v1/ROLES.md b/planning/v1/ROLES.md index 984a5a3b..80d9a6e2 100644 --- a/planning/v1/ROLES.md +++ b/planning/v1/ROLES.md @@ -1,151 +1,227 @@ -# Roles and Permissions +# Permissions ## User Section -### User Permissions +| Role Name | user.read.private | user.read.admin | user.write.self | user.write.all | +| --------------- | ----------------- | --------------- | --------------- | -------------- | +| user.basic | true | false | true | false | +| user.verified | true | false | true | false | +| user.admin | true | true | true | false | +| user.superadmin | true | true | true | true | -| Permission Name | Description | -| ---------------- | -------------------------------------- | -| user.read.all | Can read all user data | -| user.list.public | Can list/search all public accounts | -| user.list.all | Can list/search all user data | -| user.write.self | Can update/delete own account | -| user.write.all | Can create/update/delete all user data | -| | | +--- -### User Roles +## Article Section -| Role Name | user.read.all | user.list.public | user.list.all | user.write.self | user.write.all | -| --------------- | ------------- | ---------------- | ------------- | --------------- | -------------- | -| user.basic | N | Y | N | Y | N | -| user.verified | Y | Y | N | Y | N | -| user.admin | Y | Y | Y | Y | N | -| user.superadmin | Y | Y | Y | Y | Y | -| | | | | | | +| Role Name | article.read.restricted | article.read.unpublished | article.read.admin | article.write.new | article.write.self | article.write.all | article.approve.all | +| ------------------ | ----------------------- | ------------------------ | ------------------ | ----------------- | ------------------ | ----------------- | ------------------- | +| article.basic | false | false | false | false | false | false | false | +| article.student | true | false | false | false | false | false | false | +| article.faculty | false | false | false | false | false | false | false | +| article.team | true | true | true | true | true | false | false | +| article.admin | true | true | true | true | true | true | true | +| article.superadmin | true | true | true | true | true | true | true | --- -## Article Section +## Reactions Section -### Article Permissions +| Role Name | reactions.write.self | reactions.write.all | +| -------------------- | -------------------- | ------------------- | +| reactions.basic | true | false | +| reactions.admin | true | false | +| reactions.superadmin | true | true | -| Permission Name | Description | -| ------------------------ | --------------------------------------------------------- | -| article.read.restricted | Can read institute restricted articles | -| article.read.unpublished | Can read unpublished, archived or trashed articles | -| article.read.admin | Can read admin fields of an article | -| article.list.restricted | Can list/search institue restricted articles | -| article.list.unpublished | Can list/search unpublished, archived or trashed articles | -| article.write.new | Can create a new article | -| article.write.self | Can update/delete own articles | -| article.write.all | Can update/delete all articles | -| | | +_TODO: Consider combining reaction permissions in comment roles_ -### Article Roles +--- + +## Comment Section -| Role Name | article.read.restricted | article.read.unpublished | article.read.admin | article.list.restricted | article.list.unpublished | article.write.new | article.write.self | article.write.all | -| ---------------- | ----------------------- | ------------------------ | ------------------ | ----------------------- | ------------------------ | ----------------- | ------------------ | ----------------- | -| article.verified | Y | N | N | Y | N | N | N | N | -| article.pic | Y | Y | N | Y | Y | N | N | N | -| article.team | Y | Y | Y | Y | Y | N | Y | N | -| article.author | Y | Y | Y | Y | Y | Y | Y | N | -| article.admin | Y | Y | Y | Y | Y | Y | Y | Y | -| | | | | | | | | | +| Role Name | comment.read.public | comment.read.unapproved | comment.write.new | comment.write.approved | comment.write.self | comment.write.delete | comment.approve.all | +| ------------------ | ------------------- | ----------------------- | ----------------- | ---------------------- | ------------------ | -------------------- | ------------------- | +| comment.basic | true | false | true | false | true | false | false | +| comment.verified | true | false | true | true | true | false | false | +| comment.admin | true | true | true | true | true | false | true | +| comment.superadmin | true | true | true | true | true | true | true | --- ## Issue Section -### Issue Permissions +| Role Name | issue.read.unpublished | issue.read.admin | issue.write.new | issue.write.all | issue.write.delete | +| ---------------- | ---------------------- | ---------------- | --------------- | --------------- | ------------------ | +| issue.basic | false | false | false | false | false | +| issue.team | true | false | false | false | false | +| issue.admin | true | true | true | true | false | +| issue.superadmin | true | true | true | true | true | -| Permission Name | Description | -| ---------------------- | ---------------------------------- | -| issue.read.unpublished | Can read unpublished issues | -| issue.list.unpublished | Can list/search unpublished issues | -| issue.write.new | Can create a new issue | -| issue.write.all | Can update/delete all issues | -| | | +_TODO: Consider combining session and issue permissions in singular roles_ -### Issue Roles +--- -| Role Name | issue.read.unpublished | issue.list.unpublished | issue.write.new | issue.write.all | -| ----------- | ---------------------- | ---------------------- | --------------- | --------------- | -| issue.team | Y | Y | N | N | -| issue.admin | Y | Y | Y | Y | -| | | | | | +## Session Section + +| Role Name | session.write.new | session.write.all | session.write.delete | +| ------------------ | ----------------- | ----------------- | -------------------- | +| session.basic | false | false | false | +| session.admin | false | true | false | +| session.superadmin | true | true | true | + +_TODO: Consider combining session and issue permissions in singular roles_ --- ## Squiggle Section -### Squiggle Permissions +| Role Name | squiggle.read.all | squiggle.write.new | squiggle.write.all | squiggle.write.delete | +| ------------------- | ----------------- | ------------------ | ------------------ | --------------------- | +| squiggle.basic | false | false | false | false | +| squiggle.admin | true | true | false | false | +| squiggle.superadmin | true | true | true | true | + +--- + +## Poll Section -| Permission Name | Description | -| ------------------ | ------------------------- | -| squiggle.write.new | Can create a new squiggle | -| | | +| Role Name | poll.write.restricted | poll.write.all | poll.write.delete | +| --------------- | --------------------- | -------------- | ----------------- | +| poll.basic | false | false | false | +| poll.verified | true | false | false | +| poll.admin | true | true | false | +| poll.superadmin | true | true | true | -### Squiggle Roles +--- + +## Media Section -| Role Name | squiggle.write.new | -| -------------- | ------------------ | -| squiggle.admin | Y | -| | | +| Role Name | media.write.all | media.write.delete | +| ---------------- | --------------- | ------------------ | +| media.basic | false | false | +| media.admin | true | false | +| media.superadmin | true | true | + +--- + +## Album Section + +| Role Name | album.write.all | +| ---------------- | --------------- | +| album.basic | false | +| album.admin | true | +| album.superadmin | true | + +_TODO: Consider combining album permissions in media roles_ --- ## Tag Section -### Tag Permissions +| Role Name | tag.read.admin | tag.write.public | tag.write.admin | tag.write.delete | +| -------------- | -------------- | ---------------- | --------------- | ---------------- | +| tag.basic | false | false | false | false | +| tag.admin | true | true | true | false | +| tag.superadmin | true | true | true | true | -| Permission Name | Description | -| ---------------- | ------------------------------------ | -| tag.read.admin | Can read admin tags | -| tag.list.public | Can list/search public tags | -| tag.list.admin | Can list/search admin tags | -| tag.write.public | Can create/update/delete public tags | -| tag.write.admin | Can create/update/delete admin tags | -| | | +--- -### Tag Roles +## Category Map Section -| Role Name | tag.read.admin | tag.list.public | tag.list.admin | tag.write.public | tag.write.admin | -| --------- | -------------- | --------------- | -------------- | ---------------- | --------------- | -| tag.team | Y | Y | N | Y | N | -| tag.admin | Y | Y | Y | Y | Y | -| | | | | | | +| Role Name | category.write.all | +| ------------------- | ------------------ | +| category.basic | false | +| category.admin | false | +| category.superadmin | true | --- -### Live Section +## Role Section -### Live Permissions +| Role Name | role.write.all | +| --------------- | -------------- | +| role.basic | false | +| role.admin | false | +| role.superadmin | true | -| Permission Name | Description | -| --------------- | -------------------------- | -| live.read.all | Can read all live data | -| live.write.all | Can add/edit all live data | +--- -### Live Roles +## Club Section -| Role Name | live.read.all | live.write.all | -| --------------- | ------------- | -------------- | -| live.verified | Y | N | -| live.superadmin | Y | Y | +| Role Name | club.write.all | club.write.delete | +| --------------- | -------------- | ----------------- | +| club.basic | false | false | +| club.admin | true | false | +| club.superadmin | true | true | -### Media Section +_TODO: Consider combining club and event permissions in singular roles_ + +--- -### Media Permissions +## Event Section -| Permission Name | Description | -| ---------------- | ----------------------------- | -| media.write.new | Can add media data | -| media.write.self | Can delete own media data | -| media.write.all | Can add/delete all media data | +| Role Name | event.write.all | +| ---------------- | --------------- | +| event.basic | false | +| event.admin | true | +| event.superadmin | true | + +_TODO: Consider combining club and event permissions in singular roles_ + +--- + +## Company Section + +| Role Name | company.read.public | company.read.restricted | company.read.private | company.write.new | company.write.all | company.write.delete | +| ------------------ | ------------------- | ----------------------- | -------------------- | ----------------- | ----------------- | -------------------- | +| company.basic | true | false | false | false | false | false | +| company.verified | true | true | false | false | false | false | +| company.admin | true | true | true | true | true | false | +| company.superadmin | true | true | true | true | true | true | + +_TODO: Consider combining company permissions in live roles_ + +--- + +## Live Section + +| Role Name | live.read.public | live.read.restricted | live.read.private | live.write.new | live.write.all | +| --------------- | ---------------- | -------------------- | ----------------- | -------------- | -------------- | +| live.basic | true | false | false | false | false | +| live.verified | true | true | false | false | false | +| live.admin | true | true | true | true | true | +| live.superadmin | true | true | true | true | true | + +--- + +## Share Internship Section + +| Role Name | shareInternship.read.public | shareInternship.read.restricted | shareInternship.read.unapproved | shareInternship.write.new | shareInternship.write.all | shareInternship.approve.all | +| -------------------------- | --------------------------- | ------------------------------- | ------------------------------- | ------------------------- | ------------------------- | --------------------------- | +| shareInternship.basic | true | false | false | false | false | false | +| shareInternship.verified | true | true | false | true | false | false | +| shareInternship.admin | true | true | true | true | true | true | +| shareInternship.superadmin | true | true | true | true | true | true | + +_TODO: Consider combining shareInternship permissions in live roles_ + +--- + +## Forum Thread Section + +| Role Name | forumThread.read.public | forumThread.read.restricted | forumThread.read.unapproved | forumThread.write.new | forumThread.write.approved | forumThread.write.self | forumThread.write.all | forumThread.write.delete | forumThread.approve.all | +| ---------------- | ----------------------- | --------------------------- | --------------------------- | --------------------- | -------------------------- | ---------------------- | --------------------- | ------------------------ | ----------------------- | +| forum.basic | true | false | false | true | false | true | false | false | false | +| forum.verified | true | true | false | true | true | true | false | false | false | +| forum.admin | true | true | true | true | true | true | true | false | true | +| forum.superadmin | true | true | true | true | true | true | true | true | true | + +--- -### Media Roles +## Forum Message Section -| Role Name | media.write.new | media.write.self | media.write.all | -| ----------- | --------------- | ---------------- | --------------- | -| media.team | Y | Y | N | -| media.admin | Y | Y | Y | +| Role Name | forumMessage.read.public | forumMessage.read.unapproved | forumMessage.write.new | forumMessage.write.approved | forumMessage.write.self | forumMessage.write.delete | forumMessage.approve.all | +| ---------------- | ------------------------ | ---------------------------- | ---------------------- | --------------------------- | ----------------------- | ------------------------- | ------------------------ | +| forum.basic | true | false | true | false | true | false | false | +| forum.verified | true | false | true | true | true | false | false | +| forum.admin | true | true | true | true | true | false | true | +| forum.superadmin | true | true | true | true | true | true | true | diff --git a/server/config/apolloServer.js b/server/config/apolloServer.js index 0fdb2157..e3c1d541 100644 --- a/server/config/apolloServer.js +++ b/server/config/apolloServer.js @@ -68,6 +68,7 @@ const apolloServer = (httpServer) => graphRef: process.env.APOLLO_GRAPH_REF, graphVariant: process.env.APOLLO_GRAPH_VARIANT, }, + cache: 'bounded', stopOnTerminationSignals: false, plugins: [CustomLandingPagePlugin, ApolloServerPluginDrainHttpServer({ httpServer })], }); diff --git a/server/config/cors.js b/server/config/cors.js index 180a721a..84c23e40 100644 --- a/server/config/cors.js +++ b/server/config/cors.js @@ -1,28 +1,10 @@ const CORS = require('cors'); const logger = require('../utils/logger')('CORS'); -const ORIGIN_PATTERS = { - // Allows localhost, Apollo Studio and Heroku PR Review Apps - development: new RegExp( - /^https?:\/\/((127\.0\.0\.1|localhost)(:\d{1,})?|studio\.apollographql\.com|project-reclamation-pr-\d{0,}\.herokuapp\.com|project-tahiti-pr-\d{0,}\.herokuapp\.com)$/ - ), - - // Allows localhost, Apollo Studio, Firebase Project Domains, DashNet MM Domains, NITR MM Domains, Heroku Staging Domains and Heroku PR Review Apps - staging: new RegExp( - /^https?:\/\/((127\.0\.0\.1|localhost)(:\d{1,})?|studio\.apollographql\.com|project-infinity-98561(.{0,}\.)(web\.app|firebaseapp\.com)|mm(\.server1)?.dashnet.in|(mm|mondaymorning)\.nitrkl(\.ac)?\.in|project-(reclamation|tahiti)-(staging|pr-\d{0,})\.herokuapp\.com)$/ - ), - - // Allows Apollo Studio, Firebase Project Domains, DashNet MM Domains, and NITR MM Domains - // Does not allow localhost, Heroku Staging Domains Heroku and PR Review Apps - production: new RegExp( - /^https?:\/\/(studio\.apollographql\.com|project-infinity-98561(.{0,}\.)(web\.app|firebaseapp\.com)|mm(\.server1)?.dashnet.in|(mm|mondaymorning)\.nitrkl(\.ac)?\.in)$/ - ), -}; - const CORS_OPTIONS = { credentials: true, origin(origin, callback) { - if (!origin || origin.match(ORIGIN_PATTERS[process.env.NODE_ENV || 'production'])) { + if (!origin || origin.match(new RegExp(Buffer.from(process.env.CORS_ORIGIN_REGEX, 'base64').toString()))) { return callback(null, true); } logger.warn(`CORS blocked a request from ${origin}`); @@ -30,4 +12,4 @@ const CORS_OPTIONS = { }, }; -module.exports = { cors: CORS(CORS_OPTIONS), CORS_OPTIONS, ORIGIN_PATTERS }; +module.exports = { cors: CORS(CORS_OPTIONS), CORS_OPTIONS }; diff --git a/server/config/firebase.js b/server/config/firebase.js index 54e215e9..eba8b085 100644 --- a/server/config/firebase.js +++ b/server/config/firebase.js @@ -20,7 +20,7 @@ module.exports = { init: () => { try { /** Inititalize Firebase Admin SDK with required configuration */ - if (process.env.FIREBASE_SERVICE_ACCOUNT && process.env.NODE_ENV === 'production') { + if (process.env.FIREBASE_SERVICE_ACCOUNT && process.env.NODE_ENV !== 'development') { const firebaseServiceAccount = JSON.parse( Buffer.from(process.env.FIREBASE_SERVICE_ACCOUNT, 'base64').toString('ascii') ); @@ -28,7 +28,11 @@ module.exports = { credential: Admin.credential.cert(firebaseServiceAccount), storageBucket: process.env.FIREBASE_STORAGE_BUCKET || null, }); - logger.info('Admin Application Initialized: Production Environment'); + logger.info( + `Admin Application Initialized: ${ + process.env.NODE_ENV === 'production' ? 'Production' : 'Staging' + } Environment` + ); } else if (process.env.FIREBASE_AUTH_EMULATOR_HOST) { Admin.initializeApp({ projectId: process.env.GCLOUD_PROJECT }); logger.info('Admin Application Initialized: Emulated Environment'); diff --git a/server/config/imagekit.js b/server/config/imagekit.js new file mode 100644 index 00000000..20e39f53 --- /dev/null +++ b/server/config/imagekit.js @@ -0,0 +1,9 @@ +const ImageKit = require('imagekit'); + +const imagekit = new ImageKit({ + publicKey: process.env.IMAGEKIT_PUBLIC_KEY, + privateKey: process.env.IMAGEKIT_PRIVATE_KEY, + urlEndpoint: process.env.IMAGEKIT_URLENDPOINT, +}); + +module.export = imagekit; diff --git a/server/env/.env.sample b/server/env/.env.sample index 0fc3929d..36093d00 100644 --- a/server/env/.env.sample +++ b/server/env/.env.sample @@ -2,6 +2,7 @@ NODE_ENV=sample PORT=5000 +CORS_ORIGIN_REGEX="base64-encoded-regex" # ----- ----- END ----- ----- @@ -30,20 +31,9 @@ FIREBASE_TEST_AUTH_KEY=some-sample-jwt-token # ----- ----- END ----- ----- -# ----- ----- UUID V5 ----- ----- +# ----- ----- ACCESS API KEYS ----- ----- -UUID_NAMESPACE=some-namespace - -# ----- ----- END ----- ----- - -# ----- ----- SMTP NODEMAILER ----- ----- - -SMTP_HOST=smtp.gmail.com -SMTP_POST=587 -SMTP_SECURE_FLAG=false -SMTP_USERNAME=info.mondaymorning@gmail.com -SMTP_PASSWORD=some-password -SMTP_FROM_ADDRESS=some-address +SERVER_ACCESS_API_KEY="some-key" # ----- ----- END ----- ----- diff --git a/server/env/.env.vault b/server/env/.env.vault new file mode 100644 index 00000000..8e67ed56 --- /dev/null +++ b/server/env/.env.vault @@ -0,0 +1,25 @@ +#/-------------------.env.vault---------------------/ +#/ cloud-agnostic vaulting standard / +#/ [how it works](https://dotenv.org/env-vault) / +#/--------------------------------------------------/ + +# development +DOTENV_VAULT_DEVELOPMENT="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" +DOTENV_VAULT_DEVELOPMENT_VERSION=3 + +# ci +DOTENV_VAULT_CI="KJG9gwlS/ybYNB2uSj3Pok/NeKUXdN1Ds0xp8KCu7y5dOLTBIVodeGiZroVFPXuciw/ZGIbLh3gRiRpdJWPQ2VpAoHh3XEq+sVuCx6/x1dCT2NqN15lam83eM6mUUyjRd4+xg9IpSHZYBV1A437dYqeAIW3tl+Rs7OLND0/KqIKGFBgxYO3e231BrTdC0kl3sQD0FIGAAzKXF0GfgpOeqt6IsBsNxpn2y5qmnPwua6vevXtoaEcuaVBjeW69FCGLyb6WbG6ZwTxvxQZCPOJ6fi9dNLOxMdjHoDiE8u4D8VK2lnnor0U/3FhbrNOqaMfPQPLbTcQuFO/anwrwsausaL8JsfkuYjgJ14L6Xvh0gtARtRF4Eh6RaU0zGuKZefwUakbaMRuEkMigcAuPZ4pJoSHWiv8gCv8CAYV1txHTKW4fQKitMHj/Oja9OKrGPRnE/WrwSpUs6046WIPlJrZWSp1SxD+qUEcvJiwp3/bF8wFJq4JIpHt0CIlhGr0One1m" +DOTENV_VAULT_CI_VERSION=3 + +# production +DOTENV_VAULT_PRODUCTION="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" +DOTENV_VAULT_PRODUCTION_VERSION=5 + +# staging +DOTENV_VAULT_STAGING="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" +DOTENV_VAULT_STAGING_VERSION=5 + +#/----------------settings/metadata-----------------/ +DOTENV_VAULT="vlt_6ace8b362dbae0da30d0187ba677e92662a88674e437a7e1a3c41c00ab94b316" +DOTENV_API_URL="https://vault.dotenv.org" +DOTENV_CLI="npx dotenv-vault@latest" diff --git a/server/env/.gitignore b/server/env/.gitignore new file mode 100644 index 00000000..797ca4d1 --- /dev/null +++ b/server/env/.gitignore @@ -0,0 +1,5 @@ + +.env* +.flaskenv* +!.env.project +!.env.vault \ No newline at end of file diff --git a/server/package.json b/server/package.json index 16a74b40..e757fe84 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "description": "The Server Application for Monday Morning under Project Infinity", "main": "app.js", "scripts": { - "start": "node app.js --development", + "start": "nodemon app.js --development", "start:stage": "node app.js --staging", "start:prod": "node app.js --production" }, @@ -37,6 +37,11 @@ "url": "https://github.com/Monday-Morning/project-reclamation/issues" }, "homepage": "https://github.com/Monday-Morning/project-reclamation#readme", + "nodemonConfig": { + "ignore": [ + "./roles.json" + ] + }, "dependencies": { "@graphql-tools/stitch": "^8.3.1", "apollo-server-core": "^3.10.1", @@ -64,6 +69,7 @@ "async": "^3.2.0", "https": "^1.0.0", "mysql": "^2.18.1", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "nodemon": "^2.0.22" } } diff --git a/server/schema/article/article.resolver.js b/server/schema/article/article.resolver.js index e4d38720..221a4688 100644 --- a/server/schema/article/article.resolver.js +++ b/server/schema/article/article.resolver.js @@ -35,24 +35,57 @@ const ARTICLE_PUBLISH_TYPES = Object.fromEntries( PublishStatusEnumType.getValues().map((item) => [item.name, item.value]) ); -const canUpdateArticle = async (id, mid, session, authToken, decodedToken, fieldNodes, Article, needsAdmin = false) => { - const _fields = getFieldNodes(fieldNodes); +// TODO: add a needsAdmin check for admin APIs +const canReadArticle = (article, session, authToken, decodedToken, fieldNodes, noError = false) => { + if ( + [ARTICLE_PUBLISH_TYPES.UNPUBLISHED, ARTICLE_PUBLISH_TYPES.ARCHIVED, ARTICLE_PUBLISH_TYPES.TRASHED].includes( + article.publishStatus + ) && + !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished') + ) { + if (noError) { + return false; + } + throw APIError('NOT_FOUND', null, { reason: 'The requested article was not found.' }); + } + if ( + article.isInstituteRestricted && + !UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted') + ) { + if (noError) { + return false; + } + throw APIError('FORBIDDEN', null, { + reason: 'The requested article can only be viewed by students and faculty of NIT Rourkela.', + }); + } + + const _fields = getFieldNodes(fieldNodes); if ( _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') ) { + if (noError) { + return false; + } throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', + reason: 'The user does not have the required permissions to read the requested fields.', }); } + return true; +}; + +const canUpdateArticle = async (id, mid, session, authToken, decodedToken, fieldNodes, Article, needsAdmin = false) => { const _article = await Article.findByID.load(id); if (!_article) { throw APIError('NOT_FOUND', null, { reason: 'The requested was not found.' }); } + canReadArticle(_article, session, authToken, decodedToken, fieldNodes); + const _users = _article.users.map((user) => user.details); if ( @@ -62,7 +95,9 @@ const canUpdateArticle = async (id, mid, session, authToken, decodedToken, field throw APIError('FORBIDDEN', null, { reason: 'The user does not have required permission to perform this operation.', }); - } else if ( + } + + if ( _users.includes(mid) && !UserPermission.exists(session, authToken, decodedToken, 'article.write.self') && !UserPermission.exists(session, authToken, decodedToken, 'article.write.all') @@ -78,40 +113,13 @@ const canUpdateArticle = async (id, mid, session, authToken, decodedToken, field module.exports = { getArticleByID: async (_parent, { id }, { session, authToken, decodedToken, API: { Article } }, { fieldNodes }) => { try { - const _fields = getFieldNodes(fieldNodes); - const _article = await Article.findByID.load(id); if (!_article) { throw APIError('NOT_FOUND', null, { reason: 'The requested article was not found.' }); } - if ( - [ARTICLE_PUBLISH_TYPES.UNPUBLISHED, ARTICLE_PUBLISH_TYPES.ARCHIVED, ARTICLE_PUBLISH_TYPES.TRASHED].includes( - _article.publishStatus - ) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished') - ) { - throw APIError('NOT_FOUND', null, { reason: 'The requested article was not found.' }); - } - - if ( - _article.isInstituteRestricted && - !UserPermission(session, authToken, decodedToken, 'article.read.restricted') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The requested article can only be viewed by students and faculty of NIT Rourkela.', - }); - } - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permissions to read the requested fields.', - }); - } + canReadArticle(_article, session, authToken, decodedToken, fieldNodes); return _article; } catch (error) { @@ -125,40 +133,13 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - const _article = await Article.findByOldID.load(id); if (!_article) { throw APIError('NOT_FOUND', null, { reason: 'The requested article was not found.' }); } - if ( - [ARTICLE_PUBLISH_TYPES.UNPUBLISHED, ARTICLE_PUBLISH_TYPES.ARCHIVED, ARTICLE_PUBLISH_TYPES.TRASHED].includes( - _article.publishStatus - ) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished') - ) { - throw APIError('NOT_FOUND', null, { reason: 'The requested article was not found.' }); - } - - if ( - _article.isInstituteRestricted && - !UserPermission(session, authToken, decodedToken, 'article.read.restricted') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The requested article can only be viewed by students and faculty of NIT Rourkela.', - }); - } - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permissions to read the requested fields.', - }); - } + canReadArticle(_article, session, authToken, decodedToken, fieldNodes); return _article; } catch (error) { @@ -172,43 +153,24 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - - const _articles = await Promise.all(ids.slice(offset, offset + limit).map((id) => Article.findByID.load(id))); - - if (!_articles || _articles.length <= 0) { - throw APIError('NOT_FOUND', null, { reason: 'The requested article(s) were not found.' }); + if (!ids || ids.length <= 0) { + throw APIError('BAD_REQUEST', null, { reason: 'No IDs were provided in the arguments.' }); } - const _unpublishedPermission = UserPermission.exists( - session, - authToken, - decodedToken, - 'article.read.unpublished' - ); - const _restritedPermission = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); + const _articles = await Article.find({ _id: ids }, limit, offset); - if (_restritedPermission && _unpublishedPermission) { - return _articles; + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { + throw APIError('NOT_FOUND', null, { reason: 'No article(s) were not found.' }); } - const publicArticles = _articles.filter( - ({ publishStatus, isInstituteRestricted }) => - (_restritedPermission || !isInstituteRestricted) && (_unpublishedPermission || publishStatus) - ); - - return publicArticles.length === _articles.length - ? publicArticles - : [...publicArticles, APIError('FORBIDDEN', null, { reason: 'One or more article(s) were not found.' })]; + return _articles.map((_article) => { + try { + canReadArticle(_article, session, authToken, decodedToken, fieldNodes); + return _article; + } catch (error) { + return error; + } + }); } catch (error) { throw APIError(null, error); } @@ -220,31 +182,27 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); + if (!categoryNumbers || categoryNumbers.length <= 0) { + throw APIError('BAD_REQUEST', null, { reason: 'No category numbers were provided in the arguments.' }); } - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); onlyPublished = - onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); const _articles = await Article.findByCategories(allowRestricted, onlyPublished, categoryNumbers, limit, offset); - if (!_articles || _articles.length <= 0) { - throw APIError('NOT_FOUND', null, { reason: 'No categories were requested or no articles were found.' }); + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { + throw APIError('NOT_FOUND', null, { reason: 'No articles were found in the requested categories.' }); } - // TODO: check each category for zero articles // TODO: do not trust array order completely, try using category number based object - - return _articles; + return _articles.map( + (_articleCategory) => + _articleCategory?.filter((_article) => + canReadArticle(_article, session, authToken, decodedToken, fieldNodes, true) + ) ?? APIError('NOT_FOUND', null, { reason: 'No articles were found in one of the requested categories.' }) + ); } catch (error) { throw APIError(null, error); } @@ -256,9 +214,9 @@ module.exports = { _ ) => { try { - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); onlyPublished = - onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); const _articleCount = await Article.countOfArticleBySubCategory(allowRestricted, onlyPublished, categoryNumber); @@ -274,21 +232,11 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); onlyPublished = - onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); + // TODO: refactor this - bad practice function startAndEndDate(year, month) { return month ? [new Date(year, month - 1), new Date(year, month)] : [new Date(year, 0), new Date(year + 1, 0)]; } @@ -299,11 +247,14 @@ module.exports = { offset, startAndEndDate(year, month) ); - if (!_articles || _articles.length <= 0) { + + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { throw APIError('NOT_FOUND', null, { reason: 'No articles were found.' }); } - return _articles; + return _articles.filter((_article) => + canReadArticle(_article, session, authToken, decodedToken, fieldNodes, true) + ); } catch (error) { throw APIError(null, error); } @@ -314,9 +265,9 @@ module.exports = { { session, authToken, decodedToken, API: { Article } } ) => { try { - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); onlyPublished = - onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); return await Article.countNumberOfArticles(allowRestricted, onlyPublished); } catch (error) { @@ -330,28 +281,19 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); onlyPublished = - onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + onlyPublished || !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); const _articles = await Article.findAll(allowRestricted, onlyPublished, limit, offset); - if (!_articles || _articles.length <= 0) { + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { throw APIError('NOT_FOUND', null, { reason: 'No articles were found.' }); } - return _articles; + return _articles.filter((_article) => + canReadArticle(_article, session, authToken, decodedToken, fieldNodes, true) + ); } catch (error) { throw APIError(null, error); } @@ -363,27 +305,18 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); - const onlyPublished = !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); + const onlyPublished = !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); const _articles = await Article.search(keywords, allowRestricted, onlyPublished, limit, offset); - if (!_articles || _articles.length <= 0) { + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { throw APIError('NOT_FOUND', null, { reason: 'No articles were found with the given keywords.' }); } - return _articles; + return _articles.filter((_article) => + canReadArticle(_article, session, authToken, decodedToken, fieldNodes, true) + ); } catch (error) { throw APIError(null, error); } @@ -391,18 +324,22 @@ module.exports = { getAutoComplete: async ( _parent, { keywords, limit = DEF_LIMIT }, - { session, authToken, decodedToken, API: { Article } } + { session, authToken, decodedToken, API: { Article } }, + { fieldNodes } ) => { try { - const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.list.restricted'); - const onlyPublished = !UserPermission.exists(session, authToken, decodedToken, 'article.list.unpublished'); + const allowRestricted = UserPermission.exists(session, authToken, decodedToken, 'article.read.restricted'); + const onlyPublished = !UserPermission.exists(session, authToken, decodedToken, 'article.read.unpublished'); const _articles = await Article.autoComplete(keywords, allowRestricted, onlyPublished, limit); - if (!_articles || _articles.length <= 0) { + if (!_articles || !(_articles instanceof Array) || _articles.length <= 0) { throw APIError('NOT_FOUND', null, { reason: 'No articles were found with the given keywords.' }); } - return _articles; + + return _articles.filter((_article) => + canReadArticle(_article, session, authToken, decodedToken, fieldNodes, true) + ); } catch (error) { throw APIError(null, error); } @@ -415,17 +352,6 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'article.read.admin') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - if (!UserPermission.exists(session, authToken, decodedToken, 'article.write.new')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permissions to create an article.', @@ -597,6 +523,7 @@ module.exports = { throw APIError(null, error); } }, + // TODO: only allow server side calls or after app check validation incrementViewCount: (_parent, { id }, { API: { Article } }, _) => { try { return Article.incrementEngagementCount(id, { views: 1 }); diff --git a/server/schema/issue/issue.resolver.js b/server/schema/issue/issue.resolver.js index 26a3383a..800e09e7 100644 --- a/server/schema/issue/issue.resolver.js +++ b/server/schema/issue/issue.resolver.js @@ -10,14 +10,27 @@ */ const { APIError } = require('../../utils/exception'); +const getFieldNodes = require('../../utils/getFieldNodes'); const UserPermission = require('../../utils/userAuth/permission'); const DEF_LIMIT = 10, DEF_OFFSET = 0; +const PUBLIC_FIELDS = ['id', 'name', 'thumbnail', 'description', 'articles', 'featured', '__typename']; + module.exports = { - getIssueByID: async (_parent, { id }, { session, authToken, decodedToken, API: { Issue } }, _) => { + getIssueByID: async (_parent, { id }, { session, authToken, decodedToken, API: { Issue } }, { fieldNodes }) => { try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + const _issue = await Issue.findByID.load(id); if (!_issue.isPublished && !UserPermission.exists(session, authToken, decodedToken, 'issue.read.unpublished')) { @@ -33,10 +46,20 @@ module.exports = { _parent, { onlyPublished = true, limit = DEF_LIMIT, offset = DEF_OFFSET }, { session, authToken, decodedToken, API: { Issue } }, - _ + { fieldNodes } ) => { try { - if (!onlyPublished && !UserPermission.exists(session, authToken, decodedToken, 'issue.list.unpublished')) { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + + if (!onlyPublished && !UserPermission.exists(session, authToken, decodedToken, 'issue.read.unpublished')) { onlyPublished = true; } @@ -49,20 +72,26 @@ module.exports = { _parent, { ids, limit = DEF_LIMIT, offset = DEF_OFFSET }, { session, authToken, decodedToken, API: { Issue } }, - _ + { fieldNodes } ) => { try { - const _issues = await Issue.find({ _id: ids }, limit, offset); - - // TODO: return issues alongside the errors + const _fields = getFieldNodes(fieldNodes); if ( - _issues.some((_issue) => !_issue.isPublished) && - !UserPermission.exists(session, authToken, decodedToken, 'issue.read.unpublished') + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') ) { - throw APIError('NOT_FOUND', null, { reason: 'One or more of the requested issues were not found.' }); + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); } - return _issues; + const _issues = await Issue.find({ _id: ids }, limit, offset); + + return _issues.map((_issue) => + !_issue.isPublished && !UserPermission.exists(session, authToken, decodedToken, 'issue.read.unpublished') + ? APIError('NOT_FOUND', null, { reason: 'The requested issue is not found.' }) + : _issue + ); } catch (error) { throw APIError(null, error); } @@ -71,9 +100,19 @@ module.exports = { _parent, { name, description, startDate, endDate, articles, featured, isPublished }, { session, authToken, decodedToken, mid, API: { Issue } }, - _ + { fieldNodes } ) => { try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + if (!UserPermission.exists(session, authToken, decodedToken, 'issue.write.new')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to create a new issue.', @@ -100,9 +139,19 @@ module.exports = { _parent, { id, name, description, startDate, endDate }, { session, authToken, decodedToken, mid, API: { Issue } }, - _ + { fieldNodes } ) => { try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + if (!UserPermission.exists(session, authToken, decodedToken, 'issue.write.all')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to update this issue.', @@ -118,9 +167,19 @@ module.exports = { _parent, { id, articles, featured }, { session, authToken, decodedToken, mid, API: { Issue } }, - _ + { fieldNodes } ) => { try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'issue.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + if (!UserPermission.exists(session, authToken, decodedToken, 'issue.write.all')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to update this issue.', @@ -134,7 +193,7 @@ module.exports = { }, removeIssue: (_parent, { id }, { session, authToken, decodedToken, API: { Issue } }, _) => { try { - if (!UserPermission.exists(session, authToken, decodedToken, 'issue.write.all')) { + if (!UserPermission.exists(session, authToken, decodedToken, 'issue.write.delete')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to delete this issue.', }); diff --git a/server/schema/media/media.datasources.js b/server/schema/media/media.datasources.js index f068a5af..fcd43d7b 100644 --- a/server/schema/media/media.datasources.js +++ b/server/schema/media/media.datasources.js @@ -2,6 +2,8 @@ const DataLoader = require('dataloader'); const { APIError } = require('../../utils/exception'); const MediaModel = require('./media.model'); const UserSession = require('../../utils/userAuth/session'); +const { connection } = require('../../config/mongoose'); +const imagekit = require('../../config/imagekit'); const findByID = () => new DataLoader( @@ -18,10 +20,9 @@ const findByID = () => } ); -const create = (imageKitFileID, authors, store, storePath, mediaType, blurhash, session, authToken, mid) => { +const create = async (authors, store, storePath, mediaType, blurhash, session, authToken, mid) => { try { - const media = MediaModel.create({ - imageKitFileID, + const media = await MediaModel.create({ authors, store, storePath, @@ -35,11 +36,26 @@ const create = (imageKitFileID, authors, store, storePath, mediaType, blurhash, } }; -const deleteById = (id) => { +const deleteById = async (id, noDocument = false) => { + const mdbSession = await connection.startSession(); try { - const deleteMedia = MediaModel.findByIdAndDelete(id); - return deleteMedia; + mdbSession.startTransaction(); + + const deleteMedia = !noDocument ? await MediaModel.findByIdAndDelete(id) : null; + + const [_file] = imagekit.listFiles({ + searchQuery: `name = "${id}"`, + }); + + imagekit.deleteFile(_file.fileId); + + await mdbSession.commitTransaction(); + await mdbSession.endSession(); + + return noDocument ? _file : deleteMedia; } catch (error) { + await mdbSession.abortTransaction(); + await mdbSession.endSession(); throw APIError(error, { reason: 'Failed to delete media' }); } }; diff --git a/server/schema/media/media.mutation.js b/server/schema/media/media.mutation.js index f90d8add..08fe769e 100644 --- a/server/schema/media/media.mutation.js +++ b/server/schema/media/media.mutation.js @@ -1,7 +1,7 @@ const { GraphQLObjectType, GraphQLNonNull, GraphQLList, GraphQLID, GraphQLString, GraphQLInt } = require('../scalars'); const MediaType = require('./media.type'); -const { addMedia, deleteMediaById } = require('./media.resolver'); +const { addMedia /*, deleteMediaById*/ } = require('./media.resolver'); module.exports = new GraphQLObjectType({ name: 'MediaMutation', @@ -19,14 +19,14 @@ module.exports = new GraphQLObjectType({ }, resolve: addMedia, }, - deleteMediaById: { - type: MediaType, - description: 'delete media by id', - args: { - id: { type: new GraphQLNonNull(GraphQLID) }, - imageKitFileID: { type: new GraphQLNonNull(GraphQLID) }, - }, - resolve: deleteMediaById, - }, + // deleteMediaById: { + // type: MediaType, + // description: 'delete media by id', + // args: { + // id: { type: new GraphQLNonNull(GraphQLID) }, + // imageKitFileID: { type: new GraphQLNonNull(GraphQLID) }, + // }, + // resolve: deleteMediaById, + // }, }, }); diff --git a/server/schema/media/media.resolver.js b/server/schema/media/media.resolver.js index 48000c22..810ced92 100644 --- a/server/schema/media/media.resolver.js +++ b/server/schema/media/media.resolver.js @@ -1,12 +1,7 @@ const UserPermission = require('../../utils/userAuth/permission'); const { APIError } = require('../../utils/exception'); -const ImageKit = require('imagekit'); -const imagekit = new ImageKit({ - publicKey: process.env.IMAGEKIT_PUBLIC_KEY, - privateKey: process.env.IMAGEKIT_PRIVATE_KEY, - urlEndpoint: process.env.IMAGEKIT_URLENDPOINT, -}); +const imagekit = require('../../config/imagekit'); module.exports = { getMediaByID: async (_parent, { id }, { API: { Media } }, _) => { @@ -22,58 +17,50 @@ module.exports = { throw APIError(null, error); } }, - addMedia: ( + addMedia: async ( _parent, { imageKitFileID, authors, store, storePath, mediaType, blurhash }, { mid, session, authToken, decodedToken, API: { Media } } ) => { try { - if (!UserPermission.exists(session, authToken, decodedToken, 'media.write.new')) { + if (!UserPermission.exists(session, authToken, decodedToken, 'media.write.all')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to perform this operation.', }); } - imagekit.getFileDetails(imageKitFileID, (err, _) => { - if (err) { - throw APIError('Image not uploaded', null, err); - } else { - const media = Media.create( - imageKitFileID, - authors, - store, - storePath, - mediaType, - blurhash, - session, - authToken, - mid - ); - return media; - } - }); - } catch (error) { - throw APIError(null, error); - } - }, - deleteMediaById: (_parent, { id, imageKitFileID }, { session, authToken, decodedToken, API: { Media } }) => { - try { - if ( - !UserPermission.exists(session, authToken, decodedToken, 'media.write.all') || - !UserPermission.exists(session, authToken, decodedToken, 'media.write.self') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permission to perform this operation.', - }); + + // TODO: refactor to datasources + const _imageData = await imagekit.getFileDetails(imageKitFileID); + if (!_imageData || (_imageData.statusCode && _imageData.statusCode !== 200)) { + throw APIError('BAD_REQUEST', null, { reason: 'The media was not uploaded.' }); } - imagekit.getFileDetails(imageKitFileID, (err, res) => { - if (err) { - const _deletedMedia = Media.deleteById(id); - return _deletedMedia; - } - throw APIError('Image not deleted', null, res); - }); + + const media = await Media.create(authors, store, storePath, mediaType, blurhash, session, authToken, mid); + return media; } catch (error) { throw APIError(null, error); } }, + // deleteMediaById: async (_parent, { id }, { session, authToken, decodedToken, API: { Media } }) => { + // try { + // if ( + // !UserPermission.exists(session, authToken, decodedToken, 'media.write.all') || + // !UserPermission.exists(session, authToken, decodedToken, 'media.write.self') + // ) { + // throw APIError('FORBIDDEN', null, { + // reason: 'The user does not have the required permission to perform this operation.', + // }); + // } + + // const _media = await Media.findByID.load(id); + // if (!_media) { + // throw APIError('NOT_FOUND', null, { reason: 'The requested media was not found.' }); + // } + + // const deleteMedia = await Media.deleteById(id, true); + // return deleteMedia; + // } catch (error) { + // throw APIError(null, error); + // } + // }, }; diff --git a/server/schema/media/media.type.js b/server/schema/media/media.type.js index 9aaf6075..7968b15d 100644 --- a/server/schema/media/media.type.js +++ b/server/schema/media/media.type.js @@ -28,7 +28,6 @@ const MediaType = new GraphQLObjectType({ name: 'Media', fields: () => ({ id: { type: GraphQLID }, - imageKitFileID: { type: GraphQLString }, authors: { type: new GraphQLList(UserDetailType) }, store: { type: StoreEnumType }, storePath: { type: GraphQLString }, diff --git a/server/schema/squiggle/squiggle.resolver.js b/server/schema/squiggle/squiggle.resolver.js index a34c6f97..f59e7b45 100644 --- a/server/schema/squiggle/squiggle.resolver.js +++ b/server/schema/squiggle/squiggle.resolver.js @@ -47,9 +47,14 @@ module.exports = { } }, - // TODO: Only display if admin getSquiggleByID: async (_parent, { id }, { API: { Squiggle } }, _) => { try { + if (!UserPermission.exists(session, authToken, decodedToken, 'squiggle.read.all')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read select squiggles.', + }); + } + const _squiggle = await Squiggle.findByID(id); if (!_squiggle) { @@ -63,6 +68,12 @@ module.exports = { }, listSquiggles: async (_parent, { limit = DEF_LIMIT, offset = DEF_OFFSET }, { API: { Squiggle } }, _) => { try { + if (!UserPermission.exists(session, authToken, decodedToken, 'squiggle.read.all')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read select squiggles.', + }); + } + const _squiggles = await Squiggle.find({}, limit, offset); return _squiggles; diff --git a/server/schema/tag/tag.resolver.js b/server/schema/tag/tag.resolver.js index 05424c5b..feb254be 100644 --- a/server/schema/tag/tag.resolver.js +++ b/server/schema/tag/tag.resolver.js @@ -26,7 +26,7 @@ module.exports = { { session, authToken, decodedToken, API: { Tag } } ) => { try { - const _query = UserPermission.exists(session, authToken, decodedToken, 'tag.list.admin') + const _query = UserPermission.exists(session, authToken, decodedToken, 'tag.read.admin') ? { _id: ids } : { $and: [{ _id: ids }, { isAdmin: false }] }; @@ -55,10 +55,7 @@ module.exports = { { session, authToken, decodedToken, API: { Tag } } ) => { try { - if ( - (isAdmin && !UserPermission.exists(session, authToken, decodedToken, 'tag.list.admin')) || - (!isAdmin && !UserPermission.exists(session, authToken, decodedToken, 'tag.list.public')) - ) { + if (isAdmin && !UserPermission.exists(session, authToken, decodedToken, 'tag.read.admin')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to perform this operation.', }); @@ -73,7 +70,7 @@ module.exports = { }, createTag: async ( _parent, - { name, isAdmin = false, adminColor }, + { name, isAdmin = false, adminColor = DEFAULT_TAG_COLOR }, { mid, session, authToken, decodedToken, API: { Tag } } ) => { try { @@ -85,9 +82,6 @@ module.exports = { reason: 'The user does not have the required permissions to perform this operation', }); } - if (isAdmin && !adminColor) { - adminColor = DEFAULT_TAG_COLOR; - } const _tag = await Tag.create(name, isAdmin, isAdmin ? adminColor : undefined, session, authToken, mid); @@ -96,23 +90,25 @@ module.exports = { throw APIError(null, error); } }, - updateTag: async ( - _parent, - { id, name, isAdmin, adminColor }, - { mid, session, authToken, decodedToken, API: { Tag } } - ) => { - try { - if (!UserPermission.exists(session, authToken, decodedToken, 'tag.write.public')) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permission to perform this operation.', - }); - } - - const _tag = await Tag.update(id, name, isAdmin, adminColor, session, authToken, mid); - - return _tag; - } catch (error) { - throw APIError(null, error); - } - }, + // TODO: read tag first, then check if user has permission to update + // TODO: gracefully propagate changes to all redundancies + // updateTag: async ( + // _parent, + // { id, name, isAdmin, adminColor }, + // { mid, session, authToken, decodedToken, API: { Tag } } + // ) => { + // try { + // if (!UserPermission.exists(session, authToken, decodedToken, 'tag.write.public')) { + // throw APIError('FORBIDDEN', null, { + // reason: 'The user does not have the required permission to perform this operation.', + // }); + // } + + // const _tag = await Tag.update(id, name, isAdmin, adminColor, session, authToken, mid); + + // return _tag; + // } catch (error) { + // throw APIError(null, error); + // } + // }, }; diff --git a/server/schema/user/user.datasources.js b/server/schema/user/user.datasources.js index d79fddda..7d1c64c0 100644 --- a/server/schema/user/user.datasources.js +++ b/server/schema/user/user.datasources.js @@ -6,6 +6,70 @@ const { APIError, FirebaseAuthError } = require('../../utils/exception'); const UserSession = require('../../utils/userAuth/session'); const UserModel = require('./user.model'); +const USER_BASE_ROLES = [ + 'user.basic', + 'article.basic', + 'reactions.basic', + 'comment.basic', + 'issue.basic', + 'session.basic', + 'squiggle.basic', + 'poll.basic', + 'media.basic', + 'album.basic', + 'tag.basic', + 'category.basic', + 'role.basic', + 'club.basic', + 'event.basic', + 'company.basic', + 'live.basic', + 'shareInternship.basic', + 'forum.basic', +]; +const USER_VERIFIED_STUDENT_ROLES = [ + 'user.verified', + 'article.student', + 'reactions.basic', + 'comment.verified', + 'issue.basic', + 'session.basic', + 'squiggle.basic', + 'poll.verified', + 'media.basic', + 'album.basic', + 'tag.basic', + 'category.basic', + 'role.basic', + 'club.basic', + 'event.basic', + 'company.verified', + 'live.verified', + 'shareInternship.verified', + 'forum.verified', +]; +const USER_VERIFIED_FACULTY_ROLES = [ + 'user.verified', + 'article.faculty', + 'reactions.basic', + 'comment.verified', + 'issue.basic', + 'session.basic', + 'squiggle.basic', + 'poll.verified', + 'media.basic', + 'album.basic', + 'tag.basic', + 'category.basic', + 'role.basic', + 'club.basic', + 'event.basic', + 'company.verified', + 'live.verified', + 'shareInternship.verified', + 'forum.verified', +]; + const findByID = () => new DataLoader( async (ids) => { @@ -39,6 +103,16 @@ const findByEmail = () => } ); +const findByOldUserName = async (oldUserName) => { + try { + const _user = await UserModel.findOne({ oldUserName }); + + return _user; + } catch (error) { + throw APIError(null, error); + } +}; + const findFirebaseUserById = (id) => admin.auth().getUser(id); const findFirebaseUserByEmail = (email) => admin.auth().getUserByEmail(email); @@ -52,7 +126,7 @@ const search = (query, accountType, limit, offset) => UserModel.aggregate([ { $search: { - // TODO: rectify index name + // TODO: move const to env index: 'default', text: { query, @@ -94,28 +168,6 @@ const search = (query, accountType, limit, offset) => }, ]); -const getUserByOldUserName = () => - new DataLoader( - async (oldUserNames) => { - try { - const _users = await UserModel.find({ oldUserName: oldUserNames }); - const _returnIds = oldUserNames.map( - (oldUserName) => _users.find((_u) => _u.oldUserName === oldUserName) || null - ); - - for (const _user of _users) { - findByID().prime(_user._id, _user); - } - return _returnIds; - } catch (error) { - throw APIError(null, error); - } - }, - { - batchScheduleFn: (cb) => setTimeout(cb, 100), - } - ); - const create = async (uid, fullName, email, interestedTopics, session, authToken, mid) => { const mdbSession = await connection.startSession(); @@ -134,9 +186,8 @@ const create = async (uid, fullName, email, interestedTopics, session, authToken ); await admin.auth().setCustomUserClaims(uid, { - mid: _user.id, - // TODO: add all standard roles here - roles: ['user.basic'], + mid: _user[0].id, + roles: USER_BASE_ROLES, }); await mdbSession.commitTransaction(); @@ -151,44 +202,82 @@ const create = async (uid, fullName, email, interestedTopics, session, authToken } }; -// TODO: Update all redundancies -const updateName = (uid, id, firstName, lastName, session, authToken, mid) => { - const _updatedUser = UserModel.findByIdAndUpdate( - id, - { - firstName, - lastName, - isNameChanged: true, - updatedBy: UserSession.valid(session, authToken) ? mid : null, - }, - { new: true } - ); - const _updatedFbUser = admin.auth().updateUser(uid, { displayName: `${firstName} ${lastName}` }); +const link = async (uid, id, interestedTopics, session, authToken, mid) => { + const mdbSession = await connection.startSession(); + + try { + mdbSession.startTransaction(); - return Promise.all([_updatedUser, _updatedFbUser]); + const _user = await UserModel.findByIdAndUpdate( + id, + interestedTopics + ? { + $addToSet: { + interestedTopics, + }, + newUserLinked: true, + updatedBy: UserSession.valid(session, authToken) ? mid : null, + } + : { + newUserLinked: true, + updatedBy: UserSession.valid(session, authToken) ? mid : null, + }, + { + new: true, + session: mdbSession, + } + ); + + await admin.auth().setCustomUserClaims(uid, { + mid: id, + roles: USER_VERIFIED_STUDENT_ROLES, + }); + + await mdbSession.commitTransaction(); + await mdbSession.endSession(); + return _user; + } catch (error) { + await mdbSession.abortTransaction(); + await mdbSession.endSession(); + + await admin.auth().deleteUser(uid); + throw FirebaseAuthError(error, { reason: "The user's account could not be created." }); + } }; -const updateDetails = (id, fields, session, authToken, mid) => - UserModel.findOneAndUpdate( - id, - { - ...createUpdateObject(fields), - updatedBy: UserSession.valid(session, authToken) ? mid : null, - }, - { new: true } - ); +// TODO: Update all redundancies -const getFirebaseUser = async (email) => { +// const updateName = (uid, id, firstName, lastName, session, authToken, mid) => { +// const _updatedUser = UserModel.findByIdAndUpdate( +// id, +// { +// firstName, +// lastName, +// isNameChanged: true, +// updatedBy: UserSession.valid(session, authToken) ? mid : null, +// }, +// { new: true } +// ); +// const _updatedFbUser = admin.auth().updateUser(uid, { displayName: `${firstName} ${lastName}` }); + +// return Promise.all([_updatedUser, _updatedFbUser]); +// }; + +const updateDetails = async (id, fields, session, authToken, mid) => { try { - const _fbUser = await admin.auth().getUserByEmail(email); - if (!_fbUser) { - throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); - } - return _fbUser; + return await UserModel.findByIdAndUpdate( + id, + { + ...createUpdateObject(fields), + updatedBy: UserSession.valid(session, authToken) ? mid : null, + }, + { new: true } + ); } catch (error) { - throw FirebaseAuthError(error, { reason: "Cannot find user's roles" }); + throw FirebaseAuthError(error, { reason: "Cannot update user's details" }); } }; + const updateCustomClaims = async (email, customClaims) => { try { const _fbUser = await admin.auth().getUserByEmail(email); @@ -211,30 +300,35 @@ const updateCustomClaims = async (email, customClaims) => { } }; -const setVerified = async (id, email, token, accountType, session, authToken, mid) => { +const setNITRVerified = async (id, accountType, email, nitrMail, session, authToken, mid) => { const mdbSession = await connection.startSession(); try { mdbSession.startTransaction(); - const _user = await UserModel.findOneAndUpdate( - { $and: [{ _id: id }, { nitrMail: email }, { verfiyEmailToken: token }] }, - { accountType, verfiyEmailToken: null, updatedBy: UserSession.valid(session, authToken) ? mid : null }, + const _user = await UserModel.findByIdAndUpdate( + id, + { accountType, nitrMail, updatedBy: UserSession.valid(session, authToken) ? mid : null }, { session: mdbSession, new: true } ); if (!_user) { throw APIError('NOT_FOUND', null, { - reason: 'Either the verification token is invalid or the user does not exist.', + reason: 'The user does not exist.', }); } - const _fbUser = await findFirebaseUserByEmail(_user.email); - // TODO: update all roles as required + const _fbUser = await findFirebaseUserByEmail(nitrMail); + await admin.auth().updateUser(_fbUser.uid, { email }); + const _roles = _fbUser.customClaims.roles.map((item) => { - if (item.toString() !== 'user.basic') { - return item; + const _roleIndex = USER_BASE_ROLES.indexOf(item); + if (_roleIndex > -1 && (accountType === 1 || accountType === 2)) { + return USER_VERIFIED_STUDENT_ROLES[_roleIndex]; + } + if (_roleIndex > -1 && accountType === 3) { + return USER_VERIFIED_FACULTY_ROLES[_roleIndex]; } - return 'user.verified'; + return item; }); await admin.auth().setCustomUserClaims(_fbUser.uid, { @@ -287,7 +381,7 @@ const setBan = async (id, flag, session, authToken, mid) => { const UserDataSources = () => ({ findByID: findByID(), findByEmail: findByEmail(), - getUserByOldUserName: getUserByOldUserName(), + findByOldUserName, findFirebaseUserById, findFirebaseUserByEmail, findOne, @@ -295,12 +389,12 @@ const UserDataSources = () => ({ exists, search, create, - updateName, + link, + // updateName, updateDetails, updateCustomClaims, - setVerified, + setNITRVerified, setBan, - getFirebaseUser, }); module.exports = UserDataSources; diff --git a/server/schema/user/user.model.js b/server/schema/user/user.model.js index af0c229d..f280d8cd 100644 --- a/server/schema/user/user.model.js +++ b/server/schema/user/user.model.js @@ -73,15 +73,22 @@ const UserSchema = new Schema( required: false, }, }, + oldUserId: { + type: Number, + required: false, + }, oldUserName: { type: String, required: false, }, + newUserLinked: { + type: Boolean, + required: false, + }, interestedTopics: [ { type: Number, required: false, - min: 0, }, ], isNewsletterSubscribed: { @@ -168,20 +175,17 @@ const UserSchema = new Schema( type: Boolean, required: false, }, + // TODO: implement poll system later /** @see module:app.models.poll */ - lastPoll: { - type: Schema.Types.ObjectId, - ref: 'Poll', - required: false, - }, + // lastPoll: { + // type: Schema.Types.ObjectId, + // ref: 'Poll', + // required: false, + // }, isNameChanged: { type: Boolean, required: false, }, - verfiyEmailToken: { - type: String, - required: false, - }, createdBy: { type: Schema.Types.ObjectId, ref: 'User', diff --git a/server/schema/user/user.mutation.js b/server/schema/user/user.mutation.js index 52a91397..3291fd39 100644 --- a/server/schema/user/user.mutation.js +++ b/server/schema/user/user.mutation.js @@ -32,19 +32,18 @@ const { } = require('../scalars'); const UserType = require('./user.type'); -const FirebaseUserType = require('./firebaseUser.type'); +// const FirebaseUserType = require('./firebaseUser.type'); const { createUser, setUserBan, - updateUserName, - updateUserPicture, + // updateUserName, + updateUserProfilePicture, updateUserTopics, updateUserBio, addNITRMail, - verifyNITRMail, newsletterSubscription, setUserAccountType, - setUserRoles, + // setUserRoles, } = require('./user.resolver'); const { AccountTypeEnumType } = require('./user.enum.types'); @@ -60,29 +59,20 @@ module.exports = new GraphQLObjectType({ }, resolve: createUser, }, - updateUserName: { - type: UserType, - args: { - id: { type: new GraphQLNonNull(GraphQLID) }, - firstName: { type: new GraphQLNonNull(GraphQLString) }, - lastName: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: updateUserName, - }, - updateUserPicture: { - type: UserType, - args: { - id: { type: new GraphQLNonNull(GraphQLID) }, - url: { type: new GraphQLNonNull(GraphQLString) }, - blurhash: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: updateUserPicture, - }, + // updateUserName: { + // type: UserType, + // args: { + // id: { type: new GraphQLNonNull(GraphQLID) }, + // firstName: { type: new GraphQLNonNull(GraphQLString) }, + // lastName: { type: new GraphQLNonNull(GraphQLString) }, + // }, + // resolve: updateUserName, + // }, updateUserTopics: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) }, - interestedTopics: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, + interestedTopics: { type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) }, }, resolve: updateUserTopics, }, @@ -100,23 +90,23 @@ module.exports = new GraphQLObjectType({ }, resolve: updateUserBio, }, - - addNITRMail: { + updateUserProfilePicture: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) }, - email: { type: new GraphQLNonNull(GraphQLString) }, + store: { type: GraphQLInt }, + storePath: { type: new GraphQLNonNull(GraphQLString) }, + blurhash: { type: GraphQLString }, }, - resolve: addNITRMail, + resolve: updateUserProfilePicture, }, - verifyNITRMail: { + addNITRMail: { type: UserType, args: { - id: { type: new GraphQLNonNull(GraphQLID) }, email: { type: new GraphQLNonNull(GraphQLString) }, - token: { type: new GraphQLNonNull(GraphQLString) }, + nitrMail: { type: new GraphQLNonNull(GraphQLString) }, }, - resolve: verifyNITRMail, + resolve: addNITRMail, }, newsletterSubscription: { @@ -145,17 +135,17 @@ module.exports = new GraphQLObjectType({ }, resolve: setUserBan, }, - setUserRoles: { - type: FirebaseUserType, - args: { - email: { - description: "The user's email id", - type: new GraphQLNonNull(GraphQLString), - }, - roles: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, - }, - resolve: setUserRoles, - }, + // setUserRoles: { + // type: FirebaseUserType, + // args: { + // email: { + // description: "The user's email id", + // type: new GraphQLNonNull(GraphQLString), + // }, + // roles: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, + // }, + // resolve: setUserRoles, + // }, // TODO: update contributions from other schemas // TODO: update last poll from other schemas diff --git a/server/schema/user/user.query.js b/server/schema/user/user.query.js index 2a87164e..58cc2fbc 100644 --- a/server/schema/user/user.query.js +++ b/server/schema/user/user.query.js @@ -38,6 +38,7 @@ const { getUserByOldUserName, listAllUsers, searchUsers, + checkNITRMail, } = require('./user.resolver'); const { AccountTypeEnumType } = require('./user.enum.types'); @@ -124,7 +125,7 @@ module.exports = new GraphQLObjectType({ }, accountType: { description: "The user's account type or verification status", - type: AccountTypeEnumType, + type: new GraphQLList(AccountTypeEnumType), }, limit: { description: 'The number of results to return', @@ -137,6 +138,17 @@ module.exports = new GraphQLObjectType({ }, resolve: searchUsers, }, + checkNITRMail: { + description: 'Checks if the NITR email already exists', + type: UserType, + args: { + nitrMail: { + description: 'The NITR email ID', + type: new GraphQLNonNull(GraphQLString), + }, + }, + resolve: checkNITRMail, + }, /** Admin APIs */ listAllUsers: { diff --git a/server/schema/user/user.resolver.js b/server/schema/user/user.resolver.js index a8833f7b..fd6d2c52 100644 --- a/server/schema/user/user.resolver.js +++ b/server/schema/user/user.resolver.js @@ -10,9 +10,6 @@ * @since 0.1.0 */ -// const fetch = require('node-fetch'); -const { v5: UUID } = require('uuid'); -const { transporter } = require('../../config/nodemailer'); const UserPermission = require('../../utils/userAuth/permission'); const UserSession = require('../../utils/userAuth/session'); const { APIError, FirebaseAuthError } = require('../../utils/exception'); @@ -23,42 +20,78 @@ const PUBLIC_FIELDS = [ 'id', 'firstName', 'lastName', - 'picture', - 'pictureId', 'fullName', - 'nitrMail', - 'accountType', 'email', + 'accountType', + 'nitrMail', + 'picture', + 'profile', + 'isBanned', ]; const DEF_LIMIT = 10, - DEF_OFFSET = 0; + DEF_OFFSET = 0, + DEF_STORE = 2; const ACCOUNT_TYPES = Object.fromEntries(AccountTypeEnumType.getValues().map((item) => [item.name, item.value])); -const canUpdateUser = (id, mid, session, authToken, decodedToken, fieldNodes, needsAdmin = false) => { +// TODO: add a needsAdmin check for admin APIs +const canReadUser = (user, mid, session, authToken, decodedToken, fieldNodes, noError = false) => { + if ([ACCOUNT_TYPES.MM_TEAM, ACCOUNT_TYPES.NITR_FACULTY].includes(user.accountType)) { + return true; + } + + if (mid === (user.id ?? user._id.toString())) { + return true; + } + + if (!UserPermission.exists(session, authToken, decodedToken, 'user.read.private')) { + if (noError) { + return false; + } + throw APIError('FORBIDDEN', null, { + reason: "The user does not have the required permissions to read the requested user's data.", + }); + } + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'user.read.admin') + ) { + if (noError) { + return false; + } + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + + return true; +}; + +const canUpdateUser = async (id, mid, session, authToken, decodedToken, fieldNodes, User, needsAdmin = false) => { if (!UserSession.valid(session, authToken)) { throw APIError('UNAUTHORIZED', null, { reason: 'The user is not authenticated.' }); } + const _user = await User.findByID.load(id); + + canReadUser(_user, mid, session, authToken, decodedToken, fieldNodes); + if (needsAdmin && !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required administrative priveledges.' }); - } else if ( - (mid === id && - !UserPermission.exists(session, authToken, decodedToken, 'user.write.self') && - !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) || - (mid !== id && !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) - ) { - throw APIError('FORBIDDEN', null, { reason: 'The user does not have the permissions to perform this update.' }); } if ( - mid !== id && - _fields?.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') + !needsAdmin && + mid === id && + !UserPermission.exists(session, authToken, decodedToken, 'user.write.self') && + !UserPermission.exists(session, authToken, decodedToken, 'user.write.all') ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the permissions to read the equested fields.', - }); + throw APIError('FORBIDDEN', null, { reason: 'The user does not have the permissions to perform this update.' }); + } + + if (!needsAdmin && mid !== id && !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) { + throw APIError('FORBIDDEN', null, { reason: 'The user does not have the permissions to perform this update.' }); } return true; @@ -72,7 +105,9 @@ module.exports = { { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); + if (!id && !email) { + throw APIError('BAD_REQUEST', null, { reason: 'No ID or Email was provided in the arguments.' }); + } const _user = !id ? await User.findByEmail.load(email) : await User.findByID.load(id); @@ -80,22 +115,7 @@ module.exports = { throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); } - if ([ACCOUNT_TYPES.MM_TEAM, ACCOUNT_TYPES.NITR_FACULTY].includes(_user.accountType)) { - return _user; - } - - if (mid === _user.id) { - return _user; - } - - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permissions to read the requested fields.', - }); - } + canReadUser(_user, mid, session, authToken, decodedToken, fieldNodes); return _user; } catch (error) { @@ -103,11 +123,22 @@ module.exports = { } }, - getUserByOldUserName: async (_parent, { oldUserName }, { API: { User } }) => { + getUserByOldUserName: async ( + _parent, + { oldUserName }, + { mid, session, authToken, decodedToken, API: { User } }, + { fieldNodes } + ) => { try { - const user = await User.getUserByOldUserName.load(oldUserName); + const _user = await User.findByOldUserName(oldUserName); + + if (!_user) { + throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); + } - return user; + canReadUser(_user, mid, session, authToken, decodedToken, fieldNodes); + + return _user; } catch (error) { throw APIError(null, error); } @@ -116,25 +147,14 @@ module.exports = { getListOfUsers: async ( _parent, { ids = [], emails = [], limit = DEF_LIMIT, offset = DEF_OFFSET }, - { session, authToken, decodedToken, API: { User } }, + { mid, session, authToken, decodedToken, API: { User } }, { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); if (ids.length <= 0 && emails.length <= 0) { throw APIError('BAD_REQUEST', null, { reason: 'No IDs and Emails were provided in the arguments.' }); } - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - - // TODO: Use findByID and findByEmail const _users = await User.find({ $or: [{ _id: ids }, { email: emails }] }, limit, offset); if (!_users || !(_users instanceof Array) || _users.length <= 0) { @@ -146,71 +166,51 @@ module.exports = { User.findByEmail.prime(_user.email, _user); } - // TODO: check user account type for all values and return error acordingly - - return _users.length < ids.length + emails.length - ? [..._users, APIError('NOT_FOUND', null, { reason: 'One or more of the requested users were not found.' })] - : _users; + return _users.map((user) => { + try { + canReadUser(user, mid, session, authToken, decodedToken, fieldNodes); + return user; + } catch (error) { + return error; + } + }); } catch (error) { throw APIError(null, error); } }, - // TODO: use aggregation pipelines + searchUsers: async ( _parent, - { searchTerm, accountType, limit = DEF_LIMIT, offset = DEF_OFFSET }, - { session, authToken, decodedToken, API: { User } }, + { + searchTerm, + accountType = [ACCOUNT_TYPES.MM_TEAM, ACCOUNT_TYPES.NITR_FACULTY], + limit = DEF_LIMIT, + offset = DEF_OFFSET, + }, + { mid, session, authToken, decodedToken, API: { User } }, { fieldNodes } ) => { try { - const _fields = getFieldNodes(fieldNodes); - - if (!UserPermission.exists(session, authToken, decodedToken, 'user.list.public')) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permissions to perform this action.', - }); - } - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') + accountType.some( + (_accountType) => ![ACCOUNT_TYPES.MM_TEAM, ACCOUNT_TYPES.NITR_FACULTY].includes(_accountType) + ) && + !UserPermission.exists(session, authToken, decodedToken, 'user.read.admin') ) { throw APIError('FORBIDDEN', null, { - reason: 'The user does not have the required permissions to view the requested fields', + reason: 'The user does not the required permission to view the requested account type.', }); } - if (!accountType) { - accountType = [ACCOUNT_TYPES.MM_TEAM, ACCOUNT_TYPES.NITR_FACULTY]; - if (UserPermission.exists(session, authToken, decodedToken, 'user.list.all')) { - accountType = [ - ACCOUNT_TYPES.NORMAL, - ACCOUNT_TYPES.NITR_STUDENT, - ACCOUNT_TYPES.MM_TEAM, - ACCOUNT_TYPES.NITR_FACULTY, - ]; - } - } else { - if ( - [ACCOUNT_TYPES.NORMAL, ACCOUNT_TYPES.NITR_STUDENT].includes(accountType) && - !UserPermission.exists(session, authToken, decodedToken, 'user.list.all') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permission to view the requested account type.', - }); - } - accountType = [accountType]; - } - const _users = await User.search(searchTerm, accountType, limit, offset); - if (!_users || _users.length <= 0) { + if (!_users || !(_users instanceof Array) || _users.length <= 0) { throw APIError('NOT_FOUND', null, { reason: 'No users were found with the given search term and account type(s).', }); } - return _users; + return _users.filter((user) => canReadUser(user, mid, session, authToken, decodedToken, fieldNodes, true)); } catch (error) { throw APIError(null, error); } @@ -222,34 +222,44 @@ module.exports = { { mid, session, authToken, decodedToken, API: { User } } ) => { try { - if ( - !UserSession.valid(session, authToken) || - ((await User.getFirebaseUser(email)).email !== email && - !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) - ) { - throw APIError('METHOD_NOT_ALLOWED'); + // TODO: add data validation and cleaning + + const _fbUser = await User.findFirebaseUserByEmail(email); + + if (!UserSession.valid(session, authToken) || _fbUser.uid !== decodedToken.uid) { + throw APIError('METHOD_NOT_ALLOWED', null, { reason: 'The user does not have the required permissions.' }); + } + + const _user = await User.findByEmail.load(email); + + if (_user && _user.accountType === 2 && _user.oldUserId > 0) { + if (_fbUser.displayName !== fullName) { + return APIError('BAD_REQUEST', null, { reason: 'The name provided did not match the existing records.' }); + } + + const _mdbUser = await User.link(_fbUser.uid, _user.id, interestedTopics, session, authToken, mid); + + return _mdbUser; } - if (await User.exists({ email })) { + if (_user) { throw APIError('METHOD_NOT_ALLOWED', null, { reason: 'User already exists' }); } - // const _fbUser = await _auth.getUserByEmail(email); - const _fbUser = await User.findFirebaseUserByEmail(email); if (_fbUser.displayName !== fullName) { return APIError('BAD_REQUEST', null, { reason: 'The name provided did not match the existing records.' }); } const [_mdbUser] = await User.create(_fbUser.uid, fullName, email, interestedTopics, session, authToken, mid); - // TODO: send welcome mail if required - return _mdbUser; } catch (error) { throw FirebaseAuthError(error); } }, + // TODO: implement a way to update all redundancies gracefully + /* updateUserName: async ( _parent, { id, firstName, lastName }, @@ -257,9 +267,8 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); - // const _user = await _UserModel.findByID(id, 'firstName lastName isNameChanged'); const _user = await User.findByID(id); if (_user.firstName === firstName && _user.lastName === lastName) { @@ -269,7 +278,6 @@ module.exports = { return APIError('METHOD_NOT_ALLOWED', null, { reason: 'The user has already changed their name once.' }); } - // const _fbUser = await _auth.getUserByEmail(_user.email); const _fbUser = await User.findFirebaseUserByEmail(_user.email); const _updatedUser = await User.updateName(_fbUser.uid, id, firstName, lastName); @@ -279,58 +287,33 @@ module.exports = { throw FirebaseAuthError(error); } }, - // TODO: rewrite function with data sources - // TODO: update all redundancies - // TODO: delete older picture - // updateUserPicture: async ( - // _parent, - // { id, url, blurhash }, - // context, - // { fieldNodes }, - // _UserModel = UserModel, - // _MediaModel = MediaModel, - // _auth = admin.auth(), - // _fetch = fetch - // ) => { - // const fields = fieldNodes[0].selectionSet.selections.map((x) => x.name.value); - // try { - // if (!id || !url || !blurhash) { - // return APIError('BAD_REQUEST'); - // } - // const _writePermission = canUserUpdate(id, context, fields); - // if (_writePermission !== true) { - // return _writePermission; - // } - // if (!HasPermmission(context, 'media.read.public') || !HasPermmission(context, 'media.write.self')) { - // return APIError('FORBIDDEN'); - // } - // const _res = await _fetch(url); - // if (!_res.ok) { - // return APIError('BAD_REQUEST', { reason: 'The provided image resource was not found on the media server.' }); - // } - // const _user = await _UserModel.findByID(id); - // if (!_user) { - // return APIError('NOT_FOUND'); - // } - // const _fbUser = await _auth.getUserByEmail(_user.email); - // const _media = await _MediaModel.create({ - // storePath: url, - // blurhash, - // author: [ - // { - // name: _user.name, - // reference: id, - // }, - // ], - // }); - // const _updatedUser = _UserModel.findByIdAndUpdate(id, { picture: _media.id }); - // const _updatedFbUser = _auth.updateUser(_fbUser.uid, { displayName: `${firstName} ${lastName}` }); - // await Promise.all([_updatedUser, _updatedFbUser]); - // return _updatedUser; - // } catch (error) { - // return FirebaseAuthError(error); - // } - // }, + */ + // TODO: implement a way to update all redundancies gracefully + updateUserProfilePicture: async ( + _parent, + { id, store = DEF_STORE, storePath, blurhash }, + { mid, session, authToken, decodedToken, API: { User, Media } }, + { fieldNodes } + ) => { + try { + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User); + + const user = await User.findByID.load(id); + + if (!user) { + throw APIError('NOT_FOUND', null, { reason: 'The user does not exist.' }); + } + + if (user.picture && user.picture.storePath) { + Media.deleteById(id, true); + } + + const _user = await User.updateDetails(id, { picture: { store, storePath, blurhash } }, session, authToken, mid); + return _user; + } catch (error) { + throw APIError(null, error); + } + }, updateUserTopics: async ( _parent, { id, interestedTopics }, @@ -338,7 +321,7 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User); const _user = await User.updateDetails(id, { interestedTopics }, session, authToken, mid); return _user; @@ -353,7 +336,7 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User); const _user = await User.updateDetails( id, @@ -368,56 +351,53 @@ module.exports = { } }, - addNITRMail: async ( - _parent, - { id, email }, - { mid, session, authToken, decodedToken, API: { User } }, - { fieldNodes }, - _transporter = transporter, - _namespace = process.env.UUID_NAMESPACE, - _fromAddress = process.env.SMTP_FROM_ADDRESS - ) => { + checkNITRMail: async (_parent, { nitrMail }, { mid, session, authToken, decodedToken, API: { User } }, _) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + if (!UserSession.valid(session, authToken)) { + throw APIError('UNAUTHORIZED', null, { reason: 'The user is not logged in.' }); + } - const _uuid = UUID(JSON.stringify({ id, email, authToken }), _namespace).toString(); + const _user = await User.findOne({ nitrMail }); - // TODO: Configure proper html template - await _transporter.sendMail({ - to: email, - from: _fromAddress, - html: `Click here to verify!`, - }); + if (!_user) { + throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); + } + + if (!UserPermission.exists(session, authToken, decodedToken, 'user.read.all') && _user._id.toString() !== mid) { + const _returnUser = PUBLIC_FIELDS.map((field) => [field, _user[field]]); + return Object.fromEntries(_returnUser); + } - const _user = await User.updateDetails( - id, - { - nitrMail: email, - verifyEmailToken: _uuid, - }, - session, - authToken, - mid - ); return _user; } catch (error) { throw APIError(null, error); } }, - verifyNITRMail: async ( + + addNITRMail: async ( _parent, - { id, email, token }, + { email, nitrMail }, { mid, session, authToken, decodedToken, API: { User } }, { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + const _user = await User.findByEmail.load(email); + if (!_user) { + throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); + } - const _updatedUser = await User.setVerified( - id, + const _fbUser = await User.findFirebaseUserByEmail(nitrMail); + if (!_fbUser) { + throw APIError('BAD_REQUEST', null, { reason: 'The requested user has not been linked.' }); + } + + await canUpdateUser(_user._id.toString(), mid, session, authToken, decodedToken, fieldNodes, User); + + const _updatedUser = await User.setNITRVerified( + _user._id, + RegExp(/^([0-9]{3})([a-zA-Z]{2})([0-9]{4})(\@nitrkl\.ac\.in)$/).test(nitrMail) ? 1 : 3, email, - token, - ACCOUNT_TYPES.NITR_STUDENT, + nitrMail, session, authToken, mid @@ -425,7 +405,7 @@ module.exports = { return _updatedUser; } catch (error) { - throw FirebaseAuthError(error); + throw APIError(null, error); } }, @@ -436,9 +416,9 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User); - const _user = await User.updateDetails(id, { newsletterSubscription: flag }, session, authToken, mid); + const _user = await User.updateDetails(id, { isNewsletterSubscribed: flag }, session, authToken, mid); if (!_user) { throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); @@ -458,23 +438,15 @@ module.exports = { { fieldNodes } ) => { try { + // TODO: use canReadUser instead of custom checks const _fields = getFieldNodes(fieldNodes); - if (!UserPermission.exists(session, authToken, decodedToken, 'user.list.all')) { + if (!UserPermission.exists(session, authToken, decodedToken, 'user.read.admin')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permissions to perform this action.', }); } - if ( - _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') - ) { - throw APIError('FORBIDDEN', null, { - reason: 'The user does not the required permissions to read the requested fields.', - }); - } - const _users = await User.find(accountType ? { accountType } : {}, limit, offset); for (const _user of _users) { @@ -494,7 +466,7 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, true); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User, true); const _user = await User.updateDetails(id, { accountType }, session, authToken, mid); @@ -514,7 +486,7 @@ module.exports = { { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, true); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User, true); const _user = await User.setBan(id, flag, session, authToken, mid); @@ -523,26 +495,41 @@ module.exports = { throw FirebaseAuthError(error); } }, - getFirebaseUserByEmail: async (_parent, { email }, { API: { User } }) => { + getFirebaseUserByEmail: async (_parent, { email }, { mid, session, authToken, decodedToken, API: { User } }) => { try { - const firebaseUser = await User.getFirebaseUser(email); + const firebaseUser = await User.findFirebaseUserByEmail(email); + + if (!firebaseUser) { + throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); + } + + if ( + mid !== firebaseUser.customClaims.mid && + !UserPermission.exists(session, authToken, decodedToken, 'user.read.admin') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not the required permissions to read the requested data.', + }); + } + return firebaseUser; } catch (error) { return FirebaseAuthError(error); } }, - setUserRoles: async ( - _parent, - { email, roles }, - { mid, session, authToken, decodedToken, API: { User } }, - { fieldNodes } - ) => { - try { - canUpdateUser(null, mid, session, authToken, decodedToken, fieldNodes, true); - const _user = await User.updateCustomClaims(email, { roles }); - return _user; - } catch (error) { - return FirebaseAuthError(error); - } - }, + // TODO: use id to query the user and then update the custom claims + // setUserRoles: async ( + // _parent, + // { email, roles }, + // { mid, session, authToken, decodedToken, API: { User } }, + // { fieldNodes } + // ) => { + // try { + // await canUpdateUser(null, mid, session, authToken, decodedToken, fieldNodes, User, true); + // const _user = await User.updateCustomClaims(email, { roles }); + // return _user; + // } catch (error) { + // return FirebaseAuthError(error); + // } + // }, }; diff --git a/server/schema/user/user.type.js b/server/schema/user/user.type.js index 2a1a18de..f9b1494d 100644 --- a/server/schema/user/user.type.js +++ b/server/schema/user/user.type.js @@ -74,6 +74,7 @@ const PositionType = new GraphQLObjectType({ fields: () => ({ position: { type: PositionEnumType }, team: { type: TeamEnumType }, + // TODO: consider resolving to SessionType instead session: { type: GraphQLInt }, }), }); @@ -111,8 +112,9 @@ const UserType = new GraphQLObjectType({ isBanned: { type: GraphQLBoolean }, - lastPollID: { type: GraphQLID }, + // TODO: implement Poll System // TODO: resolve to PollType + // lastPollID: { type: GraphQLID }, /* lastPoll: { type: PollType, @@ -121,7 +123,6 @@ const UserType = new GraphQLObjectType({ */ isNameChanged: { type: GraphQLBoolean }, - verifyEmailToken: { type: GraphQLString }, createdAt: { type: GraphQLDateTime }, createdBy: { type: GraphQLID }, diff --git a/server/utils/userAuth/index.js b/server/utils/userAuth/index.js index b0c4fe21..a3f4788a 100644 --- a/server/utils/userAuth/index.js +++ b/server/utils/userAuth/index.js @@ -2,33 +2,61 @@ const { admin } = require('../../config/firebase'); const { APIError, FirebaseAuthError } = require('../exception'); const UserSession = require('./session'); +const SUPERADMIN_ROLES = [ + 'user.superadmin', + 'article.superadmin', + 'reactions.superadmin', + 'comment.superadmin', + 'issue.superadmin', + 'session.superadmin', + 'squiggle.superadmin', + 'poll.superadmin', + 'media.superadmin', + 'album.superadmin', + 'tag.superadmin', + 'category.superadmin', + 'role.superadmin', + 'club.superadmin', + 'event.superadmin', + 'company.superadmin', + 'live.superadmin', + 'shareInternship.superadmin', + 'forum.superadmin', +]; + const UserAuth = { /** * @description Authenticates a user and returns the uid * @function * @async * - * @param {String} jwt JSON Web Token + * @param {String} authToken JSON Web Token * @param {admin.Auth} _auth Firebase Authentication Library * @returns {Object | GraphQLError} decodedToken */ - authenticate: async (jwt, _auth = admin?.auth()) => { + authenticate: async (authToken, _auth = admin?.auth()) => { try { - const _decodedToken = - process.env.NODE_ENV === 'development' && process.env.FIREBASE_TEST_AUTH_KEY === jwt - ? { - uid: '', - exp: 4102444800, // Jan 1, 2100 at midnight - mid: process.env.MID, - roles: ['user.superadmin', 'article.admin', 'issue.admin', 'tag.admin', 'live.superadmin', 'media.admin'], - email_verified: true, - } - : await _auth.verifyIdToken(jwt, true); - if (!_decodedToken.email_verified) { - throw APIError('UNAUTHORIZED', null, { - reason: "The User's Email ID is not verified.", - }); + if (process.env.NODE_ENV === 'development' && process.env.FIREBASE_TEST_AUTH_KEY === authToken) { + return { + uid: '', + exp: 4102444800, // Jan 1, 2100 at midnight + mid: '', + roles: SUPERADMIN_ROLES, + email_verified: true, + }; + } + + if (process.env.SERVER_ACCESS_API_KEY === authToken) { + return { + uid: '', + exp: 4102444800, // Jan 1, 2100 at midnight + mid: '', + roles: SUPERADMIN_ROLES, + email_verified: true, + }; } + + const _decodedToken = await _auth.verifyIdToken(authToken, true); return _decodedToken; } catch (error) { throw FirebaseAuthError(error); @@ -39,32 +67,32 @@ const UserAuth = { * @description Parses the auth status * @function * - * @param {String} jwt + * @param req * @returns {NULL | Object | GraphQLError} */ getContext: async (req, _auth = admin?.auth()) => { try { - if (!req || !req.headers || !req.headers.authorization) { + if (!req || !req.headers || (!req.headers.authorization && !req.headers['x-api-key'])) { return { authToken: null, decodedToken: null, mid: null }; } - const jwt = decodeURI(req.headers.authorization); - if (!jwt) { + const authToken = decodeURI(req.headers.authorization ?? req.headers['x-api-key']); + if (!authToken) { return { authToken: null, decodedToken: null, mid: null }; } - if (UserSession.valid(req.session, jwt)) { + if (UserSession.valid(req.session, authToken)) { return { - authToken: req.session.auth.jwt, + authToken: req.session.auth.authToken, decodedToken: req.session.auth.decodedToken, mid: req.session.auth.mid, }; } - const _decodedToken = await UserAuth.authenticate(jwt, _auth); + const _decodedToken = await UserAuth.authenticate(authToken, _auth); if (!_decodedToken) { - return { authToken: req.headers.authorization, decodedToken: null, mid: null }; + return { authToken: req.headers.authorization ?? req.headers['x-api-key'], decodedToken: null, mid: null }; } const { uid, exp, roles, mid } = _decodedToken; @@ -73,7 +101,7 @@ const UserAuth = { req.session.auth = { uid, mid, - jwt: req.headers.authorization, + authToken: req.headers.authorization ?? req.headers['x-api-key'], exp, roles, decodedToken: _decodedToken, @@ -82,7 +110,7 @@ const UserAuth = { } return { - authToken: req.headers.authorization, + authToken: req.headers.authorization ?? req.headers['x-api-key'], decodedToken: _decodedToken, mid: _decodedToken.mid, }; diff --git a/server/utils/userAuth/permission.js b/server/utils/userAuth/permission.js index 93c57b6c..a6eea6ca 100644 --- a/server/utils/userAuth/permission.js +++ b/server/utils/userAuth/permission.js @@ -23,7 +23,7 @@ const UserPermission = { * @function * * @param {Object} session - * @param {String} jwt + * @param {String} authToken * @param {String} permission * @returns {Boolean | GraphQLError} */ diff --git a/server/utils/userAuth/session.js b/server/utils/userAuth/session.js index d981f88b..ddfb1a0a 100644 --- a/server/utils/userAuth/session.js +++ b/server/utils/userAuth/session.js @@ -8,17 +8,16 @@ const UserSession = { * @function * * @param {session.Session} session - * @param {String} jwt + * @param {String} authToken * @returns {Boolean} */ - valid: (session, jwt) => + valid: (session, authToken) => !session || - !jwt || + !authToken || !session.auth || - !session.auth.jwt || + !session.auth.authToken || !session.auth.exp || - !session.auth.uid || - session.auth.jwt !== jwt || + session.auth.authToken !== authToken || session.auth.exp <= Date.now() / 1000 ? false : true, @@ -29,18 +28,18 @@ const UserSession = { * @async * * @param {session.Session} session - * @param {String} jwt + * @param {String} authToken * @param {auth} _auth Firebase Authentication Library * @returns {Object | GraphQLError} decodedToken */ - start: async (session, jwt, _auth = admin?.auth()) => { + start: async (session, authToken, _auth = admin?.auth()) => { try { - const _decodedToken = await UserAuth.authenticate(jwt, _auth); + const _decodedToken = await UserAuth.authenticate(authToken, _auth); const { uid, exp, roles, mid } = _decodedToken; session.auth = { uid, mid, - jwt, + authToken, exp, roles, decodedToken: _decodedToken, @@ -60,12 +59,12 @@ const UserSession = { * @async * * @param {session.Session} session - * @param {String} jwt + * @param {String} authToken * @returns {NULL | GraphQLError} */ - end: async (session, jwt) => { + end: async (session, authToken) => { try { - if (UserSession.valid(session, jwt)) { + if (UserSession.valid(session, authToken)) { await session.destroy(); return true; } diff --git a/server/yarn.lock b/server/yarn.lock index eddc1b11..23e2798f 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -627,6 +627,11 @@ "@types/node" "*" "@types/webidl-conversions" "*" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -1212,7 +1217,7 @@ debug@4, debug@4.x, debug@^4.1.1, debug@^4.3.1, debug@~4.3.1: dependencies: ms "2.1.2" -debug@^3.2.6: +debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -1807,6 +1812,11 @@ hamming-distance@^1.0.0: resolved "https://registry.yarnpkg.com/hamming-distance/-/hamming-distance-1.0.0.tgz#39bfa46c61f39e87421e4035a1be4f725dd7b931" integrity sha512-hYz2IIKtyuZGfOqCs7skNiFEATf+v9IUNSOaQSr6Ll4JOxxWhOvXvc3mIdCW82Z3xW+zUoto7N/ssD4bDxAWoA== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -1894,6 +1904,11 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + imagekit@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/imagekit/-/imagekit-4.1.1.tgz#94525363e0aeadfe6658e0e1b173398292f18ec7" @@ -2745,6 +2760,11 @@ pseudomap@^1.0.1: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -2926,7 +2946,7 @@ semver@6.3.0, semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: +semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -2938,6 +2958,11 @@ semver@^7.2: dependencies: lru-cache "^6.0.0" +semver@~7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + semver@~7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.3.tgz#3641217233c6382173c76bf2c7ecd1e1c16b0d8a" @@ -3012,6 +3037,13 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +simple-update-notifier@^1.0.7: + version "1.1.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" + integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== + dependencies: + semver "~7.0.0" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -3146,6 +3178,13 @@ stubs@^3.0.0: resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -3191,6 +3230,13 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" @@ -3264,6 +3310,11 @@ uid-safe@~2.1.5: dependencies: random-bytes "~1.0.0" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"