mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
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:
12
src/App.tsx
12
src/App.tsx
@@ -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
24
src/AppWithWorkOS.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
62
src/main.tsx
62
src/main.tsx
@@ -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
36
src/pages/Callback.tsx
Normal 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
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
14
src/utils/workos.ts
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user