page.tsx 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. "use client";
  2. import { useState } from "react";
  3. import { useMutation } from "@tanstack/react-query";
  4. import { RECOVERY_CODE_LENGTH } from "@/lib/constants";
  5. export default function RecoverPage() {
  6. const [code, setCode] = useState("");
  7. const claimMutation = useMutation({
  8. mutationFn: async (recoveryCode: string) => {
  9. const res = await fetch("/api/auth/recovery/claim", {
  10. method: "POST",
  11. headers: { "Content-Type": "application/json" },
  12. body: JSON.stringify({ code: recoveryCode }),
  13. });
  14. const data = (await res.json()) as {
  15. ok?: boolean;
  16. error?: string;
  17. };
  18. if (!res.ok) {
  19. throw new Error(data.error || "Recovery failed");
  20. }
  21. return data as { ok: true };
  22. },
  23. onSuccess: () => {
  24. // Full document load so the new auth cookie set by the server is sent
  25. // with the next request (router.push() keeps the SPA navigation in the
  26. // existing fetch context and may race the cookie write).
  27. window.location.assign("/");
  28. },
  29. });
  30. function handleSubmit(e: React.FormEvent) {
  31. e.preventDefault();
  32. const trimmed = code.trim();
  33. if (trimmed.length === 0) return;
  34. claimMutation.mutate(trimmed);
  35. }
  36. return (
  37. <>
  38. <h1 className="mb-6 text-2xl font-bold text-center">Recover Your Account</h1>
  39. <form onSubmit={handleSubmit} className="space-y-4">
  40. <div>
  41. <label htmlFor="recovery-code" className="block text-sm font-medium mb-1">
  42. Recovery code
  43. </label>
  44. <input
  45. id="recovery-code"
  46. type="text"
  47. value={code}
  48. onChange={(e) => setCode(e.target.value)}
  49. maxLength={RECOVERY_CODE_LENGTH}
  50. placeholder="Enter your 24-character code"
  51. className="w-full rounded-lg border border-foreground/20 bg-background px-3 py-2 font-mono text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-blue-500"
  52. autoFocus
  53. autoComplete="off"
  54. spellCheck={false}
  55. />
  56. </div>
  57. {claimMutation.error && (
  58. <p className="text-sm text-red-500" role="alert">
  59. {claimMutation.error instanceof Error
  60. ? claimMutation.error.message
  61. : "Something went wrong."}
  62. </p>
  63. )}
  64. <button
  65. type="submit"
  66. disabled={claimMutation.isPending || code.trim().length === 0}
  67. className="w-full rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
  68. >
  69. {claimMutation.isPending ? "Recovering..." : "Recover account"}
  70. </button>
  71. </form>
  72. <p className="mt-6 text-center text-sm text-foreground/50">
  73. Don&apos;t have an account?{" "}
  74. <a href="/login" className="text-blue-500 hover:underline">
  75. Create one
  76. </a>
  77. </p>
  78. </>
  79. );
  80. }