Skip to content

Commit

Permalink
feat: Miminal implementation (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikuroXina authored Aug 16, 2023
1 parent f2bc255 commit d4db96a
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 30 deletions.
29 changes: 25 additions & 4 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
env:
es2021: true
node: true
extends:
- "eslint:recommended"
- "plugin:@typescript-eslint/recommended"
- eslint:recommended
- plugin:@typescript-eslint/recommended
- plugin:react/recommended
parser: "@typescript-eslint/parser"
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- "@typescript-eslint"
root: true
- "@typescript-eslint"
- react
rules:
indent:
- error
- 4
linebreak-style:
- error
- unix
quotes:
- error
- double
semi:
- error
- always
react/react-in-jsx-scope: "off"
1 change: 1 addition & 0 deletions .example.dev.vars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DISCORD_CLIENT_SECRET=
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ dist
.wrangler

# package
wrangler.toml
pnpm-lock.yaml

# env
.dev.vars
27 changes: 27 additions & 0 deletions assets/static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
html {
width: min(90svw, 60rem);
margin: 0 auto;
}

p {
font-size: 115%;
line-height: 1.6;
}

button {
cursor: pointer;
display: block;
padding: 18px;
margin: 0 auto;
font-size: 250%;
font-weight: bold;
border: 0;
border-radius: 9999px;
background-color: #222;
color: #eee;
box-shadow: #777 4px 4px 16px;
}

button:active {
box-shadow: unset;
}
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "wrangler dev src/index.ts",
"deploy": "wrangler deploy --minify src/index.ts"
"dev": "wrangler dev src/index.tsx",
"deploy": "wrangler deploy --minify src/index.tsx",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"check": "tsc --noEmit"
},
"dependencies": {
"@mikuroxina/mini-fn": "^4.1.1",
"hono": "^3.4.1"
},
"devDependencies": {
Expand All @@ -15,6 +19,8 @@
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.0.2",
"typescript": "^5.1.6",
"wrangler": "^3.1.2"
Expand Down
49 changes: 49 additions & 0 deletions src/adaptors/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Cont, Promise } from "@mikuroxina/mini-fn";

import type { Repository } from "../services";
import type { User, Connection } from "../services/patch-members";

export const withDiscordRepository =
<T>(token: string): Cont.ContT<T, Promise.PromiseHkt, Repository> =>
async (repoUser: (repo: Repository) => Promise<T>): Promise<T> => {
const repo = newRepo(token);
const result = await repoUser(repo);
await revoke(token);
return result;
};

const DISCORD_API = "https://discord.com/api/v10";

const newRepo = (token: string): Repository => ({
async user(): Promise<User> {
const meRes = await fetch(DISCORD_API + "/users/@me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
return await meRes.json();
},
async connections(): Promise<Connection[]> {
const connectionsRes = await fetch(
DISCORD_API + "/users/@me/connections",
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
return await connectionsRes.json();
},
});

async function revoke(token: string) {
await fetch(DISCORD_API + "/oauth2/token/revoke", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
token,
}),
});
}
9 changes: 9 additions & 0 deletions src/adaptors/r2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Store } from "../services/patch-members";

export class R2Store implements Store {
constructor(private readonly bucket: R2Bucket) {}

async put(id: string, entry: unknown): Promise<void> {
await this.bucket.put(id, JSON.stringify(entry));
}
}
11 changes: 0 additions & 11 deletions src/index.ts

This file was deleted.

55 changes: 55 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Result } from "@mikuroxina/mini-fn";
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";

import { withDiscordRepository } from "./adaptors/discord";
import { R2Store } from "./adaptors/r2";
import { patchMembers } from "./services/patch-members";
import { Index } from "./pages";
import { Done } from "./pages/done";

type Bindings = {
ASSOC_BUCKET: R2Bucket;
DISCORD_CLIENT_ID: string;
DISCORD_CLIENT_SECRET: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.use("/static/*", serveStatic({ root: "./" }));
app.get("/", (c) => c.html(<Index requestUrl={c.req.url} />));
app.get("/done", (c) => c.html(<Done />));
app.get("/redirect", async (c) => {
const code = c.req.query("code");
if (!code) {
return c.text("Bad Request", 400);
}

const tokenRes = await fetch("https://discord.com/api/v10/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: c.env.DISCORD_CLIENT_ID,
client_secret: c.env.DISCORD_CLIENT_SECRET,
grant_type: "authorization_code",
code,
redirect_uri: new URL("/redirect", c.req.url).toString(),
}),
});
const json = await tokenRes.json<{ access_token: string; }>();


const store = new R2Store(c.env.ASSOC_BUCKET);
return await withDiscordRepository<Response>(json.access_token)(async (repo) => {
const result = await patchMembers(repo, store);
if (Result.isErr(result)) {
console.error(result[1]);
return c.text("Internal Server Error", 500);
}
return c.redirect("/done");
});
});

export default app;
21 changes: 21 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const ASSOCIATION_TYPES = ["twitter", "github"] as const;

export type AssociationType = (typeof ASSOCIATION_TYPES)[number];
export const checkAssociationType = (type: string): type is AssociationType =>
(ASSOCIATION_TYPES as readonly string[]).includes(type);

export interface AssociatedLink {
type: AssociationType;
id: string;
name: string;
}
export const checkAssociationLink = (link: {
type: string;
id: string;
name: string;
}): link is AssociatedLink => checkAssociationType(link.type);

export interface Member {
discordId: string;
associatedLinks: AssociatedLink[];
}
12 changes: 12 additions & 0 deletions src/pages/done.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const Done = () => <html lang="ja">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Approvers メンバー情報登録 - 登録完了</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h1>登録完了</h1>
<p>登録できました. このタブは閉じてもらって構いません.</p>
</body>
</html>;
22 changes: 22 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const Index = ({ requestUrl }: { requestUrl: string; }): JSX.Element => {
const redirectUrl = `https://discord.com/api/oauth2/authorize?client_id=1141210184505639003&redirect_uri=${
encodeURIComponent(new URL("/redirect", requestUrl).toString())
}&response_type=code&scope=identify%20guilds.members.read%20connections`;
return (
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Approvers メンバー情報登録</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h1>Approvers メンバー情報登録</h1>
<h2>Approvers へようこそ</h2>
<p>こちらではあなたの Discord アカウントに関連付けてある GitHub / X (旧 Twitter) アカウントの情報を登録できます. これは Discord の OAuth を利用しており, 下記のボタンから当サービスでのトークンの利用を認可していただくことで登録処理を開始できます.</p>
<a href={redirectUrl} referrerPolicy="no-referrer" rel="noopener">
<button>アカウント情報を登録する</button>
</a>
</body>
</html>
);};
3 changes: 3 additions & 0 deletions src/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type Repository as PatchMembersRepository } from "./services/patch-members";

export type Repository = PatchMembersRepository;
38 changes: 38 additions & 0 deletions src/services/patch-members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Result } from "@mikuroxina/mini-fn";

import { type Member, checkAssociationLink } from "../models";

export interface User {
id: string;
}
export interface Connection {
id: string;
name: string;
type: string;
}

export interface Repository {
user(): Promise<User>;
connections(): Promise<Connection[]>;
}
export interface Store {
put(id: string, entry: unknown): Promise<void>;
}

export const patchMembers = async (
repository: Repository,
store: Store,
): Promise<Result.Result<Error, []>> => {
const me = await repository.user();
const connections = await repository.connections();

const member = {
discordId: me.id,
associatedLinks: connections
.map(({ type, id, name }) => ({ type, id, name }))
.filter(checkAssociationLink),
} satisfies Member;

store.put(me.id, member);
return Result.ok<[]>([]);
};
25 changes: 13 additions & 12 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"verbatimModuleSyntax": true
}
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"],
"jsx": "react-jsx",
"jsxFragmentFactory": "Fragment",
"jsxImportSource": "hono/jsx",
"verbatimModuleSyntax": true
}
}
16 changes: 16 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name = "members-assoc"
main = "src/index.tsx"
compatibility_date = "2023-06-28"

r2_buckets = [
{ binding = "ASSOC_BUCKET", bucket_name = "members-assoc", preview_bucket_name = "members-assoc-test" },
]

[site]
bucket = "./assets"

[vars]
DISCORD_CLIENT_ID = "1141210184505639003"

[dev]
port = 3000

0 comments on commit d4db96a

Please sign in to comment.