feature: new dashboard workos auth for dashboard login, dashboard optional in site config, sync server for dashboard, workos and dashboard docs updated

This commit is contained in:
Wayne Sutton
2025-12-29 22:11:52 -08:00
parent f6c9478c9d
commit e8d09fcce2
47 changed files with 15058 additions and 193 deletions

View File

@@ -7,6 +7,8 @@ import Write from "./pages/Write";
import TagPage from "./pages/TagPage";
import Unsubscribe from "./pages/Unsubscribe";
import NewsletterAdmin from "./pages/NewsletterAdmin";
import Dashboard from "./pages/Dashboard";
import Callback from "./pages/Callback";
import Layout from "./components/Layout";
import { usePageTracking } from "./hooks/usePageTracking";
import { SidebarProvider } from "./context/SidebarContext";
@@ -27,6 +29,16 @@ function App() {
return <NewsletterAdmin />;
}
// Dashboard renders without Layout (full-screen admin)
if (location.pathname === "/dashboard") {
return <Dashboard />;
}
// Callback handles OAuth redirect from WorkOS
if (location.pathname === "/callback") {
return <Callback />;
}
// Determine if we should use a custom homepage
const useCustomHomepage =
siteConfig.homepage.type !== "default" && siteConfig.homepage.slug;

24
src/AppWithWorkOS.tsx Normal file
View File

@@ -0,0 +1,24 @@
// App wrapper with WorkOS authentication providers
// This file is only loaded when WorkOS environment variables are configured
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithAuthKit } from "@convex-dev/workos";
import { workosConfig } from "./utils/workos";
import App from "./App";
interface AppWithWorkOSProps {
convex: ConvexReactClient;
}
export default function AppWithWorkOS({ convex }: AppWithWorkOSProps) {
return (
<AuthKitProvider
clientId={workosConfig.clientId}
redirectUri={workosConfig.redirectUri}
>
<ConvexProviderWithAuthKit client={convex} useAuth={useAuth}>
<App />
</ConvexProviderWithAuthKit>
</AuthKitProvider>
);
}

View File

@@ -13,6 +13,7 @@ interface ContactFormProps {
// Contact form component
// Displays a form with name, email, and message fields
// Submits to Convex which sends email via AgentMail
// Includes honeypot field for bot protection
export default function ContactForm({
source,
title,
@@ -21,6 +22,7 @@ export default function ContactForm({
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [honeypot, setHoneypot] = useState(""); // Honeypot field for bot detection
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [statusMessage, setStatusMessage] = useState("");
@@ -36,6 +38,17 @@ export default function ContactForm({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Honeypot check: if filled, silently reject (bot detected)
if (honeypot) {
// Pretend success to not alert the bot
setStatus("success");
setStatusMessage("Thanks for your message! We'll get back to you soon.");
setName("");
setEmail("");
setMessage("");
return;
}
// Basic validation
if (!name.trim()) {
setStatus("error");
@@ -103,6 +116,31 @@ export default function ContactForm({
</div>
) : (
<form onSubmit={handleSubmit} className="contact-form__form">
{/* Honeypot field: hidden from humans, visible to bots */}
<div
aria-hidden="true"
style={{
position: "absolute",
left: "-9999px",
top: "-9999px",
opacity: 0,
pointerEvents: "none",
height: 0,
overflow: "hidden",
}}
>
<label htmlFor="contact-website">Website</label>
<input
id="contact-website"
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
<div className="contact-form__field">
<label htmlFor="contact-name" className="contact-form__label">
Name

View File

@@ -14,6 +14,7 @@ interface NewsletterSignupProps {
// Newsletter signup component
// Displays email input form for newsletter subscriptions
// Integrates with Convex backend for subscriber management
// Includes honeypot field for bot protection
export default function NewsletterSignup({
source,
postSlug,
@@ -21,6 +22,7 @@ export default function NewsletterSignup({
description,
}: NewsletterSignupProps) {
const [email, setEmail] = useState("");
const [honeypot, setHoneypot] = useState(""); // Honeypot field for bot detection
const [status, setStatus] = useState<
"idle" | "loading" | "success" | "error"
>("idle");
@@ -49,6 +51,15 @@ export default function NewsletterSignup({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Honeypot check: if filled, silently reject (bot detected)
if (honeypot) {
// Pretend success to not alert the bot
setStatus("success");
setMessage("Thanks for subscribing!");
setEmail("");
return;
}
if (!email.trim()) {
setStatus("error");
setMessage("Please enter your email.");
@@ -88,6 +99,31 @@ export default function NewsletterSignup({
<p className="newsletter-signup__success">{message}</p>
) : (
<form onSubmit={handleSubmit} className="newsletter-signup__form">
{/* Honeypot field: hidden from humans, visible to bots */}
<div
aria-hidden="true"
style={{
position: "absolute",
left: "-9999px",
top: "-9999px",
opacity: 0,
pointerEvents: "none",
height: 0,
overflow: "hidden",
}}
>
<label htmlFor={`newsletter-fax-${source}`}>Fax</label>
<input
id={`newsletter-fax-${source}`}
type="text"
name="fax"
tabIndex={-1}
autoComplete="off"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
<input
type="email"
value={email}

View File

@@ -189,6 +189,14 @@ export interface MCPServerConfig {
requireAuth: boolean; // Require API key for all requests
}
// Dashboard configuration
// Controls access to the /dashboard admin page
// WorkOS authentication is optional - if not configured, dashboard shows setup instructions
export interface DashboardConfig {
enabled: boolean; // Global toggle for dashboard page
requireAuth: boolean; // Require WorkOS authentication (only works if WorkOS is configured)
}
// Social link configuration for social footer
export interface SocialLink {
platform:
@@ -302,6 +310,9 @@ export interface SiteConfig {
// MCP Server configuration (optional)
mcpServer?: MCPServerConfig;
// Dashboard configuration (optional)
dashboard?: DashboardConfig;
}
// Default site configuration
@@ -603,6 +614,17 @@ Created by [Wayne](https://x.com/waynesutton) with Convex, Cursor, and Claude Op
authenticatedRateLimit: 1000, // Requests per minute with API key
requireAuth: false, // Set to true to require API key for all requests
},
// Dashboard configuration
// Admin dashboard at /dashboard for managing content and settings
// WorkOS authentication is optional - if not configured, dashboard is open access
// Set enabled: false to disable the dashboard entirely
// WARNING: When requireAuth is false, anyone can access the dashboard
// For production, set up WorkOS and change requireAuth to true
dashboard: {
enabled: true, // Global toggle for dashboard page
requireAuth: true, // Set to true and configure WorkOS for secure authentication
},
};
// Export the config as default for easy importing

View File

@@ -1,25 +1,51 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { StrictMode, lazy, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { ConvexReactClient, ConvexProvider } from "convex/react";
import { ThemeProvider } from "./context/ThemeContext";
import { FontProvider } from "./context/FontContext";
import { isWorkOSConfigured } from "./utils/workos";
import "./styles/global.css";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ConvexProvider client={convex}>
<BrowserRouter>
<ThemeProvider>
<FontProvider>
<App />
</FontProvider>
</ThemeProvider>
</BrowserRouter>
</ConvexProvider>
</React.StrictMode>
// Lazy load the appropriate App wrapper based on WorkOS configuration
const AppWithWorkOS = lazy(() => import("./AppWithWorkOS"));
const App = lazy(() => import("./App"));
// Loading fallback
function LoadingFallback() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontFamily: "system-ui, sans-serif",
}}
>
Loading...
</div>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<ThemeProvider>
<FontProvider>
<Suspense fallback={<LoadingFallback />}>
{isWorkOSConfigured ? (
<AppWithWorkOS convex={convex} />
) : (
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
)}
</Suspense>
</FontProvider>
</ThemeProvider>
</BrowserRouter>
</StrictMode>,
);

36
src/pages/Callback.tsx Normal file
View File

@@ -0,0 +1,36 @@
// src/pages/Callback.tsx
// Handles OAuth callback from WorkOS and redirects to dashboard
import { useEffect } from "react";
import { useAuth } from "@workos-inc/authkit-react";
import { useNavigate } from "react-router-dom";
export default function Callback() {
const { isLoading, user } = useAuth();
const navigate = useNavigate();
useEffect(() => {
// Once authentication is complete, redirect to dashboard
if (!isLoading && user) {
navigate("/dashboard", { replace: true });
}
}, [isLoading, user, navigate]);
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontFamily: "system-ui, sans-serif",
backgroundColor: "var(--bg-primary, #fff)",
color: "var(--text-primary, #111)",
}}
>
<div style={{ textAlign: "center" }}>
<h2>Signing you in...</h2>
<p>Please wait while we complete your authentication.</p>
</div>
</div>
);
}

4958
src/pages/Dashboard.tsx Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

14
src/utils/workos.ts Normal file
View File

@@ -0,0 +1,14 @@
// WorkOS configuration utility
// Checks if WorkOS environment variables are set
const workosClientId = import.meta.env.VITE_WORKOS_CLIENT_ID;
const workosRedirectUri = import.meta.env.VITE_WORKOS_REDIRECT_URI;
// True if both WorkOS client ID and redirect URI are configured
export const isWorkOSConfigured = Boolean(workosClientId && workosRedirectUri);
// Export the values for use in AuthKitProvider
export const workosConfig = {
clientId: workosClientId,
redirectUri: workosRedirectUri,
};