-
Notifications
You must be signed in to change notification settings - Fork 4
feat: AUTH_MODE support, anonymous suggestions, and sign-in-to-edit UX #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
07cbcd8
82c2c3a
e5d5b9a
085006f
6090c12
71f58c5
de9d607
65567bb
a13a0fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react"; | |||||||||||||||||||||
| import { useSession, signIn } from "next-auth/react"; | ||||||||||||||||||||||
| import { useParams, useSearchParams, useRouter, usePathname } from "next/navigation"; | ||||||||||||||||||||||
| import Link from "next/link"; | ||||||||||||||||||||||
| import { ArrowLeft, Settings, FileCode, GitPullRequest, Activity, RefreshCw, Lightbulb, Eye, Keyboard, LogIn } from "lucide-react"; | ||||||||||||||||||||||
| import { ArrowLeft, Settings, FileCode, GitPullRequest, Activity, RefreshCw, Lightbulb, Eye, Keyboard, LogIn, Pencil } from "lucide-react"; | ||||||||||||||||||||||
| import { Header } from "@/components/layout/header"; | ||||||||||||||||||||||
| import { Button } from "@/components/ui/button"; | ||||||||||||||||||||||
| import { ConfirmDialog } from "@/components/ui/confirm-dialog"; | ||||||||||||||||||||||
|
|
@@ -42,6 +42,8 @@ import { useSuggestionBeacon } from "@/lib/hooks/useSuggestionBeacon"; | |||||||||||||||||||||
| import { DeleteImpactAnalysis } from "@/components/editor/DeleteImpactAnalysis"; | ||||||||||||||||||||||
| import { RemoteSyncIndicator } from "@/components/editor/RemoteSyncIndicator"; | ||||||||||||||||||||||
| import { ShareButton } from "@/components/editor/ShareButton"; | ||||||||||||||||||||||
| import { useAnonymousSuggestion } from "@/lib/hooks/useAnonymousSuggestion"; | ||||||||||||||||||||||
| import { CreditModal } from "@/components/suggestions/CreditModal"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import type { OntologySourceEditorRef } from "@/components/editor/OntologySourceEditor"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -80,6 +82,10 @@ export default function EditorPage() { | |||||||||||||||||||||
| setProjectViewMode("editor"); | ||||||||||||||||||||||
| }, [setProjectViewMode]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Auth mode — set at build time by next.config.ts | ||||||||||||||||||||||
| const zitadelConfigured = process.env.NEXT_PUBLIC_ZITADEL_CONFIGURED === "true"; | ||||||||||||||||||||||
| const authMode = process.env.NEXT_PUBLIC_AUTH_MODE || "required"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Branch state | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
| const [activeBranch, setActiveBranch] = useState<string | undefined>(undefined); | ||||||||||||||||||||||
|
|
@@ -192,6 +198,20 @@ export default function EditorPage() { | |||||||||||||||||||||
| // Suggestion session (only active for suggesters who can't directly edit) | ||||||||||||||||||||||
| const [submitDialogOpen, setSubmitDialogOpen] = useState(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Anonymous proposal mode: available when AUTH_MODE != required and user is NOT signed in as editor/suggester | ||||||||||||||||||||||
| const canPropose = authMode !== "required" && !canSuggest; | ||||||||||||||||||||||
| const [creditModalOpen, setCreditModalOpen] = useState(false); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const anonymousSuggestion = useAnonymousSuggestion({ | ||||||||||||||||||||||
| projectId, | ||||||||||||||||||||||
| onSubmitted: (prNumber) => { | ||||||||||||||||||||||
| toast.success(`Proposal submitted as PR #${prNumber}`); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| onError: (msg) => toast.error("Proposal error", msg), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const isAnonymousProposalMode = canPropose && anonymousSuggestion.isActive; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const suggestionSession = useSuggestionSession({ | ||||||||||||||||||||||
| projectId, | ||||||||||||||||||||||
| accessToken: session?.accessToken, | ||||||||||||||||||||||
|
|
@@ -637,6 +657,59 @@ export default function EditorPage() { | |||||||||||||||||||||
| iriPatternDetectedRef.current = false; | ||||||||||||||||||||||
| }, [session, projectId, activeBranch, project, sourceContent, toast, suggestionSession, setSourceContent, setSourceIriIndex]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle anonymous proposal mode class update | ||||||||||||||||||||||
| // Routes through anonymousSuggestion.saveToSession() instead of the normal commit path | ||||||||||||||||||||||
| const handleAnonymousClassUpdate = useCallback(async (classIri: string, data: ClassUpdatePayload) => { | ||||||||||||||||||||||
| if (!activeBranch && !anonymousSuggestion.branch) { | ||||||||||||||||||||||
| throw new Error("No branch selected"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Ensure session exists before saving | ||||||||||||||||||||||
| if (!anonymousSuggestion.sessionId) { | ||||||||||||||||||||||
| await anonymousSuggestion.startSession(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Load source from the anonymous suggestion branch (or current active branch as fallback) | ||||||||||||||||||||||
| const branchToLoad = anonymousSuggestion.branch || activeBranch; | ||||||||||||||||||||||
| let source = sourceContent; | ||||||||||||||||||||||
| if (!source) { | ||||||||||||||||||||||
| const response = await revisionsApi.getFileAtVersion( | ||||||||||||||||||||||
| projectId, | ||||||||||||||||||||||
| branchToLoad!, | ||||||||||||||||||||||
| undefined, // no Bearer token needed for anonymous | ||||||||||||||||||||||
| project?.git_ontology_path, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| source = response.content; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const modifiedSource = updateClassInTurtle(source, classIri, data); | ||||||||||||||||||||||
| const label = data.labels[0]?.value || getLocalName(classIri); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await anonymousSuggestion.saveToSession(modifiedSource, classIri, label); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| setSourceContent(modifiedSource); | ||||||||||||||||||||||
| toast.success(`Proposed update to "${label}"`); | ||||||||||||||||||||||
| updateNodeLabel(classIri, label); | ||||||||||||||||||||||
| setDetailRefreshKey((k) => k + 1); | ||||||||||||||||||||||
| setSourceIriIndex(new Map()); | ||||||||||||||||||||||
| iriPatternDetectedRef.current = false; | ||||||||||||||||||||||
| }, [activeBranch, anonymousSuggestion, projectId, project?.git_ontology_path, sourceContent, toast, updateNodeLabel, setSourceContent, setSourceIriIndex]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle anonymous proposal "Propose Edit" button click | ||||||||||||||||||||||
| const handleProposeEdit = useCallback(async () => { | ||||||||||||||||||||||
| if (!anonymousSuggestion.isActive) { | ||||||||||||||||||||||
| await anonymousSuggestion.startSession(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // After session starts, isAnonymousProposalMode becomes true (canPropose && isActive), | ||||||||||||||||||||||
| // which re-renders ClassDetailPanel with canEdit=true allowing form editing | ||||||||||||||||||||||
| }, [anonymousSuggestion]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle "Submit Proposal" — called by CreditModal onSubmitCredit after user fills in name/email (or skips) | ||||||||||||||||||||||
| const handleAnonymousSubmit = useCallback(async (name: string | null, email: string | null) => { | ||||||||||||||||||||||
| setCreditModalOpen(false); | ||||||||||||||||||||||
| await anonymousSuggestion.submitSession(undefined, name ?? undefined, email ?? undefined); | ||||||||||||||||||||||
| }, [anonymousSuggestion]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle drag-and-drop reparent class | ||||||||||||||||||||||
| // Fetches full class detail, modifies parent_iris, then routes through the appropriate save handler | ||||||||||||||||||||||
| const handleReparentClass = useCallback(async ( | ||||||||||||||||||||||
|
|
@@ -690,9 +763,13 @@ export default function EditorPage() { | |||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Route through the appropriate save handler | ||||||||||||||||||||||
| const saveHandler = isSuggestionMode ? handleSuggestClassUpdate : handleUpdateClass; | ||||||||||||||||||||||
| const saveHandler = isAnonymousProposalMode | ||||||||||||||||||||||
| ? handleAnonymousClassUpdate | ||||||||||||||||||||||
| : isSuggestionMode | ||||||||||||||||||||||
| ? handleSuggestClassUpdate | ||||||||||||||||||||||
| : handleUpdateClass; | ||||||||||||||||||||||
| await saveHandler(classIri, payload); | ||||||||||||||||||||||
| }, [session, projectId, activeBranch, isSuggestionMode, handleUpdateClass, handleSuggestClassUpdate]); | ||||||||||||||||||||||
| }, [session, projectId, activeBranch, isAnonymousProposalMode, isSuggestionMode, handleUpdateClass, handleSuggestClassUpdate, handleAnonymousClassUpdate]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle branch change | ||||||||||||||||||||||
| const handleBranchChange = useCallback((branchName: string) => { | ||||||||||||||||||||||
|
|
@@ -796,8 +873,8 @@ export default function EditorPage() { | |||||||||||||||||||||
| {error || "Project not found"} | ||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||
| <div className="mt-4 flex items-center justify-center gap-3"> | ||||||||||||||||||||||
| {errorKind === "private-403" && ( | ||||||||||||||||||||||
| <Button onClick={() => signIn("zitadel")} className="gap-2"> | ||||||||||||||||||||||
| {errorKind === "private-403" && zitadelConfigured && ( | ||||||||||||||||||||||
| <Button onClick={() => signIn("zitadel", { callbackUrl: window.location.href })} className="gap-2"> | ||||||||||||||||||||||
| <LogIn className="h-4 w-4" /> | ||||||||||||||||||||||
| Sign In | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
|
|
@@ -897,16 +974,24 @@ export default function EditorPage() { | |||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Sign-in CTA for unauthenticated users */} | ||||||||||||||||||||||
| {!hasValidAccess && ( | ||||||||||||||||||||||
| {/* Anonymous proposal mode indicator */} | ||||||||||||||||||||||
| {isAnonymousProposalMode && ( | ||||||||||||||||||||||
| <span className="flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"> | ||||||||||||||||||||||
| <Pencil className="h-3 w-3" /> | ||||||||||||||||||||||
| Proposing | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Sign-in CTA for unauthenticated users (only when Zitadel is configured) */} | ||||||||||||||||||||||
| {!hasValidAccess && zitadelConfigured && ( | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||
| onClick={() => signIn("zitadel")} | ||||||||||||||||||||||
| onClick={() => signIn("zitadel", { callbackUrl: window.location.href })} | ||||||||||||||||||||||
| className="gap-1 rounded-full bg-primary-50 px-3 py-1 text-xs font-medium text-primary-700 hover:bg-primary-100 dark:bg-primary-900/20 dark:text-primary-400 dark:hover:bg-primary-900/30" | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <LogIn className="h-3 w-3" /> | ||||||||||||||||||||||
| Sign in to suggest edits | ||||||||||||||||||||||
| Sign in to edit | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
@@ -927,6 +1012,34 @@ export default function EditorPage() { | |||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Submit Proposal button (anonymous mode) */} | ||||||||||||||||||||||
| {isAnonymousProposalMode && anonymousSuggestion.changesCount > 0 && ( | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="primary" | ||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||
| className="gap-2 bg-emerald-600 hover:bg-emerald-700" | ||||||||||||||||||||||
| onClick={() => setCreditModalOpen(true)} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <Pencil className="h-4 w-4" /> | ||||||||||||||||||||||
| Submit Proposal | ||||||||||||||||||||||
| <span className="rounded-full bg-emerald-500/30 px-1.5 py-0.5 text-xs"> | ||||||||||||||||||||||
| {anonymousSuggestion.changesCount} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Discard Proposal button (anonymous mode) */} | ||||||||||||||||||||||
| {isAnonymousProposalMode && ( | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||
| className="gap-2 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" | ||||||||||||||||||||||
| onClick={() => anonymousSuggestion.discardSession()} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| Discard | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Suggestions link */} | ||||||||||||||||||||||
| {isSuggestionMode && ( | ||||||||||||||||||||||
| <Link href={`/projects/${projectId}/suggestions`}> | ||||||||||||||||||||||
|
|
@@ -1063,11 +1176,11 @@ export default function EditorPage() { | |||||||||||||||||||||
| <DeveloperEditorLayout | ||||||||||||||||||||||
| projectId={projectId} | ||||||||||||||||||||||
| accessToken={session?.accessToken} | ||||||||||||||||||||||
| activeBranch={activeBranch} | ||||||||||||||||||||||
| canEdit={!!canEdit} | ||||||||||||||||||||||
| activeBranch={isAnonymousProposalMode && anonymousSuggestion.branch ? anonymousSuggestion.branch : activeBranch} | ||||||||||||||||||||||
| canEdit={isAnonymousProposalMode ? true : !!canEdit} | ||||||||||||||||||||||
| entityNavigationRef={entityNavigationRef} | ||||||||||||||||||||||
| canSuggest={!!canSuggest} | ||||||||||||||||||||||
| isSuggestionMode={isSuggestionMode} | ||||||||||||||||||||||
| isSuggestionMode={isAnonymousProposalMode ? true : isSuggestionMode} | ||||||||||||||||||||||
| nodes={nodes} | ||||||||||||||||||||||
| isTreeLoading={isTreeLoading} | ||||||||||||||||||||||
| treeError={treeError} | ||||||||||||||||||||||
|
|
@@ -1098,24 +1211,35 @@ export default function EditorPage() { | |||||||||||||||||||||
| onDeleteClass={handleDeleteClass} | ||||||||||||||||||||||
| onCopyIri={handleCopyIri} | ||||||||||||||||||||||
| selectedNodeFallback={selectedNodeFallback} | ||||||||||||||||||||||
| onUpdateClass={isSuggestionMode ? handleSuggestClassUpdate : handleUpdateClass} | ||||||||||||||||||||||
| onUpdateClass={ | ||||||||||||||||||||||
| isAnonymousProposalMode | ||||||||||||||||||||||
| ? handleAnonymousClassUpdate | ||||||||||||||||||||||
| : isSuggestionMode | ||||||||||||||||||||||
| ? handleSuggestClassUpdate | ||||||||||||||||||||||
| : handleUpdateClass | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| detailRefreshKey={detailRefreshKey} | ||||||||||||||||||||||
| onUpdateProperty={isSuggestionMode ? handleSuggestPropertyUpdate : handleUpdateProperty} | ||||||||||||||||||||||
| onUpdateIndividual={isSuggestionMode ? handleSuggestIndividualUpdate : handleUpdateIndividual} | ||||||||||||||||||||||
| onReparentClass={handleReparentClass} | ||||||||||||||||||||||
| reparentOptimistic={reparentOptimistic} | ||||||||||||||||||||||
| rollbackReparent={rollbackReparent} | ||||||||||||||||||||||
| showSignInToEdit={!hasValidAccess && zitadelConfigured && !canPropose} | ||||||||||||||||||||||
| onSignInToEdit={() => signIn("zitadel", { callbackUrl: window.location.href })} | ||||||||||||||||||||||
| canPropose={canPropose && !isAnonymousProposalMode} | ||||||||||||||||||||||
| onProposeEdit={handleProposeEdit} | ||||||||||||||||||||||
| isAnonymousProposalMode={isAnonymousProposalMode} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <StandardEditorLayout | ||||||||||||||||||||||
| projectId={projectId} | ||||||||||||||||||||||
| accessToken={session?.accessToken} | ||||||||||||||||||||||
| activeBranch={activeBranch} | ||||||||||||||||||||||
| canEdit={!!canEdit} | ||||||||||||||||||||||
| activeBranch={isAnonymousProposalMode && anonymousSuggestion.branch ? anonymousSuggestion.branch : activeBranch} | ||||||||||||||||||||||
| canEdit={isAnonymousProposalMode ? true : !!canEdit} | ||||||||||||||||||||||
| canSuggest={!!canSuggest} | ||||||||||||||||||||||
| entityNavigationRef={entityNavigationRef} | ||||||||||||||||||||||
| isSuggestionMode={isSuggestionMode} | ||||||||||||||||||||||
| isSuggestionMode={isAnonymousProposalMode ? true : isSuggestionMode} | ||||||||||||||||||||||
| nodes={nodes} | ||||||||||||||||||||||
| isTreeLoading={isTreeLoading} | ||||||||||||||||||||||
| treeError={treeError} | ||||||||||||||||||||||
|
|
@@ -1135,14 +1259,25 @@ export default function EditorPage() { | |||||||||||||||||||||
| onDeleteClass={handleDeleteClass} | ||||||||||||||||||||||
| onCopyIri={handleCopyIri} | ||||||||||||||||||||||
| selectedNodeFallback={selectedNodeFallback} | ||||||||||||||||||||||
| onUpdateClass={isSuggestionMode ? handleSuggestClassUpdate : handleUpdateClass} | ||||||||||||||||||||||
| onUpdateClass={ | ||||||||||||||||||||||
| isAnonymousProposalMode | ||||||||||||||||||||||
| ? handleAnonymousClassUpdate | ||||||||||||||||||||||
| : isSuggestionMode | ||||||||||||||||||||||
| ? handleSuggestClassUpdate | ||||||||||||||||||||||
| : handleUpdateClass | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| detailRefreshKey={detailRefreshKey} | ||||||||||||||||||||||
| sourceContent={sourceContent} | ||||||||||||||||||||||
| onUpdateProperty={isSuggestionMode ? handleSuggestPropertyUpdate : handleUpdateProperty} | ||||||||||||||||||||||
| onUpdateIndividual={isSuggestionMode ? handleSuggestIndividualUpdate : handleUpdateIndividual} | ||||||||||||||||||||||
| onReparentClass={handleReparentClass} | ||||||||||||||||||||||
| reparentOptimistic={reparentOptimistic} | ||||||||||||||||||||||
| rollbackReparent={rollbackReparent} | ||||||||||||||||||||||
| showSignInToEdit={!hasValidAccess && zitadelConfigured && !canPropose} | ||||||||||||||||||||||
| onSignInToEdit={() => signIn("zitadel", { callbackUrl: window.location.href })} | ||||||||||||||||||||||
| canPropose={canPropose && !isAnonymousProposalMode} | ||||||||||||||||||||||
| onProposeEdit={handleProposeEdit} | ||||||||||||||||||||||
| isAnonymousProposalMode={isAnonymousProposalMode} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
@@ -1240,6 +1375,13 @@ export default function EditorPage() { | |||||||||||||||||||||
| onOpenChange={setShortcutDialogOpen} | ||||||||||||||||||||||
| shortcuts={keyboardShortcuts} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Credit Modal for anonymous proposal submissions — opens before submit to collect optional credit info */} | ||||||||||||||||||||||
| <CreditModal | ||||||||||||||||||||||
| open={creditModalOpen} | ||||||||||||||||||||||
| onClose={() => handleAnonymousSubmit(null, null)} | ||||||||||||||||||||||
| onSubmitCredit={handleAnonymousSubmit} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
|
Comment on lines
+1380
to
+1384
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CreditModal The
This behavior may be intentional for "Skip" but is surprising for dismiss-by-backdrop/ESC. Users might accidentally submit when trying to cancel. 🛡️ Suggested fix: separate skip from dismiss <CreditModal
open={creditModalOpen}
- onClose={() => handleAnonymousSubmit(null, null)}
+ onClose={() => setCreditModalOpen(false)}
onSubmitCredit={handleAnonymousSubmit}
/>Then update CreditModal's 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| </BranchProvider> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,8 +407,13 @@ export default function SuggestionReviewPage() { | |
| <div className="flex-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-sm font-medium text-slate-900 dark:text-white"> | ||
| {s.submitter?.name || s.submitter?.email || "Unknown user"} | ||
| {s.submitter?.name || s.submitter?.email || (s.is_anonymous ? "Anonymous" : "Unknown user")} | ||
| </span> | ||
| {s.is_anonymous && ( | ||
| <span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 dark:bg-slate-700 dark:text-slate-300"> | ||
| Anonymous | ||
| </span> | ||
|
Comment on lines
+410
to
+415
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid the duplicated “Anonymous” label. When Also applies to: 523-531 🤖 Prompt for AI Agents |
||
| )} | ||
| {(s.revision ?? 1) > 1 && ( | ||
| <span className="rounded-sm bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"> | ||
| v{s.revision} | ||
|
|
@@ -510,9 +515,16 @@ export default function SuggestionReviewPage() { | |
| <h4 className="text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500"> | ||
| Submitted by | ||
| </h4> | ||
| <p className="mt-1 text-sm text-slate-900 dark:text-white"> | ||
| {s.submitter?.name || s.submitter?.email || "Unknown user"} | ||
| </p> | ||
| <div className="mt-1 flex items-center gap-2"> | ||
| <p className="text-sm text-slate-900 dark:text-white"> | ||
| {s.submitter?.name || s.submitter?.email || (s.is_anonymous ? "Anonymous" : "Unknown user")} | ||
| </p> | ||
| {s.is_anonymous && ( | ||
| <span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 dark:bg-slate-700 dark:text-slate-300"> | ||
| Anonymous | ||
| </span> | ||
| )} | ||
| </div> | ||
| <p className="text-xs text-slate-500 dark:text-slate-400"> | ||
| <Clock className="mr-1 inline h-3 w-3" /> | ||
| {lastActivity.toLocaleString()} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,12 @@ | ||
| import NextAuth from "next-auth"; | ||
| import type { NextAuthConfig, User } from "next-auth"; | ||
| import "next-auth/jwt"; | ||
| import { getAuthMode, isZitadelConfigured } from "@/lib/auth-mode"; | ||
|
|
||
| // When auth is not required, provide a default secret so NextAuth doesn't crash | ||
| if (getAuthMode() !== "required" && !process.env.NEXTAUTH_SECRET) { | ||
| process.env.NEXTAUTH_SECRET = "ontokit-optional-auth-secret"; | ||
| } | ||
|
Comment on lines
+6
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Expected: there should be a single source of truth for NEXTAUTH_SECRET, and production auth-capable modes should not rely on a public fallback."
echo
echo "== secret fallbacks and reads =="
rg -nP --type=ts 'NEXTAUTH_SECRET|AUTH_SECRET|ontokit-optional-auth-secret|ontokit-dev-secret' auth.ts lib
echo
echo "== auth.ts =="
sed -n '1,140p' auth.tsRepository: CatholicOS/ontokit-web Length of output: 4639 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Checking lib/env.ts context around NEXTAUTH_SECRET defaults ==="
rg -B5 -A5 'NEXTAUTH_SECRET.*default' lib/env.ts
echo
echo "=== Checking how lib/env.ts is imported/used in auth.ts ==="
rg -n 'env|import.*env' auth.ts | head -20
echo
echo "=== Full lib/env.ts to understand the schema structure ==="
wc -l lib/env.ts && head -50 lib/env.tsRepository: CatholicOS/ontokit-web Length of output: 133 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Listing files in lib directory ==="
fd . lib --max-depth 2 -t f
echo
echo "=== Check if lib/env.ts exists ==="
ls -la lib/env.ts 2>&1 || echo "File not found"
echo
echo "=== Reading lib/env.ts entirely ==="
cat -n lib/env.ts 2>&1 || echo "Cannot read file"Repository: CatholicOS/ontokit-web Length of output: 5855 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Reading lib/auth-mode.ts to understand getAuthMode() and isAuthRequired() ==="
cat -n lib/auth-mode.ts
echo
echo "=== Checking where auth.ts is imported to understand initialization order ==="
rg -l 'import.*auth' --type=ts --type=tsx | head -10Repository: CatholicOS/ontokit-web Length of output: 1395 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Check if auth.ts is imported in middleware or app entry points ==="
rg 'import.*auth' . --type ts -l | grep -E '(middleware|layout|page)' | head -10
echo
echo "=== Check middleware.ts ==="
cat -n middleware.ts 2>/dev/null || echo "No middleware.ts found"
echo
echo "=== Check app layout ==="
find . -name 'layout.ts' -o -name 'layout.tsx' | head -3 | xargs cat -n 2>/dev/null || echo "Layout file not found or error"
echo
echo "=== Key question: When auth.ts sets NEXTAUTH_SECRET, does lib/env.ts validation run before or after? ==="
echo "Checking if auth.ts is imported at module level in app initialization..."
rg 'from.*auth' . --type ts | grep -v node_modules | head -10Repository: CatholicOS/ontokit-web Length of output: 5610 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Check if lib/env.ts is imported anywhere that would run at module init ==="
rg 'import.*env' . --type ts | grep -v node_modules | head -20
echo
echo "=== Check app/providers.ts which is imported at root layout ==="
cat -n app/providers.ts 2>/dev/null || echo "Not found"
echo
echo "=== Search for where serverEnv from lib/env.ts is actually used ==="
rg 'serverEnv|validateServerEnv|clientEnv|validateClientEnv' . --type ts | grep -v node_modules | head -15Repository: CatholicOS/ontokit-web Length of output: 1480 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Search for actual usage of serverEnv export from lib/env.ts ==="
rg 'import.*serverEnv|from.*lib/env' . --type ts | grep -v test | grep -v node_modules
echo
echo "=== Check if NextAuth initialization in auth.ts happens before env validation anywhere ==="
echo "Looking for any files that import from both auth.ts and lib/env.ts..."
rg 'import.*auth' . --type ts | grep -v node_modules | xargs rg 'import.*env' | head -10
echo
echo "=== Check what actually imports auth.ts at the app level ==="
rg 'import.*auth' app --type ts 2>/dev/null | head -20Repository: CatholicOS/ontokit-web Length of output: 133 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Check what happens when auth.ts is loaded - look at auth.ts more closely ==="
cat -n auth.ts | head -15
echo
echo "=== Confirm: the NEXTAUTH_SECRET fallback in auth.ts is set BEFORE NextAuth() is called ==="
grep -n 'NextAuth(' auth.ts
echo
echo "=== When does lib/env.ts validation actually run? Check if serverEnv is exported/used ==="
rg 'serverEnv' . --type ts | grep -v test | grep -v 'function validateServerEnv' | grep -v 'export const serverEnv' | grep -v 'return validateServerEnv'
echo
echo "=== Summary: Is auth.ts imported/executed at module level by NextAuth setup? ==="
echo "NextAuth handlers are exported from auth.ts, so auth.ts runs immediately when imported."
echo "The NEXTAUTH_SECRET assignment on lines 7-8 runs BEFORE NextAuth(authConfig) on line 107."
echo "This means the fallback secret is set into process.env before any other code can read it."Repository: CatholicOS/ontokit-web Length of output: 1031 Don't ship a shared fallback Lines 7–8 set 🤖 Prompt for AI Agents |
||
|
|
||
| // Zitadel provider configuration | ||
| const zitadelProvider = { | ||
|
|
@@ -26,7 +32,9 @@ const zitadelProvider = { | |
| }; | ||
|
|
||
| export const authConfig: NextAuthConfig = { | ||
| providers: [zitadelProvider], | ||
| providers: getAuthMode() === "disabled" || !isZitadelConfigured() | ||
| ? [] | ||
| : [zitadelProvider], | ||
| events: { | ||
| async signOut(_message) { | ||
| // This event fires after local session is cleared | ||
|
|
@@ -46,6 +54,11 @@ export const authConfig: NextAuthConfig = { | |
| }; | ||
| } | ||
|
|
||
| // Skip token refresh when Zitadel is not configured | ||
| if (!isZitadelConfigured()) { | ||
| return token; | ||
| } | ||
|
|
||
| // Return previous token if the access token has not expired yet | ||
| if (Date.now() < ((token.expiresAt as number) ?? 0) * 1000) { | ||
| return token; | ||
|
|
@@ -95,10 +108,9 @@ export const authConfig: NextAuthConfig = { | |
| return session; | ||
| }, | ||
| }, | ||
| pages: { | ||
| signIn: "/auth/signin", | ||
| error: "/auth/error", | ||
| }, | ||
| pages: getAuthMode() === "required" | ||
| ? { signIn: "/auth/signin", error: "/auth/error" } | ||
| : {}, | ||
| debug: process.env.NODE_ENV === "development", | ||
| }; | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.