magic-link.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * Magic-link send + consume via first-party FDI proxy.
  3. * Briven emails open: {origin}/auth/verify?preAuthSessionId&linkCode&deviceId
  4. */
  5. import {
  6. getBrivenAuthPublicKey,
  7. getBrivenProjectId,
  8. } from "@/lib/auth-config";
  9. import {
  10. establishPortalSession,
  11. userIdFromEngineBody,
  12. } from "@/lib/portal-session";
  13. function fdiHeaders(): Record<string, string> {
  14. const projectId = getBrivenProjectId() ?? "";
  15. const publicKey = getBrivenAuthPublicKey() ?? "";
  16. const headers: Record<string, string> = {
  17. "content-type": "application/json",
  18. accept: "application/json",
  19. rid: "passwordless",
  20. "fdi-version": "1.19",
  21. "st-auth-mode": "cookie",
  22. };
  23. if (projectId.startsWith("p_")) {
  24. headers["x-briven-project-id"] = projectId;
  25. }
  26. if (publicKey.startsWith("pk_briven_auth_")) {
  27. headers.authorization = `Bearer ${publicKey}`;
  28. }
  29. return headers;
  30. }
  31. export async function sendMagicLinkEmail(input: {
  32. email: string;
  33. /** Where to go after verify succeeds */
  34. redirectTo?: string;
  35. }): Promise<{ ok: true } | { ok: false; message: string }> {
  36. const email = input.email.trim().toLowerCase();
  37. if (!email.includes("@")) {
  38. return { ok: false, message: "Enter a valid email." };
  39. }
  40. if (typeof window !== "undefined" && input.redirectTo) {
  41. sessionStorage.setItem("krypco_magic_redirect", input.redirectTo);
  42. }
  43. const magicLinkBaseUrl =
  44. typeof window !== "undefined"
  45. ? `${window.location.origin}/auth/verify`
  46. : "https://krypco.eu/auth/verify";
  47. try {
  48. const res = await fetch("/api/auth/signinup/code", {
  49. method: "POST",
  50. credentials: "include",
  51. headers: fdiHeaders(),
  52. body: JSON.stringify({
  53. email,
  54. flowType: "MAGIC_LINK",
  55. magicLinkBaseUrl,
  56. }),
  57. });
  58. const data = (await res.json().catch(() => ({}))) as Record<
  59. string,
  60. unknown
  61. >;
  62. if (!res.ok || data.status !== "OK") {
  63. return {
  64. ok: false,
  65. message:
  66. typeof data.message === "string"
  67. ? data.message
  68. : "Could not send magic link.",
  69. };
  70. }
  71. return { ok: true };
  72. } catch (e) {
  73. return {
  74. ok: false,
  75. message: e instanceof Error ? e.message : "network error",
  76. };
  77. }
  78. }
  79. export async function consumeMagicLink(input: {
  80. preAuthSessionId: string;
  81. deviceId: string;
  82. linkCode: string;
  83. }): Promise<
  84. | { ok: true; userId: string }
  85. | { ok: false; message: string }
  86. > {
  87. try {
  88. const res = await fetch("/api/auth/signinup/code/consume", {
  89. method: "POST",
  90. credentials: "include",
  91. headers: fdiHeaders(),
  92. body: JSON.stringify({
  93. preAuthSessionId: input.preAuthSessionId,
  94. deviceId: input.deviceId,
  95. linkCode: input.linkCode,
  96. }),
  97. });
  98. const data = (await res.json().catch(() => ({}))) as Record<
  99. string,
  100. unknown
  101. >;
  102. if (res.ok && data.status === "OK") {
  103. const userId = userIdFromEngineBody(data) || "session";
  104. await establishPortalSession({ userId });
  105. return { ok: true, userId };
  106. }
  107. return {
  108. ok: false,
  109. message:
  110. typeof data.message === "string"
  111. ? data.message
  112. : "This sign-in link is invalid or expired. Request a new one.",
  113. };
  114. } catch (e) {
  115. return {
  116. ok: false,
  117. message: e instanceof Error ? e.message : "network error",
  118. };
  119. }
  120. }