docuflow / src /hooks /useWorkspaceState.ts
Joedroid's picture
fix: replace simulated AuthPortal with real passkey/OAuth sign-in
4b7abda
Raw
History Blame Contribute Delete
12.9 kB
import { useState, useEffect, useRef } from "react";
import { Document, AuthUser, SyncLog, TeamMember } from "../types";
import { INITIAL_DOCUMENTS, INITIAL_TEAM_MEMBERS } from "../data";
import { DEFAULT_VALUES } from "../config";
export function useWorkspaceState() {
// Core Document State
const [documents, setDocuments] = useState<Document[]>(() => {
const cached = localStorage.getItem("workspace_documents");
return cached ? JSON.parse(cached) : INITIAL_DOCUMENTS;
});
const [activeDocId, setActiveDocId] = useState<string>("");
const activeDoc =
documents.find((d) => d.id === activeDocId) || documents[0] || INITIAL_DOCUMENTS[0];
const [editorText, setEditorText] = useState(activeDoc.content);
// Sync state & Offline detection
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [syncStatus, setSyncStatus] = useState<"synced" | "syncing" | "offline" | "error">("synced");
const [syncLogs, setSyncLogs] = useState<SyncLog[]>([]);
// Auth — no hardcoded credentials
const [user, setUser] = useState<AuthUser>(() => {
const cached = localStorage.getItem("workspace_user");
return cached
? JSON.parse(cached)
: {
email: null,
displayName: null,
isLoggedIn: false,
tier: DEFAULT_VALUES.TIER,
privateKeySeed: "",
};
});
// Team
const [teamMembers, setTeamMembers] = useState<TeamMember[]>(INITIAL_TEAM_MEMBERS);
const [teamChangesEnabled, setTeamChangesEnabled] = useState(true);
const [activeCollaborators, setActiveCollaborators] = useState(1);
// Toast
const [toast, setToast] = useState<{
show: boolean;
message: string;
timestamp: string;
}>({ show: false, message: "", timestamp: "" });
// Form states
const [showDocModal, setShowDocModal] = useState(false);
const [newDocTitle, setNewDocTitle] = useState("");
const [documentSearchTerm, setDocumentSearchTerm] = useState("");
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadLoading, setUploadLoading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
// Admin
const [adminPassword, setAdminPassword] = useState("");
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
const [adminVerifyLoading] = useState(false);
// Analytics
const [tokensConsumed, setTokensConsumed] = useState(0);
// Other UI state
const [showVersionHistory, setShowVersionHistory] = useState(false);
const [isReadingMode, setIsReadingMode] = useState(false);
const [showExportPreview, setShowExportPreview] = useState(false);
const [exportPreviewFormat, setExportPreviewFormat] = useState<"markdown" | "pdf" | "html">("pdf");
const [showPreferenceModal, setShowPreferenceModal] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [copiedDoc, setCopiedDoc] = useState(false);
const [showStatsPopover, setShowStatsPopover] = useState(false);
const [leftPanelMode, setLeftPanelMode] = useState<"edit" | "preview" | "folding">("edit");
const [foldedBlockIds, setFoldedBlockIds] = useState<Record<string, boolean>>({});
// Refs
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isFirstLocalStorageSave = useRef(true);
// Load editor text when active doc changes
useEffect(() => {
setEditorText(activeDoc.content);
}, [activeDoc.id]); // eslint-disable-line react-hooks/exhaustive-deps
// Parse shared links on mount
useEffect(() => {
const urlStr = window.location.href;
const match = urlStr.match(/\/share\/doc\/([^/?#]+)/);
if (match) {
const docId = match[1];
// Fetch the real document from the server
import("../lib/apiClient").then(({ getDocument }) => {
getDocument(docId)
.then((serverDoc) => {
const docExists = documents.some((d) => d.id === docId);
if (!docExists) {
const loadedDoc: Document = {
id: serverDoc.id,
title: serverDoc.title,
content: serverDoc.content,
version: serverDoc.version,
lastSaved: serverDoc.lastSaved,
isSynced: true
};
setDocuments((prev) => [loadedDoc, ...prev]);
}
setActiveDocId(docId);
setIsReadingMode(true);
addLog(`Shared document loaded successfully.`, "success");
})
.catch(() => {
addLog(`Shared document not found on server.`, "error");
});
});
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Load or generate device UUID, and perform guest authentication on mount
useEffect(() => {
async function initAuth() {
// Check for OAuth callback
const urlParams = new URLSearchParams(window.location.search);
const oauthCode = urlParams.get("code");
if (oauthCode) {
// Handle OAuth callback
const provider = sessionStorage.getItem("oauth_provider") || "google";
const redirectUri = sessionStorage.getItem("oauth_redirect_uri") || `${window.location.origin}/auth/callback`;
sessionStorage.removeItem("oauth_provider");
sessionStorage.removeItem("oauth_redirect_uri");
try {
const { oauthCallback } = await import("../lib/apiClient");
const res = await oauthCallback(provider, oauthCode, redirectUri);
localStorage.setItem("workspace_auth_token", res.token);
setUser({
email: res.user.email,
displayName: res.user.display_name,
isLoggedIn: true,
isGuest: false,
tier: res.user.tier,
privateKeySeed: "",
hasPasskey: res.user.has_passkey,
oauthProvider: res.user.oauth_provider,
});
addLog(`Signed in via ${provider}: ${res.user.display_name}`, "success");
// Clean URL
window.history.replaceState({}, "", window.location.pathname);
} catch (err: any) {
addLog(`OAuth callback failed: ${err.message}`, "error");
}
return;
}
// Check for existing token
const existingToken = localStorage.getItem("workspace_auth_token");
if (existingToken) {
const cached = localStorage.getItem("workspace_user");
const parsed = cached ? JSON.parse(cached) : null;
if (parsed && parsed.isLoggedIn) {
addLog(`Restored session for ${parsed.displayName || parsed.email}`, "success");
return;
}
}
// Fallback to guest auth
let deviceUuid = localStorage.getItem("workspace_device_uuid");
if (!deviceUuid) {
deviceUuid = (typeof crypto.randomUUID === "function")
? crypto.randomUUID()
: `device-${Math.random().toString(36).substring(2, 15)}`;
localStorage.setItem("workspace_device_uuid", deviceUuid);
}
try {
const { authGuest } = await import("../lib/apiClient");
const res = await authGuest(deviceUuid);
localStorage.setItem("workspace_auth_token", res.token);
setUser({
email: res.user.email,
displayName: res.user.display_name,
isLoggedIn: true,
isGuest: res.user.is_guest,
tier: res.user.tier,
privateKeySeed: "",
});
addLog(`Guest session: ${res.user.display_name}`, "success");
// Load documents from server
try {
const { getDocuments } = await import("../lib/apiClient");
const docRes = await getDocuments();
if (docRes.documents && docRes.documents.length > 0) {
const serverDocs: Document[] = docRes.documents.map((d: any) => ({
id: d.id,
title: d.title,
content: d.content,
version: d.version,
lastSaved: d.lastSaved,
isSynced: true
}));
setDocuments(serverDocs);
}
} catch {
// Keep local docs
}
} catch (err: any) {
addLog(`Auth failed: ${err.message}`, "error");
}
}
initAuth();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Online/Offline listeners
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
setSyncStatus("synced");
addLog("Network connection restored. Re-authenticating sync tunnel.", "success");
};
const handleOffline = () => {
setIsOnline(false);
setSyncStatus("offline");
addLog("Offline mode active. All edits are being buffered securely in local IndexDB.", "info");
};
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
// Auto-save to localStorage
const [autoSaveEnabled, setAutoSaveEnabled] = useState<boolean>(() => {
return localStorage.getItem("workspace_pref_autoSave") !== "false";
});
useEffect(() => {
if (!autoSaveEnabled) return;
if (isFirstLocalStorageSave.current) {
isFirstLocalStorageSave.current = false;
return;
}
const timer = setTimeout(() => {
localStorage.setItem("workspace_documents", JSON.stringify(documents));
const now = new Date();
const formattedTime = now.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
setToast({
show: true,
message: "All document progress autosaved locally",
timestamp: formattedTime,
});
}, 1500);
return () => clearTimeout(timer);
}, [documents, autoSaveEnabled]);
// Toast dismiss
useEffect(() => {
if (toast.show) {
const timer = setTimeout(() => {
setToast((prev) => ({ ...prev, show: false }));
}, 3000);
return () => clearTimeout(timer);
}
}, [toast.show]);
// Beforeunload save
useEffect(() => {
const handleBeforeUnload = () => {
if (autoSaveEnabled) {
localStorage.setItem("workspace_documents", JSON.stringify(documents));
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [documents, autoSaveEnabled]);
// Save credentials
useEffect(() => {
localStorage.setItem("workspace_user", JSON.stringify(user));
}, [user]);
// Helpers
const addLog = (text: string, type: "success" | "info" | "sync" | "error") => {
setSyncLogs((prev) => [
{
id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
text,
timestamp: new Date().toISOString(),
type,
},
...prev.slice(0, 24),
]);
};
const handleManualSave = () => {
localStorage.setItem("workspace_documents", JSON.stringify(documents));
const now = new Date();
const formattedTime = now.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
setToast({
show: true,
message: "Document progress manually saved locally",
timestamp: formattedTime,
});
addLog("Document progress manually saved locally.", "success");
};
return {
documents,
setDocuments,
activeDocId,
setActiveDocId,
activeDoc,
editorText,
setEditorText,
isOnline,
syncStatus,
setSyncStatus,
syncLogs,
user,
setUser,
teamMembers,
setTeamMembers,
teamChangesEnabled,
setTeamChangesEnabled,
activeCollaborators,
setActiveCollaborators,
toast,
setToast,
showDocModal,
setShowDocModal,
newDocTitle,
setNewDocTitle,
documentSearchTerm,
setDocumentSearchTerm,
uploadFile,
setUploadFile,
uploadLoading,
setUploadLoading,
uploadError,
setUploadError,
adminPassword,
setAdminPassword,
isAdminLoggedIn,
setIsAdminLoggedIn,
adminVerifyLoading,
tokensConsumed,
setTokensConsumed,
showVersionHistory,
setShowVersionHistory,
isReadingMode,
setIsReadingMode,
showExportPreview,
setShowExportPreview,
exportPreviewFormat,
setExportPreviewFormat,
showPreferenceModal,
setShowPreferenceModal,
showShareModal,
setShowShareModal,
isSidebarOpen,
setIsSidebarOpen,
copiedDoc,
setCopiedDoc,
showStatsPopover,
setShowStatsPopover,
leftPanelMode,
setLeftPanelMode,
foldedBlockIds,
setFoldedBlockIds,
autoSaveEnabled,
setAutoSaveEnabled,
saveTimeoutRef,
isFirstLocalStorageSave,
addLog,
handleManualSave,
};
}