mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: demo-banner (#305)
This commit is contained in:
@@ -11,8 +11,8 @@ import { env } from "@/env.mjs";
|
|||||||
* Auth is handled by withApiKey before MCP is ever touched.
|
* Auth is handled by withApiKey before MCP is ever touched.
|
||||||
* The validated key is forwarded by MCP tools to downstream /api/v1 REST calls.
|
* The validated key is forwarded by MCP tools to downstream /api/v1 REST calls.
|
||||||
*/
|
*/
|
||||||
const apiEnabled = String(env.API_ENABLED) === "true";
|
const apiEnabled = env.API_ENABLED;
|
||||||
const mcpEnabled = String(env.MCP_ENABLED) === "true";
|
const mcpEnabled = env.MCP_ENABLED;
|
||||||
|
|
||||||
export const POST = apiEnabled && mcpEnabled
|
export const POST = apiEnabled && mcpEnabled
|
||||||
? withApiKey(async (req: Request, ctx: ApiKeyContext) => {
|
? withApiKey(async (req: Request, ctx: ApiKeyContext) => {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
|
|
||||||
if (url.pathname.startsWith("/api")) {
|
if (url.pathname.startsWith("/api")) {
|
||||||
if (url.pathname.startsWith("/api/v1")) {
|
if (url.pathname.startsWith("/api/v1")) {
|
||||||
const apiEnabled = String(env.API_ENABLED) === "true";
|
const apiEnabled = env.API_ENABLED;
|
||||||
if (!apiEnabled) {
|
if (!apiEnabled) {
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -53,7 +53,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
{ status: 404, headers: { "Content-Type": "application/json" } },
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const openapiEnabled = String(env.OPENAPI_ENABLED) === "true";
|
const openapiEnabled = env.OPENAPI_ENABLED;
|
||||||
if (
|
if (
|
||||||
!openapiEnabled &&
|
!openapiEnabled &&
|
||||||
(url.pathname.startsWith("/api/v1/docs") ||
|
(url.pathname.startsWith("/api/v1/docs") ||
|
||||||
|
|||||||
+15
-8
@@ -36,8 +36,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
SMTP_SECURE: z
|
SMTP_SECURE: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((val) => val === "true")
|
.default("true")
|
||||||
.default("true"),
|
.transform((val) => val === "true"),
|
||||||
|
|
||||||
AUTH_GOOGLE_ID: z.string().optional(),
|
AUTH_GOOGLE_ID: z.string().optional(),
|
||||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||||
@@ -101,18 +101,23 @@ export const env = createEnv({
|
|||||||
|
|
||||||
OPENAPI_ENABLED: z
|
OPENAPI_ENABLED: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((val) => val === "true")
|
.default("false")
|
||||||
.default("false"),
|
.transform((val) => val === "true"),
|
||||||
|
|
||||||
API_ENABLED: z
|
API_ENABLED: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((val) => val === "true")
|
.default("false")
|
||||||
.default("false"),
|
.transform((val) => val === "true"),
|
||||||
|
|
||||||
MCP_ENABLED: z
|
MCP_ENABLED: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((val) => val === "true")
|
.default("false")
|
||||||
.default("false"),
|
.transform((val) => val === "true"),
|
||||||
|
|
||||||
|
DEMO_ENABLED: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((val) => val === "true"),
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
||||||
@@ -190,5 +195,7 @@ export const env = createEnv({
|
|||||||
API_ENABLED: process.env.API_ENABLED,
|
API_ENABLED: process.env.API_ENABLED,
|
||||||
MCP_ENABLED: process.env.MCP_ENABLED,
|
MCP_ENABLED: process.env.MCP_ENABLED,
|
||||||
|
|
||||||
|
DEMO_ENABLED: process.env.DEMO_ENABLED,
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Timer } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
function getSecondsUntilNextHour(): number {
|
||||||
|
const now = new Date();
|
||||||
|
const s = 3600 - (now.getMinutes() * 60 + now.getSeconds());
|
||||||
|
return s === 3600 ? 0 : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const s = Math.max(0, seconds);
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const sec = s % 60;
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DemoResetBanner() {
|
||||||
|
const [secondsLeft, setSecondsLeft] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tick = () => setSecondsLeft(getSecondsUntilNextHour());
|
||||||
|
|
||||||
|
const timeout = setTimeout(tick, 0);
|
||||||
|
const interval = setInterval(tick, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (secondsLeft === null) return null;
|
||||||
|
|
||||||
|
const isResetting = secondsLeft === 0;
|
||||||
|
const isWarning = secondsLeft > 0 && secondsLeft <= 300;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'hidden md:inline-flex h-7',
|
||||||
|
isResetting && 'animate-pulse border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400',
|
||||||
|
isWarning && 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Timer aria-hidden="true" />
|
||||||
|
{isResetting ? 'Resetting…' : `Demo resets in ${formatTime(secondsLeft)}`}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ import {currentUser} from "@/lib/auth/current-user";
|
|||||||
import {BreadCrumbsWrapper} from "@/components/common/bread-crumbs";
|
import {BreadCrumbsWrapper} from "@/components/common/bread-crumbs";
|
||||||
import GitHubStarsButtonCustom from "@/components/common/github-button";
|
import GitHubStarsButtonCustom from "@/components/common/github-button";
|
||||||
import {LoggedInButton} from "@/features/layout/logged-in-button.server";
|
import {LoggedInButton} from "@/features/layout/logged-in-button.server";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { DemoResetBanner } from "@/features/layout/demo-reset-banner";
|
||||||
|
|
||||||
export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
|
export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
const demoEnabled = env.DEMO_ENABLED;
|
||||||
return (
|
return (
|
||||||
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
|
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -18,6 +21,7 @@ export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
|
|||||||
<BreadCrumbsWrapper/>
|
<BreadCrumbsWrapper/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{demoEnabled && <DemoResetBanner />}
|
||||||
<GitHubStarsButtonCustom/>
|
<GitHubStarsButtonCustom/>
|
||||||
{actions}
|
{actions}
|
||||||
<LoggedInButton/>
|
<LoggedInButton/>
|
||||||
|
|||||||
Reference in New Issue
Block a user