chore: removed passkeys fe420d3c
Steve · 2025-11-21 19:38 3 file(s) · +6 −252
src/App.tsx +2 −71
1 1
import Dashboard from "./components/dashboard";
2 2
import { useQuery } from "@evolu/react";
3 -
import {
4 -
	allFeedsQuery,
5 -
	localAuth,
6 -
	service,
7 -
	useEvolu,
8 -
	ownerIds,
9 -
} from "@/lib/evolu";
3 +
import { allFeedsQuery, useEvolu } from "@/lib/evolu";
10 4
import { Button } from "@/components/ui/button";
11 5
import { Input } from "@/components/ui/input";
12 6
import * as React from "react";
31 25
	DialogHeader,
32 26
	DialogTitle,
33 27
} from "@/components/ui/dialog";
34 -
import { Upload, FileUp, Info, LogIn, Key } from "lucide-react";
28 +
import { Upload, FileUp, Info } from "lucide-react";
35 29
import * as Evolu from "@evolu/common";
36 30
import { LoadingScreen } from "@/components/loading-screen";
37 31
import { AboutDialog } from "@/components/about-dialog";
48 42
	const [isAddingFeed, setIsAddingFeed] = React.useState(false);
49 43
	const [errorMessage, setErrorMessage] = React.useState("");
50 44
	const [isRestoreDialogOpen, setIsRestoreDialogOpen] = React.useState(false);
51 -
	const [isPasskeyDialogOpen, setIsPasskeyDialogOpen] = React.useState(false);
52 45
	const [restoreMnemonic, setRestoreMnemonic] = React.useState("");
53 46
	const [isImportingOPML, setIsImportingOPML] = React.useState(false);
54 47
	const fileInputRef = React.useRef<HTMLInputElement>(null);
55 48
56 49
	const evolu = useEvolu();
57 -
58 -
	// Filter available passkeys (if any exist)
59 -
	const availablePasskeys = React.useMemo(() => ownerIds, []);
60 50
61 51
	// Handle initial loading state
62 52
	React.useEffect(() => {
95 85
			setRestoreMnemonic("");
96 86
			toast.success("Account restored successfully");
97 87
		}
98 -
	}
99 -
100 -
	async function handleLoginWithPasskey(ownerId: Evolu.OwnerId) {
101 -
		const result = await localAuth.login(ownerId, { service });
102 -
		if (result) {
103 -
			evolu.reloadApp();
104 -
		} else {
105 -
			toast.error("Failed to login with passkey");
106 -
		}
107 -
	}
108 -
109 -
	function openPasskeyDialog() {
110 -
		if (availablePasskeys.length === 0) {
111 -
			toast.error("No passkeys found on this device");
112 -
			return;
113 -
		}
114 -
		setIsPasskeyDialogOpen(true);
115 88
	}
116 89
117 90
	async function handleImportOPML(file: File) {
410 383
							<Upload className="h-4 w-4 mr-2" />
411 384
							Restore from Backup
412 385
						</Button>
413 -
						{availablePasskeys.length > 0 && (
414 -
							<Button
415 -
								variant="outline"
416 -
								onClick={openPasskeyDialog}
417 -
								className="w-full"
418 -
							>
419 -
								<Key className="h-4 w-4 mr-2" />
420 -
								Login with Passkey
421 -
							</Button>
422 -
						)}
423 386
					</div>
424 387
				</div>
425 388
			)}
450 413
							<Upload className="h-4 w-4 mr-2" />
451 414
							Restore Account
452 415
						</Button>
453 -
					</div>
454 -
				</DialogContent>
455 -
			</Dialog>
456 -
			<Dialog open={isPasskeyDialogOpen} onOpenChange={setIsPasskeyDialogOpen}>
457 -
				<DialogContent>
458 -
					<DialogHeader>
459 -
						<DialogTitle>Login with Passkey</DialogTitle>
460 -
						<DialogDescription>
461 -
							Select a passkey to authenticate and access your encrypted data.
462 -
						</DialogDescription>
463 -
					</DialogHeader>
464 -
					<div className="space-y-3">
465 -
						{availablePasskeys.map(({ ownerId, username }) => (
466 -
							<div
467 -
								key={ownerId}
468 -
								className="flex items-center justify-between p-3 bg-muted rounded-lg"
469 -
							>
470 -
								<div className="flex flex-col">
471 -
									<span className="text-sm font-medium">{username}</span>
472 -
									<span className="text-xs text-muted-foreground truncate max-w-[200px]">
473 -
										{ownerId}
474 -
									</span>
475 -
								</div>
476 -
								<Button
477 -
									size="sm"
478 -
									onClick={() => handleLoginWithPasskey(ownerId)}
479 -
								>
480 -
									<LogIn className="h-3 w-3 mr-1" />
481 -
									Login
482 -
								</Button>
483 -
							</div>
484 -
						))}
485 416
					</div>
486 417
				</DialogContent>
487 418
			</Dialog>
src/components/nav-user.tsx +2 −164
8 8
	Upload,
9 9
	Download,
10 10
	FileUp,
11 -
	Key,
12 -
	LogIn,
13 11
} from "lucide-react";
14 12
import {
15 13
	Dialog,
20 18
	DialogTrigger,
21 19
} from "@/components/ui/dialog";
22 20
import { Button } from "@/components/ui/button";
23 -
import { use, useState, useRef, useMemo } from "react";
24 -
import {
25 -
	useEvolu,
26 -
	reset,
27 -
	allFeedsQuery,
28 -
	localAuth,
29 -
	service,
30 -
	ownerIds,
31 -
	authResult,
32 -
} from "@/lib/evolu";
21 +
import { use, useState, useRef } from "react";
22 +
import { useEvolu, reset, allFeedsQuery } from "@/lib/evolu";
33 23
import { useQuery } from "@evolu/react";
34 24
import { generateOPML, parseOPML, downloadOPML } from "@/lib/opml";
35 25
import {
65 55
	const [isRestoreDialogOpen, setIsRestoreDialogOpen] = useState(false);
66 56
	const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
67 57
	const [isImportOPMLDialogOpen, setIsImportOPMLDialogOpen] = useState(false);
68 -
	const [isPasskeyDialogOpen, setIsPasskeyDialogOpen] = useState(false);
69 58
	const [backupPhrase, setBackupPhrase] = useState<Evolu.Mnemonic | null>();
70 59
	const [isRevealed, setIsRevealed] = useState(false);
71 60
	const [isCopied, setIsCopied] = useState(false);
86 75
	const evolu = useEvolu();
87 76
	const owner = use(evolu.appOwner);
88 77
	const feeds = useQuery(allFeedsQuery);
89 -
90 -
	// Get other registered passkey profiles
91 -
	const otherOwnerIds = useMemo(
92 -
		() => ownerIds.filter(({ ownerId }) => ownerId !== owner?.id),
93 -
		[owner?.id],
94 -
	);
95 78
96 79
	function backup() {
97 80
		setBackupPhrase(owner?.mnemonic);
98 81
	}
99 82
100 -
	// Passkey registration
101 -
	async function handleRegisterPasskey() {
102 -
		const username = window.prompt("Enter your username for passkey:");
103 -
		if (username == null) return;
104 -
105 -
		// Determine if this is a guest login or a new owner
106 -
		const isGuest = !Boolean(authResult?.owner);
107 -
108 -
		// Register the guest owner or create a new one if already registered
109 -
		const result = await localAuth.register(username, {
110 -
			service: service,
111 -
			mnemonic: isGuest ? owner?.mnemonic : undefined,
112 -
		});
113 -
114 -
		if (result) {
115 -
			// If this is a guest owner, clear the database and reload
116 -
			// The owner is transferred to a new database on next login
117 -
			if (isGuest) {
118 -
				void evolu.resetAppOwner({ reload: true });
119 -
			} else {
120 -
				// Otherwise, just reload the page
121 -
				evolu.reloadApp();
122 -
			}
123 -
		} else {
124 -
			alert(
125 -
				"Failed to register passkey. Make sure your device supports passkeys.",
126 -
			);
127 -
		}
128 -
	}
129 -
130 -
	// Passkey login
131 -
	async function handleLoginWithPasskey(ownerId: Evolu.OwnerId) {
132 -
		const result = await localAuth.login(ownerId, { service });
133 -
		if (result) {
134 -
			evolu.reloadApp();
135 -
		} else {
136 -
			alert("Failed to login with passkey");
137 -
		}
138 -
	}
139 -
140 -
	// Clear all passkeys and data
141 -
	async function handleClearAllPasskeys() {
142 -
		const confirmed = window.confirm(
143 -
			"Are you sure you want to clear all passkeys and data? This cannot be undone.",
144 -
		);
145 -
		if (!confirmed) return;
146 -
147 -
		await localAuth.clearAll({ service });
148 -
		void evolu.resetAppOwner({ reload: true });
149 -
	}
150 -
151 83
	async function handleExportOPML() {
152 84
		try {
153 85
			const opmlContent = generateOPML(feeds);
318 250
								</DropdownMenuGroup>
319 251
								<DropdownMenuSeparator />
320 252
								<DropdownMenuGroup>
321 -
									<DropdownMenuItem
322 -
										onClick={() => setIsPasskeyDialogOpen(true)}
323 -
									>
324 -
										<Key />
325 -
										Passkeys
326 -
									</DropdownMenuItem>
327 -
								</DropdownMenuGroup>
328 -
								<DropdownMenuSeparator />
329 -
								<DropdownMenuGroup>
330 253
									<DialogTrigger asChild>
331 254
										<DropdownMenuItem>
332 255
											<BookKey />
472 395
								</p>
473 396
							</>
474 397
						)}
475 -
					</div>
476 -
				</DialogContent>
477 -
			</Dialog>
478 -
			<Dialog open={isPasskeyDialogOpen} onOpenChange={setIsPasskeyDialogOpen}>
479 -
				<DialogContent>
480 -
					<DialogHeader>
481 -
						<DialogTitle>Passkey Management</DialogTitle>
482 -
						<DialogDescription>
483 -
							Register a passkey to securely access your account across devices
484 -
							without entering a mnemonic. Your device's biometric
485 -
							authentication (fingerprint, face ID, etc.) will protect your
486 -
							data.
487 -
						</DialogDescription>
488 -
					</DialogHeader>
489 -
					<div className="space-y-4">
490 -
						{owner && (
491 -
							<div className="p-3 bg-muted rounded-lg">
492 -
								<p className="text-xs font-medium text-muted-foreground mb-1">
493 -
									Current Account
494 -
								</p>
495 -
								<p className="text-sm font-medium">
496 -
									{authResult?.username ?? "Guest"}
497 -
								</p>
498 -
								<p className="text-xs text-muted-foreground mt-1">{owner.id}</p>
499 -
							</div>
500 -
						)}
501 -
502 -
						<div className="flex gap-2">
503 -
							<Button
504 -
								onClick={handleRegisterPasskey}
505 -
								className="flex-1"
506 -
								variant="default"
507 -
							>
508 -
								<Key className="h-4 w-4 mr-2" />
509 -
								Register Passkey
510 -
							</Button>
511 -
							<Button
512 -
								onClick={handleClearAllPasskeys}
513 -
								className="flex-1"
514 -
								variant="destructive"
515 -
							>
516 -
								<Trash2 className="h-4 w-4 mr-2" />
517 -
								Clear All
518 -
							</Button>
519 -
						</div>
520 -
521 -
						{otherOwnerIds.length > 0 && (
522 -
							<>
523 -
								<div className="border-t pt-4">
524 -
									<p className="text-sm font-medium mb-3">
525 -
										Other Registered Passkeys
526 -
									</p>
527 -
									<div className="space-y-2">
528 -
										{otherOwnerIds.map(({ ownerId, username }) => (
529 -
											<div
530 -
												key={ownerId}
531 -
												className="flex items-center justify-between p-3 bg-muted rounded-lg"
532 -
											>
533 -
												<div className="flex flex-col">
534 -
													<span className="text-sm font-medium">
535 -
														{username}
536 -
													</span>
537 -
													<span className="text-xs text-muted-foreground">
538 -
														{ownerId}
539 -
													</span>
540 -
												</div>
541 -
												<Button
542 -
													size="sm"
543 -
													variant="outline"
544 -
													onClick={() => handleLoginWithPasskey(ownerId)}
545 -
												>
546 -
													<LogIn className="h-3 w-3 mr-1" />
547 -
													Login
548 -
												</Button>
549 -
											</div>
550 -
										))}
551 -
									</div>
552 -
								</div>
553 -
							</>
554 -
						)}
555 -
556 -
						<p className="text-xs text-muted-foreground">
557 -
							💡 Passkeys use your device's secure enclave for authentication.
558 -
							You can register multiple passkeys for different devices or users.
559 -
						</p>
560 398
					</div>
561 399
				</DialogContent>
562 400
			</Dialog>
src/lib/evolu.ts +2 −17
1 1
import * as Evolu from "@evolu/common";
2 2
import { createUseEvolu } from "@evolu/react";
3 -
import { evoluReactWebDeps, localAuth } from "@evolu/react-web";
3 +
import { evoluReactWebDeps } from "@evolu/react-web";
4 4
import { Schema, type RSSFeedId } from "./scheme.ts";
5 -
6 -
// Namespace for the current app (scopes databases, passkeys, etc.)
7 -
const service = "alcove";
8 -
9 -
// Get authentication profiles and initialize owner
10 -
// This is a top-level await for simplicity - in production you may want to handle this differently
11 -
const ownerIds = await localAuth.getProfiles({ service });
12 -
const authResult = await localAuth.getOwner({ service });
13 5
14 6
// Create Evolu instance for the React web platform
15 7
export const evolu = Evolu.createEvolu(evoluReactWebDeps)(Schema, {
16 -
	name: Evolu.SimpleName.orThrow(
17 -
		`${service}-${authResult?.owner?.id ?? "guest"}`,
18 -
	),
8 +
	name: Evolu.SimpleName.orThrow("alcove"),
19 9
	reloadUrl: "/",
20 -
	encryptionKey: authResult?.owner?.encryptionKey,
21 -
	externalAppOwner: authResult?.owner,
22 10
	transports: [
23 11
		// {
24 12
		// 	type: "WebSocket",
31 19
		{ type: "WebSocket" as const, url: "ws://localhost:4000" },
32 20
	],
33 21
});
34 -
35 -
// Export authentication utilities and owner info
36 -
export { localAuth, service, ownerIds, authResult };
37 22
38 23
export const useEvolu = createUseEvolu(evolu);
39 24