Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/web/components/credentials/SignInForm.tsx
blob: a64f777982931a3488abdc91a993571b0abf8fa6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

"use client";

import { type FormEvent, useId, useState } from "react";
import { localReturnUrl } from "@/lib/credentials";
import { getCredentialsMessages } from "@/lib/credentials-messages";
import { getMessages, type Locale } from "@/lib/localization";

interface SignInFormProps {
  locale: Locale;
  returnUrl: string;
}

/**
 * Signs in to Progmasoft OAuth. The cookie belongs to the OAuth host only; the
 * account holder then goes back to the authorization request that sent them.
 */
export function SignInForm({ locale, returnUrl }: SignInFormProps) {
  const { signIn, common } = getCredentialsMessages(locale);
  const { auth } = getMessages(locale);
  const formId = useId();
  const [pending, setPending] = useState(false);
  const [message, setMessage] = useState<string | null>(null);
  const [showPassword, setShowPassword] = useState(false);

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setMessage(null);
    setPending(true);
    const form = new FormData(event.currentTarget);
    const response = await fetch("/api/v1/oauth/signin", {
      method: "POST",
      credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: String(form.get("email") ?? ""),
        password: String(form.get("password") ?? ""),
      }),
    }).catch(() => null);

    if (response?.ok) {
      // A full navigation, so that /connect/authorize on the API receives the new cookie.
      window.location.assign(localReturnUrl(returnUrl));
      return;
    }

    setPending(false);
    if (response?.status === 401) {
      // One message for every refusal: it does not tell registered addresses apart.
      setMessage(signIn.failed);
    } else {
      setMessage(common.unreachable);
    }
  }

  return (
    <form
      className="auth-form"
      onSubmit={submit}
      aria-describedby={message ? `${formId}-message` : undefined}
    >
      <div className="form-heading">
        <h2>{signIn.title}</h2>
      </div>

      {message && (
        <div
          className="form-message form-message-error"
          id={`${formId}-message`}
          role="alert"
        >
          {message}
        </div>
      )}

      <div className="field-group">
        <label htmlFor={`${formId}-email`}>{signIn.email}</label>
        <input
          id={`${formId}-email`}
          name="email"
          type="email"
          autoComplete="username"
          inputMode="email"
          maxLength={254}
          required
        />
      </div>

      <div className="field-group">
        <label htmlFor={`${formId}-password`}>{signIn.password}</label>
        <div className="password-field">
          <input
            id={`${formId}-password`}
            name="password"
            type={showPassword ? "text" : "password"}
            autoComplete="current-password"
            maxLength={256}
            required
          />
          <button
            type="button"
            className="password-toggle"
            onClick={() => setShowPassword((visible) => !visible)}
          >
            {showPassword ? auth.hide : auth.show}
          </button>
        </div>
      </div>

      <button className="button auth-submit" type="submit" disabled={pending}>
        {pending ? signIn.working : signIn.submit}
      </button>

      <p className="form-switch">
        {signIn.noAccount}{" "}
        <a href="https://account.progmasoft.com/register">
          {signIn.createAccount}
        </a>
      </p>
    </form>
  );
}