Skip to content

Commit

Permalink
use component for center
Browse files Browse the repository at this point in the history
  • Loading branch information
Hoishin committed Feb 3, 2024
1 parent 54f248c commit dcf340e
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 105 deletions.
17 changes: 17 additions & 0 deletions src/app/components/center-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { PropsWithChildren } from "react";
import { css } from "../../../styled-system/css";

export const CenterLayout = ({
children,
className,
}: PropsWithChildren<{ className?: string }>) => {
return (
<div
className={
css({ display: "grid", placeContent: "start center" }) + " " + className
}
>
{children}
</div>
);
};
19 changes: 12 additions & 7 deletions src/app/cookie.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cookie from "cookie";
import cookie, { type CookieSerializeOptions } from "cookie";
import cookieSignature from "cookie-signature";
import {
ACTIVITY_COOKIE_NAME,
Expand Down Expand Up @@ -48,14 +48,19 @@ export const getSession = async (request: Request) => {
return session;
};

export const serializeCookie = (
name: string,
value: string,
options?: CookieSerializeOptions,
) => {
const signedValue = cookieSignature.sign(value, env.SESSION_COOKIE_SECRET);
return cookie.serialize(name, signedValue, options);
};

export const serializeSessionToken = (sessionToken: string) => {
const signedSessionToken = cookieSignature.sign(
sessionToken,
env.SESSION_COOKIE_SECRET,
);
return cookie.serialize(
return serializeCookie(
SESSION_COOKIE_NAME,
signedSessionToken,
sessionToken,
sessionCookieOptions,
);
};
Expand Down
10 changes: 0 additions & 10 deletions src/app/routes/_base._center.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,45 @@ import { prisma } from "../../shared/prisma.server.js";
import { sendEmail } from "../../server/email.js";
import { createToken } from "../../shared/create-token.js";
import { env } from "../../shared/env.server.js";
import { CenterLayout } from "../components/center-layout.js";

export const loader = async ({ request }: LoaderFunctionArgs) => {
await assertNoSession(request);
return null;
};

export default function Register() {
export default () => {
const data = useActionData<typeof action>();

if (typeof data !== "undefined") {
return <div>確認メールを送信しました</div>;
}

return (
<Form
method="post"
className={css({
display: "grid",
justifyItems: "end",
gap: "4px",
})}
>
<label className={css({ width: "250px" })}>
<Text>メールアドレス</Text>
<TextField.Input
name="email"
type="email"
inputMode="email"
autoComplete="email"
required
/>
</label>
<Button type="submit">登録</Button>
</Form>
<CenterLayout>
{typeof data === "undefined" ? (
<Form
method="post"
className={css({
display: "grid",
justifyItems: "end",
gap: "4px",
})}
>
<label className={css({ width: "250px" })}>
<Text>メールアドレス</Text>
<TextField.Input
name="email"
type="email"
inputMode="email"
autoComplete="email"
required
/>
</label>
<Button type="submit">登録</Button>
</Form>
) : (
<Text>確認メールを送信しました</Text>
)}
</CenterLayout>
);
}
};

export const ErrorBoundary = () => {
return <Text color="red">エラーが発生しました</Text>;
Expand Down
11 changes: 3 additions & 8 deletions src/app/routes/_base.sign-in-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { trpc } from "../trpc";
import { startRegistration } from "@simplewebauthn/browser";
import { CenterLayout } from "../components/center-layout";

export const loader = async ({ request }: LoaderFunctionArgs) => {
const session = await assertSession(request);
Expand Down Expand Up @@ -43,13 +44,7 @@ export default function FinishRegistration() {
trpc.authentication.passkey.registration.verify.useMutation();

return (
<div
className={css({
display: "grid",
alignContent: "start",
justifyContent: "center",
})}
>
<CenterLayout>
<dl
className={css({
"& > dd": { margin: "0 0 16px 16px" },
Expand Down Expand Up @@ -142,6 +137,6 @@ export default function FinishRegistration() {
</Button>
</dd>
</dl>
</div>
</CenterLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,53 @@ import {
} from "@remix-run/react";
import { assertNoSession } from "../session.server.js";
import { startAuthentication } from "@simplewebauthn/browser";
import { CenterLayout } from "../components/center-layout.js";
import { generateAuthenticationOptions } from "@simplewebauthn/server";
import { env } from "../../shared/env.server.js";
import { PASSKEY_CHALLENGE_COOKIE_NAME } from "../../shared/constants.server.js";
import { serializeCookie } from "../cookie.server.js";

export const loader = async ({ request }: LoaderFunctionArgs) => {
await assertNoSession(request);

const { url, setCookie } = createDiscordOauthUrl();
const { url: discordOauthUrl, setCookie: discordOauthStateCookie } =
createDiscordOauthUrl();

const passkeyOptions = await generateAuthenticationOptions({
rpID: env.PASSKEY_RP_ID,
userVerification: "preferred",
});

return json(
{ discordOauthUrl: url },
{ headers: [["Set-Cookie", setCookie]] },
{ discordOauthUrl, passkeyOptions },
{
headers: [
["Set-Cookie", discordOauthStateCookie],
[
"Set-Cookie",
serializeCookie(
PASSKEY_CHALLENGE_COOKIE_NAME,
passkeyOptions.challenge,
),
],
],
},
);
};

export default function SignIn() {
const data = useLoaderData<typeof loader>();
const { mutateAsync: initializePasskeyAuthentication } =
trpc.authentication.passkey.authentication.initialize.useMutation();
const { mutateAsync: verifyPasskeyAuthentication } =
trpc.authentication.passkey.authentication.verify.useMutation();
const navigate = useNavigate();

const revalidator = useRevalidator();

return (
<div
className={css({
display: "grid",
alignContent: "start",
justifyContent: "center",
gap: "8px",
})}
>
<CenterLayout className={css({ gap: "8px" })}>
<Button
onClick={() => {
initializePasskeyAuthentication()
.then((options) => {
return startAuthentication(options);
})
startAuthentication(data.passkeyOptions)
.then((response) => {
return verifyPasskeyAuthentication(response);
})
Expand All @@ -69,6 +79,6 @@ export default function SignIn() {
<Button asChild>
<Link to="email">Eメールでログイン</Link>
</Button>
</div>
</CenterLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { sendEmail } from "../../server/email";
import { createToken } from "../../shared/create-token";
import { env } from "../../shared/env.server";
import { assertNoSession } from "../session.server";
import { CenterLayout } from "../components/center-layout";

export const loader = async ({ request }: LoaderFunctionArgs) => {
await assertNoSession(request);
Expand All @@ -20,33 +21,37 @@ export default () => {

if (typeof data !== "undefined") {
return (
<Text>
メールを送信しました。メールに記載されたリンクをクリックしてください。
</Text>
<CenterLayout>
<Text>
メールを送信しました。メールに記載されたリンクをクリックしてください。
</Text>
</CenterLayout>
);
}

return (
<Form
method="post"
className={css({
display: "grid",
justifyItems: "end",
gap: "4px",
})}
>
<label className={css({ width: "250px" })}>
<Text>メールアドレス</Text>
<TextField.Input
type="email"
name="email"
inputMode="email"
autoComplete="email"
required
/>
</label>
<Button type="submit">ログイン</Button>
</Form>
<CenterLayout>
<Form
method="post"
className={css({
display: "grid",
justifyItems: "end",
gap: "4px",
})}
>
<label className={css({ width: "250px" })}>
<Text>メールアドレス</Text>
<TextField.Input
type="email"
name="email"
inputMode="email"
autoComplete="email"
required
/>
</label>
<Button type="submit">ログイン</Button>
</Form>
</CenterLayout>
);
};

Expand Down
17 changes: 4 additions & 13 deletions src/server/routes/authentication/passkey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
generateAuthenticationOptions,
generateRegistrationOptions,
verifyAuthenticationResponse,
verifyRegistrationResponse,
Expand All @@ -15,6 +14,7 @@ import { prisma } from "../../../shared/prisma.server";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { createToken } from "../../../shared/create-token";
import { PASSKEY_CHALLENGE_COOKIE_NAME } from "../../../shared/constants.server";

const verifyRegistrationResponseSchema = z.object({
id: z.string(),
Expand Down Expand Up @@ -145,22 +145,13 @@ const passkeyRegisterationRouter = router({
}),
});

const CHALLENGE_COOKIE_NAME = "passkey_challenge";

const passkeyAuthenticationRouter = router({
initialize: publicProcedure.mutation(async ({ ctx }) => {
const options = await generateAuthenticationOptions({
rpID: env.PASSKEY_RP_ID,
userVerification: "preferred",
});
ctx.setCookie(CHALLENGE_COOKIE_NAME, options.challenge, { maxAge: 5 * 60 });
return options;
}),

verify: publicProcedure
.input(verifyAuthenticationResponseSchema)
.mutation(async ({ ctx, input }) => {
const expectedChallenge = ctx.readSignedCookie(CHALLENGE_COOKIE_NAME);
const expectedChallenge = ctx.readSignedCookie(
PASSKEY_CHALLENGE_COOKIE_NAME,
);
if (!expectedChallenge) {
throw new TRPCError({ code: "BAD_REQUEST", message: "no challenge" });
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/constants.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const DISCORD_API = "https://discord.com/api/v10";
export const VERIFY_TOKEN_DURATION = 3 * 60 * 1000;

export const SIGN_IN_REDIRECT_COOKIE_NAME = "sign_in_redirect";

export const PASSKEY_CHALLENGE_COOKIE_NAME = "passkey_challenge";

0 comments on commit dcf340e

Please sign in to comment.