Add register via username and refactor email-login to username-login (#313)

* Added config option to enable/disable the email signup

* Added register API call

* Added register button and error handling

* Show register button only if enabled in config

* Renamed login handler, added dir-attribute, removed obsolete css class

* Added separate registration page, changed email-login to internal-login

As an username is sufficient for registration, this commit changes the email-login into an username-based login. This login method is now called "internal" in the code.
This commit also introduces a new registration page instead of using the same form as for login.

* Added information texts below form fields

* Added error differentiation

* Added CHANGELOG entry

* Replace "magic string" with Enum representation

* Removed password-field to DOM rewrite

With the value attribute set, the password would be written to the DOM while typing. That's bad practise as attackers could read that password (e.g. with dirty CSS-hacks).

* Fixed backendConfig to config renaming

* Fixed links on register page being external links

* Refactored error handling to use string-enum that corresponds with i18n keys

* Fix chrome warnings regarding autocomplete and duplicated id

* Refactor login action buttons to use callbacks and handle promises directly

* Remove unnecessary async function

* Added promise chaining
This commit is contained in:
Erik Michelson 2020-08-04 23:13:12 +02:00 committed by GitHub
parent 4054e130bb
commit dbce0181a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 347 additions and 119 deletions

View file

@ -38,6 +38,7 @@
### Changed ### Changed
- The sign-in/sign-up functions are now on a separate page - The sign-in/sign-up functions are now on a separate page
- The email sign-in/registration does not require an email address anymore but uses a username
- The history shows both the entries saved in LocalStorage and the entries saved on the server together - The history shows both the entries saved in LocalStorage and the entries saved on the server together
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube - The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
- Use [Twemoji](https://twemoji.twitter.com/) as icon font - Use [Twemoji](https://twemoji.twitter.com/) as icon font

View file

@ -10,9 +10,10 @@
"google": true, "google": true,
"saml": true, "saml": true,
"oauth2": true, "oauth2": true,
"email": true, "internal": true,
"openid": true "openid": true
}, },
"allowRegister": true,
"branding": { "branding": {
"name": "ACME Corp", "name": "ACME Corp",
"logo": "http://localhost:3001/acme.png" "logo": "http://localhost:3001/acme.png"

View file

@ -3,5 +3,5 @@
"photo": "https://1.gravatar.com/avatar/767fc9c115a1b989744c755db47feb60?s=200&r=pg&d=mp", "photo": "https://1.gravatar.com/avatar/767fc9c115a1b989744c755db47feb60?s=200&r=pg&d=mp",
"name": "Test", "name": "Test",
"status": "ok", "status": "ok",
"provider": "email" "provider": "internal"
} }

View file

@ -181,7 +181,9 @@
"signInVia": "لِج عبر {{service}}", "signInVia": "لِج عبر {{service}}",
"signIn": "لِج", "signIn": "لِج",
"signOut": "خروج", "signOut": "خروج",
"register": "انشئ حسابا", "register": {
"title": "انشئ حسابا"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Entrar a través de {{service}}", "signInVia": "Entrar a través de {{service}}",
"signIn": "Entrar", "signIn": "Entrar",
"signOut": "Sortir", "signOut": "Sortir",
"register": "Registrar-se", "register": {
"title": "Registrar-se"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Přihlásit se přes {{service}}", "signInVia": "Přihlásit se přes {{service}}",
"signIn": "Přihlásit", "signIn": "Přihlásit",
"signOut": "Odhlásit", "signOut": "Odhlásit",
"register": "Registrovat", "register": {
"title": "Registrovat"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,15 +181,16 @@
"signInVia": "Einloggen über {{service}}", "signInVia": "Einloggen über {{service}}",
"signIn": "Einloggen", "signIn": "Einloggen",
"signOut": "Ausloggen", "signOut": "Ausloggen",
"register": "Registrieren", "register": {
"title": "Registrieren"
},
"auth": { "auth": {
"email": "E-Mail", "email": "E-Mail",
"password": "Passwort", "password": "Passwort",
"username": "Benutzername", "username": "Benutzername",
"error": { "error": {
"openIdLogin": "OpenID nicht korrekt", "openIdLogin": "OpenID nicht korrekt",
"emailLogin": "E-Mail oder Passwort nicht korrekt", "usernamePassword": "Benutzername oder Passwort nicht korrekt"
"ldapLogin": "Benutzername oder Passwort nicht korrekt"
} }
} }
} }

View file

@ -284,15 +284,24 @@
"signInVia": "Sign in via {{service}}", "signInVia": "Sign in via {{service}}",
"signIn": "Sign In", "signIn": "Sign In",
"signOut": "Sign Out", "signOut": "Sign Out",
"register": "Register",
"auth": { "auth": {
"email": "Email", "email": "Email",
"password": "Password", "password": "Password",
"username": "Username", "username": "Username",
"error": { "error": {
"openIdLogin": "Invalid OpenID provided", "openIdLogin": "Invalid OpenID provided",
"emailLogin": "Invalid email or password", "usernamePassword": "Invalid username or password"
"ldapLogin": "Invalid username or password" }
},
"register": {
"title": "Register",
"passwordAgain": "Password (again)",
"usernameInfo": "The username is your unique identifier for login.",
"passwordInfo": "Choose a unique and secure password. It must contain at least 8 characters.",
"infoTermsPrivacy": "With the registration of my user account I agree to the following terms:",
"error": {
"usernameExisting": "There is already an account with this username.",
"other": "There was an error while registering your account. Just try it again."
} }
} }
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Ingresar via {{service}}", "signInVia": "Ingresar via {{service}}",
"signIn": "Ingresar", "signIn": "Ingresar",
"signOut": "Salir", "signOut": "Salir",
"register": "Registrar", "register": {
"title": "Registrar"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Se connecter depuis {{service}}", "signInVia": "Se connecter depuis {{service}}",
"signIn": "Se connecter", "signIn": "Se connecter",
"signOut": "Se déconnecter", "signOut": "Se déconnecter",
"register": "S'enregistrer", "register": {
"title": "S'enregistrer"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Masuk menggunakan {{service}}", "signInVia": "Masuk menggunakan {{service}}",
"signIn": "Masuk", "signIn": "Masuk",
"signOut": "Keluar", "signOut": "Keluar",
"register": "Daftar", "register": {
"title": "Daftar"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Entra con {{service}}", "signInVia": "Entra con {{service}}",
"signIn": "Entra", "signIn": "Entra",
"signOut": "Disconettiti", "signOut": "Disconettiti",
"register": "Registrati", "register": {
"title": "Registrati"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "{{service}}でサインイン", "signInVia": "{{service}}でサインイン",
"signIn": "サインイン", "signIn": "サインイン",
"signOut": "サインアウト", "signOut": "サインアウト",
"register": "登録", "register": {
"title": "登録"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Log in via {{service}}", "signInVia": "Log in via {{service}}",
"signIn": "Inloggen", "signIn": "Inloggen",
"signOut": "Uitloggen", "signOut": "Uitloggen",
"register": "Registreren", "register": {
"title": "Registreren"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Zaloguj się poprzez {{service}}", "signInVia": "Zaloguj się poprzez {{service}}",
"signIn": "Zaloguj się", "signIn": "Zaloguj się",
"signOut": "Wyloguj się", "signOut": "Wyloguj się",
"register": "Zarejestruj", "register": {
"title": "Zarejestruj"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Entrar via {{service}}", "signInVia": "Entrar via {{service}}",
"signIn": "Entrar", "signIn": "Entrar",
"signOut": "Sair", "signOut": "Sair",
"register": "Register", "register": {
"title": "Register"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Войти с помощью {{service}}", "signInVia": "Войти с помощью {{service}}",
"signIn": "Войти", "signIn": "Войти",
"signOut": "Выйти", "signOut": "Выйти",
"register": "Регистрация", "register": {
"title": "Регистрация"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Prihlásiť sa cez {{service}}", "signInVia": "Prihlásiť sa cez {{service}}",
"signIn": "Prihlásiť sa", "signIn": "Prihlásiť sa",
"signOut": "Odhlásiť sa", "signOut": "Odhlásiť sa",
"register": "Registrovať", "register": {
"title": "Registrovať"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -180,7 +180,9 @@
"signInVia": "Пријави се уз {{service}}", "signInVia": "Пријави се уз {{service}}",
"signIn": "Пријави се", "signIn": "Пријави се",
"signOut": "Одјави се", "signOut": "Одјави се",
"register": "Региструј се", "register": {
"title": "Региструј се"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "Logga in via {{service}}", "signInVia": "Logga in via {{service}}",
"signIn": "Logga in", "signIn": "Logga in",
"signOut": "Logga ut", "signOut": "Logga ut",
"register": "Registrera", "register": {
"title": "Registrera"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -180,7 +180,9 @@
"signInVia": "Đăng nhấp với {{service}}", "signInVia": "Đăng nhấp với {{service}}",
"signIn": "Đăng nhập", "signIn": "Đăng nhập",
"signOut": "Đăng xuất", "signOut": "Đăng xuất",
"register": "Đăng ký", "register": {
"title": "Đăng ký"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "通过 {{service}} 登录", "signInVia": "通过 {{service}} 登录",
"signIn": "登录", "signIn": "登录",
"signOut": "登出", "signOut": "登出",
"register": "注册", "register": {
"title": "注册"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -181,7 +181,9 @@
"signInVia": "透過 {{service}} 登入", "signInVia": "透過 {{service}} 登入",
"signIn": "登入", "signIn": "登入",
"signOut": "登出", "signOut": "登出",
"register": "註冊", "register": {
"title": "註冊"
},
"auth": { "auth": {
"error": {} "error": {}
} }

View file

@ -1,12 +1,13 @@
import { RegisterError } from '../components/landing/pages/register/register'
import { expectResponseCode, getApiUrl } from '../utils/apiUtils' import { expectResponseCode, getApiUrl } from '../utils/apiUtils'
import { defaultFetchConfig } from './default' import { defaultFetchConfig } from './default'
export const doEmailLogin = async (email: string, password: string): Promise<void> => { export const doInternalLogin = async (username: string, password: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/auth/email', { const response = await fetch(getApiUrl() + '/auth/internal', {
...defaultFetchConfig, ...defaultFetchConfig,
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
email: email, username: username,
password: password password: password
}) })
}) })
@ -14,6 +15,23 @@ export const doEmailLogin = async (email: string, password: string): Promise<voi
expectResponseCode(response) expectResponseCode(response)
} }
export const doInternalRegister = async (username: string, password: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/auth/register', {
...defaultFetchConfig,
method: 'POST',
body: JSON.stringify({
username: username,
password: password
})
})
if (response.status === 409) {
throw new Error(RegisterError.USERNAME_EXISTING)
}
expectResponseCode(response)
}
export const doLdapLogin = async (username: string, password: string): Promise<void> => { export const doLdapLogin = async (username: string, password: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/auth/ldap', { const response = await fetch(getApiUrl() + '/auth/ldap', {
...defaultFetchConfig, ...defaultFetchConfig,

View file

@ -1,5 +1,6 @@
export interface Config { export interface Config {
allowAnonymous: boolean, allowAnonymous: boolean,
allowRegister: boolean,
authProviders: AuthProvidersState, authProviders: AuthProvidersState,
branding: BrandingConfig, branding: BrandingConfig,
banner: BannerConfig, banner: BannerConfig,
@ -36,7 +37,7 @@ export interface AuthProvidersState {
google: boolean, google: boolean,
saml: boolean, saml: boolean,
oauth2: boolean, oauth2: boolean,
email: boolean, internal: boolean,
openid: boolean, openid: boolean,
} }

View file

@ -1,64 +0,0 @@
import React, { FormEvent, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { doEmailLogin } from '../../../../../api/auth'
import { getAndSetUser } from '../../../../../utils/apiUtils'
export const ViaEMail: React.FC = () => {
const { t } = useTranslation()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(false)
const doAsyncLogin = async () => {
await doEmailLogin(email, password)
await getAndSetUser()
}
const onFormSubmit = (event: FormEvent) => {
doAsyncLogin().catch(() => setError(true))
event.preventDefault()
}
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="login.signInVia" values={{ service: 'E-Mail' }}/>
</Card.Title>
<Form onSubmit={onFormSubmit}>
<Form.Group controlId="email">
<Form.Control
isInvalid={error}
type="email"
size="sm"
placeholder={t('login.auth.email')}
onChange={(event) => setEmail(event.currentTarget.value)} className="bg-dark text-white"
/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t('login.auth.password')}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"/>
</Form.Group>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="login.auth.error.emailLogin"/>
</Alert>
<Button
type="submit"
variant="primary">
<Trans i18nKey="login.signIn"/>
</Button>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -0,0 +1,81 @@
import React, { FormEvent, useCallback, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { doInternalLogin } from '../../../../../api/auth'
import { ApplicationState } from '../../../../../redux'
import { getAndSetUser } from '../../../../../utils/apiUtils'
import { ShowIf } from '../../../../common/show-if/show-if'
export const ViaInternal: React.FC = () => {
const { t } = useTranslation()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(false)
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister)
const onLoginSubmit = useCallback((event: FormEvent) => {
doInternalLogin(username, password)
.then(() => getAndSetUser())
.catch(() => setError(true))
event.preventDefault()
}, [username, password])
return (
<Card className="bg-dark mb-4">
<Card.Body>
<Card.Title>
<Trans i18nKey="login.signInVia" values={{ service: t('login.auth.username') }}/>
</Card.Title>
<Form onSubmit={onLoginSubmit}>
<Form.Group controlId="internal-username">
<Form.Control
isInvalid={error}
type="text"
size="sm"
placeholder={t('login.auth.username')}
onChange={(event) => setUsername(event.currentTarget.value)} className="bg-dark text-white"
autoComplete='username'
/>
</Form.Group>
<Form.Group controlId="internal-password">
<Form.Control
isInvalid={error}
type="password"
size="sm"
placeholder={t('login.auth.password')}
onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"
autoComplete='current-password'
/>
</Form.Group>
<Alert className="small" show={error} variant="danger">
<Trans i18nKey="login.auth.error.usernamePassword"/>
</Alert>
<div className='flex flex-row' dir='auto'>
<Button
type="submit"
variant="primary"
className='mx-2'>
<Trans i18nKey="login.signIn"/>
</Button>
<ShowIf condition={allowRegister}>
<Link to={'/register'}>
<Button
type='button'
variant='secondary'
className='mx-2'>
<Trans i18nKey='login.register.title'/>
</Button>
</Link>
</ShowIf>
</div>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -1,4 +1,4 @@
import React, { FormEvent, useState } from 'react' import React, { FormEvent, useCallback, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap' import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
@ -17,19 +17,12 @@ export const ViaLdap: React.FC = () => {
const name = ldapCustomName ? `${ldapCustomName} (LDAP)` : 'LDAP' const name = ldapCustomName ? `${ldapCustomName} (LDAP)` : 'LDAP'
const doAsyncLogin = async () => { const onLoginSubmit = useCallback((event: FormEvent) => {
try { doLdapLogin(username, password)
await doLdapLogin(username, password) .then(() => getAndSetUser())
await getAndSetUser() .catch(() => setError(true))
} catch {
setError(true)
}
}
const onFormSubmit = (event: FormEvent) => {
doAsyncLogin().catch(() => setError(true))
event.preventDefault() event.preventDefault()
} }, [username, password])
return ( return (
<Card className="bg-dark mb-4"> <Card className="bg-dark mb-4">
@ -37,29 +30,32 @@ export const ViaLdap: React.FC = () => {
<Card.Title> <Card.Title>
<Trans i18nKey="login.signInVia" values={{ service: name }}/> <Trans i18nKey="login.signInVia" values={{ service: name }}/>
</Card.Title> </Card.Title>
<Form onSubmit={onFormSubmit}> <Form onSubmit={onLoginSubmit}>
<Form.Group controlId="username"> <Form.Group controlId="ldap-username">
<Form.Control <Form.Control
isInvalid={error} isInvalid={error}
type="text" type="text"
size="sm" size="sm"
placeholder={t('login.auth.username')} placeholder={t('login.auth.username')}
onChange={(event) => setUsername(event.currentTarget.value)} className="bg-dark text-white" onChange={(event) => setUsername(event.currentTarget.value)} className="bg-dark text-white"
autoComplete='username'
/> />
</Form.Group> </Form.Group>
<Form.Group controlId="password"> <Form.Group controlId="ldap-password">
<Form.Control <Form.Control
isInvalid={error} isInvalid={error}
type="password" type="password"
size="sm" size="sm"
placeholder={t('login.auth.password')} placeholder={t('login.auth.password')}
onChange={(event) => setPassword(event.currentTarget.value)} onChange={(event) => setPassword(event.currentTarget.value)}
className="bg-dark text-white"/> className="bg-dark text-white"
autoComplete='current-password'
/>
</Form.Group> </Form.Group>
<Alert className="small" show={error} variant="danger"> <Alert className="small" show={error} variant="danger">
<Trans i18nKey="login.auth.error.ldapLogin"/> <Trans i18nKey="login.auth.error.usernamePassword"/>
</Alert> </Alert>
<Button <Button

View file

@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'
import { Redirect } from 'react-router' import { Redirect } from 'react-router'
import { ApplicationState } from '../../../../redux' import { ApplicationState } from '../../../../redux'
import { ShowIf } from '../../../common/show-if/show-if' import { ShowIf } from '../../../common/show-if/show-if'
import { ViaEMail } from './auth/via-email' import { ViaInternal } from './auth/via-internal'
import { ViaLdap } from './auth/via-ldap' import { ViaLdap } from './auth/via-ldap'
import { OneClickType, ViaOneClick } from './auth/via-one-click' import { OneClickType, ViaOneClick } from './auth/via-one-click'
import { ViaOpenId } from './auth/via-openid' import { ViaOpenId } from './auth/via-openid'
@ -40,9 +40,9 @@ export const Login: React.FC = () => {
return ( return (
<div className="my-3"> <div className="my-3">
<Row className="h-100 flex justify-content-center"> <Row className="h-100 flex justify-content-center">
<ShowIf condition={authProviders.email || authProviders.ldap || authProviders.openid}> <ShowIf condition={authProviders.internal || authProviders.ldap || authProviders.openid}>
<Col xs={12} sm={10} lg={4}> <Col xs={12} sm={10} lg={4}>
<ShowIf condition={authProviders.email}><ViaEMail/></ShowIf> <ShowIf condition={authProviders.internal}><ViaInternal/></ShowIf>
<ShowIf condition={authProviders.ldap}><ViaLdap/></ShowIf> <ShowIf condition={authProviders.ldap}><ViaLdap/></ShowIf>
<ShowIf condition={authProviders.openid}><ViaOpenId/></ShowIf> <ShowIf condition={authProviders.openid}><ViaOpenId/></ShowIf>
</Col> </Col>

View file

@ -23,7 +23,7 @@ export const Profile: React.FC = () => {
<Row className="h-100 flex justify-content-center"> <Row className="h-100 flex justify-content-center">
<Col lg={6}> <Col lg={6}>
<ProfileDisplayName/> <ProfileDisplayName/>
<ShowIf condition={user.provider === LoginProvider.EMAIL}> <ShowIf condition={user.provider === LoginProvider.INTERNAL}>
<ProfileChangePassword/> <ProfileChangePassword/>
</ShowIf> </ShowIf>
<ProfileAccountManagement/> <ProfileAccountManagement/>

View file

@ -0,0 +1,141 @@
import React, { FormEvent, useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
import { doInternalRegister } from '../../../../api/auth'
import { ApplicationState } from '../../../../redux'
import { getAndSetUser } from '../../../../utils/apiUtils'
import { Row, Col, Card, Form, Button, Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { ShowIf } from '../../../common/show-if/show-if'
export enum RegisterError {
NONE = 'none',
USERNAME_EXISTING = 'usernameExisting',
OTHER = 'other'
}
export const Register: React.FC = () => {
const { t } = useTranslation()
const config = useSelector((state: ApplicationState) => state.config)
const user = useSelector((state: ApplicationState) => state.user)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [passwordAgain, setPasswordAgain] = useState('')
const [error, setError] = useState(RegisterError.NONE)
const [ready, setReady] = useState(false)
const doRegisterSubmit = useCallback((event: FormEvent) => {
doInternalRegister(username, password)
.then(() => getAndSetUser())
.catch((err: Error) => {
console.error(err)
setError(err.message === RegisterError.USERNAME_EXISTING ? err.message : RegisterError.OTHER)
})
event.preventDefault()
}, [username, password])
useEffect(() => {
setReady(username !== '' && password !== '' && password.length >= 8 && password === passwordAgain)
}, [username, password, passwordAgain])
if (!config.allowRegister) {
return (
<Redirect to={'/login'}/>
)
}
if (user) {
return (
<Redirect to={'/intro'}/>
)
}
return (
<div className='my-3'>
<h1 className='mb-4'><Trans i18nKey='login.register.title'/></h1>
<Row className='h-100 d-flex justify-content-center'>
<Col lg={6}>
<Card className='bg-dark mb-4 text-start'>
<Card.Body>
<Form onSubmit={doRegisterSubmit}>
<Form.Group controlId='username'>
<Form.Label><Trans i18nKey='login.auth.username'/></Form.Label>
<Form.Control
type='text'
size='sm'
value={username}
isValid={username !== ''}
onChange={(event) => setUsername(event.target.value)}
placeholder={t('login.auth.username')}
className='bg-dark text-white'
autoComplete='username'
autoFocus={true}
required
/>
<Form.Text><Trans i18nKey='login.register.usernameInfo'/></Form.Text>
</Form.Group>
<Form.Group controlId='password'>
<Form.Label><Trans i18nKey='login.auth.password'/></Form.Label>
<Form.Control
type='password'
size='sm'
isValid={password !== '' && password.length >= 8}
onChange={(event) => setPassword(event.target.value)}
placeholder={t('login.auth.password')}
className='bg-dark text-white'
minLength={8}
autoComplete='new-password'
required
/>
<Form.Text><Trans i18nKey='login.register.passwordInfo'/></Form.Text>
</Form.Group>
<Form.Group controlId='re-password'>
<Form.Label><Trans i18nKey='login.register.passwordAgain'/></Form.Label>
<Form.Control
type='password'
size='sm'
isInvalid={passwordAgain !== '' && password !== passwordAgain}
isValid={passwordAgain !== '' && password === passwordAgain}
onChange={(event) => setPasswordAgain(event.target.value)}
placeholder={t('login.register.passwordAgain')}
className='bg-dark text-white'
autoComplete='new-password'
required
/>
</Form.Group>
<ShowIf condition={!!config.specialLinks?.termsOfUse || !!config.specialLinks?.privacy}>
<Trans i18nKey='login.register.infoTermsPrivacy'/>
<ul>
<ShowIf condition={!!config.specialLinks?.termsOfUse}>
<li>
<TranslatedExternalLink i18nKey='landing.footer.termsOfUse' href={config.specialLinks.termsOfUse}/>
</li>
</ShowIf>
<ShowIf condition={!!config.specialLinks?.privacy}>
<li>
<TranslatedExternalLink i18nKey='landing.footer.privacy' href={config.specialLinks.privacy}/>
</li>
</ShowIf>
</ul>
</ShowIf>
<Button
variant='primary'
type='submit'
block={true}
disabled={!ready}>
<Trans i18nKey='login.register.title'/>
</Button>
</Form>
<br/>
<Alert show={error !== RegisterError.NONE} variant='danger'>
<Trans i18nKey={`login.register.error.${error}`}/>
</Alert>
</Card.Body>
</Card>
</Col>
</Row>
</div>
)
}

View file

@ -10,10 +10,11 @@ import { History } from './components/landing/pages/history/history'
import { Intro } from './components/landing/pages/intro/intro' import { Intro } from './components/landing/pages/intro/intro'
import { Login } from './components/landing/pages/login/login' import { Login } from './components/landing/pages/login/login'
import { Profile } from './components/landing/pages/profile/profile' import { Profile } from './components/landing/pages/profile/profile'
import { Register } from './components/landing/pages/register/register'
import { Redirector } from './components/redirector/redirector'
import './global-style/index.scss' import './global-style/index.scss'
import * as serviceWorker from './service-worker' import * as serviceWorker from './service-worker'
import { store } from './utils/store' import { store } from './utils/store'
import { Redirector } from './components/redirector/redirector'
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
@ -35,6 +36,11 @@ ReactDOM.render(
<Login/> <Login/>
</LandingLayout> </LandingLayout>
</Route> </Route>
<Route path="/register">
<LandingLayout>
<Register/>
</LandingLayout>
</Route>
<Route path="/profile"> <Route path="/profile">
<LandingLayout> <LandingLayout>
<Profile/> <Profile/>

View file

@ -4,6 +4,7 @@ import { ConfigActions, ConfigActionType, SetConfigAction } from './types'
export const initialState: Config = { export const initialState: Config = {
allowAnonymous: true, allowAnonymous: true,
allowRegister: true,
authProviders: { authProviders: {
facebook: false, facebook: false,
github: false, github: false,
@ -14,7 +15,7 @@ export const initialState: Config = {
google: false, google: false,
saml: false, saml: false,
oauth2: false, oauth2: false,
email: false, internal: false,
openid: false openid: false
}, },
branding: { branding: {

View file

@ -31,7 +31,7 @@ export enum LoginProvider {
GOOGLE = 'google', GOOGLE = 'google',
SAML = 'saml', SAML = 'saml',
OAUTH2 = 'oauth2', OAUTH2 = 'oauth2',
EMAIL = 'email', INTERNAL = 'internal',
LDAP = 'ldap', LDAP = 'ldap',
OPENID = 'openid' OPENID = 'openid'
} }