mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
feat: make auth mode configurable (#1579)
Fix https://github.com/StanGirard/quivr/issues/1524 Demo: https://github.com/StanGirard/quivr/assets/63923024/d1365b67-060e-488f-ad10-ca6ce2ac373a
This commit is contained in:
parent
f7771f9189
commit
91fbf154d0
@ -19,3 +19,5 @@ NEXT_PUBLIC_CMS_URL=https://cms.quivr.app
|
|||||||
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<ignore-me-or-change-me>
|
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<ignore-me-or-change-me>
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<ignore-me-or-change-me>
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<ignore-me-or-change-me>
|
||||||
NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL=<ignore-me-or-change-me>
|
NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL=<ignore-me-or-change-me>
|
||||||
|
|
||||||
|
NEXT_PUBLIC_AUTH_MODES=magic_link,password,google_sso
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import Field from "@/lib/components/ui/Field";
|
||||||
|
import { emailPattern } from "@/lib/config/patterns";
|
||||||
|
import { useAuthModes } from "@/lib/hooks/useAuthModes";
|
||||||
|
|
||||||
|
import { EmailAuthContextType } from "../../../types";
|
||||||
|
|
||||||
|
export const EmailInput = (): JSX.Element => {
|
||||||
|
const { register } = useFormContext<EmailAuthContextType>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { password, magicLink } = useAuthModes();
|
||||||
|
if (!password && !magicLink) {
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...register("email", {
|
||||||
|
required: true,
|
||||||
|
pattern: emailPattern,
|
||||||
|
})}
|
||||||
|
placeholder={t("email", { ns: "login" })}
|
||||||
|
label={t("email", { ns: "translation" })}
|
||||||
|
inputClassName="py-1 mt-1 mb-3"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EmailAuthContextType } from "@/app/(auth)/login/types";
|
||||||
|
import Button from "@/lib/components/ui/Button";
|
||||||
|
import { useAuthModes } from "@/lib/hooks/useAuthModes";
|
||||||
|
|
||||||
|
import { useMagicLinkLogin } from "./hooks/useMagicLinkLogin";
|
||||||
|
|
||||||
|
export const MagicLinkLogin = (): JSX.Element => {
|
||||||
|
const { t } = useTranslation(["login", "translation"]);
|
||||||
|
const { magicLink } = useAuthModes();
|
||||||
|
const { handleMagicLinkLogin } = useMagicLinkLogin();
|
||||||
|
const { watch } = useFormContext<EmailAuthContextType>();
|
||||||
|
|
||||||
|
if (!magicLink) {
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isLoading={watch("isMagicLinkSubmitting")}
|
||||||
|
className="bg-black text-white py-2 font-normal w-full"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => void handleMagicLinkLogin()}
|
||||||
|
>
|
||||||
|
{t("magicLink", { ns: "login" })}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,32 +1,21 @@
|
|||||||
import { useForm } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EmailAuthContextType } from "@/app/(auth)/login/types";
|
||||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const useMagicLinkLogin = () => {
|
export const useMagicLinkLogin = () => {
|
||||||
const { supabase } = useSupabase();
|
const { supabase } = useSupabase();
|
||||||
|
const { watch, setValue } = useFormContext<EmailAuthContextType>();
|
||||||
|
|
||||||
const { t } = useTranslation("login");
|
const { t } = useTranslation("login");
|
||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
formState: { isSubmitSuccessful, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
} = useForm<{ email: string }>({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = watch("email");
|
const email = watch("email");
|
||||||
|
|
||||||
const handleMagicLinkLogin = handleSubmit(async (_, ev) => {
|
const handleMagicLinkLogin = async () => {
|
||||||
ev?.preventDefault();
|
|
||||||
if (email === "") {
|
if (email === "") {
|
||||||
publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
@ -35,13 +24,15 @@ export const useMagicLinkLogin = () => {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setValue("isMagicLinkSubmitting", true);
|
||||||
const { error } = await supabase.auth.signInWithOtp({
|
const { error } = await supabase.auth.signInWithOtp({
|
||||||
email,
|
email,
|
||||||
options: {
|
options: {
|
||||||
emailRedirectTo: window.location.hostname, // current domain name. for eg localhost:3000, localhost:3001, https://...
|
emailRedirectTo: window.location.hostname,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
setValue("isMagicLinkSubmitting", false);
|
||||||
|
setValue("isMagicLinkSubmitted", true);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
publish({
|
publish({
|
||||||
@ -49,18 +40,11 @@ export const useMagicLinkLogin = () => {
|
|||||||
text: error.message,
|
text: error.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error; // this error is caught by react-hook-form
|
throw error;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
setValue("email", "");
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMagicLinkLogin,
|
handleMagicLinkLogin,
|
||||||
isSubmitting,
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
isSubmitSuccessful,
|
|
||||||
reset,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EmailAuthContextType } from "@/app/(auth)/login/types";
|
||||||
|
import Button from "@/lib/components/ui/Button";
|
||||||
|
import Field from "@/lib/components/ui/Field";
|
||||||
|
import { useAuthModes } from "@/lib/hooks/useAuthModes";
|
||||||
|
|
||||||
|
import { usePasswordLogin } from "./hooks/usePasswordLogin";
|
||||||
|
|
||||||
|
export const PasswordLogin = (): JSX.Element => {
|
||||||
|
const { t } = useTranslation(["login"]);
|
||||||
|
const { password } = useAuthModes();
|
||||||
|
const { handlePasswordLogin } = usePasswordLogin();
|
||||||
|
const { register, watch } = useFormContext<EmailAuthContextType>();
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Field
|
||||||
|
{...register("password", { required: true })}
|
||||||
|
placeholder={t("password", { ns: "login" })}
|
||||||
|
label={t("password", { ns: "login" })}
|
||||||
|
inputClassName="py-1 mt-1 mb-3"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
isLoading={watch("isPasswordSubmitting")}
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 font-normal w-full mb-1"
|
||||||
|
onClick={() => void handlePasswordLogin()}
|
||||||
|
>
|
||||||
|
{t("login")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EmailAuthContextType } from "@/app/(auth)/login/types";
|
||||||
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
|
import { useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const usePasswordLogin = () => {
|
||||||
|
const { supabase } = useSupabase();
|
||||||
|
const { t } = useTranslation("login");
|
||||||
|
const { publish } = useToast();
|
||||||
|
const { watch, setValue } = useFormContext<EmailAuthContextType>();
|
||||||
|
|
||||||
|
const email = watch("email");
|
||||||
|
const password = watch("password");
|
||||||
|
|
||||||
|
const handlePasswordLogin = async () => {
|
||||||
|
if (email === "") {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: t("errorMailMissed"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password === "") {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: t("errorPasswordMissed"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue("isPasswordSubmitting", true);
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
setValue("isPasswordSubmitting", false);
|
||||||
|
setValue("isPasswordSubmitted", true);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error; // this error is caught by react-hook-form
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handlePasswordLogin,
|
||||||
|
};
|
||||||
|
};
|
57
frontend/app/(auth)/login/components/EmailLogin/index.tsx
Normal file
57
frontend/app/(auth)/login/components/EmailLogin/index.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Divider } from "@/lib/components/ui/Divider";
|
||||||
|
import { useAuthModes } from "@/lib/hooks/useAuthModes";
|
||||||
|
|
||||||
|
import { EmailInput } from "./components/EmailInput";
|
||||||
|
import { MagicLinkLogin } from "./components/MagicLinkLogin/MaginLinkLogin";
|
||||||
|
import { PasswordLogin } from "./components/PasswordLogin/PasswordLogin";
|
||||||
|
import { EmailAuthContextType } from "../../types";
|
||||||
|
|
||||||
|
export const EmailLogin = (): JSX.Element => {
|
||||||
|
const { reset } = useFormContext();
|
||||||
|
const { watch } = useFormContext<EmailAuthContextType>();
|
||||||
|
|
||||||
|
const { t } = useTranslation(["login", "translation"]);
|
||||||
|
const { password, magicLink } = useAuthModes();
|
||||||
|
|
||||||
|
if (watch("isMagicLinkSubmitted")) {
|
||||||
|
return (
|
||||||
|
<div className="text-center flex flex-col gap-4">
|
||||||
|
<p>
|
||||||
|
{t("check_your_email.part1", { ns: "login" })}{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{t("check_your_email.magic_link", { ns: "login" })}
|
||||||
|
</span>{" "}
|
||||||
|
{t("check_your_email.part2", { ns: "login" })}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<span>{t("cant_find", { ns: "login" })}</span>{" "}
|
||||||
|
<span
|
||||||
|
className="cursor-pointer underline"
|
||||||
|
onClick={() => void reset()}
|
||||||
|
>
|
||||||
|
{t("try_again")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EmailInput />
|
||||||
|
<PasswordLogin />
|
||||||
|
{password && magicLink && (
|
||||||
|
<Divider
|
||||||
|
text={t("or", { ns: "translation" })}
|
||||||
|
className="my-3 uppercase"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MagicLinkLogin />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,56 +0,0 @@
|
|||||||
import { act, renderHook } from "@testing-library/react";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { useMagicLinkLogin } from "../useMagicLinkLogin";
|
|
||||||
|
|
||||||
const mockSignInWithOtp = vi.fn(() => ({ error: null }));
|
|
||||||
|
|
||||||
const mockUseSupabase = () => ({
|
|
||||||
supabase: {
|
|
||||||
auth: {
|
|
||||||
signInWithOtp: mockSignInWithOtp,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const email = "user@quivr.app";
|
|
||||||
const watchMock = vi.fn(() => email);
|
|
||||||
vi.mock("react-hook-form", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("react-hook-form")>(
|
|
||||||
"react-hook-form"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useForm: () => ({
|
|
||||||
...actual.useForm(),
|
|
||||||
watch: watchMock,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
|
||||||
useSupabase: () => mockUseSupabase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("useMagicLinkLogin", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not call signInWithOtp if email is empty", async () => {
|
|
||||||
watchMock.mockReturnValueOnce("");
|
|
||||||
const { result } = renderHook(() => useMagicLinkLogin());
|
|
||||||
await act(() => result.current.handleMagicLinkLogin());
|
|
||||||
expect(mockSignInWithOtp).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call signInWithOtp with proper arguments", async () => {
|
|
||||||
const { result } = renderHook(() => useMagicLinkLogin());
|
|
||||||
await result.current.handleMagicLinkLogin();
|
|
||||||
expect(mockSignInWithOtp).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockSignInWithOtp).toHaveBeenCalledWith({
|
|
||||||
email,
|
|
||||||
options: { emailRedirectTo: window.location.hostname },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,63 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import Button from "@/lib/components/ui/Button";
|
|
||||||
import Field from "@/lib/components/ui/Field";
|
|
||||||
import { emailPattern } from "@/lib/config/patterns";
|
|
||||||
|
|
||||||
import { useMagicLinkLogin } from "./hooks/useMagicLinkLogin";
|
|
||||||
|
|
||||||
export const MagicLinkLogin = (): JSX.Element => {
|
|
||||||
const {
|
|
||||||
handleMagicLinkLogin,
|
|
||||||
isSubmitting,
|
|
||||||
register,
|
|
||||||
isSubmitSuccessful,
|
|
||||||
reset,
|
|
||||||
} = useMagicLinkLogin();
|
|
||||||
const { t } = useTranslation(["login", "translation"]);
|
|
||||||
|
|
||||||
if (isSubmitSuccessful) {
|
|
||||||
return (
|
|
||||||
<div className="text-center flex flex-col gap-4">
|
|
||||||
<p>
|
|
||||||
{t("check_your_email.part1", { ns: "login" })}{" "}
|
|
||||||
<span className="font-semibold">
|
|
||||||
{t("check_your_email.magic_link", { ns: "login" })}
|
|
||||||
</span>{" "}
|
|
||||||
{t("check_your_email.part2", { ns: "login" })}
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<span>{t("cant_find", { ns: "login" })}</span>{" "}
|
|
||||||
<span
|
|
||||||
className="cursor-pointer underline"
|
|
||||||
onClick={() => void reset()}
|
|
||||||
>
|
|
||||||
{t("try_again")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="w-full" onSubmit={(e) => void handleMagicLinkLogin(e)}>
|
|
||||||
<Field
|
|
||||||
{...register("email", {
|
|
||||||
required: true,
|
|
||||||
pattern: emailPattern,
|
|
||||||
})}
|
|
||||||
placeholder={t("email", { ns: "login" })}
|
|
||||||
label={t("email", { ns: "translation" })}
|
|
||||||
inputClassName="py-1 mt-1 mb-3"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
className="bg-black text-white py-2 font-normal w-full"
|
|
||||||
>
|
|
||||||
{t("magicLink", { ns: "login" })}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,18 +1,28 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
|
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
|
||||||
import { Divider } from "@/lib/components/ui/Divider";
|
import { Divider } from "@/lib/components/ui/Divider";
|
||||||
|
import { useAuthModes } from "@/lib/hooks/useAuthModes";
|
||||||
|
|
||||||
|
import { EmailLogin } from "./components/EmailLogin";
|
||||||
import { GoogleLoginButton } from "./components/GoogleLogin";
|
import { GoogleLoginButton } from "./components/GoogleLogin";
|
||||||
import { MagicLinkLogin } from "./components/MagicLinkLogin";
|
|
||||||
import { useLogin } from "./hooks/useLogin";
|
import { useLogin } from "./hooks/useLogin";
|
||||||
|
import { EmailAuthContextType } from "./types";
|
||||||
|
|
||||||
const Main = (): JSX.Element => {
|
const Main = (): JSX.Element => {
|
||||||
useLogin();
|
useLogin();
|
||||||
|
const { googleSso, password, magicLink } = useAuthModes();
|
||||||
|
|
||||||
|
const methods = useForm<EmailAuthContextType>({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
const { t } = useTranslation(["translation", "login"]);
|
const { t } = useTranslation(["translation", "login"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -27,9 +37,13 @@ const Main = (): JSX.Element => {
|
|||||||
<span className="text-primary">Quivr</span>
|
<span className="text-primary">Quivr</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-5 flex flex-col">
|
<div className="mt-5 flex flex-col">
|
||||||
<MagicLinkLogin />
|
<FormProvider {...methods}>
|
||||||
<Divider text={t("or")} className="my-3 uppercase" />
|
<EmailLogin />
|
||||||
<GoogleLoginButton />
|
</FormProvider>
|
||||||
|
{googleSso && (password || magicLink) && (
|
||||||
|
<Divider text={t("or")} className="my-3 uppercase" />
|
||||||
|
)}
|
||||||
|
{googleSso && <GoogleLoginButton />}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-center">
|
<p className="text-[10px] text-center">
|
||||||
{t("restriction_message", { ns: "login" })}
|
{t("restriction_message", { ns: "login" })}
|
||||||
|
8
frontend/app/(auth)/login/types.ts
Normal file
8
frontend/app/(auth)/login/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type EmailAuthContextType = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
isMagicLinkSubmitted: boolean;
|
||||||
|
isPasswordSubmitted: boolean;
|
||||||
|
isMagicLinkSubmitting: boolean;
|
||||||
|
isPasswordSubmitting: boolean;
|
||||||
|
};
|
12
frontend/lib/hooks/useAuthModes.ts
Normal file
12
frontend/lib/hooks/useAuthModes.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useAuthModes = () => {
|
||||||
|
const authModes = process.env.NEXT_PUBLIC_AUTH_MODES?.split(",") ?? [
|
||||||
|
"password",
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
magicLink: authModes.includes("magic_link"),
|
||||||
|
password: authModes.includes("password"),
|
||||||
|
googleSso: authModes.includes("google_sso"),
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"password": "Password",
|
||||||
"googleLogin": "Continue with Google",
|
"googleLogin": "Continue with Google",
|
||||||
|
"errorPasswordMissed": "Please enter your password",
|
||||||
"magicLink": "Continue with email",
|
"magicLink": "Continue with email",
|
||||||
"errorMailMissed": "Please enter your email address",
|
"errorMailMissed": "Please enter your email address",
|
||||||
"talk_to": "Talk to",
|
"talk_to": "Talk to",
|
||||||
@ -11,5 +13,6 @@
|
|||||||
"part1":"We just sent you a ",
|
"part1":"We just sent you a ",
|
||||||
"magic_link":"Magic link",
|
"magic_link":"Magic link",
|
||||||
"part2":", check your emails and follow the steps."
|
"part2":", check your emails and follow the steps."
|
||||||
}
|
},
|
||||||
|
"login": "Login"
|
||||||
}
|
}
|
@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"googleLogin": "Continuar con Google",
|
"password": "Contraseña",
|
||||||
"magicLink": "Continuar con correo electrónico",
|
"googleLogin": "Continuar con Google",
|
||||||
"errorMailMissed": "Por favor, ingrese su dirección de correo electrónico",
|
"errorPasswordMissed": "Por favor ingrese su contraseña",
|
||||||
"talk_to": "Hablar con",
|
"magicLink": "Continuar con correo electrónico",
|
||||||
"restriction_message": "Los usuarios no pagos tienen acceso a una demostración gratuita y limitada de Quivr",
|
"errorMailMissed": "Por favor ingrese su dirección de correo electrónico",
|
||||||
"email":"Dirección de correo electrónico",
|
"talk_to": "Hablar con",
|
||||||
"cant_find":"¿No lo encuentras?",
|
"restriction_message": "Los usuarios no pagos tienen acceso a una demostración gratuita y limitada de Quivr",
|
||||||
"try_again":"Inténtalo de nuevo",
|
"email":"Dirección de correo electrónico",
|
||||||
|
"cant_find":"¿No puedes encontrarlo?",
|
||||||
|
"try_again":"Inténtalo de nuevo" ,
|
||||||
"check_your_email": {
|
"check_your_email": {
|
||||||
"part1":"Acabamos de enviarte un ",
|
"part1":"Acabamos de enviarte un ",
|
||||||
"magic_link":"enlace mágico",
|
"magic_link":"enlace mágico",
|
||||||
"part2":". Revisa tus correos electrónicos y sigue los pasos."
|
"part2":". Revisa tus correos electrónicos y sigue los pasos."
|
||||||
}
|
},
|
||||||
}
|
"login": "Iniciar sesión"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"password": "Mot de passe",
|
||||||
"googleLogin": "Continuer avec Google",
|
"googleLogin": "Continuer avec Google",
|
||||||
|
"errorPasswordMissed": "Veuillez entrer votre mot de passe",
|
||||||
"magicLink": "Continuer avec e-mail",
|
"magicLink": "Continuer avec e-mail",
|
||||||
"errorMailMissed": "Veuillez saisir votre adresse e-mail",
|
"errorMailMissed": "Veuillez saisir votre adresse e-mail",
|
||||||
"talk_to": "Parler à",
|
"talk_to": "Parler à",
|
||||||
@ -11,5 +13,6 @@
|
|||||||
"part1":"Nous venons de vous envoyer un ",
|
"part1":"Nous venons de vous envoyer un ",
|
||||||
"magic_link":"lien magique",
|
"magic_link":"lien magique",
|
||||||
"part2":". Vérifiez vos e-mails et suivez les étapes."
|
"part2":". Vérifiez vos e-mails et suivez les étapes."
|
||||||
}
|
},
|
||||||
|
"login": "Se connecter"
|
||||||
}
|
}
|
@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"password": "Senha",
|
||||||
"googleLogin": "Continuar com o Google",
|
"googleLogin": "Continuar com o Google",
|
||||||
"magicLink": "Continuar com o e-mail",
|
"errorPasswordMissed": "Por favor, digite sua senha",
|
||||||
"errorMailMissed": "Por favor, insira seu endereço de e-mail",
|
"magicLink": "Continuar com e-mail",
|
||||||
"talk_to": "Converse com",
|
"errorMailMissed": "Por favor, digite seu endereço de e-mail",
|
||||||
|
"talk_to": "Conversar com",
|
||||||
"restriction_message": "Usuários não pagos têm acesso a uma demonstração gratuita e limitada do Quivr",
|
"restriction_message": "Usuários não pagos têm acesso a uma demonstração gratuita e limitada do Quivr",
|
||||||
"email":"Email address",
|
"email":"Endereço de e-mail",
|
||||||
"cant_find":"Não consegue encontrar?",
|
"cant_find":"Não consegue encontrar?",
|
||||||
"try_again":"Tente novamente",
|
"try_again":"Tente novamente",
|
||||||
"check_your_email":{
|
"check_your_email":{
|
||||||
"part1":"Acabamos de enviar um ",
|
"part1":"Acabamos de enviar um ",
|
||||||
"magic_link":"link mágico",
|
"magic_link":"link mágico",
|
||||||
"part2":" para você, verifique seus emails e siga as instruções."
|
"part2":", verifique seus e-mails e siga as etapas."
|
||||||
}
|
},
|
||||||
|
"login":"Entrar"
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"password": "Пароль",
|
||||||
|
"errorPasswordMissed": "Пожалуйста, введите свой пароль",
|
||||||
"googleLogin": "Продолжить с Google",
|
"googleLogin": "Продолжить с Google",
|
||||||
"magicLink": "Продолжить с электронной почтой",
|
"magicLink": "Продолжить с электронной почтой",
|
||||||
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
||||||
@ -11,5 +13,6 @@
|
|||||||
"part1":"Мы только что отправили вам ",
|
"part1":"Мы только что отправили вам ",
|
||||||
"magic_link":"волшебную ссылку",
|
"magic_link":"волшебную ссылку",
|
||||||
"part2":". Проверьте свою электронную почту и следуйте инструкциям."
|
"part2":". Проверьте свою электронную почту и следуйте инструкциям."
|
||||||
}
|
},
|
||||||
|
"login": "Войти"
|
||||||
}
|
}
|
@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"googleLogin": "使用 Google 继续",
|
"password": "密码",
|
||||||
"magicLink": "使用电子邮件继续",
|
"googleLogin": "使用Google登录",
|
||||||
"errorMailMissed": "请输入您的电子邮件地址",
|
"errorPasswordMissed": "请输入密码",
|
||||||
"talk_to": "与之交谈",
|
"magicLink": "使用电子邮件继续",
|
||||||
"restriction_message": "未付费用户可以访问 Quivr 的免费和有限演示",
|
"errorMailMissed": "请输入您的电子邮件地址",
|
||||||
"email": "电子邮件地址",
|
"talk_to": "与之交谈",
|
||||||
"cant_find": "找不到吗?",
|
"restriction_message": "未付费用户可以访问Quivr的免费和有限演示",
|
||||||
"try_again": "再试一次",
|
"email":"电子邮件地址",
|
||||||
"check_your_email": {
|
"cant_find":"找不到?",
|
||||||
"part1": "我们刚刚发送了一个",
|
"try_again":"重试",
|
||||||
"magic_link": "魔法链接",
|
"check_your_email":{
|
||||||
"part2": ",请查收您的电子邮件并按照步骤进行操作。"
|
"part1":"我们刚刚发送了一个",
|
||||||
}
|
"magic_link":"魔法链接",
|
||||||
|
"part2":",请检查您的电子邮件并按照步骤操作。"
|
||||||
|
},
|
||||||
|
"login":"登录"
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user