mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 844-Comments-cannot-be-expanded-if-they-are-too-long
This commit is contained in:
commit
a678f05574
@ -5,6 +5,11 @@ GRAPHQL_PORT=4000
|
|||||||
GRAPHQL_URI=http://localhost:4000
|
GRAPHQL_URI=http://localhost:4000
|
||||||
CLIENT_URI=http://localhost:3000
|
CLIENT_URI=http://localhost:3000
|
||||||
MOCKS=false
|
MOCKS=false
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_IGNORE_TLS=true
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
JWT_SECRET="b/&&7b78BF&fv/Vd"
|
JWT_SECRET="b/&&7b78BF&fv/Vd"
|
||||||
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
|
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
|
||||||
|
|||||||
@ -44,6 +44,9 @@ or start the backend in production environment with:
|
|||||||
yarn run start
|
yarn run start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
|
||||||
|
your `.env` configuration file.
|
||||||
|
|
||||||
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
|
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
|
||||||
This will start the GraphQL service \(by default on localhost:4000\) where you
|
This will start the GraphQL service \(by default on localhost:4000\) where you
|
||||||
can issue GraphQL requests or access GraphQL Playground in the browser.
|
can issue GraphQL requests or access GraphQL Playground in the browser.
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
"apollo-client": "~2.6.3",
|
"apollo-client": "~2.6.3",
|
||||||
"apollo-link-context": "~1.0.18",
|
"apollo-link-context": "~1.0.18",
|
||||||
"apollo-link-http": "~1.5.15",
|
"apollo-link-http": "~1.5.15",
|
||||||
"apollo-server": "~2.6.3",
|
"apollo-server": "~2.6.4",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
@ -61,7 +61,7 @@
|
|||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.2",
|
"graphql-middleware": "~3.0.2",
|
||||||
"graphql-shield": "~5.6.1",
|
"graphql-shield": "~5.7.1",
|
||||||
"graphql-tag": "~2.10.1",
|
"graphql-tag": "~2.10.1",
|
||||||
"graphql-yoga": "~1.18.0",
|
"graphql-yoga": "~1.18.0",
|
||||||
"helmet": "~3.18.0",
|
"helmet": "~3.18.0",
|
||||||
@ -72,6 +72,7 @@
|
|||||||
"neo4j-driver": "~1.7.4",
|
"neo4j-driver": "~1.7.4",
|
||||||
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
|
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
|
||||||
"node-fetch": "~2.6.0",
|
"node-fetch": "~2.6.0",
|
||||||
|
"nodemailer": "^6.2.1",
|
||||||
"npm-run-all": "~4.1.5",
|
"npm-run-all": "~4.1.5",
|
||||||
"request": "~2.88.0",
|
"request": "~2.88.0",
|
||||||
"sanitize-html": "~1.20.1",
|
"sanitize-html": "~1.20.1",
|
||||||
|
|||||||
@ -2,23 +2,33 @@ import dotenv from 'dotenv'
|
|||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
export const requiredConfigs = {
|
const {
|
||||||
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
|
MAPBOX_TOKEN,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET,
|
||||||
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
|
PRIVATE_KEY_PASSPHRASE,
|
||||||
}
|
SMTP_IGNORE_TLS = true,
|
||||||
|
SMTP_HOST,
|
||||||
|
SMTP_PORT,
|
||||||
|
SMTP_USERNAME,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
NEO4J_URI = 'bolt://localhost:7687',
|
||||||
|
NEO4J_USERNAME = 'neo4j',
|
||||||
|
NEO4J_PASSWORD = 'neo4j',
|
||||||
|
GRAPHQL_PORT = 4000,
|
||||||
|
CLIENT_URI = 'http://localhost:3000',
|
||||||
|
GRAPHQL_URI = 'http://localhost:4000',
|
||||||
|
} = process.env
|
||||||
|
|
||||||
export const neo4jConfigs = {
|
export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
|
||||||
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
|
export const smtpConfigs = {
|
||||||
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
|
SMTP_HOST,
|
||||||
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
|
SMTP_PORT,
|
||||||
}
|
SMTP_IGNORE_TLS,
|
||||||
|
SMTP_USERNAME,
|
||||||
export const serverConfigs = {
|
SMTP_PASSWORD,
|
||||||
GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
|
|
||||||
CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
|
|
||||||
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
|
||||||
}
|
}
|
||||||
|
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
|
||||||
|
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
|
||||||
|
|
||||||
export const developmentConfigs = {
|
export const developmentConfigs = {
|
||||||
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
|
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
|
||||||
@ -29,6 +39,7 @@ export const developmentConfigs = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
...requiredConfigs,
|
...requiredConfigs,
|
||||||
|
...smtpConfigs,
|
||||||
...neo4jConfigs,
|
...neo4jConfigs,
|
||||||
...serverConfigs,
|
...serverConfigs,
|
||||||
...developmentConfigs,
|
...developmentConfigs,
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
const legacyUrls = [
|
|
||||||
'https://api-alpha.human-connection.org',
|
|
||||||
'https://staging-api.human-connection.org',
|
|
||||||
'http://localhost:3000',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const fixUrl = url => {
|
|
||||||
legacyUrls.forEach(legacyUrl => {
|
|
||||||
url = url.replace(legacyUrl, '')
|
|
||||||
})
|
|
||||||
if (!url.startsWith('/')) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkUrl = thing => {
|
|
||||||
return (
|
|
||||||
thing &&
|
|
||||||
typeof thing === 'string' &&
|
|
||||||
legacyUrls.find(legacyUrl => {
|
|
||||||
return thing.indexOf(legacyUrl) === 0
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fixImageURLs = (result, recursive) => {
|
|
||||||
if (checkUrl(result)) {
|
|
||||||
result = fixUrl(result)
|
|
||||||
} else if (result && Array.isArray(result)) {
|
|
||||||
result.forEach((res, index) => {
|
|
||||||
result[index] = fixImageURLs(result[index], true)
|
|
||||||
})
|
|
||||||
} else if (result && typeof result === 'object') {
|
|
||||||
Object.keys(result).forEach(key => {
|
|
||||||
result[key] = fixImageURLs(result[key], true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Mutation: async (resolve, root, args, context, info) => {
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return fixImageURLs(result)
|
|
||||||
},
|
|
||||||
Query: async (resolve, root, args, context, info) => {
|
|
||||||
let result = await resolve(root, args, context, info)
|
|
||||||
return fixImageURLs(result)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { fixImageURLs } from './fixImageUrlsMiddleware'
|
|
||||||
|
|
||||||
describe('fixImageURLs', () => {
|
|
||||||
describe('edge case: image url is exact match of legacy url', () => {
|
|
||||||
it('replaces it with `/`', () => {
|
|
||||||
const url = 'https://api-alpha.human-connection.org'
|
|
||||||
expect(fixImageURLs(url)).toEqual('/')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('image url of legacy alpha', () => {
|
|
||||||
it('removes domain', () => {
|
|
||||||
const url =
|
|
||||||
'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png'
|
|
||||||
expect(fixImageURLs(url)).toEqual(
|
|
||||||
'/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('image url of legacy staging', () => {
|
|
||||||
it('removes domain', () => {
|
|
||||||
const url =
|
|
||||||
'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg'
|
|
||||||
expect(fixImageURLs(url)).toEqual(
|
|
||||||
'/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('object', () => {
|
|
||||||
it('returns untouched', () => {
|
|
||||||
const object = { some: 'thing' }
|
|
||||||
expect(fixImageURLs(object)).toEqual(object)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('some string', () => {
|
|
||||||
it('returns untouched', () => {})
|
|
||||||
const string = "Yeah I'm a String"
|
|
||||||
expect(fixImageURLs(string)).toEqual(string)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -3,7 +3,6 @@ import activityPub from './activityPubMiddleware'
|
|||||||
import password from './passwordMiddleware'
|
import password from './passwordMiddleware'
|
||||||
import softDelete from './softDeleteMiddleware'
|
import softDelete from './softDeleteMiddleware'
|
||||||
import sluggify from './sluggifyMiddleware'
|
import sluggify from './sluggifyMiddleware'
|
||||||
import fixImageUrls from './fixImageUrlsMiddleware'
|
|
||||||
import excerpt from './excerptMiddleware'
|
import excerpt from './excerptMiddleware'
|
||||||
import dateTime from './dateTimeMiddleware'
|
import dateTime from './dateTimeMiddleware'
|
||||||
import xss from './xssMiddleware'
|
import xss from './xssMiddleware'
|
||||||
@ -25,7 +24,6 @@ export default schema => {
|
|||||||
excerpt: excerpt,
|
excerpt: excerpt,
|
||||||
notifications: notifications,
|
notifications: notifications,
|
||||||
xss: xss,
|
xss: xss,
|
||||||
fixImageUrls: fixImageUrls,
|
|
||||||
softDelete: softDelete,
|
softDelete: softDelete,
|
||||||
user: user,
|
user: user,
|
||||||
includedFields: includedFields,
|
includedFields: includedFields,
|
||||||
@ -42,7 +40,6 @@ export default schema => {
|
|||||||
'excerpt',
|
'excerpt',
|
||||||
'notifications',
|
'notifications',
|
||||||
'xss',
|
'xss',
|
||||||
'fixImageUrls',
|
|
||||||
'softDelete',
|
'softDelete',
|
||||||
'user',
|
'user',
|
||||||
'includedFields',
|
'includedFields',
|
||||||
|
|||||||
@ -147,6 +147,8 @@ const permissions = shield(
|
|||||||
CreateComment: isAuthenticated,
|
CreateComment: isAuthenticated,
|
||||||
DeleteComment: isAuthor,
|
DeleteComment: isAuthor,
|
||||||
DeleteUser: isDeletingOwnAccount,
|
DeleteUser: isDeletingOwnAccount,
|
||||||
|
requestPasswordReset: allow,
|
||||||
|
resetPassword: allow,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
72
backend/src/schema/resolvers/passwordReset.js
Normal file
72
backend/src/schema/resolvers/passwordReset.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import CONFIG from '../../config'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
|
||||||
|
|
||||||
|
const transporter = () => {
|
||||||
|
const configs = {
|
||||||
|
host: CONFIG.SMTP_HOST,
|
||||||
|
port: CONFIG.SMTP_PORT,
|
||||||
|
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
|
||||||
|
secure: false, // true for 465, false for other ports
|
||||||
|
}
|
||||||
|
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
|
||||||
|
if (user && pass) {
|
||||||
|
configs.auth = { user, pass }
|
||||||
|
}
|
||||||
|
return nodemailer.createTransport(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPasswordReset(options) {
|
||||||
|
const { driver, code, email, issuedAt = new Date() } = options
|
||||||
|
const session = driver.session()
|
||||||
|
const cypher = `
|
||||||
|
MATCH (u:User) WHERE u.email = $email
|
||||||
|
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||||
|
MERGE (u)-[:REQUESTED]->(pr)
|
||||||
|
RETURN u
|
||||||
|
`
|
||||||
|
const transactionRes = await session.run(cypher, {
|
||||||
|
issuedAt: issuedAt.toISOString(),
|
||||||
|
code,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
const users = transactionRes.records.map(record => record.get('u'))
|
||||||
|
session.close()
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Mutation: {
|
||||||
|
requestPasswordReset: async (_, { email }, { driver }) => {
|
||||||
|
const code = uuid().substring(0, 6)
|
||||||
|
const [user] = await createPasswordReset({ driver, code, email })
|
||||||
|
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
|
||||||
|
const name = (user && user.name) || ''
|
||||||
|
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
|
||||||
|
await transporter().sendMail(mailTemplate({ email, code, name }))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
resetPassword: async (_, { email, code, newPassword }, { driver }) => {
|
||||||
|
const session = driver.session()
|
||||||
|
const stillValid = new Date()
|
||||||
|
stillValid.setDate(stillValid.getDate() - 1)
|
||||||
|
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
|
||||||
|
const cypher = `
|
||||||
|
MATCH (pr:PasswordReset {code: $code})
|
||||||
|
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
|
||||||
|
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||||
|
SET pr.usedAt = datetime()
|
||||||
|
SET u.password = $newHashedPassword
|
||||||
|
RETURN pr
|
||||||
|
`
|
||||||
|
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
|
||||||
|
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||||
|
const result = !!(reset && reset.properties.usedAt)
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
180
backend/src/schema/resolvers/passwordReset.spec.js
Normal file
180
backend/src/schema/resolvers/passwordReset.spec.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
import Factory from '../../seed/factories'
|
||||||
|
import { host } from '../../jest/helpers'
|
||||||
|
import { getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import { createPasswordReset } from './passwordReset'
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
let client
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
const getAllPasswordResets = async () => {
|
||||||
|
const session = driver.session()
|
||||||
|
let transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||||
|
const resets = transactionRes.records.map(record => record.get('r'))
|
||||||
|
session.close()
|
||||||
|
return resets
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('passwordReset', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
client = new GraphQLClient(host)
|
||||||
|
await factory.create('User', {
|
||||||
|
email: 'user@example.org',
|
||||||
|
role: 'user',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('requestPasswordReset', () => {
|
||||||
|
const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }`
|
||||||
|
|
||||||
|
describe('with invalid email', () => {
|
||||||
|
const variables = { email: 'non-existent@example.org' }
|
||||||
|
|
||||||
|
it('resolves anyways', async () => {
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({
|
||||||
|
requestPasswordReset: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates no node', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const resets = await getAllPasswordResets()
|
||||||
|
expect(resets).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a valid email', () => {
|
||||||
|
const variables = { email: 'user@example.org' }
|
||||||
|
|
||||||
|
it('resolves', async () => {
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({
|
||||||
|
requestPasswordReset: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates node with label `PasswordReset`', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const resets = await getAllPasswordResets()
|
||||||
|
expect(resets).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a reset code', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const resets = await getAllPasswordResets()
|
||||||
|
const [reset] = resets
|
||||||
|
const { code } = reset.properties
|
||||||
|
expect(code).toHaveLength(6)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetPassword', () => {
|
||||||
|
const setup = async (options = {}) => {
|
||||||
|
const { email = 'user@example.org', issuedAt = new Date(), code = 'abcdef' } = options
|
||||||
|
|
||||||
|
const session = driver.session()
|
||||||
|
await createPasswordReset({ driver, email, issuedAt, code })
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }`
|
||||||
|
let email = 'user@example.org'
|
||||||
|
let code = 'abcdef'
|
||||||
|
let newPassword = 'supersecret'
|
||||||
|
let variables
|
||||||
|
|
||||||
|
describe('invalid email', () => {
|
||||||
|
it('resolves to false', async () => {
|
||||||
|
await setup()
|
||||||
|
variables = { newPassword, email: 'non-existent@example.org', code }
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('valid email', () => {
|
||||||
|
describe('but invalid code', () => {
|
||||||
|
it('resolves to false', async () => {
|
||||||
|
await setup()
|
||||||
|
variables = { newPassword, email, code: 'slkdjf' }
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({
|
||||||
|
resetPassword: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and valid code', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = {
|
||||||
|
newPassword,
|
||||||
|
email: 'user@example.org',
|
||||||
|
code: 'abcdef',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and code not expired', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setup()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves to true', async () => {
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({
|
||||||
|
resetPassword: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates PasswordReset `usedAt` property', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const requests = await getAllPasswordResets()
|
||||||
|
const [request] = requests
|
||||||
|
const { usedAt } = request.properties
|
||||||
|
expect(usedAt).not.toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates password of the user', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const checkLoginMutation = `
|
||||||
|
mutation($email: String!, $password: String!) {
|
||||||
|
login(email: $email, password: $password)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const expected = expect.objectContaining({ login: expect.any(String) })
|
||||||
|
await expect(
|
||||||
|
client.request(checkLoginMutation, {
|
||||||
|
email: 'user@example.org',
|
||||||
|
password: 'supersecret',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but expired code', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const issuedAt = new Date()
|
||||||
|
issuedAt.setDate(issuedAt.getDate() - 1)
|
||||||
|
await setup({ issuedAt })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves to false', async () => {
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual({
|
||||||
|
resetPassword: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not update PasswordReset `usedAt` property', async () => {
|
||||||
|
await client.request(mutation, variables)
|
||||||
|
const requests = await getAllPasswordResets()
|
||||||
|
const [request] = requests
|
||||||
|
const { usedAt } = request.properties
|
||||||
|
expect(usedAt).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
85
backend/src/schema/resolvers/passwordReset/emailTemplates.js
Normal file
85
backend/src/schema/resolvers/passwordReset/emailTemplates.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import CONFIG from '../../../config'
|
||||||
|
|
||||||
|
export const from = '"Human Connection" <info@human-connection.org>'
|
||||||
|
|
||||||
|
export const resetPasswordMail = options => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
|
||||||
|
supportUrl = 'https://human-connection.org/en/contact/',
|
||||||
|
} = options
|
||||||
|
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
|
||||||
|
actionUrl.searchParams.set('code', code)
|
||||||
|
actionUrl.searchParams.set('email', email)
|
||||||
|
|
||||||
|
return {
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
text: `
|
||||||
|
Hi ${name}!
|
||||||
|
|
||||||
|
You recently requested to reset your password for your Human Connection account.
|
||||||
|
Use the link below to reset it. This password reset is only valid for the next
|
||||||
|
24 hours.
|
||||||
|
|
||||||
|
${actionUrl}
|
||||||
|
|
||||||
|
If you did not request a password reset, please ignore this email or contact
|
||||||
|
support if you have questions:
|
||||||
|
|
||||||
|
${supportUrl}
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The Human Connection Team
|
||||||
|
|
||||||
|
If you're having trouble with the link above, you can manually copy and
|
||||||
|
paste the following code into your browser window:
|
||||||
|
|
||||||
|
${code}
|
||||||
|
|
||||||
|
Human Connection gemeinnützige GmbH
|
||||||
|
Bahnhofstr. 11
|
||||||
|
73235 Weilheim / Teck
|
||||||
|
Deutschland
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrongAccountMail = options => {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
subject = `We received a request to reset your password with this email address (${email})`,
|
||||||
|
supportUrl = 'https://human-connection.org/en/contact/',
|
||||||
|
} = options
|
||||||
|
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
|
||||||
|
return {
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
text: `
|
||||||
|
We received a request to reset the password to access Human Connection with your
|
||||||
|
email address, but we were unable to find an account associated with this
|
||||||
|
address.
|
||||||
|
|
||||||
|
If you use Human Connection and were expecting this email, consider trying to
|
||||||
|
request a password reset using the email address associated with your account.
|
||||||
|
Try a different email:
|
||||||
|
|
||||||
|
${actionUrl}
|
||||||
|
|
||||||
|
If you do not use Human Connection or did not request a password reset, please
|
||||||
|
ignore this email. Feel free to contact support if you have further questions:
|
||||||
|
|
||||||
|
${supportUrl}
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The Human Connection Team
|
||||||
|
|
||||||
|
Human Connection gemeinnützige GmbH
|
||||||
|
Bahnhofstr. 11
|
||||||
|
73235 Weilheim / Teck
|
||||||
|
Deutschland
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,7 +59,7 @@ export default {
|
|||||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
let result = await session.run(
|
let result = await session.run(
|
||||||
`MATCH (user:User {email: $userEmail})
|
`MATCH (user:User {email: $userEmail})
|
||||||
RETURN user {.id, .email, .password}`,
|
RETURN user {.id, .email, .password}`,
|
||||||
{
|
{
|
||||||
userEmail: user.email,
|
userEmail: user.email,
|
||||||
|
|||||||
@ -25,6 +25,8 @@ type Mutation {
|
|||||||
login(email: String!, password: String!): String!
|
login(email: String!, password: String!): String!
|
||||||
signup(email: String!, password: String!): Boolean!
|
signup(email: String!, password: String!): Boolean!
|
||||||
changePassword(oldPassword: String!, newPassword: String!): String!
|
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||||
|
requestPasswordReset(email: String!): Boolean!
|
||||||
|
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
|
||||||
report(id: ID!, description: String): Report
|
report(id: ID!, description: String): Report
|
||||||
disable(id: ID!): ID
|
disable(id: ID!): ID
|
||||||
enable(id: ID!): ID
|
enable(id: ID!): ID
|
||||||
|
|||||||
@ -1110,10 +1110,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
|
||||||
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
|
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
|
||||||
|
|
||||||
"@types/yup@0.26.16":
|
"@types/yup@0.26.17":
|
||||||
version "0.26.16"
|
version "0.26.17"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.16.tgz#75c428236207c48d9f8062dd1495cda8c5485a15"
|
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
|
||||||
integrity sha512-E2RNc7DSeQ+2EIJ1H3+yFjYu6YiyQBUJ7yNpIxomrYJ3oFizLZ5yDS3T1JTUNBC2OCRkgnhLS0smob5UuCHfNA==
|
integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
|
||||||
|
|
||||||
"@types/zen-observable@^0.5.3":
|
"@types/zen-observable@^0.5.3":
|
||||||
version "0.5.4"
|
version "0.5.4"
|
||||||
@ -1342,18 +1342,6 @@ apollo-engine-reporting-protobuf@0.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
protobufjs "^6.8.6"
|
protobufjs "^6.8.6"
|
||||||
|
|
||||||
apollo-engine-reporting@1.3.1:
|
|
||||||
version "1.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.1.tgz#f2c2c63f865871a57c15cdbb2a3bcd4b4af28115"
|
|
||||||
integrity sha512-e0Xp+0yite8DH/xm9fnJt42CxfWAcY6waiq3icCMAgO9T7saXzVOPpl84SkuA+hIJUBtfaKrTnC+7Jxi/I7OrQ==
|
|
||||||
dependencies:
|
|
||||||
apollo-engine-reporting-protobuf "0.3.1"
|
|
||||||
apollo-graphql "^0.3.0"
|
|
||||||
apollo-server-core "2.6.3"
|
|
||||||
apollo-server-env "2.4.0"
|
|
||||||
async-retry "^1.2.1"
|
|
||||||
graphql-extensions "0.7.2"
|
|
||||||
|
|
||||||
apollo-engine-reporting@1.3.2:
|
apollo-engine-reporting@1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.2.tgz#b2569f79eb1a7a7380f49340db61465f449284fe"
|
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.2.tgz#b2569f79eb1a7a7380f49340db61465f449284fe"
|
||||||
@ -1383,14 +1371,6 @@ apollo-errors@^1.9.0:
|
|||||||
assert "^1.4.1"
|
assert "^1.4.1"
|
||||||
extendable-error "^0.1.5"
|
extendable-error "^0.1.5"
|
||||||
|
|
||||||
apollo-graphql@^0.3.0:
|
|
||||||
version "0.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7"
|
|
||||||
integrity sha512-tbhtzNAAhNI34v4XY9OlZGnH7U0sX4BP1cJrUfSiNzQnZRg1UbQYZ06riHSOHpi5RSndFcA9LDM5C1ZKKOUeBg==
|
|
||||||
dependencies:
|
|
||||||
apollo-env "0.5.1"
|
|
||||||
lodash.sortby "^4.7.0"
|
|
||||||
|
|
||||||
apollo-graphql@^0.3.2:
|
apollo-graphql@^0.3.2:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
|
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
|
||||||
@ -1442,32 +1422,6 @@ apollo-server-caching@0.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^5.0.0"
|
lru-cache "^5.0.0"
|
||||||
|
|
||||||
apollo-server-core@2.6.3:
|
|
||||||
version "2.6.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.3.tgz#786c8251c82cf29acb5cae9635a321f0644332ae"
|
|
||||||
integrity sha512-tfC0QO1NbJW3ShkB5pRCnUaYEkW2AwnswaTeedkfv//EO3yiC/9LeouCK5F22T8stQG+vGjvCqf0C8ldI/XsIA==
|
|
||||||
dependencies:
|
|
||||||
"@apollographql/apollo-tools" "^0.3.6"
|
|
||||||
"@apollographql/graphql-playground-html" "1.6.20"
|
|
||||||
"@types/ws" "^6.0.0"
|
|
||||||
apollo-cache-control "0.7.2"
|
|
||||||
apollo-datasource "0.5.0"
|
|
||||||
apollo-engine-reporting "1.3.1"
|
|
||||||
apollo-server-caching "0.4.0"
|
|
||||||
apollo-server-env "2.4.0"
|
|
||||||
apollo-server-errors "2.3.0"
|
|
||||||
apollo-server-plugin-base "0.5.2"
|
|
||||||
apollo-tracing "0.7.2"
|
|
||||||
fast-json-stable-stringify "^2.0.0"
|
|
||||||
graphql-extensions "0.7.2"
|
|
||||||
graphql-subscriptions "^1.0.0"
|
|
||||||
graphql-tag "^2.9.2"
|
|
||||||
graphql-tools "^4.0.0"
|
|
||||||
graphql-upload "^8.0.2"
|
|
||||||
sha.js "^2.4.11"
|
|
||||||
subscriptions-transport-ws "^0.9.11"
|
|
||||||
ws "^6.0.0"
|
|
||||||
|
|
||||||
apollo-server-core@2.6.4:
|
apollo-server-core@2.6.4:
|
||||||
version "2.6.4"
|
version "2.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.4.tgz#0372e3a28f221b9db83bdfbb0fd0b2960cd29bab"
|
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.4.tgz#0372e3a28f221b9db83bdfbb0fd0b2960cd29bab"
|
||||||
@ -1516,10 +1470,10 @@ apollo-server-errors@2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
|
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
|
||||||
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
|
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
|
||||||
|
|
||||||
apollo-server-express@2.6.3:
|
apollo-server-express@2.6.4:
|
||||||
version "2.6.3"
|
version "2.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.3.tgz#62034c978f84207615c0430fb37ab006f71146fe"
|
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.4.tgz#fc1d661be73fc1880aa53a56e1abe3733d08eada"
|
||||||
integrity sha512-8ca+VpKArgNzFar0D3DesWnn0g9YDtFLhO56TQprHh2Spxu9WxTnYNjsYs2MCCNf+iV/uy7vTvEknErvnIcZaQ==
|
integrity sha512-U6hiZxty/rait39V5d+QeueNHlwfl68WbYtsutDUVxnq2Jws2ZDrvIkaWWN6HQ77+nBy5gGVxycvWIyoHHfi+g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/graphql-playground-html" "1.6.20"
|
"@apollographql/graphql-playground-html" "1.6.20"
|
||||||
"@types/accepts" "^1.3.5"
|
"@types/accepts" "^1.3.5"
|
||||||
@ -1527,7 +1481,7 @@ apollo-server-express@2.6.3:
|
|||||||
"@types/cors" "^2.8.4"
|
"@types/cors" "^2.8.4"
|
||||||
"@types/express" "4.17.0"
|
"@types/express" "4.17.0"
|
||||||
accepts "^1.3.5"
|
accepts "^1.3.5"
|
||||||
apollo-server-core "2.6.3"
|
apollo-server-core "2.6.4"
|
||||||
body-parser "^1.18.3"
|
body-parser "^1.18.3"
|
||||||
cors "^2.8.4"
|
cors "^2.8.4"
|
||||||
graphql-subscriptions "^1.0.0"
|
graphql-subscriptions "^1.0.0"
|
||||||
@ -1555,11 +1509,6 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
|
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
|
||||||
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
|
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
|
||||||
|
|
||||||
apollo-server-plugin-base@0.5.2:
|
|
||||||
version "0.5.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.2.tgz#f97ba983f1e825fec49cba8ff6a23d00e1901819"
|
|
||||||
integrity sha512-j81CpadRLhxikBYHMh91X4aTxfzFnmmebEiIR9rruS6dywWCxV2aLW87l9ocD1MiueNam0ysdwZkX4F3D4csNw==
|
|
||||||
|
|
||||||
apollo-server-plugin-base@0.5.3:
|
apollo-server-plugin-base@0.5.3:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.3.tgz#234c6330c412a2e83ff49305a0c2f991fb40a266"
|
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.3.tgz#234c6330c412a2e83ff49305a0c2f991fb40a266"
|
||||||
@ -1572,13 +1521,13 @@ apollo-server-testing@~2.6.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.6.4"
|
apollo-server-core "2.6.4"
|
||||||
|
|
||||||
apollo-server@~2.6.3:
|
apollo-server@~2.6.4:
|
||||||
version "2.6.3"
|
version "2.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.3.tgz#71235325449c6d3881a5143975ca44c07a07d2d7"
|
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.4.tgz#34b3a50135e20b8df8c194a14e4636eb9c2898b2"
|
||||||
integrity sha512-pTIXE5xEMAikKLTIBIqLNvimMETiZbzmiqDb6BGzIUicAz4Rxa1/+bDi1ZeJWrZQjE/TfBLd2Si3qam7dZGrjw==
|
integrity sha512-f0TZOc969XNNlSm8sVsU34D8caQfPNwS0oqmWUxb8xXl88HlFzB+HBmOU6ZEKdpMCksTNDbqYo0jXiGJ0rL/0g==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.6.3"
|
apollo-server-core "2.6.4"
|
||||||
apollo-server-express "2.6.3"
|
apollo-server-express "2.6.4"
|
||||||
express "^4.0.0"
|
express "^4.0.0"
|
||||||
graphql-subscriptions "^1.0.0"
|
graphql-subscriptions "^1.0.0"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
@ -3840,12 +3789,12 @@ graphql-request@~1.8.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cross-fetch "2.2.2"
|
cross-fetch "2.2.2"
|
||||||
|
|
||||||
graphql-shield@~5.6.1:
|
graphql-shield@~5.7.1:
|
||||||
version "5.6.1"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.6.1.tgz#f4c9fb5ed329f823a738ad974b300d4a982691ca"
|
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
|
||||||
integrity sha512-Zrxrvx1Ep/nDdfQh/wN5PrH9JE4OEFdUmLzuyZSIGIAQWyXDk8FAl0cuNulnqI+zqrDzZ9TUj/zO3oV4hNKqCA==
|
integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yup" "0.26.16"
|
"@types/yup" "0.26.17"
|
||||||
lightercollective "^0.3.0"
|
lightercollective "^0.3.0"
|
||||||
object-hash "^1.3.1"
|
object-hash "^1.3.1"
|
||||||
yup "^0.27.0"
|
yup "^0.27.0"
|
||||||
@ -5762,6 +5711,11 @@ node-releases@^1.1.19:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
|
|
||||||
|
nodemailer@^6.2.1:
|
||||||
|
version "6.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3"
|
||||||
|
integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==
|
||||||
|
|
||||||
nodemon@~1.19.1:
|
nodemon@~1.19.1:
|
||||||
version "1.19.1"
|
version "1.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
|
||||||
|
|||||||
@ -5,6 +5,11 @@ data:
|
|||||||
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
|
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
|
||||||
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
|
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
|
||||||
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
|
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
|
||||||
|
SMTP_HOST:
|
||||||
|
SMTP_PORT: 587
|
||||||
|
SMTP_USERNAME:
|
||||||
|
SMTP_PASSWORD:
|
||||||
|
SMTP_IGNORE_TLS:
|
||||||
metadata:
|
metadata:
|
||||||
name: human-connection
|
name: human-connection
|
||||||
namespace: human-connection
|
namespace: human-connection
|
||||||
|
|||||||
@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]})
|
|||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
b.key = badge.key,
|
b.key = badge.key,
|
||||||
b.type = badge.type,
|
b.type = badge.type,
|
||||||
b.icon = badge.image.path,
|
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
|
||||||
b.status = badge.status,
|
b.status = badge.status,
|
||||||
b.createdAt = badge.createdAt.`$date`,
|
b.createdAt = badge.createdAt.`$date`,
|
||||||
b.updatedAt = badge.updatedAt.`$date`
|
b.updatedAt = badge.updatedAt.`$date`
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
[?] unique: true, // Unique value is not enforced in Nitro?
|
[?] unique: true, // Unique value is not enforced in Nitro?
|
||||||
[-] index: true
|
[-] index: true
|
||||||
},
|
},
|
||||||
[ ] type: {
|
[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post'
|
||||||
[ ] type: String,
|
[ ] type: String,
|
||||||
[ ] required: true,
|
[ ] required: true,
|
||||||
[-] index: true
|
[-] index: true
|
||||||
@ -50,7 +50,7 @@
|
|||||||
[?] required: true // Not required in Nitro
|
[?] required: true // Not required in Nitro
|
||||||
},
|
},
|
||||||
[ ] hasMore: { type: Boolean },
|
[ ] hasMore: { type: Boolean },
|
||||||
[?] teaserImg: { type: String }, // Path is incorrect in Nitro
|
[X] teaserImg: { type: String },
|
||||||
[ ] language: {
|
[ ] language: {
|
||||||
[ ] type: String,
|
[ ] type: String,
|
||||||
[ ] required: true,
|
[ ] required: true,
|
||||||
@ -131,7 +131,7 @@ MERGE (p:Post {id: post._id["$oid"]})
|
|||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
p.title = post.title,
|
p.title = post.title,
|
||||||
p.slug = post.slug,
|
p.slug = post.slug,
|
||||||
p.image = post.teaserImg,
|
p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''),
|
||||||
p.content = post.content,
|
p.content = post.content,
|
||||||
p.contentExcerpt = post.contentExcerpt,
|
p.contentExcerpt = post.contentExcerpt,
|
||||||
p.visibility = toLower(post.visibility),
|
p.visibility = toLower(post.visibility),
|
||||||
|
|||||||
@ -49,8 +49,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ ] timezone: { type: String },
|
[ ] timezone: { type: String },
|
||||||
[?] avatar: { type: String }, // Path is incorrect in Nitro
|
[X] avatar: { type: String },
|
||||||
[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this?
|
[X] coverImg: { type: String },
|
||||||
[ ] doiToken: { type: String },
|
[ ] doiToken: { type: String },
|
||||||
[ ] confirmedAt: { type: Date },
|
[ ] confirmedAt: { type: Date },
|
||||||
[?] badgeIds: [], // Verify this is working properly
|
[?] badgeIds: [], // Verify this is working properly
|
||||||
@ -102,8 +102,8 @@ u.name = user.name,
|
|||||||
u.slug = user.slug,
|
u.slug = user.slug,
|
||||||
u.email = user.email,
|
u.email = user.email,
|
||||||
u.password = user.password,
|
u.password = user.password,
|
||||||
u.avatar = user.avatar,
|
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
|
||||||
u.coverImg = user.coverImg,
|
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
|
||||||
u.wasInvited = user.wasInvited,
|
u.wasInvited = user.wasInvited,
|
||||||
u.wasSeeded = user.wasSeeded,
|
u.wasSeeded = user.wasSeeded,
|
||||||
u.role = toLower(user.role),
|
u.role = toLower(user.role),
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
version: "3.4"
|
version: "3.4"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
mailserver:
|
||||||
|
image: djfarrelly/maildev
|
||||||
|
ports:
|
||||||
|
- 1080:80
|
||||||
|
networks:
|
||||||
|
- hc-network
|
||||||
webapp:
|
webapp:
|
||||||
build:
|
build:
|
||||||
context: webapp
|
context: webapp
|
||||||
@ -20,6 +26,10 @@ services:
|
|||||||
- backend_node_modules:/nitro-backend/node_modules
|
- backend_node_modules:/nitro-backend/node_modules
|
||||||
- uploads:/nitro-backend/public/uploads
|
- uploads:/nitro-backend/public/uploads
|
||||||
command: yarn run dev
|
command: yarn run dev
|
||||||
|
environment:
|
||||||
|
- SMTP_HOST=mailserver
|
||||||
|
- SMTP_PORT=25
|
||||||
|
- SMTP_IGNORE_TLS=true
|
||||||
neo4j:
|
neo4j:
|
||||||
environment:
|
environment:
|
||||||
- NEO4J_AUTH=none
|
- NEO4J_AUTH=none
|
||||||
|
|||||||
@ -83,6 +83,7 @@ export default {
|
|||||||
deleteContributions: false,
|
deleteContributions: false,
|
||||||
deleteComments: false,
|
deleteComments: false,
|
||||||
deleteEnabled: false,
|
deleteEnabled: false,
|
||||||
|
enableDeletionValue: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { mount, createLocalVue } from '@vue/test-utils'
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
import FilterMenu from './FilterMenu.vue'
|
import FilterMenu from './FilterMenu.vue'
|
||||||
import Styleguide from '@human-connection/styleguide'
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import VTooltip from 'v-tooltip'
|
||||||
|
|
||||||
const localVue = createLocalVue()
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
localVue.use(Styleguide)
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(VTooltip)
|
||||||
|
|
||||||
describe('FilterMenu.vue', () => {
|
describe('FilterMenu.vue', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|||||||
@ -11,18 +11,21 @@
|
|||||||
id="oldPassword"
|
id="oldPassword"
|
||||||
model="oldPassword"
|
model="oldPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-old-password')"
|
:label="$t('settings.security.change-password.label-old-password')"
|
||||||
/>
|
/>
|
||||||
<ds-input
|
<ds-input
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
model="newPassword"
|
model="newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password')"
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
/>
|
/>
|
||||||
<ds-input
|
<ds-input
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
model="confirmPassword"
|
model="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
/>
|
/>
|
||||||
<password-strength :password="formData.newPassword" />
|
<password-strength :password="formData.newPassword" />
|
||||||
|
|||||||
83
webapp/components/PasswordReset/ChangePassword.spec.js
Normal file
83
webapp/components/PasswordReset/ChangePassword.spec.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import ChangePassword from './ChangePassword'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('ChangePassword ', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {}
|
||||||
|
mocks = {
|
||||||
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
loading: false,
|
||||||
|
mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(ChangePassword, {
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given email and verification code', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.email = 'mail@example.org'
|
||||||
|
propsData.code = '123456'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submitting new password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#newPassword').setValue('supersecret')
|
||||||
|
wrapper.find('input#confirmPassword').setValue('supersecret')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls resetPassword graphql mutation', () => {
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers new password to backend', () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' },
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('password reset successful', () => {
|
||||||
|
it('displays success message', () => {
|
||||||
|
const expected = 'verify-code.form.change-password.success'
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after animation', () => {
|
||||||
|
beforeEach(jest.runAllTimers)
|
||||||
|
|
||||||
|
it('emits `change-password-sucess`', () => {
|
||||||
|
expect(wrapper.emitted('passwordResetResponse')).toEqual([['success']])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
140
webapp/components/PasswordReset/ChangePassword.vue
Normal file
140
webapp/components/PasswordReset/ChangePassword.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card class="verify-code">
|
||||||
|
<ds-space margin="large">
|
||||||
|
<template>
|
||||||
|
<ds-form
|
||||||
|
v-if="!changePasswordResult"
|
||||||
|
v-model="formData"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="handleSubmitPassword"
|
||||||
|
@input="handleInput"
|
||||||
|
@input-valid="handleInputValid"
|
||||||
|
class="change-password"
|
||||||
|
>
|
||||||
|
<ds-input
|
||||||
|
id="newPassword"
|
||||||
|
model="newPassword"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
|
/>
|
||||||
|
<ds-input
|
||||||
|
id="confirmPassword"
|
||||||
|
model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
|
/>
|
||||||
|
<password-strength :password="formData.newPassword" />
|
||||||
|
<ds-space margin-top="base">
|
||||||
|
<ds-button :loading="$apollo.loading" :disabled="disabled" primary>
|
||||||
|
{{ $t('settings.security.change-password.button') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-space>
|
||||||
|
</ds-form>
|
||||||
|
<ds-text v-else>
|
||||||
|
<template v-if="changePasswordResult === 'success'">
|
||||||
|
<sweetalert-icon icon="success" />
|
||||||
|
<ds-text>
|
||||||
|
{{ $t(`verify-code.form.change-password.success`) }}
|
||||||
|
</ds-text>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<sweetalert-icon icon="error" />
|
||||||
|
<ds-text align="left">
|
||||||
|
{{ $t(`verify-code.form.change-password.error`) }}
|
||||||
|
{{ $t('verify-code.form.change-password.help') }}
|
||||||
|
</ds-text>
|
||||||
|
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
|
||||||
|
</template>
|
||||||
|
</ds-text>
|
||||||
|
</template>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PasswordStrength from '../Password/Strength'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SweetalertIcon,
|
||||||
|
PasswordStrength,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
email: { type: String, required: true },
|
||||||
|
code: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
newPassword: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: this.$t('settings.security.change-password.message-new-password-required'),
|
||||||
|
},
|
||||||
|
confirmPassword: [
|
||||||
|
{ validator: this.matchPassword },
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: this.$t(
|
||||||
|
'settings.security.change-password.message-new-password-confirm-required',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
changePasswordResult: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleInput() {
|
||||||
|
this.disabled = true
|
||||||
|
},
|
||||||
|
async handleInputValid() {
|
||||||
|
this.disabled = false
|
||||||
|
},
|
||||||
|
async handleSubmitPassword() {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation($code: String!, $email: String!, $newPassword: String!) {
|
||||||
|
resetPassword(code: $code, email: $email, newPassword: $newPassword)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { newPassword } = this.formData
|
||||||
|
const { email, code } = this
|
||||||
|
const variables = { newPassword, email, code }
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { resetPassword },
|
||||||
|
} = await this.$apollo.mutate({ mutation, variables })
|
||||||
|
this.changePasswordResult = resetPassword ? 'success' : 'error'
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('passwordResetResponse', this.changePasswordResult)
|
||||||
|
}, 3000)
|
||||||
|
this.formData = {
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.$toast.error(err.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
matchPassword(rule, value, callback, source, options) {
|
||||||
|
var errors = []
|
||||||
|
if (this.formData.newPassword !== value) {
|
||||||
|
errors.push(
|
||||||
|
new Error(this.$t('settings.security.change-password.message-new-password-missmatch')),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(errors)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
77
webapp/components/PasswordReset/Request.spec.js
Normal file
77
webapp/components/PasswordReset/Request.spec.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import Request from './Request'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('Request', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
loading: false,
|
||||||
|
mutate: jest.fn().mockResolvedValue({ data: { reqestPasswordReset: true } }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(Request, {
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders a password reset form', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('.password-reset').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#email').setValue('mail@example.org')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls requestPasswordReset graphql mutation', () => {
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers email to backend', () => {
|
||||||
|
const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } })
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides form to avoid re-submission', () => {
|
||||||
|
expect(wrapper.find('form').exists()).not.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays a message that a password email was requested', () => {
|
||||||
|
const expected = ['password-reset.form.submitted', { email: 'mail@example.org' }]
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith(...expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after animation', () => {
|
||||||
|
beforeEach(jest.runAllTimers)
|
||||||
|
|
||||||
|
it('emits `handleSubmitted`', () => {
|
||||||
|
expect(wrapper.emitted('handleSubmitted')).toEqual([[{ email: 'mail@example.org' }]])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
107
webapp/components/PasswordReset/Request.vue
Normal file
107
webapp/components/PasswordReset/Request.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card class="password-reset">
|
||||||
|
<ds-space margin="large">
|
||||||
|
<ds-form
|
||||||
|
v-if="!submitted"
|
||||||
|
@input="handleInput"
|
||||||
|
@input-valid="handleInputValid"
|
||||||
|
v-model="formData"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<ds-input
|
||||||
|
:placeholder="$t('login.email')"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
model="email"
|
||||||
|
name="email"
|
||||||
|
icon="envelope"
|
||||||
|
/>
|
||||||
|
<ds-space margin-botton="large">
|
||||||
|
<ds-text>
|
||||||
|
{{ $t('password-reset.form.description') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
<ds-button
|
||||||
|
:disabled="disabled"
|
||||||
|
:loading="$apollo.loading"
|
||||||
|
primary
|
||||||
|
fullwidth
|
||||||
|
name="submit"
|
||||||
|
type="submit"
|
||||||
|
icon="envelope"
|
||||||
|
>
|
||||||
|
{{ $t('password-reset.form.submit') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-form>
|
||||||
|
<div v-else>
|
||||||
|
<transition name="ds-transition-fade">
|
||||||
|
<ds-flex centered>
|
||||||
|
<sweetalert-icon icon="info" />
|
||||||
|
</ds-flex>
|
||||||
|
</transition>
|
||||||
|
<ds-text v-html="submitMessage" />
|
||||||
|
</div>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SweetalertIcon,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
email: {
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
message: this.$t('common.validations.email'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
submitted: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submitMessage() {
|
||||||
|
const { email } = this.formData
|
||||||
|
return this.$t('password-reset.form.submitted', { email })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.disabled = true
|
||||||
|
},
|
||||||
|
handleInputValid() {
|
||||||
|
this.disabled = false
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation($email: String!) {
|
||||||
|
requestPasswordReset(email: $email)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { email } = this.formData
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({ mutation, variables: { email } })
|
||||||
|
this.submitted = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('handleSubmitted', { email })
|
||||||
|
}, 3000)
|
||||||
|
} catch (err) {
|
||||||
|
this.$toast.error(err.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
webapp/components/PasswordReset/VerifyCode.spec.js
Normal file
53
webapp/components/PasswordReset/VerifyCode.spec.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import VerifyCode from './VerifyCode'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('VerifyCode ', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(),
|
||||||
|
}
|
||||||
|
propsData = {
|
||||||
|
email: 'mail@example.org',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(VerifyCode, {
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
propsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders a verify code form', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('.verify-code').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after verification code given', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#code').setValue('123456')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits `verifyCode`', () => {
|
||||||
|
const expected = [[{ code: '123456', email: 'mail@example.org' }]]
|
||||||
|
expect(wrapper.emitted('verification')).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
67
webapp/components/PasswordReset/VerifyCode.vue
Normal file
67
webapp/components/PasswordReset/VerifyCode.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card class="verify-code">
|
||||||
|
<ds-space margin="large">
|
||||||
|
<ds-form
|
||||||
|
v-model="formData"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="handleSubmitVerify"
|
||||||
|
@input="handleInput"
|
||||||
|
@input-valid="handleInputValid"
|
||||||
|
>
|
||||||
|
<ds-input
|
||||||
|
:placeholder="$t('verify-code.form.code')"
|
||||||
|
model="code"
|
||||||
|
name="code"
|
||||||
|
id="code"
|
||||||
|
icon="question-circle"
|
||||||
|
/>
|
||||||
|
<ds-space margin-botton="large">
|
||||||
|
<ds-text>
|
||||||
|
{{ $t('verify-code.form.description') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
<ds-button :disabled="disabled" primary fullwidth name="submit" type="submit">
|
||||||
|
{{ $t('verify-code.form.next') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-form>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
email: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
code: '',
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
min: 6,
|
||||||
|
max: 6,
|
||||||
|
required: true,
|
||||||
|
message: this.$t('common.validations.verification-code'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleInput() {
|
||||||
|
this.disabled = true
|
||||||
|
},
|
||||||
|
async handleInputValid() {
|
||||||
|
this.disabled = false
|
||||||
|
},
|
||||||
|
handleSubmitVerify() {
|
||||||
|
const { code } = this.formData
|
||||||
|
const email = this.email
|
||||||
|
this.$emit('verification', { email, code })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -8,11 +8,32 @@
|
|||||||
"logout": "Ausloggen",
|
"logout": "Ausloggen",
|
||||||
"email": "Deine E-Mail",
|
"email": "Deine E-Mail",
|
||||||
"password": "Dein Passwort",
|
"password": "Dein Passwort",
|
||||||
|
"forgotPassword": "Passwort vergessen?",
|
||||||
"moreInfo": "Was ist Human Connection?",
|
"moreInfo": "Was ist Human Connection?",
|
||||||
"moreInfoURL": "https://human-connection.org",
|
"moreInfoURL": "https://human-connection.org",
|
||||||
"moreInfoHint": "zur Präsentationsseite",
|
"moreInfoHint": "zur Präsentationsseite",
|
||||||
"hello": "Hallo"
|
"hello": "Hallo"
|
||||||
},
|
},
|
||||||
|
"password-reset": {
|
||||||
|
"title": "Passwort zurücksetzen",
|
||||||
|
"form": {
|
||||||
|
"description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.",
|
||||||
|
"submit": "Email anfordern",
|
||||||
|
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verify-code": {
|
||||||
|
"form": {
|
||||||
|
"code": "Code eingeben",
|
||||||
|
"description": "Öffne dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
|
||||||
|
"next": "Weiter",
|
||||||
|
"change-password":{
|
||||||
|
"success": "Änderung des Passworts war erfolgreich!",
|
||||||
|
"error": "Passwort Änderung fehlgeschlagen. Möglicherweise falscher Sicherheitscode?",
|
||||||
|
"help": "Falls Probleme auftreten, schreib uns gerne eine Mail an:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"placeholder": "Schreib etwas Inspirierendes..."
|
"placeholder": "Schreib etwas Inspirierendes..."
|
||||||
},
|
},
|
||||||
@ -197,7 +218,11 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"loadMore": "mehr laden",
|
"loadMore": "mehr laden",
|
||||||
"loading": "wird geladen",
|
"loading": "wird geladen",
|
||||||
"reportContent": "Melden"
|
"reportContent": "Melden",
|
||||||
|
"validations": {
|
||||||
|
"email": "muss eine gültige E-Mail Adresse sein",
|
||||||
|
"verification-code": "muss genau 6 Buchstaben lang sein"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"loading": "lade",
|
"loading": "lade",
|
||||||
|
|||||||
@ -8,11 +8,32 @@
|
|||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"email": "Your Email",
|
"email": "Your Email",
|
||||||
"password": "Your Password",
|
"password": "Your Password",
|
||||||
|
"forgotPassword": "Forgot Password?",
|
||||||
"moreInfo": "What is Human Connection?",
|
"moreInfo": "What is Human Connection?",
|
||||||
"moreInfoURL": "https://human-connection.org/en/",
|
"moreInfoURL": "https://human-connection.org/en/",
|
||||||
"moreInfoHint": "to the presentation page",
|
"moreInfoHint": "to the presentation page",
|
||||||
"hello": "Hello"
|
"hello": "Hello"
|
||||||
},
|
},
|
||||||
|
"password-reset": {
|
||||||
|
"title": "Reset your password",
|
||||||
|
"form": {
|
||||||
|
"description": "A password reset email will be sent to the given email address.",
|
||||||
|
"submit": "Request email",
|
||||||
|
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verify-code": {
|
||||||
|
"form": {
|
||||||
|
"code": "Enter your code",
|
||||||
|
"description": "Open your inbox and enter the code that we've sent to you.",
|
||||||
|
"next": "Continue",
|
||||||
|
"change-password": {
|
||||||
|
"success": "Changing your password was successful!",
|
||||||
|
"error": "Changing your password failed. Maybe the security code was not correct?",
|
||||||
|
"help": "In case of problems, feel free to ask for help by sending us a mail to:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"placeholder": "Leave your inspirational thoughts..."
|
"placeholder": "Leave your inspirational thoughts..."
|
||||||
},
|
},
|
||||||
@ -198,7 +219,11 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"loadMore": "load more",
|
"loadMore": "load more",
|
||||||
"loading": "loading",
|
"loading": "loading",
|
||||||
"reportContent": "Report"
|
"reportContent": "Report",
|
||||||
|
"validations": {
|
||||||
|
"email": "must be a valid email address",
|
||||||
|
"verification-code": "must be 6 characters long"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"loading": "loading",
|
"loading": "loading",
|
||||||
|
|||||||
@ -25,7 +25,18 @@ module.exports = {
|
|||||||
|
|
||||||
env: {
|
env: {
|
||||||
// pages which do NOT require a login
|
// pages which do NOT require a login
|
||||||
publicPages: ['login', 'logout', 'register', 'signup', 'reset', 'reset-token', 'pages-slug'],
|
publicPages: [
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'password-reset-request',
|
||||||
|
'password-reset-verify-code',
|
||||||
|
'password-reset-change-password',
|
||||||
|
'register',
|
||||||
|
'signup',
|
||||||
|
'reset',
|
||||||
|
'reset-token',
|
||||||
|
'pages-slug',
|
||||||
|
],
|
||||||
// pages to keep alive
|
// pages to keep alive
|
||||||
keepAlivePages: ['index'],
|
keepAlivePages: ['index'],
|
||||||
// active locales
|
// active locales
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"!**/?(*.)+(spec|test).js?(x)"
|
"!**/?(*.)+(spec|test).js?(x)"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
|
"text",
|
||||||
"lcov"
|
"lcov"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
@ -94,7 +95,7 @@
|
|||||||
"eslint-config-standard": "~12.0.0",
|
"eslint-config-standard": "~12.0.0",
|
||||||
"eslint-loader": "~2.1.2",
|
"eslint-loader": "~2.1.2",
|
||||||
"eslint-plugin-import": "~2.17.3",
|
"eslint-plugin-import": "~2.17.3",
|
||||||
"eslint-plugin-jest": "~22.6.4",
|
"eslint-plugin-jest": "~22.7.0",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.1.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.1.1",
|
"eslint-plugin-promise": "~4.1.1",
|
||||||
@ -110,4 +111,4 @@
|
|||||||
"vue-jest": "~3.0.4",
|
"vue-jest": "~3.0.4",
|
||||||
"vue-svg-loader": "~0.12.0"
|
"vue-svg-loader": "~0.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,6 +45,11 @@
|
|||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
|
<ds-space class="password-reset-link" margin-bottom="large">
|
||||||
|
<nuxt-link to="/password-reset/request">
|
||||||
|
{{ $t('login.forgotPassword') }}
|
||||||
|
</nuxt-link>
|
||||||
|
</ds-space>
|
||||||
<ds-button
|
<ds-button
|
||||||
:loading="pending"
|
:loading="pending"
|
||||||
primary
|
primary
|
||||||
|
|||||||
22
webapp/pages/password-reset.vue
Normal file
22
webapp/pages/password-reset.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<ds-container width="small">
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item :width="{ base: '100%' }" centered>
|
||||||
|
<ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered>
|
||||||
|
<nuxt-child />
|
||||||
|
</ds-space>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
layout: 'default',
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (store.getters['auth/isLoggedIn']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
28
webapp/pages/password-reset/change-password.vue
Normal file
28
webapp/pages/password-reset/change-password.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<change-password
|
||||||
|
:email="email"
|
||||||
|
:code="code"
|
||||||
|
@passwordResetResponse="handlePasswordResetResponse"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChangePassword from '~/components/PasswordReset/ChangePassword'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
const { email = '', code = '' } = this.$route.query
|
||||||
|
return { email, code }
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ChangePassword,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePasswordResetResponse(response) {
|
||||||
|
if (response === 'success') {
|
||||||
|
this.$router.push('/login')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
18
webapp/pages/password-reset/request.vue
Normal file
18
webapp/pages/password-reset/request.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<request @handleSubmitted="handlePasswordResetRequested" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Request from '~/components/PasswordReset/Request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Request,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePasswordResetRequested({ email }) {
|
||||||
|
this.$router.push({ path: 'verify-code', query: { email } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
22
webapp/pages/password-reset/verify-code.vue
Normal file
22
webapp/pages/password-reset/verify-code.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<verify-code :email="email" @verification="handleVerification" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VerifyCode from '~/components/PasswordReset/VerifyCode'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
VerifyCode,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const { email = '' } = this.$route.query
|
||||||
|
return { email }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleVerification({ email, code }) {
|
||||||
|
this.$router.push({ path: 'change-password', query: { email, code } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -4300,10 +4300,10 @@ eslint-plugin-import@~2.17.3:
|
|||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@~22.6.4:
|
eslint-plugin-jest@~22.7.0:
|
||||||
version "22.6.4"
|
version "22.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.0.tgz#a1d325bccb024b04f5354c56fe790baba54a454c"
|
||||||
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
|
integrity sha512-0U9nBd9V6+GKpM/KvRDcmMuPsewSsdM7NxCozgJkVAh8IrwHmQ0aw44/eYuVkhT8Fcdhsz0zYiyPtKg147eXMQ==
|
||||||
|
|
||||||
eslint-plugin-node@~9.1.0:
|
eslint-plugin-node@~9.1.0:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user