mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-13 16:36:41 +00:00
I18n auth (#23)
* New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys in app/auth * Fix build --------- Co-authored-by: Lokowitz <marvinlokowitz@gmail.com>
This commit is contained in:
@@ -311,7 +311,7 @@ export default function CreateShareLinkForm({
|
||||
<CommandInput placeholder={t('resourceSearch')} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t('resourceNotFound')}
|
||||
{t('resourcesNotFound')}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{resources.map(
|
||||
|
||||
@@ -57,9 +57,11 @@ const createSiteFormSchema = z.object({
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Name must be at least 2 characters."
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters."
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
method: z.enum(["wireguard", "newt", "local"])
|
||||
});
|
||||
@@ -273,6 +275,8 @@ PersistentKeepalive = 5`
|
||||
|
||||
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return loadingPage ? (
|
||||
<LoaderPlaceholder height="300px" />
|
||||
) : (
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type ValidateOidcTokenParams = {
|
||||
orgId: string;
|
||||
@@ -36,11 +37,13 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||
|
||||
const { licenseStatus, isLicenseViolation } = useLicenseStatusContext();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
async function validate() {
|
||||
setLoading(true);
|
||||
|
||||
console.log("Validating OIDC token", {
|
||||
console.log(t('idpOidcTokenValidating'), {
|
||||
code: props.code,
|
||||
expectedState: props.expectedState,
|
||||
stateCookie: props.stateCookie
|
||||
@@ -59,7 +62,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||
storedState: props.stateCookie
|
||||
});
|
||||
|
||||
console.log("Validate OIDC token response", res.data);
|
||||
console.log(t('idpOidcTokenResponse'), res.data);
|
||||
|
||||
const redirectUrl = res.data.data.redirectUrl;
|
||||
|
||||
@@ -76,7 +79,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||
router.push(res.data.data.redirectUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(formatAxiosError(e, "Error validating OIDC token"));
|
||||
setError(formatAxiosError(e, t('idpErrorOidcTokenValidating')));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -89,20 +92,20 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Connecting to {props.idp.name}</CardTitle>
|
||||
<CardDescription>Validating your identity</CardDescription>
|
||||
<CardTitle>{t('idpConnectingTo', {name: props.idp.name})}</CardTitle>
|
||||
<CardDescription>{t('idpConnectingToDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center space-y-4">
|
||||
{loading && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<span>Connecting...</span>
|
||||
<span>{t('idpConnectingToProcess')}</span>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && (
|
||||
<div className="flex items-center space-x-2 text-green-600">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
<span>Connected</span>
|
||||
<span>{t('idpConnectingToFinished')}</span>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
@@ -110,9 +113,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<AlertDescription className="flex flex-col space-y-2">
|
||||
<span>
|
||||
There was a problem connecting to{" "}
|
||||
{props.idp.name}. Please contact your
|
||||
administrator.
|
||||
{t('idpErrorConnectingTo', {name: props.idp.name})}
|
||||
</span>
|
||||
<span className="text-xs">{error}</span>
|
||||
</AlertDescription>
|
||||
|
||||
@@ -3,6 +3,7 @@ import ValidateOidcToken from "./ValidateOidcToken";
|
||||
import { idp } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ orgId: string; idpId: string }>;
|
||||
@@ -23,8 +24,10 @@ export default async function Page(props: {
|
||||
.from(idp)
|
||||
.where(eq(idp.idpId, parseInt(params.idpId!)));
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
if (!idpRes) {
|
||||
return <div>IdP not found</div>;
|
||||
return <div>{t('idpErrorNotFound')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AxiosResponse } from "axios";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import { cache } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Auth - Pangolin`,
|
||||
@@ -21,6 +22,7 @@ type AuthLayoutProps = {
|
||||
export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
const t = useTranslations();
|
||||
|
||||
const licenseStatusRes = await cache(
|
||||
async () =>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type DashboardLoginFormProps = {
|
||||
redirect?: string;
|
||||
@@ -38,23 +39,25 @@ export default function DashboardLoginForm({
|
||||
// logout();
|
||||
// });
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<Image
|
||||
src={`/logo/pangolin_orange.svg`}
|
||||
alt="Pangolin Logo"
|
||||
alt={t('pangolinLogoAlt')}
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
<h1 className="text-2xl font-bold mt-1">
|
||||
Welcome to Pangolin
|
||||
{t('welcome')}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Log in to get started
|
||||
{t('loginStart')}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import db from "@server/db";
|
||||
import { idp } from "@server/db/schemas";
|
||||
import { LoginFormIDP } from "@app/components/LoginForm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -40,6 +41,8 @@ export default async function Page(props: {
|
||||
name: idp.name
|
||||
})) as LoginFormIDP[];
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInvite && (
|
||||
@@ -47,11 +50,10 @@ export default async function Page(props: {
|
||||
<div className="flex flex-col items-center">
|
||||
<Mail className="w-12 h-12 mb-4 text-primary" />
|
||||
<h2 className="text-2xl font-bold mb-2 text-center">
|
||||
Looks like you've been invited!
|
||||
{t('inviteAlready')}
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must log in or create an
|
||||
account.
|
||||
{t('inviteAlreadyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +72,7 @@ export default async function Page(props: {
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Sign up
|
||||
{t('signup')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const requestSchema = z.object({
|
||||
email: z.string().email()
|
||||
@@ -122,6 +123,8 @@ export default function ResetPasswordForm({
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
async function onRequest(data: z.infer<typeof requestSchema>) {
|
||||
const { email } = data;
|
||||
|
||||
@@ -200,9 +203,9 @@ export default function ResetPasswordForm({
|
||||
<div>
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Reset Password</CardTitle>
|
||||
<CardTitle>{t('passwordReset')}</CardTitle>
|
||||
<CardDescription>
|
||||
Follow the steps to reset your password
|
||||
{t('passwordResetDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -221,14 +224,13 @@ export default function ResetPasswordForm({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
We'll send a password reset
|
||||
code to this email address.
|
||||
{t('passwordResetSent')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -249,7 +251,7 @@ export default function ResetPasswordForm({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@@ -268,7 +270,7 @@ export default function ResetPasswordForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Reset Code
|
||||
{t('passwordResetCode')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -278,8 +280,7 @@ export default function ResetPasswordForm({
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Check your email for the
|
||||
reset code.
|
||||
{t('passwordResetCodeDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -292,7 +293,7 @@ export default function ResetPasswordForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
New Password
|
||||
{t('passwordNew')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -310,7 +311,7 @@ export default function ResetPasswordForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Confirm New Password
|
||||
{t('passwordNewConfirm')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -339,7 +340,7 @@ export default function ResetPasswordForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Authenticator Code
|
||||
{t('pincodeAuth')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
@@ -407,8 +408,8 @@ export default function ResetPasswordForm({
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{state === "reset"
|
||||
? "Reset Password"
|
||||
: "Submit Code"}
|
||||
? t('passwordReset')
|
||||
: t('pincodeSubmit2')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -422,7 +423,7 @@ export default function ResetPasswordForm({
|
||||
{isSubmitting && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Request Reset
|
||||
{t('passwordResetSubmit')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -436,7 +437,7 @@ export default function ResetPasswordForm({
|
||||
mfaForm.reset();
|
||||
}}
|
||||
>
|
||||
Back to Password
|
||||
{t('passwordBack')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -450,7 +451,7 @@ export default function ResetPasswordForm({
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
Back to Email
|
||||
{t('backToEmail')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cache } from "react";
|
||||
import ResetPasswordForm from "./ResetPasswordForm";
|
||||
import Link from "next/link";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -27,6 +28,8 @@ export default async function Page(props: {
|
||||
redirectUrl = cleanRedirect(searchParams.redirect);
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResetPasswordForm
|
||||
@@ -44,7 +47,7 @@ export default async function Page(props: {
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Go back to log in
|
||||
{t('loginBack')}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AuthWithAccessTokenResponse } from "@server/routers/resource";
|
||||
import { AxiosResponse } from "axios";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type AccessTokenProps = {
|
||||
token: string;
|
||||
@@ -29,6 +30,8 @@ export default function AccessToken({
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
function appendRequestToken(url: string, token: string) {
|
||||
const fullUrl = new URL(url);
|
||||
fullUrl.searchParams.append(
|
||||
@@ -76,7 +79,7 @@ export default function AccessToken({
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking access token", e);
|
||||
console.error(t('accessTokenError'), e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -115,9 +118,9 @@ export default function AccessToken({
|
||||
|
||||
function renderTitle() {
|
||||
if (isValid) {
|
||||
return "Access Granted";
|
||||
return t('accessGranted');
|
||||
} else {
|
||||
return "Access URL Invalid";
|
||||
return t('accessUrlInvalid');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,18 +128,16 @@ export default function AccessToken({
|
||||
if (isValid) {
|
||||
return (
|
||||
<div>
|
||||
You have been granted access to this resource. Redirecting
|
||||
you...
|
||||
{t('accessGrantedDescription')}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
This shared access URL is invalid. Please contact the
|
||||
resource owner for a new URL.
|
||||
{t('accessUrlInvalidDescription')}
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">Go Home</Link>
|
||||
<Link href="/">{t('goHome')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,21 +9,23 @@ import {
|
||||
CardTitle,
|
||||
} from "@app/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ResourceAccessDenied() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl font-bold">
|
||||
Access Denied
|
||||
{t('accessDenied')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
You're not allowed to access this resource. If this is a mistake,
|
||||
please contact the administrator.
|
||||
{t('accessDeniedDescription')}
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">Go Home</Link>
|
||||
<Link href="/">{t('goHome')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import Link from "next/link";
|
||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const pinSchema = z.object({
|
||||
pin: z
|
||||
@@ -170,6 +171,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
return fullUrl.toString();
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const onWhitelistSubmit = (values: any) => {
|
||||
setLoadingLogin(true);
|
||||
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
|
||||
@@ -183,8 +186,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
setOtpState("otp_sent");
|
||||
submitOtpForm.setValue("email", values.email);
|
||||
toast({
|
||||
title: "OTP Sent",
|
||||
description: "An OTP has been sent to your email"
|
||||
title: t('otpEmailSent'),
|
||||
description: t('otpEmailSentDescription')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -200,7 +203,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setWhitelistError(
|
||||
formatAxiosError(e, "Failed to authenticate with email")
|
||||
formatAxiosError(e, t('otpEmailErrorAuthenticate'))
|
||||
);
|
||||
})
|
||||
.then(() => setLoadingLogin(false));
|
||||
@@ -225,7 +228,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setPincodeError(
|
||||
formatAxiosError(e, "Failed to authenticate with pincode")
|
||||
formatAxiosError(e, t('pincodeErrorAuthenticate'))
|
||||
);
|
||||
})
|
||||
.then(() => setLoadingLogin(false));
|
||||
@@ -253,7 +256,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setPasswordError(
|
||||
formatAxiosError(e, "Failed to authenticate with password")
|
||||
formatAxiosError(e, t('passwordErrorAuthenticate'))
|
||||
);
|
||||
})
|
||||
.finally(() => setLoadingLogin(false));
|
||||
@@ -280,7 +283,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
{t('poweredBy')}{" "}
|
||||
<Link
|
||||
href="https://github.com/fosrl/pangolin"
|
||||
target="_blank"
|
||||
@@ -293,11 +296,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Authentication Required</CardTitle>
|
||||
<CardTitle>{t('authenticationRequired')}</CardTitle>
|
||||
<CardDescription>
|
||||
{numMethods > 1
|
||||
? `Choose your preferred method to access ${props.resource.name}`
|
||||
: `You must authenticate to access ${props.resource.name}`}
|
||||
? t('authenticationMethodChoose', {name: props.resource.name})
|
||||
: t('authenticationRequest', {name: props.resource.name})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -327,19 +330,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
{props.methods.password && (
|
||||
<TabsTrigger value="password">
|
||||
<Key className="w-4 h-4 mr-1" />{" "}
|
||||
Password
|
||||
{t('password')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{props.methods.sso && (
|
||||
<TabsTrigger value="sso">
|
||||
<User className="w-4 h-4 mr-1" />{" "}
|
||||
User
|
||||
{t('user')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{props.methods.whitelist && (
|
||||
<TabsTrigger value="whitelist">
|
||||
<AtSign className="w-4 h-4 mr-1" />{" "}
|
||||
Email
|
||||
{t('email')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
@@ -362,7 +365,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
6-digit PIN Code
|
||||
{t('pincodeInput')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
@@ -431,7 +434,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
disabled={loadingLogin}
|
||||
>
|
||||
<LockIcon className="w-4 h-4 mr-2" />
|
||||
Log in with PIN
|
||||
{t('pincodeSubmit')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -457,7 +460,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Password
|
||||
{t('password')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -485,7 +488,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
disabled={loadingLogin}
|
||||
>
|
||||
<LockIcon className="w-4 h-4 mr-2" />
|
||||
Log In with Password
|
||||
{t('passwordSubmit')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -526,7 +529,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email
|
||||
{t('email')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -535,10 +538,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
A one-time
|
||||
code will be
|
||||
sent to this
|
||||
email.
|
||||
{t('otpEmailDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -560,7 +560,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
disabled={loadingLogin}
|
||||
>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Send One-time Code
|
||||
{t('otpEmailSend')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -582,9 +582,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
One-Time
|
||||
Password
|
||||
(OTP)
|
||||
{t('otpEmail')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -612,7 +610,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
disabled={loadingLogin}
|
||||
>
|
||||
<LockIcon className="w-4 h-4 mr-2" />
|
||||
Submit OTP
|
||||
{t('otpEmailSubmit')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -624,7 +622,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
submitOtpForm.reset();
|
||||
}}
|
||||
>
|
||||
Back to Email
|
||||
{t('backToEmail')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -637,9 +635,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
{supporterStatus?.visible && (
|
||||
<div className="text-center mt-2">
|
||||
<span className="text-sm text-muted-foreground opacity-50">
|
||||
Server is running without a supporter key.
|
||||
<br />
|
||||
Consider supporting the project!
|
||||
{t('noSupportKey')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,20 +7,23 @@ import {
|
||||
CardTitle,
|
||||
} from "@app/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default async function ResourceNotFound() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl font-bold">
|
||||
Resource Not Found
|
||||
{t('resourceNotFound')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
The resource you're trying to access does not exist.
|
||||
{t('resourceNotFoundDescription')}
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">Go Home</Link>
|
||||
<Link href="/">{t('goHome')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -71,6 +71,8 @@ export default function SignupForm({
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const { email, password } = values;
|
||||
|
||||
@@ -113,15 +115,13 @@ export default function SignupForm({
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<Image
|
||||
src={`/logo/pangolin_orange.svg`}
|
||||
alt="Pangolin Logo"
|
||||
alt={t('pangolinLogoAlt')}
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Mail } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -20,6 +21,8 @@ export default async function Page(props: {
|
||||
|
||||
const isInvite = searchParams?.redirect?.includes("/invite");
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
if (env.flags.disableSignupWithoutInvite && !isInvite) {
|
||||
redirect("/");
|
||||
}
|
||||
@@ -54,11 +57,10 @@ export default async function Page(props: {
|
||||
<div className="flex flex-col items-center">
|
||||
<Mail className="w-12 h-12 mb-4 text-primary" />
|
||||
<h2 className="text-2xl font-bold mb-2 text-center">
|
||||
Looks like you've been invited!
|
||||
{t('inviteAlready')}
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must log in or create an
|
||||
account.
|
||||
{t('inviteAlreadyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +73,7 @@ export default async function Page(props: {
|
||||
/>
|
||||
|
||||
<p className="text-center text-muted-foreground mt-4">
|
||||
Already have an account?{" "}
|
||||
{t('signupQuestion')}{" "}
|
||||
<Link
|
||||
href={
|
||||
!redirectUrl
|
||||
@@ -80,7 +82,7 @@ export default async function Page(props: {
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Log in
|
||||
{t('login')}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -37,6 +37,7 @@ import { formatAxiosError } from "@app/lib/api";;
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const FormSchema = z.object({
|
||||
email: z.string().email({ message: "Invalid email address" }),
|
||||
@@ -71,6 +72,8 @@ export default function VerifyEmailForm({
|
||||
},
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
setIsSubmitting(true);
|
||||
|
||||
@@ -114,8 +117,7 @@ export default function VerifyEmailForm({
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Verification code resent",
|
||||
description:
|
||||
"We've resent a verification code to your email address. Please check your inbox.",
|
||||
description: "We've resent a verification code to your email address. Please check your inbox.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,9 +128,9 @@ export default function VerifyEmailForm({
|
||||
<div>
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Verify Email</CardTitle>
|
||||
<CardTitle>{t('emailVerify')}</CardTitle>
|
||||
<CardDescription>
|
||||
Enter the verification code sent to your email address.
|
||||
{t('emailVerifyDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -142,7 +144,7 @@ export default function VerifyEmailForm({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@@ -159,7 +161,7 @@ export default function VerifyEmailForm({
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Verification Code</FormLabel>
|
||||
<FormLabel>{t('verificationCode')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP
|
||||
@@ -197,8 +199,7 @@ export default function VerifyEmailForm({
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
We sent a verification code to your
|
||||
email address.
|
||||
{t('verificationCodeEmailSent')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -226,7 +227,7 @@ export default function VerifyEmailForm({
|
||||
{isSubmitting && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Submit
|
||||
{t('emailVerifySubmit')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -241,8 +242,8 @@ export default function VerifyEmailForm({
|
||||
disabled={isResending}
|
||||
>
|
||||
{isResending
|
||||
? "Resending..."
|
||||
: "Didn't receive a code? Click here to resend"}
|
||||
? t('emailVerifyResendProgress')
|
||||
: t('emailVerifyResend')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user