Skip to content

Commit

Permalink
feat(auth): email password flow (#340)
Browse files Browse the repository at this point in the history
* feat(login): merge login-with-email into login (#335)

* feat(login): merge login-with-email into login

* Thomas/create-user (#333)

* fix(Collapsible): can't have nested buttons

* refactor(user): introduce inbox repository

* feat(login): add signup page (#334)

* feat(login): add signup page

* feat(sign-in-link): send sign in link from settings

* chore(sentry): log to console in dev

* refactor(auth): use sign-in instead of login

* fix(routes): import missing getRoute

* feat(auth): add send reset password feature (#336)

feat(auth): add send reset password feature

* fix(auth): use ErrUnknownUser error code for no account

* feat(auth): add description on sign up page
  • Loading branch information
balzdur authored Jan 19, 2024
1 parent ef2d4b9 commit c72b7be
Show file tree
Hide file tree
Showing 116 changed files with 1,254 additions and 3,077 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pnpm-lock.yaml
/packages/app-builder/.cache/
/packages/app-builder/build/
/packages/app-builder/public/build/
/packages/app-builder/public/img/
/packages/app-builder/src/utils/routes/routes.ts

# marble-api
Expand Down
4 changes: 2 additions & 2 deletions packages/app-builder/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ NODE_ENV=development
SESSION_SECRET=SESSION_SECRET
SESSION_MAX_AGE=43200

MARBLE_API_DOMAIN=http://127.0.0.1:8080
MARBLE_APP_DOMAIN=http://127.0.0.1:3000
MARBLE_API_DOMAIN=http://localhost:8080
MARBLE_APP_DOMAIN=http://localhost:3000

FIREBASE_AUTH_EMULATOR=true
FIREBASE_AUTH_EMULATOR_HOST=http://localhost:9099
Expand Down
2 changes: 1 addition & 1 deletion packages/app-builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Then, it depends on the use case. For simple cases, you can directly consume the

```typescript
const { apiClient } = await authenticator.isAuthenticated(request, {
failureRedirect: '/login',
failureRedirect: getRoute('/sign-in'),
});

const decisions = await apiClient.listDecisions();
Expand Down
1 change: 1 addition & 0 deletions packages/app-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@conform-to/zod": "^0.9.1",
"@hookform/devtools": "^4.3.1",
"@hookform/resolvers": "^3.3.4",
"@lottiefiles/react-lottie-player": "^3.5.3",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
Expand Down
2,573 changes: 0 additions & 2,573 deletions packages/app-builder/public/img/login_background.svg

This file was deleted.

1 change: 1 addition & 0 deletions packages/app-builder/public/img/lottie/login_hero.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions packages/app-builder/public/locales/en/auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"great_rules_right_tools": "Great rules are built with the <RightTools>right tools</RightTools>.",
"marble_description": "Marble is a real time rule engine for fraud and compliance monitoring, designed for fintech companies and financial institutions.",
"sign_in.google": "Sign in with Google",
"errors.no_account": "No Marble account found for this address.",
"sign_in.email": "Email",
"sign_in.password": "Password",
"sign_in": "Sign in",
"sign_up": "Sign up",
"sign_in.dont_have_an_account": "Don't have an account ? <SignUp>{{signUp}}</SignUp>.",
"sign_in.forgot_password": "Forgot password?",
"sign_in.errors.user_not_found": "No user account found for this address.",
"sign_in.errors.wrong_password_error": "Wrong password.",
"sign_in.errors.invalid_login_credentials": "Invalid login credentials.",
"sign_up.description": "before creating your account, make sure an admin has added you to your organization.",
"sign_up.already_have_an_account_sign_up": "Already have an account ? <SignIn>{{signIn}}</SignIn>.",
"sign_up.errors.weak_password_error": "Password is too weak. It should be at least 6 characters long.",
"sign_up.errors.email_already_exists": "This email is already in use. Please sign in or reset your password.",
"email-verification.description": "Please check your email and click on the link to verify your account. After verifying your email, <strong>you will need to login again.</strong>",
"email-verification.resend": "resend verification email",
"email-verification.wrong_place": "Wrong place ? go back to <SignIn>{{signIn}}</SignIn>.",
"reset-password.send": "send reset password email",
"reset-password.email_sent": "An email has been sent to you with a link to reset your password.",
"reset-password.wrong_place": "Wrong place ? go back to <SignIn>{{signIn}}</SignIn>."
}
2 changes: 1 addition & 1 deletion packages/app-builder/public/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"auth.logout": "Log out",
"auth.login": "Login",
"search": "Search",
"error_one": "{{count}} error",
"error_other": "{{count}} errors",
Expand All @@ -10,6 +9,7 @@
"errors.not_found": "This page could not be found.",
"errors.edit.forbidden_not_draft": "You can only edit a draft version of a scenario.",
"errors.list.duplicate_list_name": "A list with this name already exist",
"errors.list.duplicate_email": "This email is already used",
"errors.data.duplicate_field_name": "A field with this name already exist",
"errors.data.duplicate_table_name": "A table with this name already exist",
"errors.data.duplicate_link_name": "A link with this name already exist",
Expand Down
13 changes: 0 additions & 13 deletions packages/app-builder/public/locales/en/login.json

This file was deleted.

1 change: 0 additions & 1 deletion packages/app-builder/public/locales/fr/common.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"auth.logout": "Se déconnecter",
"auth.login": "Connexion",
"cancel": "Annuler",
"error_one": "erreur",
"error_other": "erreurs",
Expand Down
3 changes: 0 additions & 3 deletions packages/app-builder/public/locales/fr/login.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"title": "Connectez-vous à votre compte",
"help.no_account": "Vous n'avez pas de compte ?",
"help.contact_us": "Contactez-nous",
"sign_in.google": "Connectez-vous avec Google",
"errors.no_account": "Aucun compte Marble trouvé pour cette adresse."
}
31 changes: 31 additions & 0 deletions packages/app-builder/src/components/Auth/AuthError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type AuthErrors } from '@app-builder/models';
import clsx from 'clsx';
import { type ParseKeys } from 'i18next';
import { useTranslation } from 'react-i18next';

import { authI18n } from './auth-i18n';

const errorLabels: Record<AuthErrors, ParseKeys<typeof authI18n>> = {
NoAccount: 'auth:errors.no_account',
Unknown: 'common:errors.unknown',
};

export function AuthError({
error,
className,
}: {
error: AuthErrors;
className?: string;
}) {
const { t } = useTranslation(authI18n);
return (
<p
className={clsx(
'text-m bg-red-05 w-full rounded-sm p-2 font-normal text-red-100',
className,
)}
>
{t(errorLabels[error])}
</p>
);
}
95 changes: 95 additions & 0 deletions packages/app-builder/src/components/Auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
FormControl,
FormError,
FormField,
FormItem,
FormLabel,
} from '@app-builder/components/Form';
import { useSendPasswordResetEmail } from '@app-builder/services/auth/auth.client';
import { clientServices } from '@app-builder/services/init.client';
import { zodResolver } from '@hookform/resolvers/zod';
import * as Sentry from '@sentry/remix';
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { ClientOnly } from 'remix-utils/client-only';
import { Button, Input } from 'ui-design-system';
import * as z from 'zod';

const resetPasswordFormSchema = z.object({
email: z.string().email(),
});
type ResetPasswordFormValues = z.infer<typeof resetPasswordFormSchema>;

export function ResetPassword() {
const { t } = useTranslation(['auth', 'common']);

const formMethods = useForm<z.infer<typeof resetPasswordFormSchema>>({
resolver: zodResolver(resetPasswordFormSchema),
defaultValues: {
email: '',
},
});
const { control } = formMethods;

const children = (
<>
<FormField
control={control}
name="email"
render={({ field }) => (
<FormItem className="flex flex-col items-start gap-2">
<FormLabel>{t('auth:sign_in.email')}</FormLabel>
<FormControl>
<Input className="w-full" type="email" {...field} />
</FormControl>
<FormError />
</FormItem>
)}
/>
<Button type="submit">{t('auth:reset-password.send')}</Button>
</>
);

return (
<FormProvider {...formMethods}>
<ClientOnly fallback={<ResetPasswordForm>{children}</ResetPasswordForm>}>
{() => <ClientResetPasswordForm>{children}</ClientResetPasswordForm>}
</ClientOnly>
</FormProvider>
);
}

function ResetPasswordForm(props: React.ComponentPropsWithoutRef<'form'>) {
return <form noValidate className="flex w-full flex-col gap-4" {...props} />;
}

function ClientResetPasswordForm({ children }: { children: React.ReactNode }) {
const { t } = useTranslation(['auth', 'common']);

const sendPasswordResetEmail = useSendPasswordResetEmail(
clientServices.authenticationClientService,
);

const { handleSubmit } = useFormContext<ResetPasswordFormValues>();

const handleResetPassword = handleSubmit(async ({ email }) => {
try {
await sendPasswordResetEmail(email);
toast.success(t('auth:reset-password.email_sent'));
} catch (error) {
Sentry.captureException(error);
toast.error(t('common:errors.unknown'));
}
});

return (
<ResetPasswordForm
onSubmit={(e) => {
void handleResetPassword(e);
}}
>
{children}
</ResetPasswordForm>
);
}
61 changes: 61 additions & 0 deletions packages/app-builder/src/components/Auth/SendEmailVerification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useResendEmailVerification } from '@app-builder/services/auth/auth.client';
import { clientServices } from '@app-builder/services/init.client';
import { getRoute } from '@app-builder/utils/routes';
import { useNavigate } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { ClientOnly } from 'remix-utils/client-only';
import { useHydrated } from 'remix-utils/use-hydrated';
import { Button } from 'ui-design-system';

function SendEmailVerificationButton({ onClick }: { onClick?: () => void }) {
const { t } = useTranslation(['auth']);
const isHydrated = useHydrated();

return (
<Button
variant="primary"
className="w-full capitalize"
onClick={onClick}
disabled={!isHydrated}
>
{t('auth:email-verification.resend')}
</Button>
);
}

function ClientSendEmailVerificationButton() {
const { t } = useTranslation(['common']);

const resendEmailVerification = useResendEmailVerification(
clientServices.authenticationClientService,
);

const navigate = useNavigate();
async function onSendClick() {
try {
const logout = () => navigate(getRoute('/ressources/auth/logout'));
await resendEmailVerification(logout);
} catch (error) {
Sentry.captureException(error);
toast.error(t('common:errors.unknown'));
}
}

return (
<SendEmailVerificationButton
onClick={() => {
void onSendClick();
}}
/>
);
}

export function SendEmailVerification() {
return (
<ClientOnly fallback={<SendEmailVerificationButton />}>
{() => <ClientSendEmailVerificationButton />}
</ClientOnly>
);
}
Loading

0 comments on commit c72b7be

Please sign in to comment.