chore: removed passkeys
fe420d3c
3 file(s) · +6 −252
| 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> |
|
| 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> |
|
| 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 | ||