sendou.ink/app/core/DiscordStrategy.server.ts
Kalle 185295d54e
User page initial with SQLite3 (#822)
* Clean away prisma migrations

* Way to migrate WIP

* SQLite3 seeding script initial

* Fetch tournament data in loader

* CheckinActions new loader data model

* Virtual banner text color columns

* Logged in user

* Count teams

* ownTeam

* Map pool tab fully working

* Teams tab

* Fix timestamp default

* Register page

* Manage team page

* Camel case checkedInTimestamp

* Clean slate

* Add .nvmrc

* Add favicon

* Package lock file version 2

* Update tsconfig

* Add Tailwind

* Add StrictMode

* Add background color

* Auth without DB

* Revert "Add Tailwind"

This reverts commit 204713c602.

* Auth with DB

* Switch back to tilde absolute import

* Import layout

* Camel case for database columns

* Move auth routes to folder

* User popover links working

* Import linters

* User page initial

* User edit page with country

* Script to delete db files before migration in dev

* Remove "youtubeName" column

* Correct avatar size on desktop

* Fix SubNav not spanning the whole page

* Remove duplicate files

* Update README
2022-05-16 17:52:54 +03:00

137 lines
3.6 KiB
TypeScript

import { DISCORD_AUTH_KEY } from "./authenticator.server";
import { db } from "~/db";
import type { User } from "~/db/types";
import type { OAuth2Profile } from "remix-auth-oauth2";
import { OAuth2Strategy } from "remix-auth-oauth2";
import invariant from "tiny-invariant";
import { z } from "zod";
interface DiscordExtraParams extends Record<string, string | number> {
scope: string;
}
export type LoggedInUser = Pick<User, "id" | "discordId" | "discordAvatar">;
const partialDiscordUserSchema = z.object({
avatar: z.string().nullish(),
discriminator: z.string(),
id: z.string(),
username: z.string(),
});
const partialDiscordConnectionsSchema = z.array(
z.object({
visibility: z.number(),
verified: z.boolean(),
name: z.string(),
id: z.string(),
type: z.string(),
})
);
const discordUserDetailsSchema = z.tuple([
partialDiscordUserSchema,
partialDiscordConnectionsSchema,
]);
export class DiscordStrategy extends OAuth2Strategy<
LoggedInUser,
OAuth2Profile,
DiscordExtraParams
> {
name = DISCORD_AUTH_KEY;
scope: string;
constructor() {
invariant(process.env.DISCORD_CLIENT_ID);
invariant(process.env.DISCORD_CLIENT_SECRET);
invariant(process.env.BASE_URL);
super(
{
authorizationURL: "https://discord.com/api/oauth2/authorize",
tokenURL: "https://discord.com/api/oauth2/token",
clientID: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
callbackURL: new URL("/auth/callback", process.env.BASE_URL).toString(),
},
async ({ accessToken }) => {
const authHeader = ["Authorization", `Bearer ${accessToken}`];
const discordResponses = await Promise.all([
fetch("https://discord.com/api/users/@me", {
headers: [authHeader],
}),
fetch("https://discord.com/api/users/@me/connections", {
headers: [authHeader],
}),
]);
const [user, connections] = discordUserDetailsSchema.parse(
await Promise.all(
discordResponses.map((res) => {
if (!res.ok) throw new Error("Call to Discord API failed");
return res.json();
})
)
);
const userFromDb = db.users.upsert({
discordAvatar: user.avatar ?? null,
discordDiscriminator: user.discriminator,
discordId: user.id,
discordName: user.username,
...this.parseConnections(connections),
});
return {
id: userFromDb.id,
discordId: userFromDb.discordId,
discordAvatar: userFromDb.discordAvatar,
};
}
);
this.scope = "identify connections";
}
private parseConnections(
connections: z.infer<typeof partialDiscordConnectionsSchema>
) {
if (!connections) throw new Error("No connections");
const result: {
twitch: string | null;
twitter: string | null;
youtubeId: string | null;
} = {
twitch: null,
twitter: null,
youtubeId: null,
};
for (const connection of connections) {
if (connection.visibility !== 1 || !connection.verified) continue;
switch (connection.type) {
case "twitch":
result.twitch = connection.name;
break;
case "twitter":
result.twitter = connection.name;
break;
case "youtube":
result.youtubeId = connection.id;
}
}
return result;
}
protected authorizationParams() {
const urlSearchParams: Record<string, string> = {
scope: this.scope,
};
return new URLSearchParams(urlSearchParams);
}
}