diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-08-11 15:32:47 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-08-11 15:46:48 +0900 |
| commit | 7089515e47fb6f7e9aa3db4bbbac2d3300ff0048 (patch) | |
| tree | f042d13143bc7a6db83297c88f2861259f573935 | |
| parent | 97fdb23b7a1b75001a2ca53ea5ec76c52c57dde3 (diff) | |
| download | phperkaigi-2025-albatross-7089515e47fb6f7e9aa3db4bbbac2d3300ff0048.tar.gz phperkaigi-2025-albatross-7089515e47fb6f7e9aa3db4bbbac2d3300ff0048.tar.zst phperkaigi-2025-albatross-7089515e47fb6f7e9aa3db4bbbac2d3300ff0048.zip | |
feat(frontend): improve error handling of login form
| -rw-r--r-- | backend/api/handler.go | 14 | ||||
| -rw-r--r-- | frontend/app/.server/auth.ts | 5 | ||||
| -rw-r--r-- | frontend/app/components/BorderedContainer.tsx | 13 | ||||
| -rw-r--r-- | frontend/app/components/InputText.tsx | 12 | ||||
| -rw-r--r-- | frontend/app/components/SubmitButton.tsx | 12 | ||||
| -rw-r--r-- | frontend/app/routes/_index.tsx | 25 | ||||
| -rw-r--r-- | frontend/app/routes/login.tsx | 150 |
7 files changed, 167 insertions, 64 deletions
diff --git a/backend/api/handler.go b/backend/api/handler.go index 134a4f9..5b53791 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -31,9 +31,19 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) userID, err := auth.Login(ctx, h.q, username, password, registrationToken) if err != nil { log.Printf("login failed: %v", err) + var msg string + if errors.Is(err, auth.ErrInvalidRegistrationToken) { + msg = "登録用 URL が無効です。イベントスタッフにお声がけください" + } else if errors.Is(err, auth.ErrNoRegistrationToken) { + msg = "登録用 URL からログインしてください。登録用 URL は Connpass のイベントページに記載しています" + } else if errors.Is(err, auth.ErrForteeLoginTimeout) { + msg = "ログインに失敗しました" + } else { + msg = "ユーザー名またはパスワードが誤っています" + } return PostLogin401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Invalid username or password", + Message: msg, }, }, nil } @@ -42,7 +52,7 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) if err != nil { return PostLogin401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Invalid username or password", + Message: "ログインに失敗しました", }, }, nil } diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index 943f424..4df0924 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -32,9 +32,12 @@ export type User = components["schemas"]["User"]; // Remix's createCookie() returns "structured" cookies, which cannot be reused directly by non-Remix servers. const tokenCookie = createUnstructuredCookie("albatross_token", cookieOptions); +/** + * @throws Error on failure + */ export async function login(request: Request): Promise<never> { const jwt = await authenticator.authenticate("default", request, { - failureRedirect: request.url, + throwOnError: true, }); const session = await sessionStorage.getSession( diff --git a/frontend/app/components/BorderedContainer.tsx b/frontend/app/components/BorderedContainer.tsx new file mode 100644 index 0000000..cbbfbde --- /dev/null +++ b/frontend/app/components/BorderedContainer.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +type Props = { + children: React.ReactNode; +}; + +export default function BorderedContainer({ children }: Props) { + return ( + <div className="bg-white border-2 border-pink-600 rounded-xl p-4"> + {children} + </div> + ); +} diff --git a/frontend/app/components/InputText.tsx b/frontend/app/components/InputText.tsx new file mode 100644 index 0000000..3f2c526 --- /dev/null +++ b/frontend/app/components/InputText.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +type InputProps = React.InputHTMLAttributes<HTMLInputElement>; + +export default function InputText(props: InputProps) { + return ( + <input + {...props} + className="p-2 block w-full border border-pink-600 rounded-md transition duration-300 focus:ring focus:ring-pink-400 focus:outline-none" + /> + ); +} diff --git a/frontend/app/components/SubmitButton.tsx b/frontend/app/components/SubmitButton.tsx new file mode 100644 index 0000000..1400a7b --- /dev/null +++ b/frontend/app/components/SubmitButton.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>; + +export default function SubmitButton(props: ButtonProps) { + return ( + <button + {...props} + className="text-lg text-white bg-pink-600 px-4 py-2 rounded transition duration-300 hover:bg-pink-500 focus:ring focus:ring-pink-400 focus:outline-none" + /> + ); +} diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index ec87e86..9f9c2bb 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -2,6 +2,7 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link } from "@remix-run/react"; import "@fortawesome/fontawesome-svg-core/styles.css"; import { ensureUserNotLoggedIn } from "../.server/auth"; +import BorderedContainer from "../components/BorderedContainer"; export const meta: MetaFunction = () => [ { title: "iOSDC Japan 2024 Albatross.swift" }, @@ -23,19 +24,21 @@ export default function Index() { <div className="text-center"> <div className="font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400 flex flex-col gap-y-2"> <div className="text-3xl">iOSDC Japan 2024</div> - <div className="text-6xl"> - Swift <wbr /> - Code Battle - </div> + <div className="text-6xl">Swift Code Battle</div> </div> </div> - <p className="text-gray-900 max-w-prose bg-white p-4 rounded-xl border-2 border-pink-600 mx-2"> - Swift コードバトルは指示された動作をする Swift - コードをより短く書けた方が勝ち、という 1 対 1 - の対戦コンテンツです。8/22(木)day0 前夜祭では 8/12 - に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での Swift - コードバトルを実施します。ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! - </p> + <div className="mx-2"> + <BorderedContainer> + <p className="text-gray-900 max-w-prose"> + Swift コードバトルは指示された動作をする Swift + コードをより短く書けた方が勝ち、という 1 対 1 + の対戦コンテンツです。8/22(木)day0 前夜祭では 8/12 + に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での + Swift + コードバトルを実施します。ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! + </p> + </BorderedContainer> + </div> <div> <Link to="/login" diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx index d6414f7..b1249e0 100644 --- a/frontend/app/routes/login.tsx +++ b/frontend/app/routes/login.tsx @@ -3,8 +3,11 @@ import type { LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; -import { Form, useLocation } from "@remix-run/react"; +import { Form, json, useActionData, useLocation } from "@remix-run/react"; import { ensureUserNotLoggedIn, login } from "../.server/auth"; +import BorderedContainer from "../components/BorderedContainer"; +import InputText from "../components/InputText"; +import SubmitButton from "../components/SubmitButton"; export const meta: MetaFunction = () => [ { title: "Login | iOSDC Japan 2024 Albatross.swift" }, @@ -15,7 +18,42 @@ export async function loader({ request }: LoaderFunctionArgs) { } export async function action({ request }: ActionFunctionArgs) { - await login(request); + const formData = await request.clone().formData(); + const username = String(formData.get("username")); + const password = String(formData.get("password")); + if (username === "" || password === "") { + return json( + { + message: "ユーザー名またはパスワードが誤っています", + errors: { + username: + username === "" ? "ユーザー名を入力してください" : undefined, + password: + password === "" ? "パスワードを入力してください" : undefined, + }, + }, + { status: 400 }, + ); + } + + try { + await login(request); + } catch (error) { + if (error instanceof Error) { + return json( + { + message: error.message, + errors: { + username: undefined, + password: undefined, + }, + }, + { status: 400 }, + ); + } else { + throw error; + } + } return null; } @@ -24,56 +62,68 @@ export default function Login() { const searchParams = new URLSearchParams(location.search); const registrationToken = searchParams.get("registration_token"); + const loginErrors = useActionData<typeof action>(); + return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> - <Form - method="post" - className="bg-white p-8 rounded shadow-md w-full max-w-sm" - > - <h2 className="text-2xl font-bold mb-6 text-center">Login</h2> - <div className="mb-4"> - <label - htmlFor="username" - className="block text-sm font-medium text-gray-700" - > - Username - </label> - <input - type="text" - name="username" - id="username" - required - className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" - /> - </div> - <div className="mb-6"> - <label - htmlFor="password" - className="block text-sm font-medium text-gray-700" - > - Password - </label> - <input - type="password" - name="password" - id="password" - autoComplete="current-password" - required - className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" - /> - </div> - <input - type="hidden" - name="registration_token" - value={registrationToken ?? ""} - /> - <button - type="submit" - className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-300" - > - Log In - </button> - </Form> + <div className="mx-2"> + <BorderedContainer> + <Form method="post" className="w-full max-w-sm p-2"> + <h2 className="text-2xl mb-6 text-center"> + fortee アカウントでログイン + </h2> + <p className="text-sm mb-4"> + fortee + のアカウントをお持ちでない場合は、イベントスタッフにお声がけください。 + </p> + {loginErrors?.message && ( + <p className="text-red-500 text-sm mb-4">{loginErrors.message}</p> + )} + <div className="mb-4 flex flex-col gap-1"> + <label + htmlFor="username" + className="block text-sm font-medium text-gray-700" + > + ユーザー名 + </label> + <InputText type="text" name="username" id="username" required /> + {loginErrors?.errors?.username && ( + <p className="text-red-500 text-sm"> + {loginErrors.errors.username} + </p> + )} + </div> + <div className="mb-6 flex flex-col gap-1"> + <label + htmlFor="password" + className="block text-sm font-medium text-gray-700" + > + パスワード + </label> + <InputText + type="password" + name="password" + id="password" + autoComplete="current-password" + required + /> + {loginErrors?.errors?.password && ( + <p className="text-red-500 text-sm"> + {loginErrors.errors.password} + </p> + )} + </div> + <input + type="hidden" + name="registration_token" + value={registrationToken ?? ""} + /> + <div className="flex justify-center"> + <SubmitButton type="submit">ログイン</SubmitButton> + </div> + </Form> + </BorderedContainer> + </div> </div> ); } |
