Skip to content

Commit d67233b

Browse files
ajha-devclaude
andcommitted
Add private documents with Cloudflare Zero Trust authentication
Features: - Cloudflare JWT validation with JWKS caching - Sharing modes: only_me, specific_people, authenticated_users, public - Document ownership assigned on first access from private domain - Sharing settings API with owner-only access control - Frontend sharing modal with email list management - Dev mode for local testing without Cloudflare New files: - lib/markdoc/auth/cloudflare.ex - CF JWT validation - lib/markdoc/auth/token.ex - Joken token config - lib/markdoc_web/plugs/cloudflare_auth.ex - HTTP auth plug - lib/markdoc_web/controllers/sharing_controller.ex - Sharing API - frontend/src/components/SharingSettings.tsx - Sharing UI - frontend/src/hooks/useSharingSettings.ts - Sharing state hook Environment variables: - CLOUDFLARE_TEAM_DOMAIN - CF team domain - CLOUDFLARE_AUD - Application audience tag - PRIVATE_DOMAIN - Private domain hostname - PUBLIC_DOMAIN - Public domain hostname - MARKDOC_AUTH_DEV_MODE - Enable dev bypass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c1faff4 commit d67233b

20 files changed

Lines changed: 1746 additions & 70 deletions

config/dev.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,18 @@ config :phoenix_live_view,
7474
debug_attributes: true,
7575
# Enable helpful, but potentially expensive runtime checks
7676
enable_expensive_runtime_checks: true
77+
78+
# Development storage - use disk backend
79+
config :markdoc, :storage,
80+
backend: :disk,
81+
disk_path: "storage/documents"
82+
83+
# Cloudflare Zero Trust auth - dev mode enabled
84+
# In dev mode, authentication is bypassed and a mock user is used
85+
# To test as different users, change dev_email
86+
config :markdoc, :auth,
87+
dev_mode: true,
88+
dev_email: "dev@localhost",
89+
# Treat localhost as the private domain for testing
90+
private_domain: "localhost",
91+
public_domain: "localhost"

config/runtime.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,13 @@ if env_backend == :s3 do
147147

148148
config :ex_aws, :s3, s3_opts
149149
end
150+
151+
# Cloudflare Zero Trust authentication configuration
152+
config :markdoc, :auth,
153+
cloudflare_team_domain: System.get_env("CLOUDFLARE_TEAM_DOMAIN", "markdoc.cloudflareaccess.com"),
154+
cloudflare_aud: System.get_env("CLOUDFLARE_AUD"),
155+
private_domain: System.get_env("PRIVATE_DOMAIN", "private.markdoc.live"),
156+
public_domain: System.get_env("PUBLIC_DOMAIN", "markdoc.live"),
157+
# Dev mode: set MARKDOC_AUTH_DEV_MODE=true and MARKDOC_AUTH_DEV_EMAIL=your@email.com
158+
dev_mode: System.get_env("MARKDOC_AUTH_DEV_MODE") == "true",
159+
dev_email: System.get_env("MARKDOC_AUTH_DEV_EMAIL", "dev@localhost")

frontend/src/components/Editor.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { nanoid } from "nanoid";
1717
import { schema } from "../lib/editorSchema";
1818
import { setThemeMode } from "../lib/syntaxHighlighting";
1919
import { PhoenixProvider } from "../lib/PhoenixProvider";
20-
import type { UserInfo } from "../lib/PhoenixProvider";
20+
import type { UserInfo, DocumentInfo } from "../lib/PhoenixProvider";
2121
import { UserPresence } from "./UserPresence";
2222
import { ConnectionStatus } from "./ConnectionStatus";
2323
import { NamePrompt } from "./NamePrompt";
@@ -27,11 +27,13 @@ import { ThemeToggle } from "./ThemeToggle";
2727
import { SearchBar } from "./SearchBar";
2828
import { KeyboardShortcutsMenu } from "./KeyboardShortcutsMenu";
2929
import { TableOfContents } from "./TableOfContents";
30+
import { SharingSettings } from "./SharingSettings";
3031
import { useTheme } from "../contexts/ThemeContext";
3132
import { useCursors } from "../hooks/useCursors";
3233
import { usePresence } from "../hooks/usePresence";
3334
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
3435
import { useCodeBlockAutoDetect } from "../hooks/useCodeBlockAutoDetect";
36+
import { useSharingSettings } from "../hooks/useSharingSettings";
3537
import { generateDocId } from "../lib/generateDocId";
3638
import { requestImport, onImportMarkdown, fetchPendingImport } from "../lib/importBridge";
3739
import { EditorContext } from "./chat/ChatBlock";
@@ -167,6 +169,8 @@ export function Editor({ docId }: EditorProps) {
167169
const [provider, setProvider] = useState<PhoenixProvider | null>(null);
168170
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
169171
const [showNamePrompt, setShowNamePrompt] = useState(false);
172+
const [documentInfo, setDocumentInfo] = useState<DocumentInfo | null>(null);
173+
const [showSharingModal, setShowSharingModal] = useState(false);
170174
const { mode } = useTheme();
171175

172176
// Update syntax highlighting theme before editor creation
@@ -268,17 +272,32 @@ export function Editor({ docId }: EditorProps) {
268272
// Enable auto-detection of code block languages
269273
useCodeBlockAutoDetect(editor);
270274

275+
// Sharing settings hook
276+
const {
277+
settings: sharingSettings,
278+
updateSettings: updateSharingSettings,
279+
} = useSharingSettings({
280+
docId,
281+
isOwner: documentInfo?.isOwner ?? false,
282+
isPrivateDomain: documentInfo?.isPrivateDomain ?? false,
283+
});
284+
271285
// Initialize Phoenix provider only after we have user info
272286
useEffect(() => {
273287
if (!userInfo) return;
274288

275-
console.log(`🚀 Initializing editor for document: ${docId}`);
289+
console.log(`Initializing editor for document: ${docId}`);
276290

277291
const phoenixProvider = new PhoenixProvider(docId, doc, userInfo);
278292
setProvider(phoenixProvider);
279293

294+
// Listen for document info from the channel
295+
phoenixProvider.onDocumentInfo((info) => {
296+
setDocumentInfo(info);
297+
});
298+
280299
return () => {
281-
console.log(`🛑 Cleaning up editor for document: ${docId}`);
300+
console.log(`Cleaning up editor for document: ${docId}`);
282301
phoenixProvider.destroy();
283302
};
284303
}, [docId, doc, userInfo]);
@@ -567,6 +586,35 @@ export function Editor({ docId }: EditorProps) {
567586
{/* Theme Toggle */}
568587
<ThemeToggle />
569588

589+
{/* Share Button - only for owners on private domain */}
590+
{documentInfo?.isOwner && documentInfo?.isPrivateDomain && (
591+
<button
592+
onClick={() => setShowSharingModal(true)}
593+
style={{
594+
padding: "6px 12px",
595+
fontSize: "14px",
596+
fontWeight: 500,
597+
color: "white",
598+
backgroundColor: "#2da44e",
599+
border: "none",
600+
borderRadius: "6px",
601+
cursor: "pointer",
602+
display: "flex",
603+
alignItems: "center",
604+
gap: "6px",
605+
transition: "background-color 0.2s",
606+
}}
607+
onMouseEnter={(e) => {
608+
e.currentTarget.style.backgroundColor = "#238636";
609+
}}
610+
onMouseLeave={(e) => {
611+
e.currentTarget.style.backgroundColor = "#2da44e";
612+
}}
613+
>
614+
Share
615+
</button>
616+
)}
617+
570618
{/* Combined Menu (New Document + Export) */}
571619
{editor && (
572620
<ExportMenu
@@ -640,6 +688,17 @@ export function Editor({ docId }: EditorProps) {
640688

641689
{/* Table of Contents */}
642690
{editor && <TableOfContents editor={editor} />}
691+
692+
{/* Sharing Settings Modal */}
693+
{documentInfo?.isOwner && documentInfo?.isPrivateDomain && (
694+
<SharingSettings
695+
docId={docId}
696+
isOpen={showSharingModal}
697+
onClose={() => setShowSharingModal(false)}
698+
settings={sharingSettings}
699+
onSave={updateSharingSettings}
700+
/>
701+
)}
643702
</div>
644703
</>
645704
);

0 commit comments

Comments
 (0)