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/env/.env.vault b/server/env/.env.vault new file mode 100644 index 00000000..c401293e --- /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=2 + +# ci +DOTENV_VAULT_CI="YkAJbDOB8HtOCbvJrDUk1MDLgbjRpBVgG72uIgHDM2LvN+5xC99Sjvo76bAa5GlMNAjTo9BPTtjSOhCJN+iQS4aLl1wiRfF2IjlSwm8yVhbgLU4wyQei4WpggGCDFC2CKJ9qq4F3tVV4w0m3plok4fR1upBZScnNyysgoT4nPTPvR1PGsNlyHmudj8Mf/0fl/5CmhYEgR2sTkmnlmtm+/4sp0ms2GRnjwT2VboZi0L/aRlNFNhLxbaMsNHeDXwBIRimCCcSeWX8l61Lx+vOUW3edVDDMIrTccyZqC6TNiAySxnrY6pRAotlZXhWgaCOmYSCm7qsPsUHBINcHIG3RCSUTFWOM9eF4Y88U/Drbw29VfmnK/8CMAEbMduYUWOM4SLwHAKxAay+NrEwJUkKtxfauFvRkZNfnkajDRxfPWIsgsDxCTr8gEGB3nQfNDykqYR7RctQnuI4cArvBAMyIT2m38y2Smyw4vSAU" +DOTENV_VAULT_CI_VERSION=2 + +# staging +DOTENV_VAULT_STAGING="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" +DOTENV_VAULT_STAGING_VERSION=3 + +# production +DOTENV_VAULT_PRODUCTION="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" +DOTENV_VAULT_PRODUCTION_VERSION=3 + +#/----------------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/schema/article/article.resolver.js b/server/schema/article/article.resolver.js index 9f8f2f69..ffee06c9 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.exists(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); } @@ -411,27 +348,17 @@ module.exports = { createArticle: async ( _parent, { articleType, title, authors, photographers, designers, tech, categories }, - { session, authToken, decodedToken, API: { Article } }, - { fieldNodes } + { session, authToken, decodedToken, API: { Article } } ) => { 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.', }); } + // TODO: add data validation and cleaning + const _article = await Article.create( articleType, title, @@ -558,6 +485,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 792d866d..acc3b67f 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']; + 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,15 +100,26 @@ module.exports = { _parent, { name, description, startDate, endDate, 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.new')) { throw APIError('FORBIDDEN', null, { reason: 'The user does not have the required permission to create a new issue.', }); } + // TODO: consider moving article validation to the resolver from datasource return Issue.create(name, description, startDate, endDate, articles, featured, session, authToken, mid); } catch (error) { throw APIError(null, error); @@ -89,9 +129,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.', @@ -107,9 +157,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.', @@ -123,7 +183,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.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 20a6ec5d..810ced92 100644 --- a/server/schema/media/media.resolver.js +++ b/server/schema/media/media.resolver.js @@ -23,12 +23,13 @@ module.exports = { { 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.', }); } + // 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.' }); @@ -40,26 +41,26 @@ module.exports = { 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.', - }); - } + // 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 _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); - } - }, + // const deleteMedia = await Media.deleteById(id, true); + // return deleteMedia; + // } catch (error) { + // throw APIError(null, error); + // } + // }, }; 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 3f5b48e1..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(); @@ -135,8 +187,7 @@ const create = async (uid, fullName, email, interestedTopics, session, authToken await admin.auth().setCustomUserClaims(uid, { mid: _user[0].id, - // TODO: add all standard roles here - roles: ['user.basic'], + roles: USER_BASE_ROLES, }); await mdbSession.commitTransaction(); @@ -179,8 +230,7 @@ const link = async (uid, id, interestedTopics, session, authToken, mid) => { await admin.auth().setCustomUserClaims(uid, { mid: id, - // TODO: add all standard roles here - roles: ['user.basic'], + roles: USER_VERIFIED_STUDENT_ROLES, }); await mdbSession.commitTransaction(); @@ -197,21 +247,21 @@ const link = async (uid, id, interestedTopics, session, authToken, mid) => { // 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}` }); - - return Promise.all([_updatedUser, _updatedFbUser]); -}; +// 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 { @@ -270,12 +320,15 @@ const setNITRVerified = async (id, accountType, email, nitrMail, session, authTo const _fbUser = await findFirebaseUserByEmail(nitrMail); await admin.auth().updateUser(_fbUser.uid, { email }); - // TODO: update all roles as required 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, { @@ -328,7 +381,7 @@ const setBan = async (id, flag, session, authToken, mid) => { const UserDataSources = () => ({ findByID: findByID(), findByEmail: findByEmail(), - getUserByOldUserName: getUserByOldUserName(), + findByOldUserName, findFirebaseUserById, findFirebaseUserByEmail, findOne, @@ -337,7 +390,7 @@ const UserDataSources = () => ({ search, create, link, - updateName, + // updateName, updateDetails, updateCustomClaims, setNITRVerified, diff --git a/server/schema/user/user.model.js b/server/schema/user/user.model.js index 22b13cc5..f280d8cd 100644 --- a/server/schema/user/user.model.js +++ b/server/schema/user/user.model.js @@ -175,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 e9494238..3291fd39 100644 --- a/server/schema/user/user.mutation.js +++ b/server/schema/user/user.mutation.js @@ -32,18 +32,18 @@ const { } = require('../scalars'); const UserType = require('./user.type'); -const FirebaseUserType = require('./firebaseUser.type'); +// const FirebaseUserType = require('./firebaseUser.type'); const { createUser, setUserBan, - updateUserName, + // updateUserName, updateUserProfilePicture, updateUserTopics, updateUserBio, addNITRMail, newsletterSubscription, setUserAccountType, - setUserRoles, + // setUserRoles, } = require('./user.resolver'); const { AccountTypeEnumType } = require('./user.enum.types'); @@ -59,15 +59,15 @@ 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, - }, + // 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: { @@ -135,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 1aad19b7..58cc2fbc 100644 --- a/server/schema/user/user.query.js +++ b/server/schema/user/user.query.js @@ -125,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', diff --git a/server/schema/user/user.resolver.js b/server/schema/user/user.resolver.js index 79bd17ef..3cd385d8 100644 --- a/server/schema/user/user.resolver.js +++ b/server/schema/user/user.resolver.js @@ -10,15 +10,11 @@ * @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'); const { AccountTypeEnumType } = require('./user.enum.types'); const getFieldNodes = require('../../utils/getFieldNodes'); -// const imagekit = require('../../config/imagekit'); const PUBLIC_FIELDS = [ 'id', @@ -29,38 +25,73 @@ const PUBLIC_FIELDS = [ 'accountType', 'nitrMail', 'picture', - 'pictureId', + 'profile', + 'isBanned', ]; const DEF_LIMIT = 10, 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; @@ -74,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); @@ -82,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) { @@ -105,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); - return user; + if (!_user) { + throw APIError('NOT_FOUND', null, { reason: 'The requested user does not exist.' }); + } + + canReadUser(_user, mid, session, authToken, decodedToken, fieldNodes); + + return _user; } catch (error) { throw APIError(null, error); } @@ -118,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) { @@ -148,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); } @@ -224,18 +222,17 @@ module.exports = { { mid, session, authToken, decodedToken, API: { User } } ) => { try { - if ( - !UserSession.valid(session, authToken) || - ((await User.findFirebaseUserByEmail(email)).uid !== decodedToken.uid && - !UserPermission.exists(session, authToken, decodedToken, 'user.write.all')) - ) { + // 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) { - 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.' }); } @@ -249,21 +246,20 @@ module.exports = { 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 }, @@ -271,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) { @@ -283,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); @@ -293,16 +287,16 @@ module.exports = { throw FirebaseAuthError(error); } }, - // TODO: rewrite function with data sources - // TODO: update all redundancies - // TODO: delete older picture + */ + // 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 } } + { mid, session, authToken, decodedToken, API: { User, Media } }, + { fieldNodes } ) => { try { - canUpdateUser(id, mid, session, authToken, decodedToken); + await canUpdateUser(id, mid, session, authToken, decodedToken, fieldNodes, User); const user = await User.findByID.load(id); @@ -327,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; @@ -342,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, @@ -397,7 +391,7 @@ module.exports = { throw APIError('BAD_REQUEST', null, { reason: 'The requested user has not been linked.' }); } - canUpdateUser(_user._id.toString(), mid, session, authToken, decodedToken, fieldNodes); + await canUpdateUser(_user._id.toString(), mid, session, authToken, decodedToken, fieldNodes, User); const _updatedUser = await User.setNITRVerified( _user._id, @@ -422,7 +416,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, { isNewsletterSubscribed: flag }, session, authToken, mid); @@ -444,6 +438,7 @@ module.exports = { { fieldNodes } ) => { try { + // TODO: use canReadUser instead of custom checks const _fields = getFieldNodes(fieldNodes); if (!UserPermission.exists(session, authToken, decodedToken, 'user.list.all')) { @@ -480,7 +475,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); @@ -500,7 +495,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); @@ -519,7 +514,7 @@ module.exports = { if ( mid !== firebaseUser.customClaims.mid && - !UserPermission.exists(session, authToken, decodedToken, 'user.read.all') + !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.', @@ -531,18 +526,19 @@ module.exports = { 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 bd3355c9..a3f4788a 100644 --- a/server/utils/userAuth/index.js +++ b/server/utils/userAuth/index.js @@ -2,6 +2,28 @@ 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 @@ -19,7 +41,7 @@ const UserAuth = { uid: '', exp: 4102444800, // Jan 1, 2100 at midnight mid: '', - roles: ['user.superadmin', 'article.admin', 'issue.admin', 'tag.admin', 'live.superadmin', 'media.admin'], + roles: SUPERADMIN_ROLES, email_verified: true, }; } @@ -29,7 +51,7 @@ const UserAuth = { uid: '', exp: 4102444800, // Jan 1, 2100 at midnight mid: '', - roles: ['user.superadmin', 'article.admin', 'issue.admin', 'tag.admin', 'live.superadmin', 'media.admin'], + roles: SUPERADMIN_ROLES, email_verified: true, }; } diff --git a/server/yarn.lock b/server/yarn.lock index 574642a4..1a290737 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -979,7 +979,7 @@ charm@~0.1.1: resolved "https://registry.yarnpkg.com/charm/-/charm-0.1.2.tgz#06c21eed1a1b06aeb67553cdc53e23274bac2296" integrity sha1-BsIe7RobBq62dVPNxT4jJ0usIpY= -chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.5.1, chokidar@^3.5.2: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -2332,7 +2332,7 @@ mime@^2.2.0: resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== -minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==