import React, { useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Field, Form } from 'react-final-form'
import { useDispatch } from 'react-redux'
import Button from '@material-ui/core/Button'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CircularProgress from '@material-ui/core/CircularProgress'
import Link from '@material-ui/core/Link'
import TextField from '@material-ui/core/TextField'
import { ThemeProvider, makeStyles } from '@material-ui/core/styles'
import {
createMuiTheme,
useLogin,
useNotify,
useTranslate,
useVersion,
} from 'react-admin'
import Logo from '../icons/android-icon-192x192.png'
import Notification from './Notification'
import useCurrentTheme from '../themes/useCurrentTheme'
import config from '../config'
import { clearQueue } from '../actions'
import { INSIGHTS_DOC_URL } from '../consts.js'
const useStyles = makeStyles(
(theme) => ({
main: {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'flex-start',
background: `url(${config.loginBackgroundURL})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'center',
},
card: {
minWidth: 300,
marginTop: '6em',
overflow: 'visible',
},
avatar: {
margin: '1em',
display: 'flex',
justifyContent: 'center',
marginTop: '-3em',
},
icon: {
backgroundColor: 'transparent',
width: '6.3em',
height: '6.3em',
},
systemName: {
marginTop: '1em',
display: 'flex',
justifyContent: 'center',
color: '#3f51b5',
},
welcome: {
marginTop: '1em',
padding: '0 1em 1em 1em',
display: 'flex',
justifyContent: 'center',
flexWrap: 'wrap',
color: '#3f51b5',
},
form: {
padding: '0 1em 1em 1em',
},
input: {
marginTop: '1em',
},
actions: {
padding: '0 1em 1em 1em',
},
button: {},
systemNameLink: {
textDecoration: 'none',
},
message: {
marginTop: '1em',
padding: '0 1em 1em 1em',
textAlign: 'center',
wordBreak: 'break-word',
fontSize: '0.875em',
},
}),
{ name: 'NDLogin' },
)
const renderInput = ({
meta: { touched, error } = {},
input: { ...inputProps },
...props
}) => (
<TextField
error={!!(touched && error)}
inputProps={{
autocapitalize: 'none',
autocorrect: 'off',
...inputProps,
}}
helperText={touched && error}
{...props}
fullWidth
/>
)
const FormLogin = ({ loading, handleSubmit, validate }) => {
const translate = useTranslate()
const classes = useStyles()
return (
<Form
onSubmit={handleSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
<div className={classes.main}>
<Card className={classes.card}>
<div className={classes.avatar}>
<img src={Logo} className={classes.icon} alt={'logo'} />
</div>
<div className={classes.systemName}>
<a
href="https://www.navidrome.org"
target="_blank"
rel="noopener noreferrer"
className={classes.systemNameLink}
>
Navidrome
</a>
</div>
{config.welcomeMessage && (
<div
className={classes.welcome}
dangerouslySetInnerHTML={{ __html: config.welcomeMessage }}
/>
)}
<div className={classes.form}>
<div className={classes.input}>
<Field
autoFocus
name="username"
component={renderInput}
label={translate('ra.auth.username')}
disabled={loading}
spellCheck={false}
/>
</div>
<div className={classes.input}>
<Field
name="password"
component={renderInput}
label={translate('ra.auth.password')}
type="password"
disabled={loading}
/>
</div>
</div>
<CardActions className={classes.actions}>
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
className={classes.button}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate('ra.auth.sign_in')}
</Button>
</CardActions>
</Card>
<Notification />
</div>
</form>
)}
/>
)
}
const InsightsNotice = ({ url }) => {
const translate = useTranslate()
const classes = useStyles()
const anchorRegex = /\[(.+?)]/g
const originalMsg = translate('ra.auth.insightsCollectionNote')
const lines = originalMsg.split('\n')
const renderedLines = lines.map((line, lineIndex) => {
const segments = []
let lastIndex = 0
let match
while ((match = anchorRegex.exec(line)) !== null) {
const bracketText = match[1]
segments.push(line.slice(lastIndex, match.index))
segments.push(
<Link
href={url}
target="_blank"
rel="noopener noreferrer"
key={`${lineIndex}-${match.index}`}
style={{ cursor: 'pointer' }}
>
{bracketText}
</Link>,
)
lastIndex = match.index + match[0].length
}
segments.push(line.slice(lastIndex))
return (
<React.Fragment key={lineIndex}>
{segments}
{lineIndex < lines.length - 1 && <br />}
</React.Fragment>
)
})
return <div className={classes.message}>{renderedLines}</div>
}
const FormSignUp = ({ loading, handleSubmit, validate }) => {
const translate = useTranslate()
const classes = useStyles()
return (
<Form
onSubmit={handleSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
<div className={classes.main}>
<Card className={classes.card}>
<div className={classes.avatar}>
<img src={Logo} className={classes.icon} alt={'logo'} />
</div>
<div className={classes.welcome}>
{translate('ra.auth.welcome1')}
</div>
<div className={classes.welcome}>
{translate('ra.auth.welcome2')}
</div>
<div className={classes.form}>
<div className={classes.input}>
<Field
autoFocus
name="username"
component={renderInput}
label={translate('ra.auth.username')}
disabled={loading}
spellCheck={false}
/>
</div>
<div className={classes.input}>
<Field
name="password"
component={renderInput}
label={translate('ra.auth.password')}
type="password"
disabled={loading}
/>
</div>
<div className={classes.input}>
<Field
name="confirmPassword"
component={renderInput}
label={translate('ra.auth.confirmPassword')}
type="password"
disabled={loading}
/>
</div>
</div>
<CardActions className={classes.actions}>
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
className={classes.button}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate('ra.auth.buttonCreateAdmin')}
</Button>
</CardActions>
<InsightsNotice url={INSIGHTS_DOC_URL} />
</Card>
<Notification />
</div>
</form>
)}
/>
)
}
const Login = ({ location }) => {
const [loading, setLoading] = useState(false)
const translate = useTranslate()
const notify = useNotify()
const login = useLogin()
const dispatch = useDispatch()
const handleSubmit = useCallback(
(auth) => {
setLoading(true)
dispatch(clearQueue())
login(auth, location.state ? location.state.nextPathname : '/').catch(
(error) => {
setLoading(false)
notify(
typeof error === 'string'
? error
: typeof error === 'undefined' || !error.message
? 'ra.auth.sign_in_error'
: error.message,
'warning',
)
},
)
},
[dispatch, login, notify, setLoading, location],
)
const validateLogin = useCallback(
(values) => {
const errors = {}
if (!values.username) {
errors.username = translate('ra.validation.required')
}
if (!values.password) {
errors.password = translate('ra.validation.required')
}
return errors
},
[translate],
)
const validateSignup = useCallback(
(values) => {
const errors = validateLogin(values)
const regex = /^\w+$/g
if (values.username && !values.username.match(regex)) {
errors.username = translate('ra.validation.invalidChars')
}
if (!values.confirmPassword) {
errors.confirmPassword = translate('ra.validation.required')
}
if (values.confirmPassword !== values.password) {
errors.confirmPassword = translate('ra.validation.passwordDoesNotMatch')
}
return errors
},
[translate, validateLogin],
)
if (config.firstTime) {
return (
<FormSignUp
handleSubmit={handleSubmit}
validate={validateSignup}
loading={loading}
/>
)
}
return (
<FormLogin
handleSubmit={handleSubmit}
validate={validateLogin}
loading={loading}
/>
)
}
Login.propTypes = {
authProvider: PropTypes.func,
previousRoute: PropTypes.string,
}
const LoginWithTheme = (props) => {
const theme = useCurrentTheme()
const version = useVersion()
return (
<ThemeProvider theme={createMuiTheme(theme)}>
<Login key={version} {...props} />
</ThemeProvider>
)
}
export default LoginWithTheme