Merge pull request #994 from Human-Connection/signup_as_admin

Signup new users as admin
This commit is contained in:
Robert Schäfer 2019-07-10 11:46:50 +02:00 committed by GitHub
commit e6e9b1b7d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 791 additions and 141 deletions

View File

@ -276,9 +276,9 @@ When("I fill the password form with:", table => {
table = table.rowsHash();
cy.get("input[id=oldPassword]")
.type(table["Your old password"])
.get("input[id=newPassword]")
.get("input[id=password]")
.type(table["Your new passsword"])
.get("input[id=confirmPassword]")
.get("input[id=passwordConfirmation]")
.type(table["Confirm new password"]);
});

View File

@ -54,7 +54,7 @@ describe('ChangePassword.vue', () => {
describe('match', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('some secret')
wrapper.find('input#newPassword').setValue('some secret')
wrapper.find('input#password').setValue('some secret')
})
it('invalid', () => {
@ -90,8 +90,8 @@ describe('ChangePassword.vue', () => {
describe('given valid input', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('supersecret')
wrapper.find('input#newPassword').setValue('superdupersecret')
wrapper.find('input#confirmPassword').setValue('superdupersecret')
wrapper.find('input#password').setValue('superdupersecret')
wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
})
describe('submit form', () => {
@ -109,8 +109,8 @@ describe('ChangePassword.vue', () => {
expect.objectContaining({
variables: {
oldPassword: 'supersecret',
newPassword: 'superdupersecret',
confirmPassword: 'superdupersecret',
password: 'superdupersecret',
passwordConfirmation: 'superdupersecret',
},
}),
)
@ -135,8 +135,8 @@ describe('ChangePassword.vue', () => {
/* describe('mutation rejects', () => {
beforeEach(async () => {
await wrapper.find('input#oldPassword').setValue('supersecret')
await wrapper.find('input#newPassword').setValue('supersecret')
await wrapper.find('input#confirmPassword').setValue('supersecret')
await wrapper.find('input#password').setValue('supersecret')
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
})
it('displays error message', async () => {

View File

@ -1,12 +1,6 @@
<template>
<ds-form
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
@input="handleInput"
@input-valid="handleInputValid"
>
<template>
<ds-form v-model="formData" :schema="formSchema" @submit="handleSubmit">
<template slot-scope="{ errors }">
<ds-input
id="oldPassword"
model="oldPassword"
@ -15,22 +9,22 @@
:label="$t('settings.security.change-password.label-old-password')"
/>
<ds-input
id="newPassword"
model="newPassword"
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />
<password-strength :password="formData.password" />
<ds-space margin-top="base">
<ds-button :loading="loading" :disabled="disabled" primary>
<ds-button :loading="loading" :disabled="errors" primary>
{{ $t('settings.security.change-password.button') }}
</ds-button>
</ds-space>
@ -41,6 +35,7 @@
<script>
import gql from 'graphql-tag'
import PasswordStrength from './Strength'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export default {
name: 'ChangePassword',
@ -48,11 +43,11 @@ export default {
PasswordStrength,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: '',
...passwordForm.formData,
},
formSchema: {
oldPassword: {
@ -60,38 +55,18 @@ export default {
required: true,
message: this.$t('settings.security.change-password.message-old-password-required'),
},
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',
),
},
],
...passwordForm.formSchema,
},
loading: false,
disabled: true,
}
},
methods: {
async handleInput(data) {
this.disabled = true
},
async handleInputValid(data) {
this.disabled = false
},
async handleSubmit(data) {
this.loading = true
const mutation = gql`
mutation($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
mutation($oldPassword: String!, $password: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $password)
}
`
const variables = this.formData
@ -102,8 +77,8 @@ export default {
this.$toast.success(this.$t('settings.security.change-password.success'))
this.formData = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
password: '',
passwordConfirmation: '',
}
} catch (err) {
this.$toast.error(err.message)
@ -111,15 +86,6 @@ export default {
this.loading = false
}
},
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

@ -47,8 +47,8 @@ describe('ChangePassword ', () => {
describe('submitting new password', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('input#newPassword').setValue('supersecret')
wrapper.find('input#confirmPassword').setValue('supersecret')
wrapper.find('input#password').setValue('supersecret')
wrapper.find('input#passwordConfirmation').setValue('supersecret')
wrapper.find('form').trigger('submit')
})
@ -58,7 +58,7 @@ describe('ChangePassword ', () => {
it('delivers new password to backend', () => {
const expected = expect.objectContaining({
variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' },
variables: { code: '123456', email: 'mail@example.org', password: 'supersecret' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})

View File

@ -1,54 +1,52 @@
<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-form
v-if="!changePasswordResult"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitPassword"
class="change-password"
>
<template slot-scope="{ errors }">
<ds-input
id="newPassword"
model="newPassword"
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />
<password-strength :password="formData.password" />
<ds-space margin-top="base">
<ds-button :loading="$apollo.loading" :disabled="disabled" primary>
<ds-button :loading="$apollo.loading" :disabled="errors" 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>
</template>
</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>
</ds-space>
</ds-card>
</template>
@ -57,6 +55,7 @@
import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export default {
components: {
@ -68,48 +67,28 @@ export default {
code: { type: String, required: true },
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
newPassword: '',
confirmPassword: '',
...passwordForm.formData,
},
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',
),
},
],
...passwordForm.formSchema,
},
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)
mutation($code: String!, $email: String!, $password: String!) {
resetPassword(code: $code, email: $email, newPassword: $password)
}
`
const { newPassword } = this.formData
const { password } = this.formData
const { email, code } = this
const variables = { newPassword, email, code }
const variables = { password, email, code }
try {
const {
data: { resetPassword },
@ -119,22 +98,13 @@ export default {
this.$emit('passwordResetResponse', this.changePasswordResult)
}, 3000)
this.formData = {
newPassword: '',
confirmPassword: '',
password: '',
passwordConfirmation: '',
}
} 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,132 @@
import { mount, createLocalVue } from '@vue/test-utils'
import CreateUserAccount, { SignupVerificationMutation } from './CreateUserAccount'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('CreateUserAccount', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn(),
},
}
propsData = {}
})
describe('mount', () => {
Wrapper = () => {
return mount(CreateUserAccount, {
mocks,
propsData,
localVue,
})
}
describe('given email and nonce', () => {
beforeEach(() => {
propsData.nonce = '666777'
propsData.email = 'sixseven@example.org'
})
it('renders a form to create a new user', () => {
wrapper = Wrapper()
expect(wrapper.find('.create-user-account').exists()).toBe(true)
})
describe('submit', () => {
let action
beforeEach(() => {
action = async () => {
wrapper = Wrapper()
wrapper.find('input#name').setValue('John Doe')
wrapper.find('input#password').setValue('hellopassword')
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
it('calls CreateUserAccount graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers data to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: {
about: '',
name: 'John Doe',
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case mutation resolves', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockResolvedValue({
data: {
SignupVerification: {
id: 'u1',
name: 'John Doe',
slug: 'john-doe',
},
},
})
})
it('displays success', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith('registration.create-user-account.success')
})
describe('after timeout', () => {
beforeEach(jest.useFakeTimers)
it('emits `userCreated` with { password, email }', async () => {
await action()
jest.runAllTimers()
expect(wrapper.emitted('userCreated')).toEqual([
[
{
email: 'sixseven@example.org',
password: 'hellopassword',
},
],
])
})
})
})
describe('in case mutation rejects', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce'))
})
it('displays form errors', async () => {
await action()
expect(wrapper.find('.backendErrors').text()).toContain('Invalid nonce')
})
})
})
})
})
})

View File

@ -0,0 +1,142 @@
<template>
<ds-card v-if="success" class="success">
<ds-space>
<sweetalert-icon icon="success" />
<ds-text align="center" bold color="success">
{{ $t('registration.create-user-account.success') }}
</ds-text>
</ds-space>
</ds-card>
<ds-form
v-else
class="create-user-account"
v-model="formData"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }">
<ds-card :header="$t('registration.create-user-account.title')">
<ds-input
id="name"
model="name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<ds-input
id="bio"
model="about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<ds-input
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.password" />
<template slot="footer">
<ds-space class="backendErrors" v-if="backendErrors">
<ds-text align="center" bold color="danger">
{{ backendErrors.message }}
</ds-text>
</ds-space>
<ds-button
style="float: right;"
icon="check"
type="submit"
:loading="$apollo.loading"
:disabled="errors"
primary
>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export const SignupVerificationMutation = gql`
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
id
name
slug
}
}
`
export default {
components: {
PasswordStrength,
SweetalertIcon,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
name: '',
about: '',
...passwordForm.formData,
},
formSchema: {
name: {
type: 'string',
required: true,
min: 3,
},
about: {
type: 'string',
required: false,
},
...passwordForm.formSchema,
},
disabled: true,
success: null,
backendErrors: null,
}
},
props: {
nonce: { type: String, required: true },
email: { type: String, required: true },
},
methods: {
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: { name, password, about, email, nonce },
})
this.success = true
setTimeout(() => {
this.$emit('userCreated', {
email,
password,
})
}, 3000)
} catch (err) {
this.backendErrors = err
}
},
},
}
</script>

View File

@ -0,0 +1,146 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('Signup', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }),
},
}
propsData = {}
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
return mount(Signup, {
mocks,
propsData,
localVue,
})
}
describe('without invitation code', () => {
it('renders signup form', () => {
wrapper = Wrapper()
expect(wrapper.find('.signup').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 Signup graphql mutation', () => {
const expected = expect.objectContaining({ mutation: SignupMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers email to backend', () => {
const expected = expect.objectContaining({
variables: { email: 'mail@example.org', token: null },
})
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 mail for email verification was sent', () => {
const expected = ['registration.signup.form.success', { 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' }]])
})
})
})
})
describe('with invitation code', () => {
let action
beforeEach(() => {
propsData.token = '666777'
action = async () => {
wrapper = Wrapper()
wrapper.find('input#email').setValue('mail@example.org')
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
describe('submit', () => {
it('calls SignupByInvitation graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupByInvitationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers invitation token to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: { email: 'mail@example.org', token: '666777' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case a user account with the email already exists', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: User account with this email already exists.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith('registration.signup.form.errors.email-exists')
})
})
describe('in case the invitation code was incorrect', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: Invitation code already used or does not exist.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith(
'registration.signup.form.errors.invalid-invitation-token',
)
})
})
})
})
})
})

View File

@ -0,0 +1,141 @@
<template>
<ds-card class="signup">
<ds-space margin="large">
<ds-form
v-if="!success && !error"
@input="handleInput"
@input-valid="handleInputValid"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<h1>{{ $t('registration.signup.title') }}</h1>
<ds-space v-if="token" margin-botton="large">
<ds-text v-html="$t('registration.signup.form.invitation-code', { code: token })" />
</ds-space>
<ds-space margin-botton="large">
<ds-text>
{{ $t('registration.signup.form.description') }}
</ds-text>
</ds-space>
<ds-input
:placeholder="$t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<ds-button
:disabled="disabled"
:loading="$apollo.loading"
primary
fullwidth
name="submit"
type="submit"
icon="envelope"
>
{{ $t('registration.signup.form.submit') }}
</ds-button>
</ds-form>
<div v-else>
<template v-if="!error">
<sweetalert-icon icon="info" />
<ds-text align="center" v-html="submitMessage" />
</template>
<template v-else>
<sweetalert-icon icon="error" />
<ds-text align="center">
{{ error.message }}
</ds-text>
</template>
</div>
</ds-space>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql`
mutation($email: String!) {
Signup(email: $email) {
email
}
}
`
export const SignupByInvitationMutation = gql`
mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) {
email
}
}
`
export default {
components: {
SweetalertIcon,
},
props: {
token: { type: String, default: null },
},
data() {
return {
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
disabled: true,
success: false,
error: null,
}
},
computed: {
submitMessage() {
const { email } = this.formData
return this.$t('registration.signup.form.success', { email })
},
},
methods: {
handleInput() {
this.disabled = true
},
handleInputValid() {
this.disabled = false
},
async handleSubmit() {
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
const { email } = this.formData
const { token } = this
try {
await this.$apollo.mutate({ mutation, variables: { email, token } })
this.success = true
setTimeout(() => {
this.$emit('handleSubmitted', { email })
}, 3000)
} catch (err) {
const { message } = err
const mapping = {
'User account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {
if (message.includes(pattern))
this.error = { key, message: this.$t(`registration.signup.form.errors.${key}`) }
}
if (!this.error) {
this.$toast.error(message)
}
}
},
},
}
</script>

View File

@ -0,0 +1,36 @@
export default function PasswordForm({ translate }) {
const passwordMismatchMessage = translate(
'settings.security.change-password.message-new-password-missmatch',
)
return {
formData: {
password: '',
passwordConfirmation: '',
},
formSchema: {
password: {
type: 'string',
required: true,
message: translate('settings.security.change-password.message-new-password-required'),
},
passwordConfirmation: [
{
validator(rule, value, callback, source, options) {
var errors = []
if (source.password !== value) {
errors.push(new Error(passwordMismatchMessage))
}
callback(errors)
},
},
{
type: 'string',
required: true,
message: translate(
'settings.security.change-password.message-new-password-confirm-required',
),
},
],
},
}
}

View File

@ -29,7 +29,8 @@
"moreInfo": "Was ist Human Connection?",
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"hello": "Hallo"
"hello": "Hallo",
"success": "Du bist eingeloggt!"
},
"password-reset": {
"title": "Passwort zurücksetzen",
@ -39,6 +40,24 @@
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
}
},
"registration": {
"signup": {
"title": "Mach mit bei Human Connection!",
"form": {
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
"errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
},
"submit": "Konto erstellen",
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
}
},
"create-user-account": {
"title": "Benutzerkonto anlegen",
"success": "Dein Benutzerkonto wurde erstellt!"
}
},
"verify-code": {
"form": {
"code": "Code eingeben",
@ -189,6 +208,11 @@
},
"settings": {
"name": "Einstellungen"
},
"invites": {
"name": "Benutzer einladen",
"title": "Benutzer als Admin anmelden",
"description": "Dieses Anmeldeformular ist zu sehen sobald die Anmeldung öffentlich zugänglich ist."
}
},
"post": {

View File

@ -29,7 +29,8 @@
"moreInfo": "What is Human Connection?",
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"hello": "Hello"
"hello": "Hello",
"success": "You are logged in!"
},
"password-reset": {
"title": "Reset your password",
@ -39,6 +40,25 @@
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
}
},
"registration": {
"signup": {
"title": "Join Human Connection!",
"form": {
"description": "To get started, enter your email address:",
"invitation-code": "Your invitation code is: <b>{code}</b>",
"errors": {
"email-exists": "There is already a user account with this email address!",
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
},
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
}
},
"create-user-account": {
"title": "Create user account",
"success": "Your account has been created!"
}
},
"verify-code": {
"form": {
"code": "Enter your code",
@ -189,6 +209,11 @@
},
"settings": {
"name": "Settings"
},
"invites": {
"name": "Invite users",
"title": "Signup users as admin",
"description": "This registration form will be visible as soon as the registration is open to the public."
}
},
"post": {

View File

@ -31,10 +31,10 @@ module.exports = {
'password-reset-request',
'password-reset-verify-code',
'password-reset-change-password',
'register',
'signup',
'reset',
'reset-token',
// 'registration-signup', TODO: uncomment to open public registration
'registration-signup-by-invitation-code',
'registration-verify-code',
'registration-create-user-account',
'pages-slug',
],
// pages to keep alive

View File

@ -52,6 +52,10 @@ export default {
name: this.$t('admin.tags.name'),
path: `/admin/tags`,
},
{
name: this.$t('admin.invites.name'),
path: `/admin/invite`,
},
// TODO implement
/* {
name: this.$t('admin.settings.name'),

View File

@ -0,0 +1,23 @@
<template>
<ds-section>
<ds-space>
<ds-heading size="h3">
{{ $t('admin.invites.title') }}
</ds-heading>
<ds-text>
{{ $t('admin.invites.description') }}
</ds-text>
</ds-space>
<signup />
</ds-section>
</template>
<script>
import Signup from '~/components/Registration/Signup'
export default {
components: {
Signup,
},
}
</script>

View File

@ -115,7 +115,7 @@ export default {
async onSubmit() {
try {
await this.$store.dispatch('auth/login', { ...this.form })
this.$toast.success('You are logged in!')
this.$toast.success(this.$t('login.success'))
this.$router.replace(this.$route.query.path || '/')
} catch (err) {
this.$toast.error(err.message)

View File

@ -0,0 +1,14 @@
<template>
<nuxt-child />
</template>
<script>
export default {
layout: 'default',
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
}
</script>

View File

@ -0,0 +1,27 @@
<template>
<create-user-account @userCreated="handleUserCreated" :email="email" :nonce="nonce" />
</template>
<script>
import CreateUserAccount from '~/components/Registration/CreateUserAccount'
export default {
data() {
const { nonce = '', email = '' } = this.$route.query
return { nonce, email }
},
components: {
CreateUserAccount,
},
methods: {
async handleUserCreated({ email, password }) {
try {
await this.$store.dispatch('auth/login', { email, password })
this.$toast.success('You are logged in!')
this.$router.push('/')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>