feat: add trusted URLs

This commit is contained in:
Stavros
2025-04-15 13:44:23 +03:00
parent 7413b3f931
commit e11d14fda0
8 changed files with 57 additions and 15 deletions

View File

@@ -5,6 +5,20 @@ import ChainedBackend from "i18next-chained-backend";
import resourcesToBackend from "i18next-resources-to-backend";
import HttpBackend from "i18next-http-backend";
const backends = [
HttpBackend,
resourcesToBackend(
(language: string) => import(`./locales/${language}.json`),
),
]
const backendOptions = [
{
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
},
{}
]
i18n
.use(ChainedBackend)
.use(LanguageDetector)
@@ -20,17 +34,8 @@ i18n
load: "currentOnly",
backend: {
backends: [
HttpBackend,
resourcesToBackend(
(language: string) => import(`./locales/${language}.json`),
),
],
backendOptions: [
{
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
},
],
backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(),
backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse()
},
});

View File

@@ -42,5 +42,8 @@
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
"unauthorizedButton": "Try again"
"unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
}

View File

@@ -42,5 +42,8 @@
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
"unauthorizedButton": "Try again"
"unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
"cancelTitle": "Cancel"
}

View File

@@ -4,7 +4,7 @@ import { Navigate } from "react-router";
import { useUserContext } from "../context/user-context";
import { Layout } from "../components/layouts/layout";
import { ReactNode } from "react";
import { isQueryValid } from "../utils/utils";
import { escapeRegex, isQueryValid } from "../utils/utils";
import { useAppContext } from "../context/app-context";
import { Trans, useTranslation } from "react-i18next";
@@ -14,7 +14,7 @@ export const ContinuePage = () => {
const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn } = useUserContext();
const { disableContinue } = useAppContext();
const { disableContinue, domain } = useAppContext();
const { t } = useTranslation();
if (!isLoggedIn) {
@@ -51,6 +51,30 @@ export const ContinuePage = () => {
);
}
const regex = new RegExp(`^.*${escapeRegex(domain)}$`)
if (!regex.test(uri.hostname)) {
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
{t("untrustedRedirectTitle")}
</Text>
<Trans
i18nKey="untrustedRedirectSubtitle"
t={t}
components={{ Code: <Code /> }}
values={{ domain: domain }}
/>
<Button fullWidth mt="xl" color="red" onClick={redirect}>
{t('continueTitle')}
</Button>
<Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
{t('cancelTitle')}
</Button>
</ContinuePageLayout>
)
}
if (disableContinue) {
window.location.href = redirectUri;
return (
@@ -79,6 +103,9 @@ export const ContinuePage = () => {
<Button fullWidth mt="xl" color="yellow" onClick={redirect}>
{t("continueTitle")}
</Button>
<Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
{t('cancelTitle')}
</Button>
</ContinuePageLayout>
);
}

View File

@@ -5,6 +5,7 @@ export const appContextSchema = z.object({
disableContinue: z.boolean(),
title: z.string(),
genericName: z.string(),
domain: z.string(),
});
export type AppContextSchemaType = z.infer<typeof appContextSchema>;

View File

@@ -1,2 +1,3 @@
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null";
export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&");

View File

@@ -446,6 +446,7 @@ func (h *Handlers) AppHandler(c *gin.Context) {
DisableContinue: h.Config.DisableContinue,
Title: h.Config.Title,
GenericName: h.Config.GenericName,
Domain: h.Config.Domain,
}
// Return app context

View File

@@ -46,6 +46,7 @@ type AppContext struct {
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
Domain string `json:"domain"`
}
// Totp request is the request for the totp endpoint