Merge remote-tracking branch 'origin/master' into 779-tags-of-contribution-in-text

This commit is contained in:
Wolfgang Huß 2019-06-24 07:47:42 +02:00
commit a80320007c
38 changed files with 1211 additions and 219 deletions

View File

@ -5,6 +5,11 @@ GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCKS=false
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true
SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -44,6 +44,9 @@ or start the backend in production environment with:
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/)
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.

View File

@ -47,12 +47,12 @@
"apollo-client": "~2.6.3",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.6.3",
"apollo-server": "~2.6.4",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.34",
"date-fns": "2.0.0-alpha.35",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
"express": "~4.17.1",
@ -61,9 +61,9 @@
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.8",
"graphql-shield": "~5.7.1",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"graphql-yoga": "~1.18.0",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
@ -72,6 +72,7 @@
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
"node-fetch": "~2.6.0",
"nodemailer": "^6.2.1",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
@ -87,7 +88,7 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.6.3",
"apollo-server-testing": "~2.6.4",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0",
@ -97,7 +98,7 @@
"eslint-config-prettier": "~5.0.0",
"eslint-config-standard": "~12.0.0",
"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-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",

View File

@ -2,23 +2,33 @@ import dotenv from 'dotenv'
dotenv.config()
export const requiredConfigs = {
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
JWT_SECRET: process.env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
}
const {
MAPBOX_TOKEN,
JWT_SECRET,
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 = {
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
}
export const serverConfigs = {
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 requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,
SMTP_IGNORE_TLS,
SMTP_USERNAME,
SMTP_PASSWORD,
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
@ -29,6 +39,7 @@ export const developmentConfigs = {
export default {
...requiredConfigs,
...smtpConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,

View File

@ -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)
},
}

View File

@ -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)
})
})

View File

@ -3,7 +3,6 @@ import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import fixImageUrls from './fixImageUrlsMiddleware'
import excerpt from './excerptMiddleware'
import dateTime from './dateTimeMiddleware'
import xss from './xssMiddleware'
@ -25,7 +24,6 @@ export default schema => {
excerpt: excerpt,
notifications: notifications,
xss: xss,
fixImageUrls: fixImageUrls,
softDelete: softDelete,
user: user,
includedFields: includedFields,
@ -42,7 +40,6 @@ export default schema => {
'excerpt',
'notifications',
'xss',
'fixImageUrls',
'softDelete',
'user',
'includedFields',

View File

@ -147,6 +147,8 @@ const permissions = shield(
CreateComment: isAuthenticated,
DeleteComment: isAuthor,
DeleteUser: isDeletingOwnAccount,
requestPasswordReset: allow,
resetPassword: allow,
},
User: {
email: isMyOwn,

View 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
},
},
}

View 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()
})
})
})
})
})
})

View 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
`,
}
}

View File

@ -59,7 +59,7 @@ export default {
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
const session = driver.session()
let result = await session.run(
`MATCH (user:User {email: $userEmail})
`MATCH (user:User {email: $userEmail})
RETURN user {.id, .email, .password}`,
{
userEmail: user.email,

View File

@ -25,6 +25,8 @@ type Mutation {
login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID

View File

@ -1020,16 +1020,7 @@
"@types/node" "*"
"@types/range-parser" "*"
"@types/express@*", "@types/express@^4.11.1":
version "4.16.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19"
integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/express@4.17.0":
"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287"
integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==
@ -1119,10 +1110,10 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
"@types/yup@0.26.16":
version "0.26.16"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.16.tgz#75c428236207c48d9f8062dd1495cda8c5485a15"
integrity sha512-E2RNc7DSeQ+2EIJ1H3+yFjYu6YiyQBUJ7yNpIxomrYJ3oFizLZ5yDS3T1JTUNBC2OCRkgnhLS0smob5UuCHfNA==
"@types/yup@0.26.17":
version "0.26.17"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
"@types/zen-observable@^0.5.3":
version "0.5.4"
@ -1351,17 +1342,17 @@ apollo-engine-reporting-protobuf@0.3.1:
dependencies:
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==
apollo-engine-reporting@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.2.tgz#b2569f79eb1a7a7380f49340db61465f449284fe"
integrity sha512-Q9XUZ3CTqddjCswlbn+OD2oYxZ5p4lCAnsWOGMfYnSmCXLagyNK28UFFQodjFOy73p6nlTAg9cwaJ9yMOBeeXA==
dependencies:
apollo-engine-reporting-protobuf "0.3.1"
apollo-graphql "^0.3.0"
apollo-server-core "2.6.3"
apollo-graphql "^0.3.2"
apollo-server-core "2.6.4"
apollo-server-env "2.4.0"
async-retry "^1.2.1"
graphql-extensions "0.7.2"
graphql-extensions "0.7.3"
apollo-env@0.5.1:
version "0.5.1"
@ -1380,10 +1371,10 @@ apollo-errors@^1.9.0:
assert "^1.4.1"
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==
apollo-graphql@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A==
dependencies:
apollo-env "0.5.1"
lodash.sortby "^4.7.0"
@ -1431,24 +1422,24 @@ apollo-server-caching@0.4.0:
dependencies:
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==
apollo-server-core@2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.4.tgz#0372e3a28f221b9db83bdfbb0fd0b2960cd29bab"
integrity sha512-GBF+tQoJ/ysaY2CYMkuuAwJM1nk1yLJumrsBTFfcvalSzS64VdS5VN/zox1eRI+LHQQzHM18HYEAgDGa/EX+gw==
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-engine-reporting "1.3.2"
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-server-plugin-base "0.5.3"
apollo-tracing "0.7.2"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.7.2"
graphql-extensions "0.7.3"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
@ -1479,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"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.3.tgz#62034c978f84207615c0430fb37ab006f71146fe"
integrity sha512-8ca+VpKArgNzFar0D3DesWnn0g9YDtFLhO56TQprHh2Spxu9WxTnYNjsYs2MCCNf+iV/uy7vTvEknErvnIcZaQ==
apollo-server-express@2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.4.tgz#fc1d661be73fc1880aa53a56e1abe3733d08eada"
integrity sha512-U6hiZxty/rait39V5d+QeueNHlwfl68WbYtsutDUVxnq2Jws2ZDrvIkaWWN6HQ77+nBy5gGVxycvWIyoHHfi+g==
dependencies:
"@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5"
@ -1490,7 +1481,7 @@ apollo-server-express@2.6.3:
"@types/cors" "^2.8.4"
"@types/express" "4.17.0"
accepts "^1.3.5"
apollo-server-core "2.6.3"
apollo-server-core "2.6.4"
body-parser "^1.18.3"
cors "^2.8.4"
graphql-subscriptions "^1.0.0"
@ -1518,25 +1509,25 @@ 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"
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:
version "0.5.3"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.3.tgz#234c6330c412a2e83ff49305a0c2f991fb40a266"
integrity sha512-Ax043vQTzPgFeJk6m6hmPm9NMfogO3LlTKJfrWHuyZhPNeTLweHNK30vpdzzgPalcryyDMDfLYFzxuDm0W+rRQ==
apollo-server-testing@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.3.tgz#a0199a5d42000e60ecf0dea44b851f5f581e280e"
integrity sha512-LTkegcGVSkM+pA0FINDSYVl3TiFYKZyfjlKrEr/LN6wLiL6gbRgy6LMtk2j+qli/bnTDqqQREX8OEqmV8FKUoQ==
apollo-server-testing@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.4.tgz#615dfaa6ea840b6ea3ce4fd2297b28402f2d5208"
integrity sha512-s9AeZnnndhz4WRBmgFM388BFKqD2H90L6ax0e6uNEmtZk3/iODqd16RbTNHbx+PkxFvZ8BQbX1/4rbvQn6r9CA==
dependencies:
apollo-server-core "2.6.3"
apollo-server-core "2.6.4"
apollo-server@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.3.tgz#71235325449c6d3881a5143975ca44c07a07d2d7"
integrity sha512-pTIXE5xEMAikKLTIBIqLNvimMETiZbzmiqDb6BGzIUicAz4Rxa1/+bDi1ZeJWrZQjE/TfBLd2Si3qam7dZGrjw==
apollo-server@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.4.tgz#34b3a50135e20b8df8c194a14e4636eb9c2898b2"
integrity sha512-f0TZOc969XNNlSm8sVsU34D8caQfPNwS0oqmWUxb8xXl88HlFzB+HBmOU6ZEKdpMCksTNDbqYo0jXiGJ0rL/0g==
dependencies:
apollo-server-core "2.6.3"
apollo-server-express "2.6.3"
apollo-server-core "2.6.4"
apollo-server-express "2.6.4"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -2070,6 +2061,13 @@ busboy@^0.2.14:
dicer "0.2.5"
readable-stream "1.1.x"
busboy@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
dependencies:
dicer "0.3.0"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@ -2586,10 +2584,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.34:
version "2.0.0-alpha.34"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.34.tgz#5d3ae7ca0d08915ccfc87a20545250af4e9c3cae"
integrity sha512-yjSYUHASHvzOZl++cEms+Tw7oQOFA+7Z6/lL7L3lRO9j6pMfT48N6oEyvCGo/MVlH08XWmydgf8X9Y1eedf9sQ==
date-fns@2.0.0-alpha.35:
version "2.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.35.tgz#185813cdc51b05cc1468a95116494bb3f3440934"
integrity sha512-dAY1ujqRtyUsa9mVeupyMzUluWo1d7kAMwyXTQHFImKYSHKvxDw/dipiY6fdswQOs8CwpGoiKysGfaaRP5r3bA==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
@ -2721,6 +2719,13 @@ dicer@0.2.5:
readable-stream "1.1.x"
streamsearch "0.1.2"
dicer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
dependencies:
streamsearch "0.1.2"
diff-sequences@^24.3.0:
version "24.3.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
@ -3048,10 +3053,10 @@ eslint-plugin-import@~2.17.3:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.6.4:
version "22.6.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
eslint-plugin-jest@~22.7.0:
version "22.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.0.tgz#a1d325bccb024b04f5354c56fe790baba54a454c"
integrity sha512-0U9nBd9V6+GKpM/KvRDcmMuPsewSsdM7NxCozgJkVAh8IrwHmQ0aw44/eYuVkhT8Fcdhsz0zYiyPtKg147eXMQ==
eslint-plugin-node@~9.1.0:
version "9.1.0"
@ -3535,6 +3540,11 @@ fs-capacitor@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0"
integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==
fs-capacitor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@ -3718,6 +3728,13 @@ graphql-extensions@0.7.2:
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.3.tgz#2ab7331c72ae657e4cbfa4ff004c400b19f56cdf"
integrity sha512-D+FZM0t5gFntJUizeRCurZuUqsyVP13CRejRX+cDJivyGkE6OMWYkCWlzHcbye79q+hYN1m6a3NhlrJRaD9D0w==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@^0.0.x, graphql-extensions@~0.0.9:
version "0.0.10"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d"
@ -3772,12 +3789,12 @@ graphql-request@~1.8.2:
dependencies:
cross-fetch "2.2.2"
graphql-shield@~5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.8.tgz#f9e7ad2285f6cfbe20a8a49154ce6c1b184e3893"
integrity sha512-33rQ8U5jMurHIapctHk7hBcUg3nxC7fmMIMtyWiomJXhBmztFq/SG7jNaapnL5M7Q/0BmoaSQd3FLSpelP9KPw==
graphql-shield@~5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
dependencies:
"@types/yup" "0.26.16"
"@types/yup" "0.26.17"
lightercollective "^0.3.0"
object-hash "^1.3.1"
yup "^0.27.0"
@ -3812,20 +3829,20 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.4:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-upload@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856"
integrity sha512-u8a5tKPfJ0rU4MY+B3skabL8pEjMkm3tUzq25KBx6nT0yEWmqUO7Z5tdwvwYLFpkLwew94Gue0ARbZtar3gLTw==
graphql-upload@^8.0.0, graphql-upload@^8.0.2:
version "8.0.7"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3"
integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g==
dependencies:
busboy "^0.2.14"
fs-capacitor "^1.0.0"
http-errors "^1.7.1"
busboy "^0.3.1"
fs-capacitor "^2.0.4"
http-errors "^1.7.2"
object-path "^0.11.4"
graphql-yoga@~1.17.4:
version "1.17.4"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.17.4.tgz#6d325a6270399edf0776fb5f60a2e9e19512e63c"
integrity sha512-zOXFtmS43xDLoECKiuA3xVWH/wLDvLH1D/5fBKcaMFdF43ifDfnA9N6dlGggqAoOhqBnrqHwDpoKlJ6sI1LuxA==
graphql-yoga@~1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a"
integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA==
dependencies:
"@types/cors" "^2.8.4"
"@types/express" "^4.11.1"
@ -3847,6 +3864,7 @@ graphql-yoga@~1.17.4:
graphql-playground-middleware-lambda "1.7.12"
graphql-subscriptions "^0.5.8"
graphql-tools "^4.0.0"
graphql-upload "^8.0.0"
subscriptions-transport-ws "^0.9.8"
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.3.1:
@ -4045,7 +4063,7 @@ htmlparser2@^3.10.0, htmlparser2@^3.9.1:
inherits "^2.0.1"
readable-stream "^3.0.6"
http-errors@1.7.2, http-errors@~1.7.2:
http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@ -4056,7 +4074,7 @@ http-errors@1.7.2, http-errors@~1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@^1.7.0, http-errors@^1.7.1:
http-errors@^1.7.0:
version "1.7.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027"
integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==
@ -5693,6 +5711,11 @@ node-releases@^1.1.19:
dependencies:
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:
version "1.19.1"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"

View File

@ -5,6 +5,11 @@ data:
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
SMTP_HOST:
SMTP_PORT: 587
SMTP_USERNAME:
SMTP_PASSWORD:
SMTP_IGNORE_TLS:
metadata:
name: human-connection
namespace: human-connection

View File

@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]})
ON CREATE SET
b.key = badge.key,
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.createdAt = badge.createdAt.`$date`,
b.updatedAt = badge.updatedAt.`$date`

View File

@ -28,7 +28,7 @@
[?] unique: true, // Unique value is not enforced in Nitro?
[-] index: true
},
[ ] type: {
[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post'
[ ] type: String,
[ ] required: true,
[-] index: true
@ -50,7 +50,7 @@
[?] required: true // Not required in Nitro
},
[ ] hasMore: { type: Boolean },
[?] teaserImg: { type: String }, // Path is incorrect in Nitro
[X] teaserImg: { type: String },
[ ] language: {
[ ] type: String,
[ ] required: true,
@ -131,7 +131,7 @@ MERGE (p:Post {id: post._id["$oid"]})
ON CREATE SET
p.title = post.title,
p.slug = post.slug,
p.image = post.teaserImg,
p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''),
p.content = post.content,
p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility),

View File

@ -49,8 +49,8 @@
}
},
[ ] timezone: { type: String },
[?] avatar: { type: String }, // Path is incorrect in Nitro
[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this?
[X] avatar: { type: String },
[X] coverImg: { type: String },
[ ] doiToken: { type: String },
[ ] confirmedAt: { type: Date },
[?] badgeIds: [], // Verify this is working properly
@ -102,8 +102,8 @@ u.name = user.name,
u.slug = user.slug,
u.email = user.email,
u.password = user.password,
u.avatar = user.avatar,
u.coverImg = user.coverImg,
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
u.wasInvited = user.wasInvited,
u.wasSeeded = user.wasSeeded,
u.role = toLower(user.role),

View File

@ -1,6 +1,12 @@
version: "3.4"
services:
mailserver:
image: djfarrelly/maildev
ports:
- 1080:80
networks:
- hc-network
webapp:
build:
context: webapp
@ -20,6 +26,10 @@ services:
- backend_node_modules:/nitro-backend/node_modules
- uploads:/nitro-backend/public/uploads
command: yarn run dev
environment:
- SMTP_HOST=mailserver
- SMTP_PORT=25
- SMTP_IGNORE_TLS=true
neo4j:
environment:
- NEO4J_AUTH=none

View File

@ -83,6 +83,7 @@ export default {
deleteContributions: false,
deleteComments: false,
deleteEnabled: false,
enableDeletionValue: null,
}
},
computed: {

View File

@ -1,10 +1,12 @@
import { mount, createLocalVue } from '@vue/test-utils'
import FilterMenu from './FilterMenu.vue'
import Styleguide from '@human-connection/styleguide'
import VTooltip from 'v-tooltip'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
describe('FilterMenu.vue', () => {
let wrapper

View File

@ -11,18 +11,21 @@
id="oldPassword"
model="oldPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-old-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" />

View 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']])
})
})
})
})
})
})
})

View 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>

View 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' }]])
})
})
})
})
})

View 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>

View 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)
})
})
})
})

View 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>

View File

@ -8,11 +8,32 @@
"logout": "Ausloggen",
"email": "Deine E-Mail",
"password": "Dein Passwort",
"forgotPassword": "Passwort vergessen?",
"moreInfo": "Was ist Human Connection?",
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"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": {
"placeholder": "Schreib etwas Inspirierendes...",
"mention": {
@ -199,7 +220,11 @@
"name": "Name",
"loadMore": "mehr laden",
"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": {
"loading": "lade",

View File

@ -8,11 +8,32 @@
"logout": "Logout",
"email": "Your Email",
"password": "Your Password",
"forgotPassword": "Forgot Password?",
"moreInfo": "What is Human Connection?",
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"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": {
"placeholder": "Leave your inspirational thoughts...",
"mention": {
@ -199,7 +220,11 @@
"name": "Name",
"loadMore": "load more",
"loading": "loading",
"reportContent": "Report"
"reportContent": "Report",
"validations": {
"email": "must be a valid email address",
"verification-code": "must be 6 characters long"
}
},
"actions": {
"loading": "loading",

View File

@ -25,7 +25,18 @@ module.exports = {
env: {
// 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
keepAlivePages: ['index'],
// active locales

View File

@ -29,6 +29,7 @@
"!**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
],
"transform": {
@ -59,7 +60,7 @@
"apollo-client": "~2.6.3",
"cookie-universal-nuxt": "~2.0.16",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.34",
"date-fns": "2.0.0-alpha.35",
"express": "~4.17.1",
"graphql": "~14.3.1",
"jsonwebtoken": "~8.5.1",
@ -94,7 +95,7 @@
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~2.1.2",
"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-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",
@ -110,4 +111,4 @@
"vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0"
}
}
}

View File

@ -45,6 +45,11 @@
name="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
:loading="pending"
primary

View 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>

View 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>

View 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>

View 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>

View File

@ -3754,10 +3754,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.34:
version "2.0.0-alpha.34"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.34.tgz#5d3ae7ca0d08915ccfc87a20545250af4e9c3cae"
integrity sha512-yjSYUHASHvzOZl++cEms+Tw7oQOFA+7Z6/lL7L3lRO9j6pMfT48N6oEyvCGo/MVlH08XWmydgf8X9Y1eedf9sQ==
date-fns@2.0.0-alpha.35:
version "2.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.35.tgz#185813cdc51b05cc1468a95116494bb3f3440934"
integrity sha512-dAY1ujqRtyUsa9mVeupyMzUluWo1d7kAMwyXTQHFImKYSHKvxDw/dipiY6fdswQOs8CwpGoiKysGfaaRP5r3bA==
date-now@^0.1.4:
version "0.1.4"
@ -4300,10 +4300,10 @@ eslint-plugin-import@~2.17.3:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.6.4:
version "22.6.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
eslint-plugin-jest@~22.7.0:
version "22.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.0.tgz#a1d325bccb024b04f5354c56fe790baba54a454c"
integrity sha512-0U9nBd9V6+GKpM/KvRDcmMuPsewSsdM7NxCozgJkVAh8IrwHmQ0aw44/eYuVkhT8Fcdhsz0zYiyPtKg147eXMQ==
eslint-plugin-node@~9.1.0:
version "9.1.0"