Skip to content
Snippets Groups Projects
Commit 3bcbeff7 authored by Alexandros Georgantas's avatar Alexandros Georgantas
Browse files

feat(models): graphql schema and resolvers added

parent 0e987dfc
No related branches found
No related tags found
2 merge requests!52Docx,!17Graphql api
Showing
with 973 additions and 0 deletions
/**
* Only used by jest, not part of the distributed package
*/
const { deferConfig } = require('config/defer')
const components = require('./components')
module.exports = {
'pubsweet-server': {
baseUrl: deferConfig(cfg => {
const { protocol, host, port } = cfg['pubsweet-server']
return `${protocol}://${host}${port ? `:${port}` : ''}`
}),
db: {
host: 'localhost',
port: '5432',
......@@ -13,6 +18,14 @@ module.exports = {
user: 'test_user',
password: 'password',
},
emailVerificationTokenExpiry: {
amount: 24,
unit: 'hours',
},
passwordResetTokenExpiry: {
amount: 24,
unit: 'hours',
},
},
pubsweet: {
components,
......
......@@ -41,6 +41,7 @@
"helmet": "^3.22.1",
"http-status-codes": "^1.4.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"morgan": "^1.10.0",
"node-cron": "^2.0.3",
"objection": "^2.2.15",
......
const config = require('config')
/*
Email with email verification token to new users
*/
const identityVerification = context => {
try {
const { verificationToken, email } = context
const link = `${
config.get('pubsweet-server.externalURL') ||
config.get('pubsweet-server.baseURL')
}/email-verification/${verificationToken}`
const content = `
<p>Thank you for signing up!</p>
<p>Click on <a href="${link}">this link</a> to verify your account.</p>
`
const data = {
content,
subject: 'Account Verification',
to: email,
}
return data
} catch (e) {
throw new Error(e)
}
}
const passwordUpdate = context => {
const { email } = context
const content = `
<p>
Your password has been successfully updated.
<br/>
If you did not initiate this change, please contact your system administrator.
</p>
`
const data = {
subject: 'Password changed',
content,
to: email,
}
return data
}
const requestResetPasswordEmailNotFound = context => {
const { email } = context
const content = `
<p>
You or someone else tried to change the password of an account using this
email address.
<br/>
The requested change failed, as this email does not exist in our database.
<br/>
If this action was performed by you, please use the email address that
you have connected with your account.
</p>
`
const data = {
content,
subject: 'Account access attempted',
to: email,
}
return data
}
const requestResetPassword = context => {
const { email, token } = context
const pathToPage = config.has('password-reset.pathToPage')
? config.get('password-reset.pathToPage')
: '/password-reset'
const link = `${
config.get('pubsweet-server.externalURL') ||
config.get('pubsweet-server.baseURL')
}${pathToPage}/${token}`
const content = `
<p>
Follow the link below to reset your password in the microPublication
platform.
<br/>
This link will be valid for 24 hours.
</p>
<p>
<a href="${link}">Reset your password</a>
</p>
`
const data = {
subject: 'Password reset',
content,
to: email,
}
return data
}
module.exports = {
identityVerification,
requestResetPasswordEmailNotFound,
requestResetPassword,
passwordUpdate,
}
const cleanUndefined = object =>
Object.keys(object)
.filter(k => object[k] !== undefined)
.reduce((acc, k) => {
acc[k] = object[k]
return acc
}, {})
module.exports = { cleanUndefined }
type ChatMessage {
id: ID!
chatThreadId: ID!
content: String!
timestamp: String!
isDeleted: Boolean!
mentions: [String!]!
user: User!
}
input SendChatMessageInput {
content: String!
chatThreadId: ID!
userId: ID!
mentions: [String!]
}
input EditChatMessageInput {
id: ID!
content: String!
mentions: [String!]
}
type ChatThread {
id: ID!
created: DateTime!
updated: DateTime!
chatType: String!
relatedObjectId: ID!
messages: [ChatMessage!]!
}
input CreateChatThreadInput {
chatType: String!
relatedObjectId: ID!
}
extend type Mutation {
createChatThread(input: CreateInput!): ChatThread!
deleteChatThread(id: ID!): ID!
sendMessage(input: SendChatMessageInput!): ChatMessage!
editMessage(input: EditChatMessageInput!): ChatMessage!
deleteMessage(id: ID!): ChatMessage!
}
module.exports = {
labels: {
IDENTITY_LOADER: '[IDENTITY LOADER] -',
IDENTITY_CONTROLLER: '[IDENTITY CONTROLLER] -',
IDENTITY_RESOLVER: '[IDENTITY RESOLVER] -',
},
subscriptions: {
// identifiers
},
}
type Identity {
id: ID!
created: DateTime!
updated: DateTime
email: String!
isDefault: Boolean!
isSocial: Boolean!
isVerified: Boolean!
provider: String
}
const { logger } = require('@pubsweet/logger')
const { Identity } = require('../index')
const {
labels: { IDENTITY_LOADER },
} = require('./constants')
const identitiesBasedOnUserIdsLoader = async userIds => {
try {
const userIdentities = await Identity.query().whereIn('userId', userIds)
return userIds.map(userId =>
userIdentities.find(identity => identity.userId === userId),
)
} catch (e) {
logger.error(
`${IDENTITY_LOADER} identitiesBasedOnUserIdsLoader: ${e.message}`,
)
throw new Error(e)
}
}
const defaultIdentityBasedOnUserIdsLoader = async userIds => {
try {
const userIdentities = await Identity.query()
.whereIn('userId', userIds)
.andWhere({ isDefault: true })
return userIds.map(userId =>
userIdentities.find(identity => identity.userId === userId),
)
} catch (e) {
logger.error(
`${IDENTITY_LOADER} defaultIdentityBasedOnUserIdsLoader: ${e.message}`,
)
throw new Error(e)
}
}
module.exports = {
identitiesBasedOnUserIdsLoader,
defaultIdentityBasedOnUserIdsLoader,
}
const model = require('./identity.model')
const {
identitiesBasedOnUserIdsLoader,
defaultIdentityBasedOnUserIdsLoader,
} = require('./identity.loaders')
module.exports = {
model,
modelName: 'Identity',
modelLoaders: {
identitiesBasedOnUserIdsLoader,
defaultIdentityBasedOnUserIdsLoader,
},
}
type Team {
id: ID!
role: String!
displayName: String!
objectId: ID
objectType: String
members: [TeamMember!]
global: Boolean!
}
input TeamInput {
role: String!
displayName: String!
objectId: ID
objectType: String
members: [TeamMemberInput]
global: Boolean!
}
input TeamWhereInput {
role: String
objectId: ID
objectType: String
global: Boolean
}
input UpdateTeamMembershipInput {
teams: [UpdateTeamMembershipTeamInput!]!
}
input UpdateTeamMembershipTeamInput {
teamId: ID!
members: [ID!]!
}
extend type Query {
team(id: ID!): Team!
teams(where: TeamWhereInput!): [Team!]!
getGlobalTeams: [Team!]!
getObjectTeams(objectId: ID!, objectType: String!): [Team!]!
}
extend type Mutation {
createTeam(input: TeamInput!): Team!
updateTeam(id: ID!, input: TeamInput!): Team!
updateGlobalTeamMembership(input: UpdateTeamMembershipInput!): Boolean!
updateObjectTeamMembership(input: UpdateTeamMembershipInput!): Boolean!
deleteTeam(id: ID!): ID!
}
type TeamMember {
id: ID
user: User
status: String
}
input TeamMemberInput {
id: ID
user: TeamMemberUserInput
status: String
}
input TeamMemberUserInput {
id: ID!
}
module.exports = {
labels: {
USER_LOADER: '[USER LOADER] -',
USER_CONTROLLER: '[USER CONTROLLER] -',
USER_RESOLVER: '[USER RESOLVER] -',
},
subscriptions: {
// identifiers
},
}
const config = require('config')
const crypto = require('crypto')
const moment = require('moment')
const { AuthorizationError, ValidationError } = require('@pubsweet/errors')
const { User } = require('../index')
const { logger, createJWT, useTransaction } = require('../../index')
const Identity = require('../identity/identity.model')
const { cleanUndefined } = require('../_helpers/utilities')
const {
identityVerification,
passwordUpdate,
requestResetPassword,
requestResetPasswordEmailNotFound,
} = require('../_helpers/emailTemplates')
const { sendEmail } = require('../../services/email')
const { labels: USER_CONTROLLER } = require('./constants')
const getUser = async (id, options = {}) => {
try {
logger.info(`${USER_CONTROLLER} getUser: fetching user with id ${id}`)
return User.findById(id, options)
} catch (e) {
logger.error(`${USER_CONTROLLER} getUser: ${e.message}`)
throw new Error(e)
}
}
const getUsers = async (options = {}) => {
try {
logger.info(
`${USER_CONTROLLER} getUsers: fetching all users based on provided options ${options}`,
)
return User.find({}, options)
} catch (e) {
logger.error(`${USER_CONTROLLER} getUsers: ${e.message}`)
throw new Error(e)
}
}
const deleteUser = async (id, options = {}) => {
try {
logger.info(`${USER_CONTROLLER} deleteUser: removing user with id ${id}`)
return User.deleteById(id, options)
} catch (e) {
logger.error(`${USER_CONTROLLER} deleteUser: ${e.message}`)
throw new Error(e)
}
}
const deactivateUser = async (id, options = {}) => {
try {
logger.info(
`${USER_CONTROLLER} deactivateUser: deactivating user with id ${id}`,
)
return User.patchAndFetchById(id, { isActive: false })
} catch (e) {
logger.error(`${USER_CONTROLLER} deactivateUser: ${e.message}`)
throw new Error(e)
}
}
const updateUser = async (id, data, options = {}) => {
try {
const cleanedData = cleanUndefined(data)
const { email, identityId, ...restData } = cleanedData
const { trx, ...restOptions } = options
logger.info(`${USER_CONTROLLER} updateUser: updating user with id ${id}`)
return useTransaction(
async tr => {
if (!email) {
return User.patchAndFetchById(
id,
{ ...restData },
{
trx: tr,
...restOptions,
},
)
}
logger.info(
`${USER_CONTROLLER} updateUser: updating user identity with provided email`,
)
if (!identityId) {
throw new Error(
`${USER_CONTROLLER} updateUser: cannot update email without identity id`,
)
}
await Identity.patchAndFetchById(identityId, { email }, { trx: tr })
if (Object.keys(restData).length !== 0) {
logger.info(
`${USER_CONTROLLER} updateUser: updating user with provided info`,
)
return User.patchAndFetchById(id, ...restData, {
trx: tr,
...restOptions,
})
}
return User.findById(id, {
trx: tr,
...restOptions,
})
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} updateUser: ${e.message}`)
throw new Error(e)
}
}
const login = async (password, email = undefined, username = undefined) => {
try {
let isValid = false
let user
if (!username) {
logger.info(
`${USER_CONTROLLER} login: searching for user with email ${email}`,
)
const identity = await Identity.findOne({ email })
user = User.findById(identity.userId)
} else {
logger.info(
`${USER_CONTROLLER} login: searching for user with username ${username}`,
)
user = await User.findOne({ username })
}
logger.info(
`${USER_CONTROLLER} login: checking password validity for user with id ${user.id}`,
)
isValid = await user.validPassword(password)
if (!isValid) {
throw new AuthorizationError('Wrong username or password.')
}
logger.info(`${USER_CONTROLLER} login: password is valid`)
return {
user,
token: createJWT(user),
}
} catch (e) {
logger.error(`${USER_CONTROLLER} login: ${e.message}`)
throw new Error(e)
}
}
const signUp = async (data, options = {}) => {
try {
const cleanedData = cleanUndefined(data)
const { email, ...restData } = cleanedData
const { trx } = options
return useTransaction(
async tr => {
if (!email) {
logger.info(`${USER_CONTROLLER} signUp: creating user`)
return User.insert({ ...restData }, { trx: tr })
}
logger.info(`${USER_CONTROLLER} signUp: creating user`)
const user = await User.insert({ ...restData }, { trx: tr })
logger.info(
`${USER_CONTROLLER} signUp: creating user local identity with provided email`,
)
await Identity.insert(
{ userId: user.id, email, isSocial: false },
{ trx: tr },
)
return user
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} signUp: ${e.message}`)
throw new Error(e)
}
}
const verifyEmail = async (token, options = {}) => {
try {
const { trx } = options
logger.info(`${USER_CONTROLLER} verifyEmail: verifying user email`)
return useTransaction(
async tr => {
const identity = await Identity.findOne(
{
verificationToken: token,
},
{ trx: tr },
)
const emailVerificationExpiryAmount = config.get(
'pubsweet-server.emailVerificationTokenExpiry.amount',
)
const emailVerificationExpiryUnit = config.get(
'pubsweet-server.emailVerificationTokenExpiry.unit',
)
if (!identity)
throw new Error(`${USER_CONTROLLER} verifyEmail: invalid token`)
if (identity.isVerified)
throw new Error(
`${USER_CONTROLLER} verifyEmail: identity has already been confirmed`,
)
if (!identity.verificationTokenTimestamp) {
throw new Error(
`${USER_CONTROLLER} verifyEmail: confirmation token does not have a corresponding timestamp`,
)
}
if (
moment()
.subtract(
emailVerificationExpiryAmount,
emailVerificationExpiryUnit,
)
.isAfter(identity.verificationTokenTimestamp)
) {
throw new Error(`${USER_CONTROLLER} verifyEmail: Token expired`)
}
await Identity.patch(
{ column: 'id', value: identity.id },
{
isVerified: true,
},
{ trx: tr },
)
return true
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} verifyEmail: ${e.message}`)
throw new Error(e)
}
}
const resendVerificationEmail = async (token, options = {}) => {
try {
const { trx } = options
logger.info(
`${USER_CONTROLLER} resendVerificationEmail: resending verification email to user`,
)
return useTransaction(
async tr => {
const identity = await Identity.findOne(
{
verificationToken: token,
},
{ trx: tr },
)
if (!identity)
throw new Error(
`${USER_CONTROLLER} resendVerificationEmail: Token does not correspond to an identity`,
)
const verificationToken = crypto.randomBytes(64).toString('hex')
const verificationTokenTimestamp = new Date()
await Identity.patch(
{ column: 'id', value: identity.id },
{
verificationToken,
verificationTokenTimestamp,
},
{ trx: tr },
)
const emailData = identityVerification({
verificationToken,
email: identity.email,
})
sendEmail(emailData)
return true
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} resendVerificationEmail: ${e.message}`)
throw new Error(e)
}
}
const resendVerificationEmailFromLogin = async (username, options = {}) => {
try {
const { trx } = options
logger.info(
`${USER_CONTROLLER} resendVerificationEmailFromLogin: resending verification email based on form`,
)
return useTransaction(
async tr => {
const user = await User.findOne({ username }, { trx: tr })
if (!user)
throw new Error(
`${USER_CONTROLLER} resendVerificationEmailFromLogin: no user with username ${username} found`,
)
const identity = await Identity.findOne(
{
isDefault: true,
userId: user.id,
},
{ trx: tr },
)
if (!identity)
throw new Error(
`${USER_CONTROLLER} resendVerificationEmailFromLogin: no default identity found for user with id ${user.id}`,
)
const verificationToken = crypto.randomBytes(64).toString('hex')
const verificationTokenTimestamp = new Date()
await Identity.patch(
{ column: 'id', value: identity.id },
{
verificationToken,
verificationTokenTimestamp,
},
{ trx: tr },
)
const emailData = identityVerification({
verificationToken,
email: identity.email,
})
sendEmail(emailData)
return true
},
{ trx },
)
} catch (e) {
logger.error(
`${USER_CONTROLLER} resendVerificationEmailFromLogin: ${e.message}`,
)
throw new Error(e)
}
}
const updatePassword = async (id, currentPassword, newPassword) => {
try {
logger.info(`${USER_CONTROLLER} updatePassword: updating user password`)
await User.updatePassword(id, currentPassword, newPassword)
const identity = await Identity.findOne({ isDefault: true, userId: id })
const emailData = passwordUpdate({
email: identity.email,
})
sendEmail(emailData)
return true
} catch (e) {
logger.error(`${USER_CONTROLLER} updatePassword: ${e.message}`)
throw new Error(e)
}
}
const sendPasswordResetEmail = async (email, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
const tokenLength = config.has('password-reset.token-length')
? config.get('password-reset.token-length')
: 32
const identity = await Identity.findOne(
{
isDefault: true,
email: email.toLowerCase(),
},
{ trx: tr },
)
if (!identity) {
const emailData = requestResetPasswordEmailNotFound({
email: email.toLowerCase(),
})
sendEmail(emailData)
return true
}
const user = await User.findById(identity.userId, { trx: tr })
const resetToken = crypto.randomBytes(tokenLength).toString('hex')
await User.patch(
{ column: 'id', value: user.id },
{
passwordResetTimestamp: new Date(),
passwordResetToken: resetToken,
},
{ trx: tr },
)
logger.info(
`${USER_CONTROLLER} sendPasswordResetEmail: sending password reset email`,
)
const emailData = requestResetPassword({
email: email.toLowerCase(),
token: resetToken,
})
sendEmail(emailData)
return true
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} sendPasswordResetEmail: ${e.message}`)
throw new Error(e)
}
}
const resetPassword = async (token, password, options = {}) => {
try {
const { trx } = options
logger.info(`${USER_CONTROLLER} resetPassword: resetting user password`)
return useTransaction(
async tr => {
const user = await User.findOne(
{ passwordResetToken: token },
{ trx: tr, related: 'defaultIdentity' },
)
if (!user) {
throw new Error(
`${USER_CONTROLLER} resetPassword: no user found with token ${token}`,
)
}
const passwordResetTokenExpiryAmount = config.get(
'pubsweet-server.passwordResetTokenTokenExpiry.amount',
)
const passwordResetTokenExpiryUnit = config.get(
'pubsweet-server.passwordResetTokenTokenExpiry.unit',
)
if (
moment()
.subtract(
passwordResetTokenExpiryAmount,
passwordResetTokenExpiryUnit,
)
.isAfter(user.passwordResetTimestamp)
) {
throw new ValidationError(
`${USER_CONTROLLER} resetPassword: your token has expired`,
)
}
await User.patch(
{ column: 'id', value: user.id },
{
password,
passwordResetTimestamp: null,
passwordResetToken: null,
},
{ trx: tr },
)
const emailData = passwordUpdate({
email: user.defaultIdentity.email,
})
sendEmail(emailData)
return true
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} resetPassword: ${e.message}`)
throw new Error(e)
}
}
module.exports = {
getUser,
getUsers,
deleteUser,
deactivateUser,
updateUser,
login,
signUp,
verifyEmail,
resendVerificationEmail,
resendVerificationEmailFromLogin,
updatePassword,
sendPasswordResetEmail,
resetPassword,
}
scalar DateTime
type User {
id: ID!
created: DateTime!
updated: DateTime!
email: String!
username: String
surname: String
givenNames: String
displayName: String
agreedTc: Boolean!
isActive: Boolean!
identities: [Identity!]!
defaultIdentity: Identity!
titlePre: String
titlePost: String
}
type LoginResult {
user: User!
token: String!
}
input SignUpInput {
username: String!
email: String!
password: String!
givenNames: String!
surname: String!
agreedTc: Boolean!
titlePre: String
titlePost: String
}
input LoginInput {
email: String
username: String
password: String!
}
input UpdateInput {
email: String
identityId: ID
username: String
surname: String
givenNames: String
agreedTc: Boolean
titlePre: String
titlePost: String
}
input UpdatePasswordInput {
currentPassword: String!
newPassword: String!
}
extend type Query {
user(id: ID): User
users: [User]!
currentUser: User!
}
extend type Mutation {
deleteUser(id: ID!): ID!
deactivateUser(id: ID!): User!
updateUser(id: ID, input: UpdateInput!): User!
login(input: LoginInput!): LoginResult!
signUp(input: SignUpInput!): User!
verifyEmail(token: String!): Boolean!
resendVerificationEmail(token: String!): Boolean!
resendVerificationEmailFromLogin(
username: String!
password: String!
): Boolean!
updatePassword(input: UpdatePasswordInput!): Boolean!
sendPasswordResetEmail(email: String!): Boolean!
resetPassword(token: String!, password: String!): Boolean!
}
module.exports = {
labels: {
EMAIL_SERVICE: '[EMAIL SERVICE] -',
},
}
const config = require('config')
const mailer = require('@pubsweet/component-send-email')
const { logger } = require('../index')
const {
labels: { EMAIL_SERVICE },
} = require('./constants')
const sendEmail = data => {
const { content, subject, to } = data
const emailData = {
from: config.get('mailer.from'),
html: `<p>${content}</p>`,
subject: `${subject}`,
text: content,
to,
}
mailer.send(emailData)
logger.info(
`${EMAIL_SERVICE} sendEmail: email sent to ${to} with subject ${subject}`,
)
}
module.exports = { sendEmail }
......@@ -8163,6 +8163,11 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
 
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
morgan@^1.10.0, morgan@^1.8.2:
version "1.10.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment