+5
-5
@@ -1,9 +1,9 @@
|
|||||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/moh_sass?schema=public"
|
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/moh_sass?schema=public"
|
||||||
NEXT_PUBLIC_APP_URL="https://mohfarawati.de"
|
NEXT_PUBLIC_APP_URL="https://mohfarawati.de"
|
||||||
NEXT_PUBLIC_SITE_URL="https://mohfarawati.de"
|
NEXT_PUBLIC_SITE_URL="https://mohfarawati.de"
|
||||||
NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de"
|
NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de"
|
||||||
NEXT_TELEMETRY_DISABLED="1"
|
NEXT_TELEMETRY_DISABLED="1"
|
||||||
ADMIN_PASSWORD="123Yolo!321"
|
ADMIN_PASSWORD="change-me"
|
||||||
ADMIN_AUTH_SECRET="Us0z76jwlTQLOeQWAGGxAxDcc0rHwp4q"
|
ADMIN_AUTH_SECRET="replace-with-a-long-random-secret"
|
||||||
ROOT_BASIC_AUTH_USER="root"
|
ROOT_BASIC_AUTH_USER="change-me"
|
||||||
ROOT_BASIC_AUTH_PASS="123Yolo!321"
|
ROOT_BASIC_AUTH_PASS="change-me"
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
|||||||
import { buildLocalizedMetadata } from "@/lib/metadata";
|
import { buildLocalizedMetadata } from "@/lib/metadata";
|
||||||
|
|
||||||
type AboutPageProps = {
|
type AboutPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: AboutPageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: AboutPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "aboutPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "aboutPage" });
|
||||||
|
|
||||||
@@ -35,7 +34,8 @@ export async function generateMetadata({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AboutPage({ params: { locale } }: AboutPageProps) {
|
export default async function AboutPage({ params }: AboutPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const [t, siteSettings, mediaBindings] = await Promise.all([
|
const [t, siteSettings, mediaBindings] = await Promise.all([
|
||||||
getTranslations({ locale: localeKey, namespace: "aboutPage" }),
|
getTranslations({ locale: localeKey, namespace: "aboutPage" }),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard";
|
import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard";
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { submitContactFormAction } from "./actions";
|
import { submitContactFormAction } from "./actions";
|
||||||
|
|
||||||
type ContactPageProps = {
|
type ContactPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: ContactPageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: ContactPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "contactPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "contactPage" });
|
||||||
|
|
||||||
@@ -34,7 +33,8 @@ export async function generateMetadata({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ContactPage({ params: { locale } }: ContactPageProps) {
|
export default async function ContactPage({ params }: ContactPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const [t, protection] = await Promise.all([
|
const [t, protection] = await Promise.all([
|
||||||
getTranslations({ locale: localeKey, namespace: "contactPage" }),
|
getTranslations({ locale: localeKey, namespace: "contactPage" }),
|
||||||
|
|||||||
@@ -11,23 +11,24 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
|||||||
|
|
||||||
type SiteLayoutProps = {
|
type SiteLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
export default async function SiteLayout({ children, params: { locale } }: SiteLayoutProps) {
|
export default async function SiteLayout({ children, params }: SiteLayoutProps) {
|
||||||
noStore();
|
noStore();
|
||||||
|
const { locale } = await params;
|
||||||
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const [maintenanceEnabled, mediaBindings] = await Promise.all([
|
const [maintenanceEnabled, mediaBindings] = await Promise.all([
|
||||||
getMaintenanceMode(),
|
getMaintenanceMode(),
|
||||||
getSiteSettingsMediaBindings(),
|
getSiteSettingsMediaBindings(),
|
||||||
]);
|
]);
|
||||||
const authenticated = isAdminAuthenticated();
|
const authenticated = await isAdminAuthenticated();
|
||||||
|
|
||||||
if (maintenanceEnabled && !authenticated) {
|
if (maintenanceEnabled && !authenticated) {
|
||||||
redirect(getLocalizedPath(localeKey, "/coming-soon"));
|
redirect(getLocalizedPath(localeKey, "/coming-soon"));
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import { CardContent } from "@/components/ui/card";
|
|||||||
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
||||||
|
|
||||||
type HomePageProps = {
|
type HomePageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceIcons = [
|
const serviceIcons = [
|
||||||
@@ -41,9 +41,8 @@ const serviceIcons = [
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: HomePageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: HomePageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const [t, siteSettings] = await Promise.all([
|
const [t, siteSettings] = await Promise.all([
|
||||||
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
||||||
@@ -59,7 +58,8 @@ export async function generateMetadata({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePage({ params: { locale } }: HomePageProps) {
|
export default async function HomePage({ params }: HomePageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const [t, marqueeSettings] = await Promise.all([
|
const [t, marqueeSettings] = await Promise.all([
|
||||||
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
||||||
|
|||||||
@@ -13,17 +13,16 @@ import {
|
|||||||
} from "@/lib/portfolio";
|
} from "@/lib/portfolio";
|
||||||
|
|
||||||
type PortfolioItemPageProps = {
|
type PortfolioItemPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: PortfolioItemPageProps): Promise<Metadata> {
|
||||||
params: { locale, slug },
|
const { locale, slug } = await params;
|
||||||
}: PortfolioItemPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const item = await getPublishedPortfolioProjectBySlug(slug);
|
const item = await getPublishedPortfolioProjectBySlug(slug);
|
||||||
|
|
||||||
@@ -45,8 +44,9 @@ export async function generateMetadata({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function PortfolioItemPage({
|
export default async function PortfolioItemPage({
|
||||||
params: { locale, slug },
|
params,
|
||||||
}: PortfolioItemPageProps) {
|
}: PortfolioItemPageProps) {
|
||||||
|
const { locale, slug } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const item = await getPublishedPortfolioProjectBySlug(slug);
|
const item = await getPublishedPortfolioProjectBySlug(slug);
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,16 @@ import {
|
|||||||
} from "@/lib/portfolio";
|
} from "@/lib/portfolio";
|
||||||
|
|
||||||
type PortfolioCategoryPageProps = {
|
type PortfolioCategoryPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: PortfolioCategoryPageProps): Promise<Metadata> {
|
||||||
params: { locale, slug },
|
const { locale, slug } = await params;
|
||||||
}: PortfolioCategoryPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
||||||
const category = await getActivePortfolioCategoryBySlug(slug);
|
const category = await getActivePortfolioCategoryBySlug(slug);
|
||||||
@@ -49,8 +48,9 @@ export async function generateMetadata({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function PortfolioCategoryPage({
|
export default async function PortfolioCategoryPage({
|
||||||
params: { locale, slug },
|
params,
|
||||||
}: PortfolioCategoryPageProps) {
|
}: PortfolioCategoryPageProps) {
|
||||||
|
const { locale, slug } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
||||||
const [categories, category, projects] = await Promise.all([
|
const [categories, category, projects] = await Promise.all([
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { permanentRedirect } from "next/navigation";
|
|||||||
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
||||||
|
|
||||||
type PortfolioCategoryIndexPageProps = {
|
type PortfolioCategoryIndexPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PortfolioCategoryIndexPage({
|
export default async function PortfolioCategoryIndexPage({
|
||||||
params: { locale },
|
params,
|
||||||
}: PortfolioCategoryIndexPageProps) {
|
}: PortfolioCategoryIndexPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
|
|
||||||
permanentRedirect(getLocalizedPath(localeKey, "/portfolio"));
|
permanentRedirect(getLocalizedPath(localeKey, "/portfolio"));
|
||||||
|
|||||||
@@ -11,19 +11,18 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
|||||||
import { getActivePortfolioCategories, getPublishedPortfolioProjects } from "@/lib/portfolio";
|
import { getActivePortfolioCategories, getPublishedPortfolioProjects } from "@/lib/portfolio";
|
||||||
|
|
||||||
type PortfolioPageProps = {
|
type PortfolioPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
searchParams?: {
|
searchParams?: Promise<{
|
||||||
category?: string;
|
category?: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: PortfolioPageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: PortfolioPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
||||||
|
|
||||||
@@ -36,12 +35,16 @@ export async function generateMetadata({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function PortfolioPage({
|
export default async function PortfolioPage({
|
||||||
params: { locale },
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PortfolioPageProps) {
|
}: PortfolioPageProps) {
|
||||||
|
const [{ locale }, resolvedSearchParams] = await Promise.all([
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
]);
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
|
||||||
const selectedCategory = searchParams?.category ?? "";
|
const selectedCategory = resolvedSearchParams?.category ?? "";
|
||||||
|
|
||||||
if (selectedCategory) {
|
if (selectedCategory) {
|
||||||
redirect(getLocalizedPath(localeKey, `/portfolio/category/${selectedCategory}`));
|
redirect(getLocalizedPath(localeKey, `/portfolio/category/${selectedCategory}`));
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
type SuccessPageProps = {
|
type SuccessPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: SuccessPageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: SuccessPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
|
||||||
|
|
||||||
@@ -32,7 +31,8 @@ export async function generateMetadata({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function SuccessPage({ params: { locale } }: SuccessPageProps) {
|
export default async function SuccessPage({ params }: SuccessPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
|
const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ import { buildLocalizedMetadata } from "@/lib/metadata";
|
|||||||
import { resolveLocale } from "@/lib/locale";
|
import { resolveLocale } from "@/lib/locale";
|
||||||
|
|
||||||
type ComingSoonPageProps = {
|
type ComingSoonPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: ComingSoonPageProps): Promise<Metadata> {
|
||||||
params: { locale },
|
const { locale } = await params;
|
||||||
}: ComingSoonPageProps): Promise<Metadata> {
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
|
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
|
||||||
const siteSettings = await getSiteSettings();
|
const siteSettings = await getSiteSettings();
|
||||||
@@ -32,8 +31,9 @@ export async function generateMetadata({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function ComingSoonPage({
|
export default async function ComingSoonPage({
|
||||||
params: { locale },
|
params,
|
||||||
}: ComingSoonPageProps) {
|
}: ComingSoonPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
|
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
|
||||||
const isArabic = localeKey === "ar";
|
const isArabic = localeKey === "ar";
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { getDirection } from "@/lib/locale";
|
|||||||
|
|
||||||
type LocaleLayoutProps = {
|
type LocaleLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
params: {
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -23,9 +23,10 @@ export function generateStaticParams() {
|
|||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({
|
||||||
children,
|
children,
|
||||||
params: { locale },
|
params,
|
||||||
}: LocaleLayoutProps) {
|
}: LocaleLayoutProps) {
|
||||||
noStore();
|
noStore();
|
||||||
|
const { locale } = await params;
|
||||||
|
|
||||||
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
||||||
notFound();
|
notFound();
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"
|
|||||||
import { getLocalizedPath } from "@/lib/locale";
|
import { getLocalizedPath } from "@/lib/locale";
|
||||||
import { setMaintenanceMode } from "@/lib/app-config";
|
import { setMaintenanceMode } from "@/lib/app-config";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMaintenanceModeAction(formData: FormData) {
|
export async function updateMaintenanceModeAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const nextValue = formData.get("enabled") === "true";
|
const nextValue = formData.get("enabled") === "true";
|
||||||
const redirectPath = String(formData.get("redirectPath") ?? "/");
|
const redirectPath = String(formData.get("redirectPath") ?? "/");
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootMaintenancePage() {
|
export default async function RootMaintenancePage() {
|
||||||
const authenticated = isAdminAuthenticated();
|
const authenticated = await isAdminAuthenticated();
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
@@ -38,7 +38,7 @@ export default async function RootMaintenancePage() {
|
|||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
|
|
||||||
import { toInternalAdminPath } from "@/lib/admin-routing";
|
import { toInternalAdminPath } from "@/lib/admin-routing";
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
@@ -10,9 +10,9 @@ import { updateMarqueeSettings } from "@/lib/app-config";
|
|||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
import { getLocalizedPath } from "@/lib/locale";
|
import { getLocalizedPath } from "@/lib/locale";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ async function revalidateMarqueePages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveMarqueeSettingsAction(formData: FormData) {
|
export async function saveMarqueeSettingsAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const germanSettings = {
|
const germanSettings = {
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootMarqueePage() {
|
export default async function RootMarqueePage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { MediaKind } from "@prisma/client";
|
import { MediaKind } from "@prisma/client";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
|
|
||||||
import { toInternalAdminPath } from "@/lib/admin-routing";
|
import { toInternalAdminPath } from "@/lib/admin-routing";
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
@@ -12,9 +12,9 @@ import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media
|
|||||||
import { isManagedMediaFilePath } from "@/lib/media-storage";
|
import { isManagedMediaFilePath } from "@/lib/media-storage";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ function revalidateMediaPages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createMediaAssetAction(formData: FormData) {
|
export async function createMediaAssetAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const kindValue = String(formData.get("kind") ?? "IMAGE");
|
const kindValue = String(formData.get("kind") ?? "IMAGE");
|
||||||
@@ -59,7 +59,7 @@ export async function createMediaAssetAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMediaAssetAction(formData: FormData) {
|
export async function deleteMediaAssetAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const assetId = String(formData.get("assetId") ?? "");
|
const assetId = String(formData.get("assetId") ?? "");
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootMediaPage() {
|
export default async function RootMediaPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -25,9 +25,9 @@ import { getAdminMediaAssets } from "@/lib/media";
|
|||||||
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
|
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
|
||||||
|
|
||||||
type RootPageProps = {
|
type RootPageProps = {
|
||||||
searchParams?: {
|
searchParams?: Promise<{
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -61,25 +61,26 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPage({ searchParams }: RootPageProps) {
|
export default async function RootPage({ searchParams }: RootPageProps) {
|
||||||
|
const resolvedSearchParams = await searchParams;
|
||||||
const authConfigured = isAdminAuthConfigured();
|
const authConfigured = isAdminAuthConfigured();
|
||||||
const basicConfigured = Boolean(
|
const basicConfigured = Boolean(
|
||||||
process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS,
|
process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS,
|
||||||
);
|
);
|
||||||
const authenticated = isAdminAuthenticated();
|
const authenticated = await isAdminAuthenticated();
|
||||||
const lockState = getAdminLockState();
|
const lockState = await getAdminLockState();
|
||||||
|
|
||||||
async function loginAction(formData: FormData) {
|
async function loginAction(formData: FormData) {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const password = String(formData.get("password") ?? "");
|
const password = String(formData.get("password") ?? "");
|
||||||
const currentLockState = getAdminLockState();
|
const currentLockState = await getAdminLockState();
|
||||||
|
|
||||||
if (currentLockState.locked) {
|
if (currentLockState.locked) {
|
||||||
redirect("/?error=locked");
|
redirect("/?error=locked");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
|
if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
|
||||||
const failState = registerFailedAdminAttempt();
|
const failState = await registerFailedAdminAttempt();
|
||||||
if (failState.locked) {
|
if (failState.locked) {
|
||||||
redirect("/?error=locked");
|
redirect("/?error=locked");
|
||||||
}
|
}
|
||||||
@@ -87,15 +88,15 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
redirect("/?error=invalid");
|
redirect("/?error=invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAdminFailedAttempts();
|
await resetAdminFailedAttempts();
|
||||||
setAdminSessionCookie();
|
await setAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +125,13 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error === "invalid" ? (
|
{resolvedSearchParams?.error === "invalid" ? (
|
||||||
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||||
{copy.invalidLogin}
|
{copy.invalidLogin}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error === "locked" || lockState.locked ? (
|
{resolvedSearchParams?.error === "locked" || lockState.locked ? (
|
||||||
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||||
{copy.lockedLogin}
|
{copy.lockedLogin}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { MediaUsageType, Prisma } from "@prisma/client";
|
import { MediaUsageType, Prisma } from "@prisma/client";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
@@ -23,9 +23,9 @@ import {
|
|||||||
sectionInputSchema,
|
sectionInputSchema,
|
||||||
} from "@/lib/portfolio-validation";
|
} from "@/lib/portfolio-validation";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ async function removeManagedPaths(paths: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertCategoryAction(formData: FormData) {
|
export async function upsertCategoryAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const redirectPath = getRedirectPath(formData, "/portfolio/categories");
|
const redirectPath = getRedirectPath(formData, "/portfolio/categories");
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export async function upsertCategoryAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCategoryAction(formData: FormData) {
|
export async function deleteCategoryAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const redirectPath = getRedirectPath(formData, "/portfolio/categories");
|
const redirectPath = getRedirectPath(formData, "/portfolio/categories");
|
||||||
const id = String(formData.get("id") ?? "");
|
const id = String(formData.get("id") ?? "");
|
||||||
@@ -189,7 +189,7 @@ export async function deleteCategoryAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveProjectAction(formData: FormData) {
|
export async function saveProjectAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const fallbackRedirect = String(formData.get("id") ?? "").trim()
|
const fallbackRedirect = String(formData.get("id") ?? "").trim()
|
||||||
? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}`
|
? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}`
|
||||||
@@ -564,7 +564,7 @@ export async function saveProjectAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProjectAction(formData: FormData) {
|
export async function deleteProjectAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const id = String(formData.get("id") ?? "");
|
const id = String(formData.get("id") ?? "");
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPortfolioCategoriesPage() {
|
export default async function RootPortfolioCategoriesPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,31 +21,33 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioPageProps = {
|
type RootPortfolioPageProps = {
|
||||||
searchParams?: {
|
searchParams?: Promise<{
|
||||||
category?: string;
|
category?: string;
|
||||||
status?: "all" | "draft" | "published";
|
status?: "all" | "draft" | "published";
|
||||||
success?: string;
|
success?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) {
|
export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) {
|
||||||
if (!isAdminAuthenticated()) {
|
const resolvedSearchParams = await searchParams;
|
||||||
|
|
||||||
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCategory = searchParams?.category && searchParams.category !== "__all__"
|
const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
|
||||||
? searchParams.category
|
? resolvedSearchParams.category
|
||||||
: "";
|
: "";
|
||||||
const selectedStatus = searchParams?.status === "draft" || searchParams?.status === "published"
|
const selectedStatus = resolvedSearchParams?.status === "draft" || resolvedSearchParams?.status === "published"
|
||||||
? searchParams.status
|
? resolvedSearchParams.status
|
||||||
: "all";
|
: "all";
|
||||||
const [categories, projects] = await Promise.all([
|
const [categories, projects] = await Promise.all([
|
||||||
getAdminPortfolioCategories(),
|
getAdminPortfolioCategories(),
|
||||||
|
|||||||
@@ -44,29 +44,31 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioProjectPageProps = {
|
type RootPortfolioProjectPageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPortfolioProjectPage({
|
export default async function RootPortfolioProjectPage({
|
||||||
params,
|
params,
|
||||||
}: RootPortfolioProjectPageProps) {
|
}: RootPortfolioProjectPageProps) {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
const [categories, mediaOptions, project] = await Promise.all([
|
const [categories, mediaOptions, project] = await Promise.all([
|
||||||
getActivePortfolioCategories(),
|
getActivePortfolioCategories(),
|
||||||
getMediaOptions(),
|
getMediaOptions(),
|
||||||
getAdminPortfolioProjectById(params.id),
|
getAdminPortfolioProjectById(id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootNewPortfolioProjectPage() {
|
export default async function RootNewPortfolioProjectPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,31 +21,33 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioProjectsPageProps = {
|
type RootPortfolioProjectsPageProps = {
|
||||||
searchParams?: {
|
searchParams?: Promise<{
|
||||||
category?: string;
|
category?: string;
|
||||||
status?: "all" | "draft" | "published";
|
status?: "all" | "draft" | "published";
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPortfolioProjectsPage({
|
export default async function RootPortfolioProjectsPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: RootPortfolioProjectsPageProps) {
|
}: RootPortfolioProjectsPageProps) {
|
||||||
if (!isAdminAuthenticated()) {
|
const resolvedSearchParams = await searchParams;
|
||||||
|
|
||||||
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCategory = searchParams?.category && searchParams.category !== "__all__"
|
const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
|
||||||
? searchParams.category
|
? resolvedSearchParams.category
|
||||||
: "";
|
: "";
|
||||||
const selectedStatus = searchParams?.status === "draft" || searchParams?.status === "published"
|
const selectedStatus = resolvedSearchParams?.status === "draft" || resolvedSearchParams?.status === "published"
|
||||||
? searchParams.status
|
? resolvedSearchParams.status
|
||||||
: "all";
|
: "all";
|
||||||
const [categories, projects] = await Promise.all([
|
const [categories, projects] = await Promise.all([
|
||||||
getAdminPortfolioCategories(),
|
getAdminPortfolioCategories(),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { MediaUsageType } from "@prisma/client";
|
import { MediaUsageType } from "@prisma/client";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY,
|
SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY,
|
||||||
@@ -25,9 +25,9 @@ import { removeManagedMediaFile } from "@/lib/media-storage";
|
|||||||
import { mediaFieldInputSchema } from "@/lib/media-validation";
|
import { mediaFieldInputSchema } from "@/lib/media-validation";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ async function revalidateSiteSettingsPages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSiteSettingsAction(formData: FormData) {
|
export async function saveSiteSettingsAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
const createdMediaAssetIds: string[] = [];
|
const createdMediaAssetIds: string[] = [];
|
||||||
const uploadedPaths: string[] = [];
|
const uploadedPaths: string[] = [];
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootSiteSettingsPage() {
|
export default async function RootSiteSettingsPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
|
|
||||||
import { toInternalAdminPath } from "@/lib/admin-routing";
|
import { toInternalAdminPath } from "@/lib/admin-routing";
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
@@ -14,9 +14,9 @@ import {
|
|||||||
import { sendTestEmail } from "@/lib/mail";
|
import { sendTestEmail } from "@/lib/mail";
|
||||||
import type { MailSettings } from "@/lib/mail-settings";
|
import type { MailSettings } from "@/lib/mail-settings";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ function parseMailSettingsFormData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveMailSettingsAction(formData: FormData) {
|
export async function saveMailSettingsAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingMailSettings = await getMailSettings();
|
const existingMailSettings = await getMailSettings();
|
||||||
@@ -95,7 +95,7 @@ export async function saveMailSettingsAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendTestEmailAction() {
|
export async function sendTestEmailAction() {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendTestEmail();
|
await sendTestEmail();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect-error";
|
||||||
|
|
||||||
import { toInternalAdminPath } from "@/lib/admin-routing";
|
import { toInternalAdminPath } from "@/lib/admin-routing";
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
} from "@/lib/app-config";
|
} from "@/lib/app-config";
|
||||||
import type { ContactProtectionSettings } from "@/lib/contact-protection";
|
import type { ContactProtectionSettings } from "@/lib/contact-protection";
|
||||||
|
|
||||||
function ensureAdmin() {
|
async function ensureAdmin() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ function parseContactProtectionFormData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveContactProtectionSettingsAction(formData: FormData) {
|
export async function saveContactProtectionSettingsAction(formData: FormData) {
|
||||||
ensureAdmin();
|
await ensureAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingSettings = await getContactProtectionSettings();
|
const existingSettings = await getContactProtectionSettings();
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootSMTPProtectionPage() {
|
export default async function RootSMTPProtectionPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootSMTPPage() {
|
export default async function RootSMTPPage() {
|
||||||
if (!isAdminAuthenticated()) {
|
if (!(await isAdminAuthenticated())) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const copy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootUiKitPage() {
|
export default async function RootUiKitPage() {
|
||||||
const authenticated = isAdminAuthenticated();
|
const authenticated = await isAdminAuthenticated();
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
@@ -30,7 +30,7 @@ export default async function RootUiKitPage() {
|
|||||||
async function logoutAction() {
|
async function logoutAction() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
clearAdminSessionCookie();
|
await clearAdminSessionCookie();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import path from "path";
|
|||||||
import { resolveMediaUploadPath } from "@/lib/media-storage";
|
import { resolveMediaUploadPath } from "@/lib/media-storage";
|
||||||
|
|
||||||
type MediaFileRouteProps = {
|
type MediaFileRouteProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
segments: string[];
|
segments: string[];
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -23,7 +23,8 @@ const CONTENT_TYPES: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function GET(_: Request, { params }: MediaFileRouteProps) {
|
export async function GET(_: Request, { params }: MediaFileRouteProps) {
|
||||||
const relativePath = params.segments.join("/");
|
const { segments } = await params;
|
||||||
|
const relativePath = segments.join("/");
|
||||||
const publicPath = `/uploads/media/${relativePath}`;
|
const publicPath = `/uploads/media/${relativePath}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTypeScript from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const config = [
|
||||||
|
...nextCoreWebVitals,
|
||||||
|
...nextTypeScript,
|
||||||
|
{
|
||||||
|
files: ["components/**/*.tsx"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/set-state-in-effect": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["eslint.config.mjs"],
|
||||||
|
rules: {
|
||||||
|
"import/no-anonymous-default-export": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ["prisma/seed.js"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default config;
|
||||||
+12
-12
@@ -64,8 +64,8 @@ export function isPasswordValid(password: string): boolean {
|
|||||||
return timingSafeEqual(provided, expected);
|
return timingSafeEqual(provided, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAdminSessionCookie(): void {
|
export async function setAdminSessionCookie(): Promise<void> {
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
store.set(ADMIN_SESSION_COOKIE, buildToken(), {
|
store.set(ADMIN_SESSION_COOKIE, buildToken(), {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
@@ -75,8 +75,8 @@ export function setAdminSessionCookie(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAdminSessionCookie(): void {
|
export async function clearAdminSessionCookie(): Promise<void> {
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
store.set(ADMIN_SESSION_COOKIE, "", {
|
store.set(ADMIN_SESSION_COOKIE, "", {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
@@ -108,8 +108,8 @@ function parseFailState(rawValue: string | undefined): FailState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAdminLockState(): { locked: boolean; remainingSeconds: number } {
|
export async function getAdminLockState(): Promise<{ locked: boolean; remainingSeconds: number }> {
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
|
const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ export function getAdminLockState(): { locked: boolean; remainingSeconds: number
|
|||||||
return { locked: false, remainingSeconds: 0 };
|
return { locked: false, remainingSeconds: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerFailedAdminAttempt(): { locked: boolean; remainingSeconds: number } {
|
export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; remainingSeconds: number }> {
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
|
const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
|
||||||
const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1;
|
const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1;
|
||||||
@@ -145,8 +145,8 @@ export function registerFailedAdminAttempt(): { locked: boolean; remainingSecond
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetAdminFailedAttempts(): void {
|
export async function resetAdminFailedAttempts(): Promise<void> {
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
store.set(ADMIN_FAIL_COOKIE, "", {
|
store.set(ADMIN_FAIL_COOKIE, "", {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
@@ -156,12 +156,12 @@ export function resetAdminFailedAttempts(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAdminAuthenticated(): boolean {
|
export async function isAdminAuthenticated(): Promise<boolean> {
|
||||||
if (!isAdminAuthConfigured()) {
|
if (!isAdminAuthConfigured()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = cookies();
|
const store = await cookies();
|
||||||
const token = store.get(ADMIN_SESSION_COOKIE)?.value;
|
const token = store.get(ADMIN_SESSION_COOKIE)?.value;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
type ContactProtectionSettings,
|
type ContactProtectionSettings,
|
||||||
} from "@/lib/contact-protection";
|
} from "@/lib/contact-protection";
|
||||||
|
|
||||||
function getClientIpFromHeaders() {
|
async function getClientIpFromHeaders() {
|
||||||
const requestHeaders = headers();
|
const requestHeaders = await headers();
|
||||||
const forwardedFor = requestHeaders.get("x-forwarded-for");
|
const forwardedFor = requestHeaders.get("x-forwarded-for");
|
||||||
|
|
||||||
if (forwardedFor) {
|
if (forwardedFor) {
|
||||||
@@ -41,7 +41,7 @@ export async function enforceContactRateLimit(settings: ContactProtectionSetting
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ip = getClientIpFromHeaders();
|
const ip = await getClientIpFromHeaders();
|
||||||
const key = getRateLimitKey(ip, settings.rateLimit.windowMinutes);
|
const key = getRateLimitKey(ip, settings.rateLimit.windowMinutes);
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
@@ -87,7 +87,7 @@ export async function verifyTurnstileToken(
|
|||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
body.set("secret", settings.turnstile.secretKey);
|
body.set("secret", settings.turnstile.secretKey);
|
||||||
body.set("response", token);
|
body.set("response", token);
|
||||||
body.set("remoteip", getClientIpFromHeaders());
|
body.set("remoteip", await getClientIpFromHeaders());
|
||||||
|
|
||||||
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Generated
+1609
-966
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -4,9 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate deploy",
|
"db:migrate": "prisma migrate deploy",
|
||||||
@@ -30,13 +30,13 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.35.0",
|
"framer-motion": "^12.35.0",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"next": "14.2.35",
|
"next": "^16.1.6",
|
||||||
"next-intl": "^4.8.3",
|
"next-intl": "^4.8.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^8.0.1",
|
"nodemailer": "^8.0.1",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"react": "^18",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^18",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
@@ -45,10 +45,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "^18",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^19.2.3",
|
||||||
"eslint": "^8",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-next": "14.2.35",
|
"eslint-config-next": "^16.1.6",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prisma": "^7.4.2",
|
"prisma": "^7.4.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
|||||||
+21
-6
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -10,7 +14,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -18,9 +22,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
}
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user