page.tsx 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. "use client";
  2. /**
  3. * Magic-link landing page.
  4. * Briven emails: {appOrigin}/auth/verify?preAuthSessionId=…&linkCode=…&deviceId=…
  5. */
  6. import Link from "next/link";
  7. import { useEffect, useState } from "react";
  8. import { consumeMagicLink } from "@/lib/magic-link";
  9. export default function MagicLinkVerifyPage() {
  10. const [message, setMessage] = useState("Signing you in…");
  11. const [failed, setFailed] = useState(false);
  12. useEffect(() => {
  13. let cancelled = false;
  14. async function run() {
  15. const params = new URLSearchParams(window.location.search);
  16. const preAuthSessionId = params.get("preAuthSessionId") || "";
  17. const linkCode = params.get("linkCode") || "";
  18. const deviceId = params.get("deviceId") || "";
  19. if (!preAuthSessionId || !linkCode || !deviceId) {
  20. if (!cancelled) {
  21. setFailed(true);
  22. setMessage(
  23. "This sign-in link is incomplete. Go back to sign in and request a new magic link.",
  24. );
  25. }
  26. return;
  27. }
  28. const res = await consumeMagicLink({
  29. preAuthSessionId,
  30. deviceId,
  31. linkCode,
  32. });
  33. if (cancelled) return;
  34. if (res.ok) {
  35. setMessage("Signed in. Taking you to your dashboard…");
  36. window.dispatchEvent(new Event("krypco-auth-change"));
  37. const next =
  38. sessionStorage.getItem("krypco_magic_redirect") ||
  39. params.get("next") ||
  40. "/portal/dashboard";
  41. sessionStorage.removeItem("krypco_magic_redirect");
  42. const safe =
  43. next.startsWith("/") && !next.startsWith("//")
  44. ? next
  45. : "/portal/dashboard";
  46. window.location.replace(safe);
  47. return;
  48. }
  49. setFailed(true);
  50. setMessage(
  51. res.message ||
  52. "This sign-in link is invalid or expired. Request a new one.",
  53. );
  54. }
  55. void run();
  56. return () => {
  57. cancelled = true;
  58. };
  59. }, []);
  60. return (
  61. <main className="flex min-h-screen items-center justify-center bg-background px-4 text-foreground">
  62. <div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
  63. <p
  64. className="text-sm leading-relaxed text-foreground"
  65. role={failed ? "alert" : "status"}
  66. >
  67. {message}
  68. </p>
  69. {failed ? (
  70. <p className="mt-6 text-sm">
  71. <Link
  72. href="/portal/sign-in"
  73. className="text-accent-hover underline-offset-4 hover:underline"
  74. >
  75. Back to sign in
  76. </Link>
  77. </p>
  78. ) : null}
  79. </div>
  80. </main>
  81. );
  82. }