switch to rails

This commit is contained in:
jane 2022-04-09 11:31:01 -04:00
parent 83bc981152
commit 68c64808c5
71 changed files with 0 additions and 21335 deletions

View file

@ -1,62 +0,0 @@
import { PrismaClient } from "@prisma/client";
import invariant from "tiny-invariant";
let prisma: PrismaClient;
declare global {
var __db__: PrismaClient;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (process.env.NODE_ENV === "production") {
prisma = getClient();
} else {
if (!global.__db__) {
global.__db__ = getClient();
}
prisma = global.__db__;
}
function getClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
const databaseUrl = new URL(DATABASE_URL);
const isLocalHost = databaseUrl.hostname === "localhost";
const PRIMARY_REGION = isLocalHost ? null : process.env.PRIMARY_REGION;
const FLY_REGION = isLocalHost ? null : process.env.FLY_REGION;
const isReadReplicaRegion = !PRIMARY_REGION || PRIMARY_REGION === FLY_REGION;
if (!isLocalHost) {
databaseUrl.host = `${FLY_REGION}.${databaseUrl.host}`;
if (!isReadReplicaRegion) {
// 5433 is the read-replica port
databaseUrl.port = "5433";
}
}
console.log(`🔌 setting up prisma client to ${databaseUrl.host}`);
// NOTE: during development if you change anything in this function, remember
// that this only runs once per server restart and won't automatically be
// re-run per request like everything else is. So if you need to change
// something in this file, you'll need to manually restart the server.
const client = new PrismaClient({
datasources: {
db: {
url: databaseUrl.toString(),
},
},
});
// connect eagerly
client.$connect();
return client;
}
export { prisma };

View file

@ -1,124 +0,0 @@
import axios from 'axios';
import { prisma } from "~/db.server";
import { createUserSession } from '~/session.server';
export interface DiscordUser {
id: string;
username: string;
discriminator: string;
avatar: string;
bot?: boolean;
system?: boolean;
mfa_enabled?: boolean;
}
export interface AccessTokenResponse {
access_token: string;
expires: number;
refresh_token: string;
}
export function getAccessToken(code: string): Promise<AccessTokenResponse> {
return new Promise(
(resolve, reject) => {
const body = new FormData();
body.append("grant_type", "authorization_code");
body.append("code", code);
body.append("redirect_url", process.env.DISCORD_REDIRECT_URI || "");
axios.post("https://discord.com/api/oauth2/token", body)
.then((res) => {
const { access_token, refresh_token, expires_in } = res.data;
resolve({
access_token,
refresh_token,
expires: Date.now() + expires_in * 1000
});
})
.catch(reject);
}
)
}
export async function authorize(access_code: string): Promise<AccessTokenResponse | undefined> {
return new Promise(
(resolve, reject) => {
const formData = new FormData();
formData.append("client_id", process.env.DISCORD_CLIENT_ID || "");
formData.append("client_secret", process.env.DISCORD_CLIENT_SECRET || "");
formData.append("grant_type", "authorization_code");
formData.append("redirect_uri", process.env.DISCORD_REDIRECT_URI || "");
formData.append("scope", "identify");
formData.append("code", access_code);
axios.post("https://discord.com/api/oauth2/token", formData)
.then((res) => {
const { access_token, refresh_token, expires_in } = res.data;
resolve({
access_token,
refresh_token,
expires: Date.now() + expires_in * 1000
});
})
.catch(reject);
}
)
}
export function getDiscordUser(token: string): Promise<DiscordUser> {
return new Promise(
(resolve, reject) => {
axios.get("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then(
(res) => {
resolve(res.data);
}
)
.catch(reject);
}
)
}
export async function discordLogin(request: Request, code: string): Promise<Response> {
return new Promise(
(resolve, reject) => {
getAccessToken(code)
.then((token) => {
getDiscordUser(token.access_token)
.then(async (user) => {
prisma.discordUser.upsert({
create: {
discord_id: user.id,
access_token: token.access_token,
refresh_token: token.refresh_token,
expires: new Date(token.expires),
username: user.username,
discriminator: user.discriminator,
},
update: {
discord_id: user.id,
access_token: token.access_token,
refresh_token: token.refresh_token,
expires: new Date(token.expires),
username: user.username,
discriminator: user.discriminator,
},
where: {
discord_id: user.id
}
})
.then(async () => {
resolve(await createUserSession({request, discord_id: user.id, redirectTo: '/'}));
})
})
})
}
)
}

View file

@ -1,4 +0,0 @@
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document);

View file

@ -1,21 +0,0 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}

View file

@ -1,65 +0,0 @@
import { prisma } from "~/db.server";
import type {User} from "@prisma/client";
import { AccessTokenResponse, DiscordUser } from "~/discord/index";
export type {User, Border} from "@prisma/client";
export async function getUserByDiscordId(discord_id: User["discord_id"]) {
return prisma.user.findUnique({ where: { discord_id: discord_id || undefined }});
}
export async function getUserById(id: User["id"]) {
return prisma.user.findUnique({ where: { id } });
}
/// SHOULD ONLY BE USED WITH A CORRESPONDING OAUTH TOKEN
export async function createUser(discord_id: User["discord_id"]) {
return prisma.user.create({
data: {
discord_id
},
});
}
export async function createDiscordLogin(discord_id: DiscordUser["id"], token_response: AccessTokenResponse) {
return prisma.discordUser.create({
data: {
discord_id,
access_token: token_response.access_token,
refresh_token: token_response.refresh_token,
expires: new Date(token_response.expires)
}
})
}
export type SessionInformation = {
bearer_token: string,
refresh_token: string
};
export async function discordIdentify(bearer_token: string, refresh_token: string): Promise<DiscordUser | SessionInformation | undefined> {
let user_info = await (
await fetch("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${bearer_token}`
}
})
).json();
if (!user_info["id"]) {
const form_data = new FormData();
form_data.append("client_id", process.env.DISCORD_CLIENT_ID || "");
form_data.append("client_secret", process.env.DISCORD_CLIENT_SECRET || "");
form_data.append("grant_type", "refresh_token");
form_data.append("refresh_token", refresh_token);
let refresh_info = await (await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: form_data
})).json();
return refresh_info;
}
return user_info;
}

View file

@ -1,40 +0,0 @@
import {
json,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "remix";
import type { LinksFunction, MetaFunction, LoaderFunction } from "remix";
import tailwindStylesheetUrl from "./styles/tailwind.css";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};
export const meta: MetaFunction = () => ({
charset: "utf-8",
title: "Border Selector",
viewport: "width=device-width,initial-scale=1",
});
export default function App() {
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full">
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

View file

@ -1,23 +0,0 @@
// learn more: https://fly.io/docs/reference/configuration/#services-http_checks
import type { LoaderFunction } from "remix";
import { prisma } from "~/db.server";
export const loader: LoaderFunction = async ({ request }) => {
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
try {
// if we can connect to the database and make a simple query
// and make a HEAD request to ourselves, then we're good.
await Promise.all([
prisma.user.count(),
fetch(`http://${host}`, { method: "HEAD" }).then((r) => {
if (!r.ok) return Promise.reject(r);
}),
]);
return new Response("OK");
} catch (error: unknown) {
console.log("healthcheck ❌", { error });
return new Response("ERROR", { status: 500 });
}
};

View file

@ -1,23 +0,0 @@
import { json, Link, LoaderFunction, useLoaderData } from "remix";
import { getUser, getSession } from "~/session.server";
import { useOptionalUser } from "~/utils";
export const loader: LoaderFunction = async ({ request }) => {
const user_info = await getUser(request);
return json(user_info);
}
export default function Index() {
const discordUser = useLoaderData();
console.log("discordUser is", discordUser);
return (
<main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center">
<h1>Do you love the color of the sky?</h1>
<br />
<Link
to="/loginstart">
Log in
</Link>
</main>
);
}

View file

@ -1,39 +0,0 @@
import * as React from "react";
import type { ActionFunction, LoaderFunction, MetaFunction } from "remix";
import {
Form,
json,
Link,
useActionData,
redirect,
useSearchParams,
} from "remix";
import { discordLogin } from "~/discord";
import { createUserSession, getUserId } from "~/session.server";
export const loader: LoaderFunction = async ({ request }) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
const url = new URL(await request.url);
const accessCode = url.searchParams.get("code") || "";
const response = await discordLogin(request, accessCode);
console.log(response);
return ""
};
export const meta: MetaFunction = () => {
return {
title: "Login",
};
};
export default function LoginPage() {
return (
<div className="flex min-h-full flex-col justify-center">
<p>Error logging in.</p>
</div>
)
}

View file

@ -1,25 +0,0 @@
import type { LoaderFunction, MetaFunction } from "remix";
import {
json,
redirect,
} from "remix";
import { getUserId } from "~/session.server";
export const loader: LoaderFunction = async ({ request }) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
const client_id = process.env.DISCORD_CLIENT_ID || "";
const redirect_uri = process.env.DISCORD_REDIRECT_URI || "";
return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` +
`&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`);
};
export const meta: MetaFunction = () => {
return {
title: "Login",
};
};
export default function LoginPage() {
return <div></div>
}

View file

@ -1,11 +0,0 @@
import type { ActionFunction, LoaderFunction } from "remix";
import { redirect } from "remix";
import { logout } from "~/session.server";
export const action: ActionFunction = async ({ request }) => {
return logout(request);
};
export const loader: LoaderFunction = async () => {
return redirect("/");
};

View file

@ -1,97 +0,0 @@
import { createCookieSessionStorage, redirect } from "remix";
import invariant from "tiny-invariant";
import { discordIdentify, User, SessionInformation } from "~/models/user.server";
import { getUserByDiscordId } from "~/models/user.server";
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 0,
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
},
});
const USER_SESSION_KEY = "userId";
const BEARER_TOKEN_KEY = "bearerToken";
const REFRESH_TOKEN_KEY = "refreshToken";
export async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
export async function getUserId(request: Request): Promise<string | undefined> {
const session = await getSession(request);
const userId = session.get(USER_SESSION_KEY);
return userId;
}
export async function getUser(request: Request): Promise<null | User> {
const userId = await getUserId(request);
if (userId === undefined) return null;
const user = await getUserByDiscordId(userId);
if (user) return user;
throw await logout(request);
}
export async function requireUserId(
request: Request,
redirectTo: string = new URL(request.url).pathname
): Promise<string> {
const userId = await getUserId(request);
if (!userId) {
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
throw redirect(`/loginstart?${searchParams}`);
}
return userId;
}
export async function requireUser(request: Request) {
const session = await getSession(request);
const user_id = session.get(USER_SESSION_KEY);
const client_id = process.env.DISCORD_CLIENT_ID || "";
const redirect_uri = process.env.DISCORD_REDIRECT_URI || "";
if (!user_id) {
return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` +
`&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`);
}
}
export async function createUserSession({
request,
discord_id,
redirectTo,
}: {
request: Request;
discord_id: string;
redirectTo: string;
}) {
const session = await getSession(request);
session.set(USER_SESSION_KEY, discord_id);
console.log(discord_id);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: 60 * 60 * 24 * 7 // 7 days
}),
},
});
}
export async function logout(request: Request) {
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await sessionStorage.destroySession(session),
},
});
}

View file

@ -1,44 +0,0 @@
import { useMemo } from "react";
import { useMatches } from "remix";
import type { User } from "~/models/user.server";
/**
* This base hook is used in other hooks to quickly search for specific data
* across all loader data using useMatches.
* @param {string} id The route id
* @returns {JSON|undefined} The router data or undefined if not found
*/
export function useMatchesData(
id: string
): Record<string, unknown> | undefined {
const matchingRoutes = useMatches();
const route = useMemo(
() => matchingRoutes.find((route) => route.id === id),
[matchingRoutes, id]
);
return route?.data;
}
function isUser(user: any): user is User {
return user && typeof user === "object" && typeof user.discord_id === "string";
}
export function useOptionalUser(): User | undefined {
const data = useMatchesData("root");
console.log(data)
if (!data || !isUser(data.user)) {
return undefined;
}
return data.user;
}
export function useUser(): User {
const maybeUser = useOptionalUser();
if (!maybeUser) {
throw new Error(
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
);
}
return maybeUser;
}