switch to rails
This commit is contained in:
parent
83bc981152
commit
68c64808c5
71 changed files with 0 additions and 21335 deletions
|
@ -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 };
|
|
@ -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: '/'}));
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import { hydrate } from "react-dom";
|
||||
import { RemixBrowser } from "remix";
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
40
app/root.tsx
40
app/root.tsx
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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("/");
|
||||
};
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
}
|
44
app/utils.ts
44
app/utils.ts
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue