Skip to content

Commit

Permalink
Migrate into Remix (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikuroXina authored Aug 26, 2024
1 parent 199af01 commit e613b3f
Show file tree
Hide file tree
Showing 57 changed files with 758 additions and 4,666 deletions.
8 changes: 0 additions & 8 deletions .eslintrc.yaml

This file was deleted.

1 change: 1 addition & 0 deletions .example.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
COOKIE_SECRET=
DISCORD_CLIENT_SECRET=
TWITTER_CLIENT_SECRET=
GITHUB_CLIENT_SECRET=
13 changes: 4 additions & 9 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,10 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Run lint
run: pnpm run lint
run: bun run lint
- name: Run check
run: pnpm run check
run: bun run check
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# Wrangler CLI
.wrangler/
.dev.vars

22 changes: 22 additions & 0 deletions app/.server/store/association.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from "zod";

const associationLinksSchema = z.array(
z.object({
type: z.union([z.literal("github"), z.literal("twitter")]),
id: z.string(),
name: z.string(),
}),
);
export type AssociationLinks = z.infer<typeof associationLinksSchema>;

export async function getAssociationLinks(
discordId: string,
): Promise<AssociationLinks> {
const associationsRes = await fetch(
`https://members.approvers.dev/members/${discordId}/associations`,
);
const associations = associationLinksSchema.parse(
await associationsRes.json(),
);
return associations;
}
150 changes: 150 additions & 0 deletions app/.server/store/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
createCookieSessionStorage,
createMemorySessionStorage,
} from "@remix-run/cloudflare";
import { Authenticator } from "remix-auth";
import { GitHubStrategy } from "remix-auth-github";
import { OAuth2Strategy } from "remix-auth-oauth2";

import {
DISCORD_CLIENT_ID,
GITHUB_CLIENT_ID,
TWITTER_CLIENT_ID,
} from "./consts";

const urlBase =
process.env.NODE_ENV === "production"
? "https://edit.members.approvers.dev"
: "http://localhost:3000";

export type Member = {
discordToken: string;
discordId: string;
};
export const getAuthenticator = (
cookieSecret: string,
discordClientSecret: string,
) => {
const authenticator = new Authenticator<Member>(
createCookieSessionStorage({
cookie: {
name: "edit.members.approvers.dev",
sameSite: "lax",
path: "/",
httpOnly: true,
secrets: [cookieSecret],
secure: process.env.NODE_ENV === "production",
},
}),
);
authenticator.use(
new OAuth2Strategy(
{
clientId: DISCORD_CLIENT_ID,
clientSecret: discordClientSecret,
authorizationEndpoint: "https://discord.com/oauth2/authorize",
tokenEndpoint: "https://discord.com/api/v10/oauth2/token",
redirectURI: new URL("/redirect", urlBase),
tokenRevocationEndpoint:
"https://discord.com/api/v10/oauth2/token/revoke",
codeChallengeMethod: "S256",
scopes: ["identify", "guilds.members.read"],
authenticateWith: "request_body",
},
async ({ tokens }) => {
const discordMeRes = await fetch(
"https://discord.com/api/v10/users/@me",
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
},
);
const { id: discordId } = await discordMeRes.json<{
id: string;
}>();
return {
discordToken: tokens.access_token,
discordId,
};
},
),
"discord-oauth",
);
return authenticator;
};

export type GitHubAssociation = {
id: string;
name: string;
};
export const getGithubAssocAuthenticator = (githubClientSecret: string) => {
const assocAuthenticator = new Authenticator<GitHubAssociation>(
createMemorySessionStorage(),
);

assocAuthenticator.use(
new GitHubStrategy(
{
clientId: GITHUB_CLIENT_ID,
clientSecret: githubClientSecret,
redirectURI: new URL("/dashboard/redirect-github", urlBase),
},
async ({ profile }) => {
return { id: profile.id, name: profile.displayName };
},
),
"github-oauth",
);
return assocAuthenticator;
};

export type TwitterAssociation = {
id: string;
name: string;
};
export const getTwitterAssocAuthenticator = (twitterClientSecret: string) => {
const assocAuthenticator = new Authenticator<TwitterAssociation>(
createMemorySessionStorage(),
);
assocAuthenticator.use(
new OAuth2Strategy(
{
clientId: TWITTER_CLIENT_ID,
clientSecret: twitterClientSecret,
authorizationEndpoint: "https://twitter.com/i/oauth2/authorize",
tokenEndpoint: "https://api.twitter.com/2/oauth2/token",
redirectURI: new URL("/dashboard/redirect-twitter", urlBase),
scopes: ["tweet.read", "users.read"],
codeChallengeMethod: "S256",
authenticateWith: "http_basic_auth",
},
async ({ tokens }) => {
const params = new URLSearchParams({
"user.fields": "id,username",
});
const meRes = await fetch(
"https://api.twitter.com/2/users/me?" + params,
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
},
);
if (!meRes.ok) {
console.log(await meRes.text());
throw new Error("failed getting twitter account");
}
const json = await meRes.json<{
data: { id: string; username: string };
}>();
const {
data: { id, username: name },
} = json;
return { id, name };
},
),
"twitter-oauth",
);
return assocAuthenticator;
};
File renamed without changes.
23 changes: 23 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "./tailwind.css";

import { Links, Meta, Outlet, Scripts } from "@remix-run/react";

export default function App() {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
29 changes: 29 additions & 0 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ActionFunctionArgs } from "@remix-run/cloudflare";
import { Form } from "@remix-run/react";

import { getAuthenticator } from "../.server/store/auth.js";

export default function Index() {
return (
<main className="flex min-h-screen flex-col items-center justify-center gap-4">
<h1 className="text-xl font-bold">Approvers メンバー情報編集</h1>
<Form method="post">
<button className="bg-indigo-500 text-slate-100 p-4 rounded-2xl">
Discord でログイン
</button>
</Form>
</main>
);
}

export async function action({ request, context }: ActionFunctionArgs) {
const { COOKIE_SECRET, DISCORD_CLIENT_SECRET } = context.cloudflare.env;
return getAuthenticator(COOKIE_SECRET, DISCORD_CLIENT_SECRET).authenticate(
"discord-oauth",
request,
{
successRedirect: "/dashboard",
failureRedirect: "/",
},
);
}
36 changes: 36 additions & 0 deletions app/routes/dashboard.add-github.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type ActionFunctionArgs, redirect } from "@remix-run/cloudflare";

import {
getAuthenticator,
getGithubAssocAuthenticator,
} from "../.server/store/auth";

export async function action({ request, context }: ActionFunctionArgs) {
const { COOKIE_SECRET, DISCORD_CLIENT_SECRET, GITHUB_CLIENT_SECRET } =
context.cloudflare.env;
await getAuthenticator(
COOKIE_SECRET,
DISCORD_CLIENT_SECRET,
).isAuthenticated(request, {
failureRedirect: "/",
});
return getGithubAssocAuthenticator(GITHUB_CLIENT_SECRET).authenticate(
"github-oauth",
request,
{
failureRedirect: "/dashboard",
},
);
}

export async function loader() {
return redirect("/dashboard");
}

export default function Redirect(): JSX.Element {
return (
<main>
<h1>画面遷移中…</h1>
</main>
);
}
36 changes: 36 additions & 0 deletions app/routes/dashboard.add-twitter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type ActionFunctionArgs, redirect } from "@remix-run/cloudflare";

import {
getAuthenticator,
getTwitterAssocAuthenticator,
} from "../.server/store/auth";

export async function action({ request, context }: ActionFunctionArgs) {
const { COOKIE_SECRET, DISCORD_CLIENT_SECRET, TWITTER_CLIENT_SECRET } =
context.cloudflare.env;
await getAuthenticator(
COOKIE_SECRET,
DISCORD_CLIENT_SECRET,
).isAuthenticated(request, {
failureRedirect: "/",
});
return getTwitterAssocAuthenticator(TWITTER_CLIENT_SECRET).authenticate(
"twitter-oauth",
request,
{
failureRedirect: "/dashboard",
},
);
}

export async function loader() {
return redirect("/dashboard");
}

export default function Redirect(): JSX.Element {
return (
<main>
<h1>画面遷移中…</h1>
</main>
);
}
Loading

0 comments on commit e613b3f

Please sign in to comment.