all repos — caroster @ e5ab2b72830ceac154edfa35c7106a508edebebc

[Octree] Group carpool to your event https://caroster.io

feat: :sparkles: Use magic link for users authentication

#562
Tim Izzo tim@5ika.ch
Mon, 16 Dec 2024 15:17:32 +0100
commit

e5ab2b72830ceac154edfa35c7106a508edebebc

parent

edd58e5bd4a42b2d725d3bcdfbaace7d4148b0f8

M backend/src/api/email/locales/en.jsonbackend/src/api/email/locales/en.json

@@ -8,6 +8,10 @@ "ConfirmEmail": {

"title": "Please confirm your email address", "content": "## Welcome on Caroster !\nPlease confirm your account by clicking on this link:\n\n[<%= confirmationLink %>](<%= confirmationLink %>)" }, + "MagicLinkLogin": { + "title": "Log in to Caroster", + "content": "## Log in to Caroster\nClick on the link below to log in:\n\n[<%= magicLink %>](<%= magicLink %>)" + }, "NewTrip": { "title": "<%= event.name %> - A new trip is available", "content": "## New trip is available for \"<%= event.name %>\"\n\n**Trip: <%= vehicleName %>**\nCount of seats: <%= travel.seats %>\nDeparture location: <%= travel.meeting %>\nInformation: <%= travel.details %>.\n\n[Book my seat](<%= host %>/e/<%= event.uuid %>).\n\nYou are receiving this email because you are on the waiting list to find a carpool spot in this event."
M backend/src/api/email/locales/fr.jsonbackend/src/api/email/locales/fr.json

@@ -8,6 +8,10 @@ "ConfirmEmail": {

"title": "Merci de confirmer votre adresse email", "content": "## Bienvenue sur Caroster !\nMerci de confirmer votre compte en cliquant sur le lien ci-dessous:\n\n[<%= confirmationLink %>](<%= confirmationLink %>)" }, + "MagicLinkLogin": { + "title": "Connectez-vous à Caroster", + "content": "## Connectez-vous à Caroster\nCliquez sur le lien ci-dessous pour vous connecter:\n\n[<%= magicLink %>](<%= magicLink %>)" + }, "NewTrip": { "title": "<%= event.name %> - Nouveau trajet disponible", "content": "## Nouveau trajet pour \"<%= event.name %>\"\n\n**Trajet: <%= vehicleName %>**\nNombre de places: <%= travel.seats %>\nLieu de départ: <%= travel.meeting %>\nInformations: <%= travel.details %>.\n\n[Réserver ma place](<%= host %>/e/<%= event.uuid %>).\n\nVous recevez cet e-mail car vous êtes inscrit en liste d'attente pour trouver une place de covoiturage dans cet événement."
A backend/src/extensions/users-permissions/routes/user.ts

@@ -0,0 +1,66 @@

+export default [ + { + method: "POST", + path: "/auth/magic-link", + handler: async (ctx) => { + const { token } = ctx.request.body; + + try { + const payload = await strapi.services[ + "plugin::users-permissions.user" + ].magicLink.verifyMagicToken(token); + const email = payload.email; + if (!email) throw new Error("No email in token"); + const existingUser = await strapi.db + .query("plugin::users-permissions.user") + .findOne({ + where: { email }, + }); + if (existingUser) { + const jwt = strapi + .plugin("users-permissions") + .service("jwt") + .issue({ id: existingUser.id }); + return { + jwt, + user: { + id: existingUser.id, + email: existingUser.email, + firstname: existingUser.firstname, + lang: existingUser.lang, + }, + }; + } + const user = await strapi + .plugin("users-permissions") + .service("user") + .add({ + email, + username: email, + provider: "local", + confirmed: true, + }); + const jwt = strapi + .plugin("users-permissions") + .service("jwt") + .issue({ id: user.id }); + return { + jwt, + user: { + id: user.id, + email: user.email, + firstname: user.firstname, + lang: user.lang, + }, + }; + } catch (error) { + strapi.log.warn(error); + return ctx.unauthorized("Invalid magic link token"); + } + }, + config: { + prefix: "", + auth: false, + }, + }, +];
A backend/src/extensions/users-permissions/services/magic-link.ts

@@ -0,0 +1,24 @@

+import jwt from "jsonwebtoken"; + +const MAGICLINK_SECRET = process.env.MAGICLINK_SECRET; + +export const generateMagicToken = async (email: string) => { + const existingUser = await strapi.db + .query("plugin::users-permissions.user") + .findOne({ + where: { email }, + }); + + if (existingUser.provider === "google") { + strapi.log.warn( + `User ${email} is linked to Google account. Can't login with magic link.` + ); + throw new Error("GoogleAccount"); + } + if (!MAGICLINK_SECRET) throw new Error("No MAGICLINK_SECRET provided"); + + return jwt.sign({ email }, MAGICLINK_SECRET, { expiresIn: "20m" }); +}; + +export const verifyMagicToken = (token: string) => + jwt.verify(token, MAGICLINK_SECRET);
D backend/src/extensions/users-permissions/services/sendConfirmationEmail.ts

@@ -1,32 +0,0 @@

-import crypto from "crypto"; -import urlJoin from "url-join"; -import { getAbsoluteServerUrl, sanitize } from "@strapi/utils"; - -export default async (user) => { - const userSchema = strapi.getModel("plugin::users-permissions.user"); - - const confirmationToken = crypto.randomBytes(20).toString("hex"); - strapi.entityService.update("plugin::users-permissions.user", user.id, { - data: { confirmationToken }, - populate: ["role"], - }); - - const sanitizedUserInfo = await sanitize.sanitizers.defaultSanitizeOutput( - userSchema, - user - ); - const apiPrefix = strapi.config.get("api.rest.prefix"); - const url = urlJoin( - getAbsoluteServerUrl(strapi.config), - apiPrefix, - "/auth/email-confirmation" - ); - const confirmationLink = `${url}?confirmation=${confirmationToken}`; - await strapi - .service("api::email.email") - .sendEmailNotif(user.email, "ConfirmEmail", user.lang, { - confirmationToken, - confirmationLink, - user: sanitizedUserInfo, - }); -};
M backend/src/extensions/users-permissions/strapi-server.tsbackend/src/extensions/users-permissions/strapi-server.ts

@@ -1,4 +1,5 @@

-import sendConfirmationEmail from "./services/sendConfirmationEmail"; +import * as magicLink from "./services/magic-link"; +import customRoutes from "./routes/user"; export default (plugin) => { const userServices = plugin.services.user;

@@ -6,8 +7,9 @@ plugin.services.user = (params) => {

const services = userServices(params); return { ...services, - sendConfirmationEmail, + magicLink, }; }; + plugin.routes["content-api"].routes.push(...customRoutes); return plugin; };
M backend/src/graphql/user/index.tsbackend/src/graphql/user/index.ts

@@ -26,6 +26,13 @@ },

}); }, }), + nexus.mutationField("sendMagicLink", { + type: "Boolean", + args: { + email: nexus.nonNull("String"), + lang: "String", + }, + }), ], resolvers: { Query: {

@@ -89,6 +96,29 @@ resourceUID: "plugin::users-permissions.user",

}); }, }, + sendMagicLink: { + async resolve(_root, args) { + const { email, lang } = args; + const magicToken = await strapi.services[ + "plugin::users-permissions.user" + ].magicLink.generateMagicToken(email); + const magicLink = `${strapi.config.get( + "server.url" + )}/auth/magic-link?token=${magicToken}`; + + try { + await strapi + .service("api::email.email") + .sendEmailNotif(email, "MagicLinkLogin", lang || "en", { + magicLink, + }); + return true; + } catch (error) { + strapi.log.error(error); + return false; + } + }, + }, }, }, resolversConfig: {

@@ -96,6 +126,9 @@ "Query.me": {

auth: { scope: ["plugin::users-permissions.user.me"], }, + }, + "Mutation.sendMagicLink": { + auth: false, }, "UsersPermissionsUser.events": { auth: true,
M frontend/containers/CreateEvent/Step1.tsxfrontend/containers/CreateEvent/Step1.tsx

@@ -101,32 +101,24 @@ {t('event.creation.next')}

</Button> {!isAuthenticated && ( - <Box sx={{marginTop: theme.spacing(8), textAlign: 'center'}}> - <Typography variant="body1"> - {t('event.creation.addFromAccount.title')} - </Typography> - <Typography variant="body2"> - {t('event.creation.addFromAccount.subtitle')} - </Typography> - <CardActions - sx={{ - marginTop: theme.spacing(1), - justifyContent: 'space-evenly', - textAlign: 'center', - }} - > - <NextLink href="/auth/register" passHref> - <Button variant="text"> - {t('event.creation.addFromAccount.actions.register')} - </Button> - </NextLink> - <NextLink href="/auth/login" passHref> - <Button color="primary"> - {t('event.creation.addFromAccount.actions.login')} - </Button> - </NextLink> - </CardActions> - </Box> + <CardActions + sx={{ + marginTop: theme.spacing(1), + justifyContent: 'space-evenly', + textAlign: 'center', + }} + > + <NextLink href="/auth/login" passHref> + <Button variant="text"> + {t('event.creation.addFromAccount.actions.register')} + </Button> + </NextLink> + <NextLink href="/auth/login" passHref> + <Button color="primary"> + {t('event.creation.addFromAccount.actions.login')} + </Button> + </NextLink> + </CardActions> )} </Box> );
D frontend/containers/LostPassword/Success.js

@@ -1,59 +0,0 @@

-import {useTranslation} from 'next-i18next'; -import {styled} from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import Icon from '@mui/material/Icon'; -import CardContent from '@mui/material/CardContent'; -import Card from '@mui/material/Card'; -import Typography from '@mui/material/Typography'; -import CardActions from '@mui/material/CardActions'; -import Link from 'next/link'; - -const PREFIX = 'Success'; - -const classes = { - successCard: `${PREFIX}-successCard`, - successIcon: `${PREFIX}-successIcon`, - actions: `${PREFIX}-actions`, -}; - -const StyledCard = styled(Card)(({theme}) => ({ - [`&.${classes.successCard}`]: { - textAlign: 'center', - }, - - [`& .${classes.successIcon}`]: { - fontSize: theme.spacing(14), - }, - - [`& .${classes.actions}`]: { - justifyContent: 'center', - }, -})); - -const Success = ({email}) => { - const {t} = useTranslation(); - - return ( - <StyledCard className={classes.successCard}> - <CardContent> - <Icon size="large" color="primary" className={classes.successIcon}> - done - </Icon> - </CardContent> - <CardContent> - <Typography variant="body1" gutterBottom> - {t('lost_password.sent', {email})} - </Typography> - </CardContent> - <CardActions className={classes.actions}> - <Link href="/auth/login" passHref> - <Button id="LostPasswordRegister" color="primary" variant="contained"> - {t('lost_password.actions.login')} - </Button> - </Link> - </CardActions> - </StyledCard> - ); -}; - -export default Success;
D frontend/containers/LostPassword/index.js

@@ -1,128 +0,0 @@

-import {useCallback, useState, useEffect} from 'react'; -import {styled} from '@mui/material/styles'; -import {useTranslation} from 'next-i18next'; -import router from 'next/router'; -import TextField from '@mui/material/TextField'; -import Button from '@mui/material/Button'; -import CardContent from '@mui/material/CardContent'; -import Card from '@mui/material/Card'; -import CircularProgress from '@mui/material/CircularProgress'; -import CardActions from '@mui/material/CardActions'; -import Link from '@mui/material/Link'; -import LostPasswordSuccess from './Success'; -import useToastStore from '../../stores/useToastStore'; -import useProfile from '../../hooks/useProfile'; -import {useForgotPasswordMutation} from '../../generated/graphql'; - -const PREFIX = 'LostPassword'; - -const classes = { - loader: `${PREFIX}-loader`, - actions: `${PREFIX}-actions`, -}; - -const Root = styled('form')(({theme}) => ({ - [`& .${classes.loader}`]: { - marginLeft: theme.spacing(4), - }, - - [`& .${classes.actions}`]: { - marginTop: theme.spacing(2), - justifyContent: 'flex-end', - }, -})); - -const LostPassword = () => { - const {t} = useTranslation(); - - const addToast = useToastStore(s => s.addToast); - const {profile} = useProfile(); - const [sendForgotPassword, {loading}] = useForgotPasswordMutation(); - const [isSent, setIsSent] = useState(false); - const [error, setError] = useState(''); - const [email, setEmail] = useState(''); - const canSubmit = email.length > 4; - - useEffect(() => { - if (profile?.confirmed) router.replace('/confirm'); - else if (profile) router.replace('/dashboard'); - }, [profile]); - - const onSubmit = useCallback( - async e => { - if (e.preventDefault) e.preventDefault(); - - try { - await sendForgotPassword({variables: {email}}); - setIsSent(true); - } catch (error) { - if (error.message === 'Bad Request') { - addToast(t('lost_password.error')); - setError(t('lost_password.error')); - } else { - addToast(t('generic.errors.unknown')); - } - } - return false; - }, - [sendForgotPassword, email, addToast, t] - ); - - if (!loading && isSent) return <LostPasswordSuccess email={email} />; - - return ( - <Root onSubmit={onSubmit}> - <Card> - <CardContent> - <TextField - label={t('lost_password.email')} - fullWidth - required={true} - margin="dense" - value={email} - onChange={({target: {value = ''}}) => setEmail(value)} - id="LostPasswordEmail" - name="email" - type="email" - error={!!error} - helperText={ - error && ( - <> - {error}&nbsp; - <Link href="/auth/register"> - {t('lost_password.actions.register')} - </Link> - </> - ) - } - /> - </CardContent> - <CardActions className={classes.actions}> - <Button id="LostPasswordRegister" href="/auth/login"> - {t('lost_password.actions.cancel')} - </Button> - - <Button - color="primary" - variant="contained" - type="submit" - disabled={!canSubmit} - aria-disabled={!canSubmit} - id="LostPasswordSubmit" - > - {t('lost_password.actions.send')} - {loading && ( - <CircularProgress - className={classes.loader} - color="primary" - size={20} - /> - )} - </Button> - </CardActions> - </Card> - </Root> - ); -}; - -export default LostPassword;
D frontend/containers/MailSignUpForm/SignupActions.tsx

@@ -1,24 +0,0 @@

-import Link from 'next/link'; -import {useTheme} from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import CardActions from '@mui/material/CardActions'; -import {useTranslation} from 'next-i18next'; - -const SignUpActions = () => { - const theme = useTheme(); - const {t} = useTranslation(); - - return ( - <CardActions - sx={{justifyContent: 'center', marginBottom: theme.spacing(2)}} - > - <Link href="/auth/login" passHref> - <Button id="SignUpLogin" variant="text"> - {t('signup.login')} - </Button> - </Link> - </CardActions> - ); -}; - -export default SignUpActions;
D frontend/containers/MailSignUpForm/index.tsx

@@ -1,230 +0,0 @@

-import {useState, useMemo} from 'react'; -import Box from '@mui/material/Box'; -import Divider from '@mui/material/Divider'; -import Checkbox from '@mui/material/Checkbox'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import TextField from '@mui/material/TextField'; -import Button from '@mui/material/Button'; -import CardContent from '@mui/material/CardContent'; -import Typography from '@mui/material/Typography'; -import CircularProgress from '@mui/material/CircularProgress'; -import {useTheme} from '@mui/material/styles'; -import {Trans, useTranslation} from 'next-i18next'; -import {useRouter} from 'next/router'; -import useToastsStore from '../../stores/useToastStore'; -import SignUpActions from './SignupActions'; -import {useRegisterMutation} from '../../generated/graphql'; -import useSettings from '../../hooks/useSettings'; -import moment from 'moment'; - -const SignUp = () => { - const {t, i18n} = useTranslation(); - const theme = useTheme(); - const addToast = useToastsStore(s => s.addToast); - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [newsletterConsent, setNewsletterConsent] = useState(false); - const [tosConsent, setTosConsent] = useState(false); - const [register] = useRegisterMutation(); - const settings = useSettings(); - - const canSubmit = useMemo( - () => - [firstName, lastName, email, password].filter(s => s.length < 2) - .length === 0 && tosConsent, - [firstName, lastName, email, password, tosConsent] - ); - - const onSubmit = async e => { - e.preventDefault?.(); - if (isLoading) return; - setIsLoading(true); - const tosAcceptationDate = tosConsent ? moment().toISOString() : null; - try { - const lang = i18n.language; - await register({ - variables: { - user: { - username: email, - email, - password, - firstName, - lastName, - newsletterConsent, - tosAcceptationDate, - lang, - }, - }, - }); - router.push('/auth/confirm'); - } catch (error) { - const strapiError = error.message; - console.error({strapiError}); - if (strapiError === 'Email or Username are already taken') - setError(t('signup.errors.email_taken')); - else addToast(t(`generic.errors.unknown`)); - } - setIsLoading(false); - return false; - }; - - const contentSx = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - padding: theme.spacing(0, 4), - }; - - return ( - <form onSubmit={onSubmit}> - <CardContent sx={contentSx}> - <Typography - variant="h6" - align="center" - sx={{ - whiteSpace: 'pre-line', - paddingBottom: theme.spacing(4), - }} - > - {t('signup.createForm')} - </Typography> - <Box sx={contentSx}> - <TextField - label={t('signup.firstName')} - fullWidth - autoFocus - margin="dense" - value={firstName} - InputLabelProps={{required: false}} - required={true} - onChange={({target: {value = ''}}) => setFirstName(value)} - id="SignUpFirstName" - name="firstName" - /> - <TextField - label={t('signup.lastName')} - fullWidth - InputLabelProps={{required: false}} - required={true} - margin="dense" - value={lastName} - onChange={({target: {value = ''}}) => setLastName(value)} - id="SignUpLastName" - name="lastName" - /> - <TextField - label={t('signup.email')} - fullWidth - InputLabelProps={{required: false}} - required={true} - error={!!error} - helperText={error} - margin="dense" - value={email} - onChange={({target: {value = ''}}) => setEmail(value)} - id="SignUpEmail" - name="email" - type="email" - /> - <TextField - label={t('signup.password')} - fullWidth - InputLabelProps={{required: false}} - required={true} - margin="dense" - value={password} - onChange={({target: {value = ''}}) => setPassword(value)} - id="SignUpEmail" - name="password" - type="password" - /> - </Box> - <FormControlLabel - sx={{ - width: '100%', - mt: 2, - px: 5.5, - }} - componentsProps={{typography: {align: 'left', variant: 'body2'}}} - control={ - <Checkbox - sx={{p: 0, mr: 2}} - color="primary" - value={newsletterConsent} - onChange={({target: {checked = false}}) => - setNewsletterConsent(checked) - } - /> - } - label={t('signup.newsletter.consent')} - /> - <FormControlLabel - sx={{width: '100%', mt: 2, px: 5.5}} - componentsProps={{typography: {align: 'left', variant: 'body2'}}} - label={ - <Trans - i18nKey="signup.tos.consent" - components={{ - 'tos-link': <a href={settings.tos_link} target="_blank" />, - 'data-privacy-link': ( - <a href={settings.data_policy_link} target="_blank" /> - ), - }} - /> - } - control={ - <Checkbox - sx={{p: 0, mr: 2}} - value={tosConsent} - onChange={e => setTosConsent(e.target.checked)} - /> - } - /> - - <Box sx={contentSx} mt={2}> - <Button - color="primary" - variant="contained" - fullWidth - type="submit" - disabled={!canSubmit} - sx={{margin: theme.spacing(1)}} - aria-disabled={!canSubmit} - id="SignUpSubmit" - endIcon={ - isLoading && ( - <CircularProgress - sx={{ - marginLeft: '14px', - color: theme.palette.background.paper, - }} - size={20} - color="secondary" - /> - ) - } - > - {t('signup.submit')} - </Button> - </Box> - <Box - sx={{width: '100%', textAlign: 'center', margin: theme.spacing(2, 0)}} - > - <Divider /> - </Box> - <Typography align="center" variant="body2"> - {t('signup.account_already')} - </Typography> - </CardContent> - <SignUpActions /> - </form> - ); -}; - -export default SignUp;
D frontend/containers/ResetPassword/index.tsx

@@ -1,78 +0,0 @@

-import React from 'react'; -import {styled} from '@mui/material/styles'; -import Card from '@mui/material/Card'; -import CardHeader from '@mui/material/CardHeader'; -import CardContent from '@mui/material/CardContent'; -import CardActions from '@mui/material/CardActions'; -import Button from '@mui/material/Button'; -import {useTranslation} from 'next-i18next'; -import TextField from '@mui/material/TextField'; -const PREFIX = 'ResetPassword'; - -const classes = { - actions: `${PREFIX}-actions`, -}; - -const StyledCard = styled(Card)(({theme}) => ({ - [`& .${classes.actions}`]: { - justifyContent: 'flex-end', - marginTop: theme.spacing(2), - }, -})); - -const ResetPassword = ({ - password, - setPassword, - passwordConfirmation, - setPasswordConfirmation, - error, - isLoading, -}) => { - const {t} = useTranslation(); - - return ( - <StyledCard> - <CardHeader title={t('profile.actions.change_password')} /> - <CardContent> - <TextField - label={t('lost_password.password')} - type="password" - fullWidth - autoFocus - margin="dense" - value={password} - onChange={({target: {value = ''}}) => setPassword(value)} - id="ResetPasswordNewPassword" - name="new_password" - error={!!error} - helperText={error} - /> - <TextField - type="password" - label={t('lost_password.password_confirmation')} - fullWidth - margin="dense" - value={passwordConfirmation} - onChange={({target: {value = ''}}) => setPasswordConfirmation(value)} - id="ResetPasswordNewPasswordConfirmation" - name="new_password_confirmation" - /> - </CardContent> - <CardActions className={classes.actions}> - <Button - type="submit" - color="primary" - variant="contained" - disabled={ - isLoading || - password.length < 4 || - password !== passwordConfirmation - } - > - {t('lost_password.actions.save_new_password')} - </Button> - </CardActions> - </StyledCard> - ); -}; -export default ResetPassword;
D frontend/containers/SignInForm/index.tsx

@@ -1,175 +0,0 @@

-import {useState, useMemo} from 'react'; -import NextLink from 'next/link'; -import TextField from '@mui/material/TextField'; -import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; -import CardContent from '@mui/material/CardContent'; -import FormHelperText from '@mui/material/FormHelperText'; -import CardActions from '@mui/material/CardActions'; -import Divider from '@mui/material/Divider'; -import Box from '@mui/material/Box'; -import {useTheme} from '@mui/material/styles'; -import {useTranslation} from 'next-i18next'; -import {signIn} from 'next-auth/react'; -import useAddToEvents from '../../hooks/useAddToEvents'; -import LoginGoogle from '../LoginGoogle'; -import {useRouter} from 'next/router'; - -interface Props { - error?: string; -} - -const errorsMap = { - CredentialsSignin: 'signin.errors.CredentialsSignin', - EmailNotConfirmed: 'signin.errors.EmailNotConfirmed', -}; - -const SignIn = (props: Props) => { - const {error} = props; - const {t} = useTranslation(); - const router = useRouter(); - - const theme = useTheme(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const {saveStoredEvents} = useAddToEvents(); - - const canSubmit = useMemo( - () => [email, password].filter(s => s.length < 4).length === 0, - [email, password] - ); - - const onSubmit = async e => { - e.preventDefault?.(); - try { - await signIn('credentials', { - email, - password, - callbackUrl: (router.query?.redirectPath as string) || '/dashboard', - }); - saveStoredEvents(); - } catch (error) { - console.error(error); - } - - return false; - }; - - const spaceAround = { - width: '100%', - textAlign: 'center', - margin: theme.spacing(2, 0), - }; - - return ( - <form onSubmit={onSubmit}> - <CardContent - sx={{ - display: 'flex', - flexDirection: 'column', - px: 2, - }} - > - <Typography variant="h6" align="center"> - {t('signin.title')} - </Typography> - {error && ( - <FormHelperText error sx={{textAlign: 'center'}}> - {t(errorsMap[error])} - </FormHelperText> - )} - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - }} - > - <TextField - label={t('signin.email')} - fullWidth - required={true} - InputLabelProps={{required: false}} - margin="dense" - value={email} - onChange={({target: {value = ''}}) => setEmail(value)} - name="email" - type="email" - error={!!error} - /> - <TextField - label={t('signin.password')} - fullWidth - InputLabelProps={{required: false}} - required={true} - margin="dense" - value={password} - onChange={({target: {value = ''}}) => setPassword(value)} - id="SignInEmail" - name="password" - type="password" - error={!!error} - /> - </Box> - - <Box sx={spaceAround}> - <NextLink href="/auth/lost-password" passHref> - <Typography - align="center" - variant="body2" - color="primary" - sx={{ - textDecoration: 'underline', - textDecorationColor: 'primary', - }} - > - {t('lost_password.message')} - </Typography> - </NextLink> - </Box> - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - pb: 1, - }} - > - <Button - color="primary" - variant="contained" - fullWidth - type="submit" - disabled={!canSubmit} - aria-disabled={!canSubmit} - > - {t('signin.login')} - </Button> - <Box sx={spaceAround}> - <Typography>{t('signin.or')}</Typography> - </Box> - <LoginGoogle /> - </Box> - </CardContent> - <Divider /> - <CardActions - sx={{ - flexDirection: 'column', - justifyContent: 'center', - marginBottom: theme.spacing(2), - textAlign: 'center', - px: 2, - }} - > - <Typography align="center" variant="body2" sx={{pt: 2}}> - {t('signin.no_account')} - </Typography> - <NextLink href="/auth/register" passHref> - <Button size="small">{t('signin.register')}</Button> - </NextLink> - </CardActions> - </form> - ); -}; - -export default SignIn;
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -465,6 +465,7 @@ register: UsersPermissionsLoginPayload;

removeFile?: Maybe<UploadFileEntityResponse>; /** Reset user password. Confirm with a code (resetToken from forgotPassword) */ resetPassword?: Maybe<UsersPermissionsLoginPayload>; + sendMagicLink?: Maybe<Scalars['Boolean']['output']>; setTripAlert?: Maybe<TripAlertEntityResponse>; updateEvent?: Maybe<EventEntityResponse>; /** Update an event using its UUID */

@@ -674,6 +675,12 @@ export type MutationResetPasswordArgs = {

code: Scalars['String']['input']; password: Scalars['String']['input']; passwordConfirmation: Scalars['String']['input']; +}; + + +export type MutationSendMagicLinkArgs = { + email: Scalars['String']['input']; + lang?: InputMaybe<Scalars['String']['input']>; };

@@ -1794,30 +1801,13 @@

export type SetTripAlertMutation = { __typename?: 'Mutation', setTripAlert?: { __typename?: 'TripAlertEntityResponse', data?: { __typename?: 'TripAlertEntity', id?: string | null, attributes?: { __typename?: 'TripAlert', latitude?: number | null, longitude?: number | null, address?: string | null, enabled?: boolean | null } | null } | null } | null }; -export type MeFieldsFragment = { __typename?: 'UsersPermissionsMe', id: string, username: string, email?: string | null, confirmed?: boolean | null }; - -export type RegisterMutationVariables = Exact<{ - user: UsersPermissionsRegisterInput; -}>; - - -export type RegisterMutation = { __typename?: 'Mutation', register: { __typename?: 'UsersPermissionsLoginPayload', jwt?: string | null, user: { __typename?: 'UsersPermissionsMe', id: string, username: string, email?: string | null, confirmed?: boolean | null } } }; - -export type ForgotPasswordMutationVariables = Exact<{ +export type SendMagicLinkMutationVariables = Exact<{ email: Scalars['String']['input']; + lang?: InputMaybe<Scalars['String']['input']>; }>; -export type ForgotPasswordMutation = { __typename?: 'Mutation', forgotPassword?: { __typename?: 'UsersPermissionsPasswordPayload', ok: boolean } | null }; - -export type ResetPasswordMutationVariables = Exact<{ - password: Scalars['String']['input']; - passwordConfirmation: Scalars['String']['input']; - code: Scalars['String']['input']; -}>; - - -export type ResetPasswordMutation = { __typename?: 'Mutation', resetPassword?: { __typename?: 'UsersPermissionsLoginPayload', jwt?: string | null, user: { __typename?: 'UsersPermissionsMe', id: string, username: string, email?: string | null, confirmed?: boolean | null } } | null }; +export type SendMagicLinkMutation = { __typename?: 'Mutation', sendMagicLink?: boolean | null }; export type EventFieldsFragment = { __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, description?: string | null, enabled_modules?: any | null, email: string, lang?: Enum_Event_Lang | null, administrators?: Array<string | null> | null, date?: any | null, address?: string | null, latitude?: number | null, longitude?: number | null, isReturnEvent?: boolean | null, waitingPassengers?: { __typename?: 'PassengerRelationResponseCollection', data: Array<{ __typename?: 'PassengerEntity', id?: string | null, attributes?: { __typename?: 'Passenger', name: string, email?: string | null, location?: string | null, user?: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', firstName?: string | null, lastName?: string | null } | null } | null } | null } | null }> } | null, travels?: { __typename?: 'TravelRelationResponseCollection', data: Array<{ __typename?: 'TravelEntity', id?: string | null, attributes?: { __typename?: 'Travel', meeting?: string | null, meeting_latitude?: number | null, meeting_longitude?: number | null, departureDate?: any | null, departureTime?: string | null, details?: string | null, vehicleName?: string | null, firstname?: string | null, lastname?: string | null, phone_number?: string | null, phoneCountry?: string | null, seats?: number | null, user?: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', firstName?: string | null, lastName?: string | null } | null } | null } | null, passengers?: { __typename?: 'PassengerRelationResponseCollection', data: Array<{ __typename?: 'PassengerEntity', id?: string | null, attributes?: { __typename?: 'Passenger', name: string, location?: string | null, email?: string | null, phone?: string | null, phoneCountry?: string | null, user?: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', firstName?: string | null, lastName?: string | null } | null } | null } | null } | null }> } | null } | null }> } | null } | null };

@@ -1956,14 +1946,6 @@

export type UpdateMeMutation = { __typename?: 'Mutation', updateMe: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null, enabled_modules?: any | null } | null }> } | null } | null } | null } }; -export const MeFieldsFragmentDoc = gql` - fragment MeFields on UsersPermissionsMe { - id - username - email - confirmed -} - `; export const EventFieldsFragmentDoc = gql` fragment EventFields on EventEntity { id

@@ -2242,117 +2224,38 @@ }

export type SetTripAlertMutationHookResult = ReturnType<typeof useSetTripAlertMutation>; export type SetTripAlertMutationResult = Apollo.MutationResult<SetTripAlertMutation>; export type SetTripAlertMutationOptions = Apollo.BaseMutationOptions<SetTripAlertMutation, SetTripAlertMutationVariables>; -export const RegisterDocument = gql` - mutation register($user: UsersPermissionsRegisterInput!) { - register(input: $user) { - jwt - user { - ...MeFields - } - } -} - ${MeFieldsFragmentDoc}`; -export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>; - -/** - * __useRegisterMutation__ - * - * To run a mutation, you first call `useRegisterMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useRegisterMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [registerMutation, { data, loading, error }] = useRegisterMutation({ - * variables: { - * user: // value for 'user' - * }, - * }); - */ -export function useRegisterMutation(baseOptions?: Apollo.MutationHookOptions<RegisterMutation, RegisterMutationVariables>) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation<RegisterMutation, RegisterMutationVariables>(RegisterDocument, options); - } -export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>; -export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>; -export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>; -export const ForgotPasswordDocument = gql` - mutation forgotPassword($email: String!) { - forgotPassword(email: $email) { - ok - } +export const SendMagicLinkDocument = gql` + mutation sendMagicLink($email: String!, $lang: String) { + sendMagicLink(email: $email, lang: $lang) } `; -export type ForgotPasswordMutationFn = Apollo.MutationFunction<ForgotPasswordMutation, ForgotPasswordMutationVariables>; +export type SendMagicLinkMutationFn = Apollo.MutationFunction<SendMagicLinkMutation, SendMagicLinkMutationVariables>; /** - * __useForgotPasswordMutation__ + * __useSendMagicLinkMutation__ * - * To run a mutation, you first call `useForgotPasswordMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useForgotPasswordMutation` returns a tuple that includes: + * To run a mutation, you first call `useSendMagicLinkMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSendMagicLinkMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [forgotPasswordMutation, { data, loading, error }] = useForgotPasswordMutation({ + * const [sendMagicLinkMutation, { data, loading, error }] = useSendMagicLinkMutation({ * variables: { * email: // value for 'email' + * lang: // value for 'lang' * }, * }); */ -export function useForgotPasswordMutation(baseOptions?: Apollo.MutationHookOptions<ForgotPasswordMutation, ForgotPasswordMutationVariables>) { +export function useSendMagicLinkMutation(baseOptions?: Apollo.MutationHookOptions<SendMagicLinkMutation, SendMagicLinkMutationVariables>) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation<ForgotPasswordMutation, ForgotPasswordMutationVariables>(ForgotPasswordDocument, options); + return Apollo.useMutation<SendMagicLinkMutation, SendMagicLinkMutationVariables>(SendMagicLinkDocument, options); } -export type ForgotPasswordMutationHookResult = ReturnType<typeof useForgotPasswordMutation>; -export type ForgotPasswordMutationResult = Apollo.MutationResult<ForgotPasswordMutation>; -export type ForgotPasswordMutationOptions = Apollo.BaseMutationOptions<ForgotPasswordMutation, ForgotPasswordMutationVariables>; -export const ResetPasswordDocument = gql` - mutation resetPassword($password: String!, $passwordConfirmation: String!, $code: String!) { - resetPassword( - password: $password - passwordConfirmation: $passwordConfirmation - code: $code - ) { - jwt - user { - ...MeFields - } - } -} - ${MeFieldsFragmentDoc}`; -export type ResetPasswordMutationFn = Apollo.MutationFunction<ResetPasswordMutation, ResetPasswordMutationVariables>; - -/** - * __useResetPasswordMutation__ - * - * To run a mutation, you first call `useResetPasswordMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useResetPasswordMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [resetPasswordMutation, { data, loading, error }] = useResetPasswordMutation({ - * variables: { - * password: // value for 'password' - * passwordConfirmation: // value for 'passwordConfirmation' - * code: // value for 'code' - * }, - * }); - */ -export function useResetPasswordMutation(baseOptions?: Apollo.MutationHookOptions<ResetPasswordMutation, ResetPasswordMutationVariables>) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation<ResetPasswordMutation, ResetPasswordMutationVariables>(ResetPasswordDocument, options); - } -export type ResetPasswordMutationHookResult = ReturnType<typeof useResetPasswordMutation>; -export type ResetPasswordMutationResult = Apollo.MutationResult<ResetPasswordMutation>; -export type ResetPasswordMutationOptions = Apollo.BaseMutationOptions<ResetPasswordMutation, ResetPasswordMutationVariables>; +export type SendMagicLinkMutationHookResult = ReturnType<typeof useSendMagicLinkMutation>; +export type SendMagicLinkMutationResult = Apollo.MutationResult<SendMagicLinkMutation>; +export type SendMagicLinkMutationOptions = Apollo.BaseMutationOptions<SendMagicLinkMutation, SendMagicLinkMutationVariables>; export const CreateEventDocument = gql` mutation createEvent($eventData: EventInput!) { createEvent(data: $eventData) {
M frontend/graphql/auth.gqlfrontend/graphql/auth.gql

@@ -1,38 +1,3 @@

-fragment MeFields on UsersPermissionsMe { - id - username - email - confirmed -} - -mutation register($user: UsersPermissionsRegisterInput!) { - register(input: $user) { - jwt - user { - ...MeFields - } - } -} - -mutation forgotPassword($email: String!) { - forgotPassword(email: $email) { - ok - } -} - -mutation resetPassword( - $password: String! - $passwordConfirmation: String! - $code: String! -) { - resetPassword( - password: $password - passwordConfirmation: $passwordConfirmation - code: $code - ) { - jwt - user { - ...MeFields - } - } +mutation sendMagicLink($email: String!, $lang: String) { + sendMagicLink(email: $email, lang: $lang) }
A frontend/i18n-unused.config.js

@@ -0,0 +1,7 @@

+/** @type {import('i18n-unused').RunOptions} */ +module.exports = { + localesPath: 'locales', + srcPath: '.', + translationKeyMatcher: + /t\(\s*["'`]?([\s\S]+?)["'`]?\s*(?:\)|,)|i18nKey="([\s\S]+?)"/gi, +};
M frontend/locales/de.jsonfrontend/locales/de.json

@@ -1,5 +1,4 @@

{ - "lost_password.sent": "Eine E-Mail mit einem Link zur Wiederherstellung Ihres Passworts wurde an {{email}} gesendet", "alert.button.label": "Speichern", "alert.create": "Ihr Alert wurde erfolgreich konfiguriert", "alert.description": "Richten Sie einen Alarm ein, um eine E-Mail zu erhalten, wenn eine Abfahrt in der Nähe ist",

@@ -10,7 +9,6 @@ "alert.radius.label": "Gewünschter Radius",

"alert.title": "Alarme", "confirm.google.title": "Anmeldung abschließen", "event.creation.name": "Name der Veranstaltung", - "lost_password.email": "Deine E-Mail", "menu.new_event": "Einen Caroster erstellen", "confirm.text": "Sie haben eine E-Mail mit einem Link erhalten. Bitte klicken Sie auf diesen Link, um Ihr Konto zu bestätigen.", "confirm.title": "E-Mail bestätigen",

@@ -39,8 +37,6 @@ "event.add_to_my_events.register": "$t(menu.register)",

"event.add_to_my_events.title": "Sie müssen eingeloggt sein", "event.creation.addFromAccount.actions.login": "$t(menu.login)", "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "Erstellen Sie es über Ihr Konto", - "event.creation.addFromAccount.title": "Möchten Sie diesen Caroster zu Ihren Veranstaltungen hinzufügen?", "event.creation.creator_email": "Ihre E-Mail", "event.creation.date": "Datum der Veranstaltung", "event.creation.description": "Beschreibung",

@@ -86,17 +82,6 @@ "generic.errors.not_found": "Ressource wurde nicht gefunden",

"generic.errors.unknown": "Ein unbekannter Fehler ist aufgetreten", "generic.me": "Ich", "generic.remove": "Entfernen", - "lost_password.actions.cancel": "Abbrechen", - "lost_password.actions.login": "Zurück zum Anmeldebildschirm", - "lost_password.actions.register": "Konto erstellen?", - "lost_password.actions.save_new_password": "Aktualisieren", - "lost_password.actions.send": "Senden einer Wiederherstellungs-E-Mail", - "lost_password.change_success": "Ihr Passwort wurde geändert", - "lost_password.message": "Passwort vergessen?", - "lost_password.password": "Neues Passwort", - "lost_password.password_confirmation": "Bestätigen Sie das neue Passwort", - "lost_password.reset_title": "Definition eines neuen Passworts", - "lost_password.title": "Passwort-Wiederherstellung", "menu.about": "Entdecken Sie mehr über Caroster", "menu.code": "Caroster ist Open Source", "menu.dashboard": "Meine Carosters",

@@ -174,37 +159,17 @@ "profile.password_changed": "Passwort aktualisiert",

"profile.stripe_link.button": "Verlauf", "profile.title": "Profil", "signin.emailConfirmation": "Ihr Konto wurde bestätigt. Sie können sich jetzt anmelden.", - "signin.errors.EmailNotConfirmed": "Ihr Konto wurde nicht bestätigt. Bitte überprüfen Sie Ihre E-Mails", "signin.login": "$t(menu.login)", "signin.no_account": "Sie haben noch kein Konto ?", "signin.or": "oder", - "signin.password": "Passwort", - "signin.register": "$t(menu.register)", "signin.title": "Anmelden", "signin.withGoogle": "Google-Konto verwenden", - "signup.account_already": "Haben Sie bereits ein Konto?", "signup.create": "Konto erstellen", "signup.createForm": "Konto erstellen\nInformationen zum Ausfüllen", "signup.email": "E-Mail", "signup.errors.email_taken": "Diese E-Mail ist bereits mit einem Konto verbunden", "signup.lastName": "Nachname", - "signup.login": "$t(menu.login)", - "signup.password": "Passwort", - "signup.submit": "Ihr Konto erstellen", - "signup.title": "Registrieren", - "signup.with_mail": "Weiter mit einer E-Mail", "supportCaroster": "Caroster unterstützen", - "tour.creator.step1": "Fügen Sie eine neue Fahrt hinzu, indem Sie auf diese Schaltfläche klicken.", - "tour.creator.step2": "Die Warteliste umfasst Fahrgäste, die noch keinen Platz für eine Fahrt haben.", - "tour.creator.step3": "Die Informationen zur Veranstaltung können in diesem Menü geändert werden.", - "tour.creator.step4": "Die Veranstaltung kann durch Klicken auf die Schaltfläche \"Bearbeiten\" bearbeitet werden.", - "tour.user.step1": "Fügen Sie eine neue Reise hinzu, indem Sie auf diese Schaltfläche klicken.", - "tour.user.step2": "Möchten Sie einen Platz in einer Fahrt? Melden Sie sich auf der Warteliste oder direkt bei einer Fahrt an.", - "tour.user.step4": "Sie können den Link von nun an kopieren, um ihn per E-Mail, Whatsapp, Telegram usw. zu teilen.", - "tour.welcome.nope": "Später", - "tour.welcome.onboard": "Okay, los geht's!", - "tour.welcome.text": "Möchten Sie eine Feature-Tour machen?", - "tour.welcome.title": "Willkommen bei Caroster!", "travel.actions.remove_alert": "Sind Sie sicher, dass Sie diese Fahrt entfernen und die Nutzer auf die Warteliste setzen wollen?", "travel.actions.remove_alert.caroster_plus": "Sind Sie sicher, dass Sie diese Fahrt entfernen möchten? Die Passagiere werden benachrichtigt.", "travel.actions.removed.caroster_plus": "Die Fahrt wurde gestrichen und die Passagiere wurden informiert.",

@@ -265,7 +230,6 @@ "notification.type.AddedAsAdmin.content": "Sie sind zum Administrator der Veranstaltung befördert worden.",

"notification.type.EnabledCarosterPlus.content": "Caroster Plus wurde für Ihre Veranstaltung aktiviert.", "notification.type.NewPassengerInYourTrip.content": "Ein Passagier wurde zu Ihrer Fahrt hinzugefügt.", "event.loginToSetAlert": "Die Benachrichtigungen sind nur für die Teilnehmer dieser Fahrgemeinschaft verfügbar.", - "lost_password.error": "Diese E-Mail existiert nicht", "notification.type.PassengerJoinTrip.content": "Ein neuer Passagier möchte Sie kontaktieren, um mit Ihnen zu reisen.", "event.add_to_my_events.text": "Um <bold>{{eventName}}</bold> zu Ihren Veranstaltern hinzuzufügen, müssen Sie eingeloggt sein oder ein Konto erstellen.", "confirm.creating": "Erstellen des Kontos",

@@ -286,7 +250,6 @@ "profile.email": "E-Mail",

"profile.stripe_link.title": "Abrechnung", "travel.creation.car.title": "Auto und Fahrer", "passenger.success.added_to_waitlist": "{{name}} wurde zur Warteliste hinzugefügt", - "signin.errors.CredentialsSignin": "Überprüfen Sie Ihre E-Mail und Ihr Passwort. Wenn Ihr Konto mit Google verknüpft ist, verwenden Sie bitte Login mit Google.", "signup.firstName": "Vorname", "tour.user.step3": "Über dieses Menü können Sie auf die Veranstaltungsinformationen zugreifen.", "travel.creation.travel.dateHelper": "Bitte geben Sie ein gültiges Datum an",
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -41,8 +41,6 @@ "event.linked.goEvent": "Go",

"event.linked.returnEvent": "Return", "event.creation.addFromAccount.actions.login": "$t(menu.login)", "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "Create it from your account", - "event.creation.addFromAccount.title": "Do you want to add this Caroster to your events?", "event.creation.creator_email": "Your e-mail", "event.creation.date": "Date of the event", "event.creation.description": "Description",

@@ -94,20 +92,6 @@ "generic.me": "Me",

"generic.remove": "Remove", "generic.save": "Save", "generic.select": "Select", - "lost_password.actions.cancel": "Cancel", - "lost_password.actions.login": "Return to the login screen", - "lost_password.actions.register": "Create an account?", - "lost_password.actions.save_new_password": "Update", - "lost_password.actions.send": "Send a recovery email", - "lost_password.change_success": "Your password has been changed", - "lost_password.email": "Your email", - "lost_password.error": "This email does not exist", - "lost_password.message": "Lost your password?", - "lost_password.password": "New password", - "lost_password.password_confirmation": "Confirmation of the new password", - "lost_password.reset_title": "Definition of a new password", - "lost_password.sent": "An email has been sent to {{email}}, with a link to recover your password", - "lost_password.title": "Password recovery", "menu.about": "Discover more about Caroster", "menu.code": "Caroster is Open Source", "menu.dashboard": "My Carosters",

@@ -205,10 +189,10 @@ "profile.stripe_link.title": "Billing",

"profile.title": "Profile", "signin.email": "Email", "signin.emailConfirmation": "Your account has been confirmed. You can now login.", - "signin.errors.CredentialsSignin": "Check your email and password. If your account is linked to Google, please use the Google auth. button.", - "signin.errors.EmailNotConfirmed": "Your account has not been confirmed. Please check your emails", + "signin.errors.CredentialsSignin": "Login link has expired. Please enter your email to receive a new link.", "signin.login": "$t(menu.login)", "signin.no_account": "You don't have an account ?", + "signin.check_email": "A login link has been sent to your email. Please check your email to complete the login.", "signin.or": "OR", "signin.password": "Password", "signin.register": "$t(menu.register)",

@@ -216,34 +200,8 @@ "signin.title": "Sign in",

"signin.withGoogle": "Use a Google account", "emailConfirmation.invalidMessage": "This confirmation link is no longer valid because it has already been used or has expired.", "emailConfirmation.toLogin": "Go to login page", - "signup.account_already": "Do you already have an account ?", - "signup.create": "Create an account", - "signup.createForm": "Create an account\ninformation to fullfill", - "signup.email": "Email", - "signup.errors.email_taken": "This email is already associated with an account", - "signup.firstName": "First name", - "signup.lastName": "Last name", - "signup.login": "$t(menu.login)", - "signup.newsletter.consent": "I am interested in carpooling, I want to subscribe to the newsletter.", - "signup.password": "Password", - "signup.submit": "Create your account", - "signup.title": "Sign up", "signup.tos.consent": "I accept <tos-link>terms of service</tos-link> and <data-privacy-link>data privacy policy</data-privacy-link>", - "signup.with_mail": "Continue with an email", "supportCaroster": "Support Caroster", - "tour.creator.step1": "Add a new trip by clicking on this button.", - "tour.creator.step2": "The waiting list includes passengers who do not yet have a seat for a trip.", - "tour.creator.step3": "The event information can be modified from this menu.", - "tour.creator.step4": "The event can be edited by clicking on the edit button.", - "tour.creator.step5": "You can copy the link from now on to share it via email, whatsapp, telegram, etc.", - "tour.user.step1": "Add a new trip by clicking on this button.", - "tour.user.step2": "Would you like a place in a trip? Register on the waiting list or directly in a trip.", - "tour.user.step3": "The event information can be accessed from this menu.", - "tour.user.step4": "You can copy the link from now on to share it via email, whatsapp, telegram, etc.", - "tour.welcome.nope": "Later", - "tour.welcome.onboard": "OK, let's go!", - "tour.welcome.text": "Would you like to take a feature tour?", - "tour.welcome.title": "Welcome to Caroster!", "travel.actions.remove_alert": "Are you sure you want to remove this trip and add the subscribers to the waiting list?", "travel.actions.remove_alert.caroster_plus": "Are you sure you want to remove this trip? Passengers will be notified.", "travel.actions.removed": "The trip has been removed and its passengers moved to the waiting list.",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -4,13 +4,9 @@ "alert.create": "Votre alerte a bien été configurée",

"alert.description": "Configurez une alerte pour être notifié des nouveaux trajets ajoutés.", "alert.errors.cant_create": "Impossible de créer votre alerte", "alert.location.label": "Votre localisation", - "alert.optional": "Optionnel", "alert.radius.label": "Rayon désiré", "alert.title": "Alertes", - "confirm.creating": "Création de compte", "confirm.google.title": "Finaliser l'inscription", - "confirm.text": "Vous avez reçu un email avec un lien. Merci de cliquer sur ce lien pour confirmer votre compte.", - "confirm.title": "Confirmez votre email", "dashboard.actions.add_event": "Créer un caroster", "dashboard.actions.see_event": "Accéder au caroster", "dashboard.noEvent.create_event": "$t(menu.new_event)",

@@ -41,8 +37,6 @@ "event.linked.goEvent": "Aller",

"event.linked.returnEvent": "Retour", "event.creation.addFromAccount.actions.login": "$t(menu.login)", "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "Créez-le depuis votre compte", - "event.creation.addFromAccount.title": "Voulez-vous ajouter ce caroster à vos évènements ?", "event.creation.creator_email": "Votre e-mail", "event.creation.date": "Date de l'événement", "event.creation.description": "Description",

@@ -95,20 +89,6 @@ "generic.remove": "Supprimer",

"generic.save": "Enregistrer", "generic.select": "Selectionner", "generic.optional": "Optionnel", - "lost_password.actions.cancel": "Annuler", - "lost_password.actions.login": "Retour à l'écran de connexion", - "lost_password.actions.register": "Créer un compte ?", - "lost_password.actions.save_new_password": "Mettre à jour", - "lost_password.actions.send": "Envoyer un email de récupération", - "lost_password.change_success": "Votre mot de passe a été modifié", - "lost_password.email": "Votre email", - "lost_password.error": "Cet email n'existe pas", - "lost_password.message": "Mot de passe oublié ?", - "lost_password.password": "Nouveau mot de passe", - "lost_password.password_confirmation": "Confirmation du nouveau mot de passe", - "lost_password.reset_title": "Définition d'un nouveau mot de passe", - "lost_password.sent": "Un email a été envoyé à {{email}}, avec un lien pour récupérer votre mot de passe", - "lost_password.title": "Récupération de mot de passe", "menu.about": "En savoir plus sur Caroster", "menu.code": "Caroster est Open Source", "menu.dashboard": "Mes Carosters",

@@ -204,46 +184,17 @@ "profile.stripe_link.button": "Historique",

"profile.stripe_link.title": "Facturation", "profile.title": "Profil", "signin.email": "Email", - "signin.emailConfirmation": "Votre compte a bien été confirmé. Vous pouvez maintenant vous connecter.", - "signin.errors.CredentialsSignin": "Vérifiez votre email et mot de passe. Si votre compte est lié à Google, merci d'utiliser l'authentification Google.", - "signin.errors.EmailNotConfirmed": "Votre compte n'a pas été confirmé. Merci de vérifier vos emails", + "signin.errors.CredentialsSignin": "Le lien de connexion est échu. Merci d'entrer votre email pour recevoir un nouveau lien.", "signin.login": "$t(menu.login)", - "signin.no_account": "Vous n'avez pas de compte ?", + "signin.check_email": "Un lien de connexion vous a été envoyé par e-mail. Veuillez vérifier vos e-mail pour terminer la connexion.", "signin.or": "OU", "signin.password": "Mot de passe", - "signin.register": "$t(menu.register)", "signin.title": "Se connecter", "signin.withGoogle": "Continuer avec Google", - "emailConfirmation.invalidMessage": "Ce lien de confirmation n'est plus valide car il a déjà été utilisé ou est échue.", - "emailConfirmation.toLogin": "Vers la page de connexion", - "signup.account_already": "Vous avez déjà un compte ?", - "signup.create": "Créer un compte", - "signup.createForm": "Créer un compte\ninformations à remplir", - "signup.email": "Email", - "signup.errors.email_taken": "Cet email est déjà associé à un compte", "signup.firstName": "Prénom", "signup.lastName": "Nom", - "signup.login": "$t(menu.login)", - "signup.newsletter.consent": "Le covoiturage m'intéresse, je souhaite recevoir des nouvelles de Caroster", - "signup.password": "Mot de passe", - "signup.submit": "Créer un compte", - "signup.title": "Inscription", "signup.tos.consent": "J'accepte les <tos-link>conditions d'utilisation</tos-link> et la <data-privacy-link>politique de gestion de données</data-privacy-link>", - "signup.with_mail": "Continuer avec un email", "supportCaroster": "Soutenir Caroster", - "tour.creator.step1": "Ajoutez un nouveau trajet en cliquant directement sur ce bouton.", - "tour.creator.step2": "La liste d'attente regroupe les personnes qui n'ont pas encore été placées dans un trajet.", - "tour.creator.step3": "Vous pouvez voir et modifier les informations de l'événement depuis ce menu.✨", - "tour.creator.step4": "Editez l'événement en cliquant sur le bouton d'édition.", - "tour.creator.step5": "Copiez le lien de l'événement dès maintenant pour le partager par email, whatsapp, telegram, etc.. ", - "tour.user.step1": "Ajoutez un nouveau trajet en cliquant directement sur ce bouton.", - "tour.user.step2": "Vous aimeriez une place dans un trajet ? Inscrivez-vous dans la liste d'attente ou directement dans un trajet.", - "tour.user.step3": "Les informations de l'événement sont accessibles depuis ce menu.", - "tour.user.step4": "Copiez le lien de l'événement dès maintenant pour le partager par email, whatsapp, telegram, etc..", - "tour.welcome.nope": "Pas maintenant", - "tour.welcome.onboard": "OK, c'est parti!", - "tour.welcome.text": "Faire un tour des fonctionnalités.", - "tour.welcome.title": "Bienvenue sur Caroster !", "travel.actions.remove_alert": "Voulez-vous vraiment supprimer ce trajet et ajouter les inscrits à la liste d'attente ?", "travel.actions.remove_alert.caroster_plus": "Voulez-vous vraiment supprimer ce trajet ? Les inscrits seront avertis.", "travel.actions.removed": "Le trajet a été supprimée et ses passagers déplacés dans la liste d'attente.",
M frontend/locales/it.jsonfrontend/locales/it.json

@@ -36,8 +36,6 @@ "event.add_to_my_events.register": "$t(menu.register)",

"event.add_to_my_events.title": "Devi effettuare l'accesso", "event.creation.addFromAccount.actions.login": "$t(menu.login)", "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "Crealo dal tuo account", - "event.creation.addFromAccount.title": "Vuoi aggiungere questo Caroster ai tuoi eventi?", "event.creation.address": "Indirizzo dell'evento", "event.creation.creator_email": "Il tuo indirizzo email", "event.creation.date": "Data dell'evento",

@@ -230,37 +228,15 @@ "profile.notification.value.yes": "Attivato",

"profile.password_changed": "Password aggiornata", "profile.stripe_link.button": "Cronologia", "profile.title": "Profilo", - "signin.errors.EmailNotConfirmed": "La tua registrazione non è ancora confermata. Controlla le tue email", "signin.login": "$t(menu.login)", "signin.no_account": "Non sei registrato/a?", "signin.or": "OPPURE", - "signin.password": "Password", - "signin.register": "$t(menu.register)", "signin.withGoogle": "Usa un account Google", - "signup.account_already": "Hai già un account?", - "signup.create": "Crea un account", - "signup.createForm": "Crea un account\naggiungi le informazioni", "signup.email": "Email", "signup.firstName": "Nome", "signup.lastName": "Cognome", - "signup.login": "$t(menu.login)", "signup.newsletter.consent": "Mi interessa il carpooling, desidero iscrivermi alla newsletter.", - "signup.password": "Password", - "signup.submit": "Crea il tuo account", - "signup.title": "Registrazione", - "signup.with_mail": "Continua con un'email", "supportCaroster": "Supporta Caroster", - "tour.creator.step1": "Aggiungi un passaggio cliccando su questo pulsante.", - "tour.creator.step2": "La lista d'attesa include i passeggeri che non hanno ancora trovato un passaggio.", - "tour.creator.step3": "Le informazioni sull'evento possono essere modificate da questo menu.", - "tour.creator.step5": "Ora puoi copiare il link e condividerlo per email, whatsapp, etc.", - "tour.user.step1": "Aggiungi un nuovo passaggio cliccando su questo pulsante.", - "tour.user.step2": "Vuoi trovare un passaggio? Registrati alla lista d'attesa o direttamente in passaggio.", - "tour.user.step4": "Ora puoi copiare il link e condividerlo per email, whatsapp, telegram, etc.", - "tour.welcome.nope": "Più tardi", - "tour.welcome.onboard": "Ok, si parte!", - "tour.welcome.text": "Vuoi fare un tour delle funzionalità?", - "tour.welcome.title": "Ti diamo il benvenuto su Caroster!", "travel.actions.remove_alert.caroster_plus": "Vuoi davvero rimuovere questo passaggio? I passeggeri riceveranno una notifica.", "travel.actions.removed": "Il passaggio è stato rimosso e i suoi passeggeri spostati nella lista d'attesa.", "travel.actions.removed.caroster_plus": "Il passaggio è stato rimosso e i suoi passeggeri sono stati avvisati.",

@@ -300,7 +276,6 @@ "travel.requestTrip.title": "Informazioni inviate",

"lost_password.actions.send": "Invia un'email di recupero", "notification.type.EnabledCarosterPlus.content": "Caroster Plus è stato attivato per il tuo evento.", "placeInput.mapboxUnavailable": "Non è momentaneamente possibile suggerire posizioni geolocalizzate", - "signin.errors.CredentialsSignin": "Controlla email e password. Se il tuo account è connesso a Google, usa il pulsante di login con Google.", "tour.creator.step4": "L'evento può essere modificato cliccando il pulsante Modifica.", "emailConfirmation.toLogin": "Vai alla pagina di login", "emailConfirmation.invalidMessage": "Questo link di registrazione non è più valido perché già utilizzato o scaduto.",
M frontend/locales/nl.jsonfrontend/locales/nl.json

@@ -39,8 +39,6 @@ "event.add_to_my_events.text": "U kunt <bold>{{eventName}}</bold> alleen toevoegen aan uw carosters als u bent ingelogd.",

"event.add_to_my_events.title": "U dient ingelogd te zijn", "event.creation.addFromAccount.actions.login": "$t(menu.login)", "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "Maken vanaf account", - "event.creation.addFromAccount.title": "Wilt u deze caroster toevoegen aan uw afspraken?", "event.creation.creator_email": "Mijn e-mailadres", "event.creation.date": "Afspraakdatum", "event.creation.description": "Beschrijving",

@@ -93,20 +91,6 @@ "generic.remove": "Verwijderen",

"generic.save": "Opslaan", "generic.select": "Selecteren", "generic.optional": "Optioneel", - "lost_password.actions.cancel": "Annuleren", - "lost_password.actions.login": "Terug naar inlogscherm", - "lost_password.actions.register": "Account aanmaken?", - "lost_password.actions.save_new_password": "Bijwerken", - "lost_password.actions.send": "Herstele-mail versturen", - "lost_password.change_success": "Uw wachtwoord is gewijzigd", - "lost_password.email": "Mijn e-mailadres", - "lost_password.error": "Dit e-mailadres bestaat niet", - "lost_password.message": "Bent u uw wachtwoord vergeten?", - "lost_password.password": "Nieuw wachtwoord", - "lost_password.password_confirmation": "Nieuw wachtwoord bevestigen", - "lost_password.reset_title": "Nieuw wachtwoord instellen", - "lost_password.sent": "Er is een e-mail verstuurd naar {{email}} met een link om uw wachtwoord te herstellen", - "lost_password.title": "Wachtwoordherstel", "menu.about": "Meer informatie over Caroster", "menu.code": "Caroster is open source", "menu.dashboard": "Mijn carosters",

@@ -200,43 +184,17 @@ "profile.stripe_link.title": "Facturering",

"profile.title": "Profiel", "signin.email": "E-mailadres", "signin.emailConfirmation": "Uw account is bevestigd - u kunt nu inloggen.", - "signin.errors.CredentialsSignin": "Controleer uw e-mailadres en wachtwoord. Als uw account gekoppeld is aan Google, log dan in met uw Google-account.", - "signin.errors.EmailNotConfirmed": "Uw account is nog niet bevestigd - controleer uw postvak in.", "signin.login": "$t(menu.login)", "signin.no_account": "Heeft u nog geen account?", "signin.or": "OF", - "signin.password": "Wachtwoord", - "signin.register": "$t(menu.register)", "signin.title": "Inloggen", "signin.withGoogle": "Inloggen met Google-account", - "signup.account_already": "Heeft u al een account?", - "signup.create": "Account aanmaken", - "signup.createForm": "Vul alle account-\ninformatie in", "signup.email": "E-mailadres", - "signup.errors.email_taken": "Dit e-mailadres is al toegewezen aan een account", "signup.firstName": "Voornaam", "signup.lastName": "Achternaam", - "signup.login": "$t(menu.login)", "signup.newsletter.consent": "Ik ben geïnteresseerd in carpooling en wil me abonneren op de nieuwsbrief.", - "signup.password": "Wachtwoord", - "signup.submit": "Account aanmaken", - "signup.title": "Registreren", "signup.tos.consent": "Ik ga akkoord met de <tos-link>algemene voorwaarden</tos-link> en het <data-privacy-link>privacybeleid</data-privacy-link>", - "signup.with_mail": "Doorgaan met e-mailadres", "supportCaroster": "Doneren", - "tour.creator.step1": "Klik op deze knop om een voertuig toe te voegen.", - "tour.creator.step2": "De wachtlijst bevat passagiers die nog geen plekje hebben in uw voertuig.", - "tour.creator.step3": "De afspraakinformatie kan met behulp van dit menu worden bewerkt.", - "tour.creator.step4": "De afspraak kan worden bewerkt door op de bewerkknop te klikken.", - "tour.creator.step5": "U kunt de link kopiëren en delen via e-mail, WhatsApp, Telegram, etc.", - "tour.user.step1": "Klik op deze knop om een voertuig toe te voegen.", - "tour.user.step2": "Wilt u een plekje in een voertuig reserveren? Zet uzelf dan op de wachtlijst of kies een voertuig.", - "tour.user.step3": "De afspraakinformatie is toegankelijk via dit menu.", - "tour.user.step4": "U kunt de link kopiëren en delen via e-mail, WhatsApp, Telegram, etc.", - "tour.welcome.nope": "Later", - "tour.welcome.onboard": "Ja, graag!", - "tour.welcome.text": "Wilt u een rondleiding nemen?", - "tour.welcome.title": "Welkom bij Caroster!", "travel.actions.remove_alert": "Weet u zeker dat u dit voertuig wilt verwijderen en daardoor de passagiers wilt toevoegen aan de wachtlijst?", "travel.actions.remove_alert.caroster_plus": "Weet u zeker dat u dit voertuig wilt verwijderen? Alle passagiers worden hiervan op de hoogte gesteld.", "travel.actions.removed": "Het voertuig is verwijderd en de passagiers zijn toegevoegd aan de wachtlijst.",
D frontend/locales/pl.json

@@ -1,281 +0,0 @@

-{ - "alert.button.label": "Zapisz", - "alert.location.label": "Twoja lokacja", - "alert.optional": "Opcjonalne", - "alert.title": "Ostrzeżenia", - "confirm.creating": "Tworzenie konta", - "confirm.google.title": "Zakończ rejestrację", - "confirm.text": "Wysłano wiadomość e-mail z linkiem. Kliknij ten link, aby potwierdzić swoje konto.", - "confirm.title": "Potwierdź swój e-mail", - "dashboard.actions.add_event": "", - "dashboard.actions.see_event": "", - "dashboard.noEvent.create_event": "$t(menu.new_event)", - "dashboard.noEvent.text_html": "", - "dashboard.noEvent.title": "", - "dashboard.sections.future_0": "", - "dashboard.sections.future_1": "", - "dashboard.sections.future_2": "", - "dashboard.sections.noDate_0": "", - "dashboard.sections.noDate_1": "", - "dashboard.sections.noDate_2": "", - "dashboard.sections.past_0": "", - "dashboard.sections.past_1": "", - "dashboard.sections.past_2": "", - "dashboard.title": "$t(menu.dashboard)", - "date.today": "Dzisiaj", - "drawer.alerts": "Ostrzeżenia", - "drawer.information": "Informacje", - "drawer.options": "Ustawienia", - "drawer.travels": "Podróże", - "drawer.waitingList": "Lista oczekujących", - "event.actions.add_to_my_events": "", - "event.actions.copied": "", - "event.actions.noShareCapability": "", - "event.actions.share": "", - "event.add_to_my_events.login": "$t(menu.login)", - "event.add_to_my_events.register": "$t(menu.register)", - "event.add_to_my_events.text": "", - "event.add_to_my_events.title": "", - "event.creation.addFromAccount.actions.login": "$t(menu.login)", - "event.creation.addFromAccount.actions.register": "$t(menu.register)", - "event.creation.addFromAccount.subtitle": "", - "event.creation.addFromAccount.title": "", - "event.creation.address": "Miejsce wydarzenia", - "event.creation.creator_email": "Twój adres e-mail", - "event.creation.date": "Data wydarzenia", - "event.creation.description": "Opis", - "event.creation.description_helper": "Opcjonalne", - "event.creation.name": "Nazwa wydarzenia", - "event.creation.newsletter": "Informuj mnie o rozwoju Caroster przez e-mail", - "event.creation.next": "Dalej", - "event.creation.title": "Nowe wydarzenie", - "event.details": "Informacje", - "event.details.modify": "Modyfikuj", - "event.details.save": "Zapisz", - "event.errors.cant_create": "", - "event.errors.cant_update": "", - "event.fields.address": "Miejsce wydarzenia", - "event.fields.copyLink": "Kopiuj link", - "event.fields.date": "Data wydarzenia", - "event.fields.date_placeholder": "DD/MM/YYYY", - "event.fields.description": "Opis", - "event.fields.empty": "Nie określono", - "event.fields.lang": "Język", - "event.fields.link": "Udostępnij link", - "event.fields.link_desc": "Udostępnij ten link innym osobom", - "event.fields.name": "Nazwa wydarzenia", - "event.fields.share": "Udostępnij", - "event.loginToAttend": "", - "event.loginToAttend.desc": "", - "event.loginToAttend.login": "$t(menu.login)", - "event.loginToAttend.signup": "$t(signup.title)", - "event.no_other_travel.title": "Obecnie nie ma żadnych innych samochodów", - "event.no_travel.desc": "1. Zasubskrybuj listę oczekujących\n2. Udostępnij wydarzenie\n3. Dostaniesz powiadomienie kiedy zostanie dodana nowa podróż", - "event.no_travel.title": "Obecnie nie ma żadnych samochodów", - "event.not_found": "Nie znaleziono projektu", - "event.title": "{{title}} - Caroster", - "generic.access": "Dostęp", - "generic.add": "toevoegen", - "generic.cancel": "Anuluj", - "generic.confirm": "Potwierdź", - "generic.create": "Stwórz", - "generic.delete": "Usuń", - "generic.errors.not_found": "Nie znaleziono zasobu", - "generic.errors.unknown": "Wystąpił nieznany błąd", - "generic.me": "Ja", - "generic.remove": "Usuń", - "generic.save": "Zapisz", - "generic.select": "Wybierz", - "lost_password.actions.cancel": "Anuluj", - "lost_password.actions.login": "Wróć do ekranu logowania", - "lost_password.actions.register": "Stworzyć konto?", - "lost_password.actions.save_new_password": "Aktualizuj", - "lost_password.actions.send": "", - "lost_password.change_success": "Twoje hasło zostało zmienione", - "lost_password.email": "Twój e-mail", - "lost_password.error": "Ten e-mail nie istnieje", - "lost_password.message": "", - "lost_password.password": "Nowe hasło", - "lost_password.password_confirmation": "Potwierdzenie nowego hasła", - "lost_password.reset_title": "Definiowanie nowego hasła", - "lost_password.sent": "", - "lost_password.title": "Odzyskiwanie hasła", - "menu.about": "Dowiedz się więcej o Caroster", - "menu.code": "Caroster jest Open Source", - "menu.dashboard": "", - "menu.language": "Zmień język", - "menu.login": "Zaloguj się", - "menu.logout": "Wyloguj się", - "menu.new_event": "", - "menu.profile": "Mój profil", - "menu.register": "Zarejestruj się", - "notification.type.AddedAsAdmin.content": "", - "notification.type.ContactTripCreator.content": "", - "notification.type.DeletedFromTrip.content": "", - "notification.type.DeletedTrip.content": "", - "notification.type.DeletedYourTrip.content": "", - "notification.type.EnabledCarosterPlus.content": "", - "notification.type.NewPassengerInYourTrip.content": "", - "notification.type.NewTrip.content": "", - "notifications.content": "Brak powiadomień", - "notifications.markAllRead": "", - "notifications.title": "Powiadomienia", - "options.plus.activationForbiden": "", - "options.plus.activationOK": "", - "options.plus.addAdmin": "", - "options.plus.addAdmin.email": "E-mail", - "options.plus.addAdmin.emailHelper": "", - "options.plus.addAdminError": "", - "options.plus.adminAdded": "", - "options.plus.adminDeleted": "", - "options.plus.admins": "Administratorzy", - "options.plus.creator": "Twórca", - "options.plus.deleteAdminError": "", - "options.plus.title": "Caroster plus", - "passenger.actions.place": "Przypisz", - "passenger.actions.remove_alert": "Czy na pewno chcesz usunąć <italic> <bold> {{name}} </bold> </italic> z listy oczekujących?", - "passenger.assign.assign": "Przypisz", - "passenger.assign.availableCars": "Dostępne samochody", - "passenger.assign.departure": "Czas odjazdu: ", - "passenger.assign.no_travel.desc": "", - "passenger.assign.no_travel.title": "", - "passenger.assign.seats": "", - "passenger.assign.seats_0": "", - "passenger.assign.seats_zero": "Pełne", - "passenger.assign.title": "", - "passenger.availability.seats_0": "", - "passenger.availability.seats_1": "", - "passenger.availability.seats_2": "", - "passenger.deleted": "Pasażer został usunięty z wydarzenia.", - "passenger.errors.cant_add_passenger": "Nie można dodać pasażera", - "passenger.errors.cant_remove_passenger": "Nie można usunąć pasażera", - "passenger.errors.cant_select_travel": "", - "passenger.informations.call.label": "Połączenie", - "passenger.informations.email.label": "E-mail", - "passenger.informations.name.label": "Imię", - "passenger.informations.phone.label": "Numer telefonu", - "passenger.informations.surname.label": "Nazwisko", - "passenger.informations.title": "Skontaktuj się z nami", - "passenger.success.added_self_to_car": "Dodano cię do tego samochodu", - "passenger.success.added_self_to_waitlist": "Dodano cię do listy oczekujących. Otrzymasz powiadomienie, gdy zostaną dodane nowe samochody.", - "passenger.success.added_to_car": "Dodano {{name}} do tego samochodu", - "passenger.success.added_to_waitlist": "Dodano {{name}} do listy oczekujących", - "passenger.success.goToTravels": "", - "passenger.title": "Lista oczekujących", - "placeInput.mapboxUnavailable": "", - "placeInput.noCoordinates": "", - "profile.actions.cancel": "Anuluj", - "profile.actions.change_password": "", - "profile.actions.edit": "Edytuj", - "profile.actions.logout": "Wyloguj się", - "profile.actions.save": "Zapisz", - "profile.actions.save_new_password": "Aktualizuj", - "profile.current_password": "Obecne hasło", - "profile.email": "Email", - "profile.errors.password_nomatch": "Nieprawidłowe hasło", - "profile.firstName": "Imię", - "profile.lastName": "Nazwisko", - "profile.new_password": "Nowe hasło", - "profile.newsletter.value.no": "Nie", - "profile.newsletter.value.yes": "Tak", - "profile.not_defined": "Nie określono", - "profile.notification.label": "Powiadomienia", - "profile.notification.value.no": "Wyłączone", - "profile.notification.value.yes": "Aktywowane", - "profile.password_changed": "Zaktualizowano hasło", - "profile.stripe_link.button": "Historia", - "profile.stripe_link.title": "Rachunek", - "profile.title": "Profil", - "signin.email": "E-mail", - "signin.emailConfirmation": "Twoje konto zostało potwierdzone. Teraz można się zalogować.", - "signin.errors.CredentialsSignin": "", - "signin.errors.EmailNotConfirmed": "", - "signin.login": "$t(menu.login)", - "signin.no_account": "Nie masz konta?", - "signin.or": "LUB", - "signin.password": "Hasło", - "signin.register": "$t(menu.register)", - "signin.title": "Sign in", - "signin.withGoogle": "Użyj konta Google", - "signup.account_already": "Czy masz już konto?", - "signup.create": "Stwórz konto", - "signup.createForm": "Stwórz konto\ninformacje do wypełnienia", - "signup.email": "E-mail", - "signup.errors.email_taken": "Ten adres e-mail jest już powiązany z kontem", - "signup.firstName": "Imię", - "signup.lastName": "Nazwisko", - "signup.login": "$t(menu.login)", - "signup.newsletter.consent": "Interesuje mnie car pooling, chcę zapisać się do newslettera.", - "signup.password": "Hasło", - "signup.submit": "Stwórz swoje konto", - "signup.title": "Załóż konto", - "signup.tos.consent": "", - "signup.with_mail": "Kontynuuj z e-mailem", - "supportCaroster": "Wesprzyj Caroster", - "tour.creator.step1": "Dodaj nowy samochód klikając ten przycisk.", - "tour.creator.step2": "Lista oczekujących obejmuje pasażerów, którzy nie mają jeszcze miejsca w samochodzie.", - "tour.creator.step3": "Informacje o wydarzeniu można modyfikować w tym menu.", - "tour.creator.step4": "Wydarzenie można edytować, klikając przycisk edycji.", - "tour.creator.step5": "Od teraz możesz skopiować link, aby udostępnić go za pośrednictwem poczty e-mail, WhatsAppa, Telegrama itp.", - "tour.user.step1": "Dodaj nowy samochód, klikając ten przycisk.", - "tour.user.step2": "", - "tour.user.step3": "Dostęp do informacji o wydarzeniu można uzyskać z tego menu.", - "tour.user.step4": "Od teraz możesz skopiować link, aby udostępnić go za pośrednictwem poczty e-mail, WhatsAppa, Telegrama itp.", - "tour.welcome.nope": "Później", - "tour.welcome.onboard": "Tak, ruszajmy!", - "tour.welcome.text": "Czy chcesz zobaczyć w prezentację naszych funkcji?", - "tour.welcome.title": "Witamy w Caroster!", - "travel.actions.remove_alert": "", - "travel.actions.remove_alert.caroster_plus": "", - "travel.actions.removed": "", - "travel.actions.removed.caroster_plus": "", - "travel.creation.car.title": "", - "travel.creation.created": "", - "travel.creation.date": "", - "travel.creation.notes": "Dodatkowe informacje", - "travel.creation.phone": "Numer telefonu", - "travel.creation.phoneHelper.faq": "/pl/faq", - "travel.creation.phoneHelper.why": "", - "travel.creation.seats": "", - "travel.creation.submit": "Dodaj", - "travel.creation.time": "", - "travel.creation.title": "", - "travel.creation.travel.title": "Podróż", - "travel.errors.cant_create": "", - "travel.errors.cant_remove": "", - "travel.errors.cant_remove_passenger": "", - "travel.errors.cant_update": "", - "travel.fields.details": "Notatki", - "travel.meeting": "Miejsce spotkania", - "travel.fields.phone": "Kontakt", - "travel.moved_to_waiting_list": "", - "travel.passengers.add": "", - "travel.passengers.add_me": "", - "travel.passengers.add_someone": "", - "travel.passengers.add_to_car": "", - "travel.passengers.add_to_travel": "", - "travel.passengers.add_to_waitingList": "", - "travel.passengers.email": "Email", - "travel.passengers.email_helpertext": "", - "travel.passengers.email_placeholder": "Email", - "travel.passengers.email_placeholder_optionnal": "", - "travel.passengers.empty": "", - "travel.passengers.location_helper": "", - "travel.passengers.name": "Imię", - "travel.passengers.name_placeholder": "Imię", - "travel.passengers.register_to_waiting_list": "", - "travel.passengers.registered": "Przypisano", - "travel.passengers.remove": "Usuń", - "travel.requestTrip.description": "", - "travel.requestTrip.email": "Email", - "travel.requestTrip.emailHelper": "", - "travel.requestTrip.phone": "Numer telefonu", - "travel.requestTrip.send": "Wyślij", - "travel.requestTrip.title": "", - "event.filter.dates": "Wiele dat", - "passenger.informations.notSpecify": "Nie podano", - "placeInput.item.noCoordinates": "Brak koordynatów", - "travel.removePassengerModal.cancel": "Anuluj", - "event.filter.title": "Filtruj daty", - "event.filter.allDates": "Wszystkie daty" -}
D frontend/locales/sv.json

@@ -1,253 +0,0 @@

-{ - "confirm.creating": "Skapar kontot", - "confirm.google.title": "Färdigställ registrering", - "confirm.text": "Du har fått ett E-mail med en länk. Klicka på den här länken för att bekräfta ditt konto.", - "confirm.title": "Bekräfta din E-mailadress", - "dashboard.actions.add_event": "Skapa en caroster", - "dashboard.actions.see_event": "Gå till caroster", - "dashboard.noEvent.create_event": "$t(menu.new_event)", - "dashboard.noEvent.text_html": "Här kommer du att se <strong> de carosters du deltar i </strong>, för att börja skapa en Caroster!", - "dashboard.noEvent.title": "Välkommen till Caroster", - "dashboard.sections.future_one": "Kommande Caroster", - "dashboard.sections.future_other": "Kommande Carosters", - "dashboard.sections.noDate_one": "Caroster utan datum", - "dashboard.sections.noDate_other": "Carosters utan datum", - "dashboard.sections.past_one": "", - "dashboard.sections.past_other": "", - "dashboard.title": "$t(menu.dashboard)", - "drawer.information": "", - "drawer.options": "", - "drawer.travels": "", - "drawer.waitingList": "", - "event.actions.add_to_my_events": "", - "event.actions.copied": "", - "event.actions.noShareCapability": "", - "event.actions.share": "", - "event.add_to_my_events.login": "$t(menu.login)", - "event.add_to_my_events.register": "$t(menu.register)", - "event.add_to_my_events.text": "", - "event.add_to_my_events.title": "", - "event.creation.addFromAccount.actions.login": "", - "event.creation.addFromAccount.actions.register": "", - "event.creation.addFromAccount.subtitle": "", - "event.creation.addFromAccount.title": "", - "event.creation.creator_email": "", - "event.creation.date": "", - "event.creation.description": "", - "event.creation.description_helper": "", - "event.creation.name": "", - "event.creation.newsletter": "", - "event.creation.next": "", - "event.creation.title": "", - "event.details": "", - "event.details.modify": "", - "event.details.save": "", - "event.errors.cant_create": "", - "event.errors.cant_update": "", - "event.fields.address": "", - "event.fields.copyLink": "", - "event.fields.date": "", - "event.fields.date_placeholder": "", - "event.fields.description": "", - "event.fields.empty": "", - "event.fields.lang": "", - "event.fields.link": "", - "event.fields.link_desc": "", - "event.fields.name": "", - "event.fields.share": "", - "event.loginToAttend": "", - "event.loginToAttend.desc": "", - "event.loginToAttend.login": "$t(menu.login)", - "event.loginToAttend.signup": "$t(signup.title)", - "event.no_other_travel.title": "", - "event.no_travel.desc": "", - "event.no_travel.title": "", - "event.not_found": "", - "event.title": "", - "generic.access": "", - "generic.add": "toevoegen", - "generic.cancel": "", - "generic.confirm": "", - "generic.create": "", - "generic.delete": "", - "generic.errors.not_found": "", - "generic.errors.unknown": "", - "generic.me": "", - "generic.remove": "", - "generic.save": "", - "generic.select": "", - "lost_password.actions.cancel": "", - "lost_password.actions.login": "", - "lost_password.actions.register": "", - "lost_password.actions.save_new_password": "", - "lost_password.actions.send": "", - "lost_password.change_success": "", - "lost_password.email": "", - "lost_password.error": "", - "lost_password.message": "", - "lost_password.password": "", - "lost_password.password_confirmation": "", - "lost_password.reset_title": "", - "lost_password.sent": "", - "lost_password.title": "", - "menu.about": "", - "menu.code": "Caroster is Open Source", - "menu.dashboard": "", - "menu.language": "", - "menu.login": "", - "menu.logout": "", - "menu.new_event": "", - "menu.profile": "", - "menu.register": "", - "notification.type.AddedAsAdmin.content": "", - "notification.type.ContactTripCreator.content": "", - "notification.type.DeletedFromTrip.content": "", - "notification.type.DeletedTrip.content": "", - "notification.type.DeletedYourTrip.content": "", - "notification.type.EnabledCarosterPlus.content": "", - "notification.type.NewPassengerInYourTrip.content": "", - "notification.type.NewTrip.content": "", - "notification.type.NewTripAlert.content": "", - "notifications.content": "", - "notifications.markAllRead": "", - "notifications.title": "", - "options.plus.activationForbiden": "", - "options.plus.activationOK": "", - "options.plus.addAdmin": "", - "options.plus.addAdmin.email": "", - "options.plus.addAdmin.emailHelper": "", - "options.plus.addAdminError": "", - "options.plus.adminAdded": "", - "options.plus.adminDeleted": "", - "options.plus.admins": "", - "options.plus.creator": "", - "options.plus.deleteAdminError": "", - "options.plus.title": "", - "passenger.actions.place": "", - "passenger.actions.remove_alert": "", - "passenger.assign.assign": "", - "passenger.assign.availableCars": "", - "passenger.assign.departure": "", - "passenger.assign.no_travel.desc": "", - "passenger.assign.no_travel.title": "", - "passenger.assign.seats": "", - "passenger.assign.seats_0": "Full", - "passenger.assign.title": "", - "passenger.availability.seats_one": "", - "passenger.availability.seats_other": "", - "passenger.deleted": "", - "passenger.errors.cant_add_passenger": "", - "passenger.errors.cant_remove_passenger": "", - "passenger.errors.cant_select_travel": "", - "passenger.success.added_self_to_car": "", - "passenger.success.added_self_to_waitlist": "", - "passenger.success.added_to_car": "", - "passenger.success.added_to_waitlist": "", - "passenger.success.goToTravels": "", - "passenger.title": "", - "placeInput.mapboxUnavailable": "", - "placeInput.noCoordinates": "", - "profile.actions.cancel": "", - "profile.actions.change_password": "", - "profile.actions.edit": "", - "profile.actions.logout": "", - "profile.actions.save": "", - "profile.actions.save_new_password": "", - "profile.current_password": "", - "profile.email": "", - "profile.errors.password_nomatch": "", - "profile.firstName": "", - "profile.lastName": "", - "profile.new_password": "", - "profile.not_defined": "", - "profile.password_changed": "", - "profile.stripe_link.button": "", - "profile.stripe_link.title": "", - "profile.title": "", - "signin.email": "", - "signin.emailConfirmation": "", - "signin.errors.CredentialsSignin": "", - "signin.errors.EmailNotConfirmed": "", - "signin.login": "", - "signin.no_account": "", - "signin.or": "", - "signin.password": "", - "signin.register": "", - "signin.title": "", - "signin.withGoogle": "", - "signup.account_already": "", - "signup.create": "", - "signup.createForm": "", - "signup.email": "", - "signup.errors.email_taken": "", - "signup.firstName": "", - "signup.lastName": "", - "signup.login": "", - "signup.newsletter.consent": "", - "signup.password": "", - "signup.submit": "", - "signup.title": "", - "signup.tos.consent": "", - "signup.with_mail": "", - "supportCaroster": "Support Caroster", - "tour.creator.step1": "", - "tour.creator.step2": "", - "tour.creator.step3": "", - "tour.creator.step4": "", - "tour.creator.step5": "", - "tour.user.step1": "", - "tour.user.step2": "", - "tour.user.step3": "", - "tour.user.step4": "", - "tour.welcome.nope": "", - "tour.welcome.onboard": "", - "tour.welcome.text": "", - "tour.welcome.title": "", - "travel.actions.remove_alert": "", - "travel.actions.remove_alert.caroster_plus": "", - "travel.actions.removed": "", - "travel.actions.removed.caroster_plus": "", - "travel.creation.car.title": "", - "travel.creation.created": "", - "travel.creation.date": "", - "travel.creation.notes": "", - "travel.creation.phone": "", - "travel.creation.phoneHelper.faq": "https://caroster.io/en/faq", - "travel.creation.phoneHelper.why": "", - "travel.creation.seats": "", - "travel.creation.submit": "", - "travel.creation.time": "", - "travel.creation.title": "", - "travel.creation.travel.title": "", - "travel.errors.cant_create": "", - "travel.errors.cant_remove": "", - "travel.errors.cant_remove_passenger": "", - "travel.errors.cant_update": "", - "travel.fields.details": "", - "travel.meeting": "", - "travel.fields.phone": "", - "travel.moved_to_waiting_list": "", - "travel.passengers.add": "", - "travel.passengers.add_me": "", - "travel.passengers.add_someone": "", - "travel.passengers.add_to_car": "", - "travel.passengers.add_to_travel": "", - "travel.passengers.add_to_waitingList": "", - "travel.passengers.email": "", - "travel.passengers.email_helpertext": "", - "travel.passengers.email_placeholder": "", - "travel.passengers.email_placeholder_optionnal": "", - "travel.passengers.empty": "", - "travel.passengers.location_helper": "", - "travel.passengers.name": "", - "travel.passengers.name_placeholder": "", - "travel.passengers.register_to_waiting_list": "", - "travel.passengers.registered": "", - "travel.passengers.remove": "", - "travel.requestTrip.description": "", - "travel.requestTrip.email": "", - "travel.requestTrip.emailHelper": "", - "travel.requestTrip.phone": "", - "travel.requestTrip.send": "", - "travel.requestTrip.title": "" -}
M frontend/next.config.jsfrontend/next.config.js

@@ -53,10 +53,6 @@ {

source: '/content-manager/:slug*', destination: `${STRAPI_URL}/content-manager/:slug*`, }, - { - source: '/api/auth/email-confirmation', - destination: '/api/auth/email-confirmation', - }, ]; }, });
D frontend/pages/api/auth/email-confirmation.tsx

@@ -1,21 +0,0 @@

-import type {NextApiRequest, NextApiResponse} from 'next'; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const confirmation = req.query.confirmation; - - try { - const response = await fetch( - `http://127.0.0.1:1337/api/auth/email-confirmation?confirmation=${confirmation}` - ); - if (response.redirected) - return res.redirect(302, '/auth/login?confirmed=true'); - const result = await response.json(); - if (result.error) throw new Error(result.error.name); - } catch (error) { - console.error(error); - return res.redirect(302, '/auth/email-confirmation'); - } -}
M frontend/pages/api/nauth/[...nextauth].jsfrontend/pages/api/nauth/[...nextauth].js

@@ -7,24 +7,21 @@

const authHandler = NextAuth({ providers: [ CredentialsProvider({ - name: 'Strapi', + name: 'magic-link', credentials: { - email: {label: 'Email', type: 'text'}, - password: {label: 'Password', type: 'password'}, + token: {label: 'Token', type: 'password'}, }, - async authorize(credentials, req) { - const response = await fetch(`${STRAPI_URL}/api/auth/local`, { + async authorize(credentials) { + console.log({credentials}); + const response = await fetch(`${STRAPI_URL}/api/auth/magic-link`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - identifier: credentials.email, - password: credentials.password, + token: credentials.token, }), }); const data = await response.json(); - if (data?.error?.message === 'Your account email is not confirmed') - throw new Error('EmailNotConfirmed'); - else if (!data?.jwt) return null; + if (!data?.jwt) return null; else { const {user, jwt} = data; return {...user, jwt};
D frontend/pages/auth/confirm/index.tsx

@@ -1,36 +0,0 @@

-import Typography from '@mui/material/Typography'; -import {useTheme} from '@mui/material/styles'; -import Icon from '@mui/material/Icon'; -import {useTranslation} from 'next-i18next'; -import CommonConfirm from '../../../layouts/ConfirmLayout'; -import pageUtils from '../../../lib/pageUtils'; - -const Confirm = () => { - const {t} = useTranslation(); - const theme = useTheme(); - - return ( - <CommonConfirm> - <Typography variant="subtitle1" align="center"> - {t('confirm.creating')} - </Typography> - <Typography variant="h5" align="center"> - {t('confirm.title')} - </Typography> - <Typography align="center" sx={{margin: theme.spacing(5, 0)}}> - <Icon fontSize="large">mail</Icon> - </Typography> - <Typography - sx={{margin: theme.spacing(5, 0)}} - variant="body2" - align="center" - > - {t('confirm.text')} - </Typography> - </CommonConfirm> - ); -}; - -export default Confirm; - -export const getServerSideProps = pageUtils.getServerSideProps();
D frontend/pages/auth/email-confirmation.tsx

@@ -1,59 +0,0 @@

-import { - Button, - Card, - CardActions, - CardMedia, - Container, - Typography, -} from '@mui/material'; -import {useTranslation} from 'next-i18next'; -import Layout from '../../layouts/Centered'; -import Logo from '../../components/Logo'; -import NextLink from 'next/link'; -import pageUtils from '../../lib/pageUtils'; -import {getSession} from 'next-auth/react'; - -const EmailConfirmation = () => { - const {t} = useTranslation(); - - return ( - <Layout displayMenu={false}> - <Container maxWidth="xs"> - <Card sx={{pt: 2, width: '100%'}}> - <CardMedia component={Logo} /> - <Typography sx={{p: 2}} variant="body2" align="center"> - {t(`emailConfirmation.invalidMessage`)} - </Typography> - <CardActions - sx={{ - flexDirection: 'column', - justifyContent: 'center', - textAlign: 'center', - mb: 2, - px: 2, - }} - > - <NextLink href="/auth/login" passHref> - <Button size="small">{t('emailConfirmation.toLogin')}</Button> - </NextLink> - </CardActions> - </Card> - </Container> - </Layout> - ); -}; - -export const getServerSideProps = async (context: any) => { - const session = await getSession(context); - - if (session) - return { - redirect: { - destination: '/', - permanent: false, - }, - }; - else return pageUtils.getServerSideProps()(context); -}; - -export default EmailConfirmation;
M frontend/pages/auth/login.tsxfrontend/pages/auth/login.tsx

@@ -1,45 +1,101 @@

-import CardMedia from '@mui/material/CardMedia'; -import Card from '@mui/material/Card'; -import Typography from '@mui/material/Typography'; -import {getSession} from 'next-auth/react'; -import {useTranslation} from 'next-i18next'; +import {useTranslation} from 'react-i18next'; import Layout from '../../layouts/Centered'; +import { + Button, + Card, + CardContent, + CardMedia, + Container, + Stack, + TextField, + Typography, + FormHelperText, +} from '@mui/material'; import Logo from '../../components/Logo'; -import SignInForm from '../../containers/SignInForm'; -import LanguagesIcon from '../../containers/Languages/Icon'; +import {getSession} from 'next-auth/react'; import pageUtils from '../../lib/pageUtils'; -import Container from '@mui/material/Container'; import Cookies from 'cookies'; +import {useState} from 'react'; +import LoginGoogle from '../../containers/LoginGoogle'; +import {useSendMagicLinkMutation} from '../../generated/graphql'; -interface PageProps { +interface Props { error?: string; - emailConfirmation?: boolean; } -const Login = (props: PageProps) => { - const {emailConfirmation} = props; - const {t} = useTranslation(); +const Login = (props: Props) => { + const {error} = props; + const {t, i18n} = useTranslation(); + const [email, setEmail] = useState(''); + const [sent, setSent] = useState(false); + const [sendMagicLink] = useSendMagicLinkMutation(); + + const handleSubmit = async (e: React.FormEvent<HTMLButtonElement>) => { + try { + if (email) await sendMagicLink({variables: {email, lang: i18n.language}}); + setSent(true); + } catch (error) { + console.error(error); + } + }; return ( <Layout menuTitle={t('signin.title')} displayMenu={false}> <Container maxWidth="xs"> <Card sx={{pt: 2, width: '100%'}}> <CardMedia component={Logo} /> - {emailConfirmation && ( - <Typography - sx={{p: 2}} - variant="body2" - align="center" - >{t`signin.emailConfirmation`}</Typography> - )} - <SignInForm error={props?.error} /> + + <CardContent> + <Stack spacing={2}> + <Typography variant="h6" align="center"> + {t('signin.title')} + </Typography> + {error && ( + <FormHelperText error sx={{textAlign: 'center'}}> + {t(errorsMap[error])} + </FormHelperText> + )} + {!sent && ( + <> + <TextField + label={t`signin.email`} + fullWidth + required + value={email} + onChange={e => setEmail(e.target.value)} + type="email" + /> + <Button + fullWidth + color="primary" + variant="contained" + disabled={!email} + onClick={handleSubmit} + > + {t('signin.login')} + </Button> + <Typography align="center">{t('signin.or')}</Typography> + <LoginGoogle /> + </> + )} + {sent && ( + <Typography + variant="body2" + align="center" + >{t`signin.check_email`}</Typography> + )} + </Stack> + </CardContent> </Card> </Container> - <LanguagesIcon /> </Layout> ); }; +const errorsMap = { + CredentialsSignin: 'signin.errors.CredentialsSignin', +}; + export const getServerSideProps = async (context: any) => { const session = await getSession(context);

@@ -53,7 +109,6 @@ };

else return pageUtils.getServerSideProps(async ctx => { const error = ctx.query?.error || null; - const emailConfirmation = ctx.query?.confirmed === 'true'; const redirectPath = ctx.query?.redirectPath; if (redirectPath) {

@@ -61,7 +116,7 @@ const cookies = new Cookies(ctx.req, ctx.res);

cookies.set('redirectPath', redirectPath); } - return {props: {error, emailConfirmation}}; + return {props: {error}}; })(context); };
D frontend/pages/auth/lost-password.tsx

@@ -1,18 +0,0 @@

-import {useTranslation} from 'next-i18next'; -import Layout from '../../layouts/Centered'; -import LostPasswordContainer from '../../containers/LostPassword'; -import pageUtils from '../../lib/pageUtils'; - -const LostPassword = () => { - const {t} = useTranslation(); - - return ( - <Layout menuTitle={t('lost_password.title')} displayMenu={false}> - <LostPasswordContainer /> - </Layout> - ); -}; - -export const getServerSideProps = pageUtils.getServerSideProps(); - -export default LostPassword;
A frontend/pages/auth/magic-link.tsx

@@ -0,0 +1,20 @@

+import {signIn} from 'next-auth/react'; +import {useRouter} from 'next/router'; +import {useEffect} from 'react'; + +const MagicLinkLogin = () => { + const router = useRouter(); + + useEffect(() => { + const {token} = router.query; + if (token) + signIn('credentials', { + token, + callbackUrl: '/dashboard', + }); + }, [router]); + + return null; +}; + +export default MagicLinkLogin;
D frontend/pages/auth/register/index.tsx

@@ -1,92 +0,0 @@

-import Link from 'next/link'; -import Cookies from 'cookies'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; -import Divider from '@mui/material/Divider'; -import Button from '@mui/material/Button'; -import CardMedia from '@mui/material/CardMedia'; -import {useTheme} from '@mui/material/styles'; -import {useTranslation} from 'next-i18next'; -import Layout from '../../../layouts/Centered'; -import Logo from '../../../components/Logo'; -import LanguagesIcon from '../../../containers/Languages/Icon'; -import SignUpActions from '../../../containers/MailSignUpForm/SignupActions'; -import LoginGoogle from '../../../containers/LoginGoogle'; -import pageUtils from '../../../lib/pageUtils'; - -const MailSignup = () => { - const {t} = useTranslation(); - const theme = useTheme(); - - return ( - <Layout menuTitle={t('signup.title')} displayMenu={false}> - <Card> - <CardMedia component={Logo} /> - <CardContent - sx={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - padding: theme.spacing(0, 6), - }} - > - <Typography variant="h6" align="center"> - {t('signup.create')} - </Typography> - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - maxWidth: '15rem', - mt: 4, - }} - > - <Link href="/auth/register/mail" passHref style={{width: '100%'}}> - <Button - color="primary" - variant="contained" - fullWidth - sx={{mb: 2}} - > - {t('signup.with_mail')} - </Button> - </Link> - <LoginGoogle /> - </Box> - <Box - sx={{ - width: '100%', - textAlign: 'center', - margin: theme.spacing(5, 0, 2, 0), - }} - > - <Divider /> - </Box> - <Typography align="center" variant="body2"> - {t('signup.account_already')} - </Typography> - </CardContent> - <SignUpActions /> - <LanguagesIcon /> - </Card> - </Layout> - ); -}; - -export const getServerSideProps = async (context: any) => { - return pageUtils.getServerSideProps(async ctx => { - const redirectPath = ctx.query?.redirectPath; - if (redirectPath) { - const cookies = new Cookies(ctx.req, ctx.res); - cookies.set('redirectPath', redirectPath); - } - })(context); -}; - -export default MailSignup;
D frontend/pages/auth/register/mail.tsx

@@ -1,25 +0,0 @@

-import Card from '@mui/material/Card'; -import CardMedia from '@mui/material/CardMedia'; -import {useTranslation} from 'next-i18next'; -import Layout from '../../../layouts/Centered'; -import MailSignUpForm from '../../../containers/MailSignUpForm'; -import Logo from '../../../components/Logo'; -import LanguagesIcon from '../../../containers/Languages/Icon'; -import pageUtils from '../../../lib/pageUtils'; - -const MailSignup = () => { - const {t} = useTranslation(); - return ( - <Layout menuTitle={t('signup.title')} displayMenu={false}> - <Card> - <CardMedia component={Logo} /> - <MailSignUpForm /> - <LanguagesIcon /> - </Card> - </Layout> - ); -}; - -export const getServerSideProps = pageUtils.getServerSideProps(); - -export default MailSignup;
D frontend/pages/auth/reset.tsx

@@ -1,53 +0,0 @@

-import {useState} from 'react'; -import {useRouter} from 'next/router'; -import {useTranslation} from 'next-i18next'; -import useToastStore from '../../stores/useToastStore'; -import Layout from '../../layouts/Centered'; -import ResetPasswordContainer from '../../containers/ResetPassword'; -import {useResetPasswordMutation} from '../../generated/graphql'; -import pageUtils from '../../lib/pageUtils'; - -const ResetPassword = () => { - const router = useRouter(); - const {code} = router.query; - const addToast = useToastStore(s => s.addToast); - const {t} = useTranslation(); - const [resetPassword, {loading}] = useResetPasswordMutation(); - const [password, setPassword] = useState(''); - const [passwordConfirmation, setPasswordConfirmation] = useState(''); - const [passwordError, setPasswordError] = useState(''); - - const onReset = async e => { - if (e.preventDefault) e.preventDefault(); - try { - await resetPassword({ - variables: {code: code as string, password, passwordConfirmation}, - }); - setPasswordError(''); - addToast(t('lost_password.change_success')); - router.push('/auth/login'); - } catch (err) { - if (err.message === 'Bad Request') - setPasswordError(t('generic.errors.unknown')); - } - }; - - return ( - <Layout menuTitle={t('lost_password.reset_title')} displayMenu={false}> - <form onSubmit={onReset}> - <ResetPasswordContainer - isLoading={loading} - password={password} - setPassword={setPassword} - passwordConfirmation={passwordConfirmation} - setPasswordConfirmation={setPasswordConfirmation} - error={passwordError} - /> - </form> - </Layout> - ); -}; - -export const getServerSideProps = pageUtils.getServerSideProps(); - -export default ResetPassword;
M frontend/pages/new.tsxfrontend/pages/new.tsx

@@ -26,11 +26,6 @@ label: t('menu.login'),

onClick: () => router.push('/auth/login'), id: 'LoginTabs', }, - { - label: t('menu.register'), - onClick: () => router.push('/auth/register'), - id: 'RegisterTabs', - }, ]; const loggedMenuActions = [