Login with GitHub and Magic link is working

This commit is contained in:
Matt Aitken
2022-12-07 14:22:36 +00:00
parent 0f404348bb
commit def8d50a83
18 changed files with 78 additions and 47 deletions
+2 -1
View File
@@ -5,4 +5,5 @@ GITHUB_CLIENT_ID=<random string>
GITHUB_SECRET=<random string>
MAILGUN_KEY=<random string>
FROM_EMAIL=<your email address>
MERGENT_KEY=<Mergent key>
MERGENT_KEY=<Mergent key>
APP_ORIGIN=http://localhost:3000
@@ -1,7 +1,7 @@
import { Menu, Transition } from "@headlessui/react";
import classnames from "classnames";
import type { User } from "~/models/user.server";
// import { UserProfilePhoto } from "./UserProfilePhoto";
import { UserProfilePhoto } from "./UserProfilePhoto";
const userNavigation = [{ name: "Logout", href: "/logout" }];
@@ -11,7 +11,7 @@ export function UserProfileMenu({ user }: { user: User }) {
<div>
<Menu.Button className="transitions flex max-w-xs items-center rounded-full bg-white text-sm">
<span className="sr-only">Open user menu</span>
{/* <UserProfilePhoto user={user} className="h-7 w-7" /> */}
<UserProfilePhoto user={user} className="h-7 w-7" />
</Menu.Button>
</div>
<Transition
@@ -23,7 +23,7 @@ export function UserProfileMenu({ user }: { user: User }) {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white pb-2 pt-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{/* {user.name ? (
{user.name ? (
<h2 className="mb-2 block border-b border-slate-200 py-2 pl-5 pr-2 text-sm font-semibold text-slate-600">
{user.name}
</h2>
@@ -31,7 +31,7 @@ export function UserProfileMenu({ user }: { user: User }) {
<h2 className="mb-2 block border-b border-slate-200 py-2 pl-5 pr-2 text-sm font-semibold text-slate-600">
{user.email}
</h2>
)} */}
)}
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
@@ -0,0 +1,21 @@
import { UserCircleIcon } from "@heroicons/react/24/solid";
import classNames from "classnames";
import type { User } from "~/models/user.server";
export function UserProfilePhoto({
user,
className,
}: {
user: User;
className?: string;
}) {
return user.avatarUrl ? (
<img
className={classNames("rounded-full", className)}
src={user.avatarUrl}
alt={user.name ?? user.displayName ?? "User"}
/>
) : (
<UserCircleIcon className={classNames("text-gray-400", className)} />
);
}
+6 -5
View File
@@ -1,5 +1,6 @@
import { PrismaClient, Prisma } from ".prisma/client";
import { PrismaClient } from ".prisma/client";
import invariant from "tiny-invariant";
import { env } from "./env.server";
let prisma: PrismaClient;
@@ -11,7 +12,7 @@ declare global {
// 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") {
if (env.NODE_ENV === "production") {
prisma = getClient();
} else {
if (!global.__db__) {
@@ -21,15 +22,15 @@ if (process.env.NODE_ENV === "production") {
}
function getClient() {
const { DATABASE_URL } = process.env;
const { DATABASE_URL } = 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 PRIMARY_REGION = isLocalHost ? null : env.PRIMARY_REGION;
const FLY_REGION = isLocalHost ? null : env.FLY_REGION;
const isReadReplicaRegion = !PRIMARY_REGION || PRIMARY_REGION === FLY_REGION;
+1 -1
View File
@@ -25,7 +25,7 @@ export default function handleRequest(
});
}
if (process.env.NODE_ENV === "production") {
if (env.NODE_ENV === "production") {
Sentry.init({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
+10
View File
@@ -1,6 +1,13 @@
import { z } from "zod";
const EnvironmentSchema = z.object({
NODE_ENV: z.union([
z.literal("development"),
z.literal("production"),
z.literal("test"),
]),
REMIX_APP_PORT: z.string().optional(),
DATABASE_URL: z.string(),
APP_ORIGIN: z.string().default("https://app.trigger.dev"),
SENTRY_DSN: z
.string()
@@ -14,6 +21,9 @@ const EnvironmentSchema = z.object({
MAILGUN_KEY: z.string(),
FROM_EMAIL: z.string(),
MERGENT_KEY: z.string(),
PRIMARY_REGION: z.string().optional(),
FLY_REGION: z.string().optional(),
SESSION_SECRET: z.string(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+3 -5
View File
@@ -1,22 +1,20 @@
import type { Session } from "@remix-run/node";
import { redirect } from "remix-typedjson";
import { createCookieSessionStorage } from "@remix-run/node";
import invariant from "tiny-invariant";
import { env } from "~/env.server";
export type ToastMessage = { message: string; type: "success" | "error" };
const ONE_YEAR = 1000 * 60 * 60 * 24 * 365;
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
export const { commitSession, getSession } = createCookieSessionStorage({
cookie: {
name: "__message",
path: "/",
httpOnly: true,
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
secrets: [env.SESSION_SECRET],
secure: env.NODE_ENV === "production",
},
});
+6 -9
View File
@@ -1,5 +1,6 @@
import type {
LinksFunction,
LoaderArgs,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
@@ -25,6 +26,7 @@ import { useEffect, useRef } from "react";
import posthog from "posthog-js";
import { withSentry } from "@sentry/remix";
import { env } from "./env.server";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
@@ -36,18 +38,12 @@ export const meta: MetaFunction = () => ({
viewport: "width=device-width,initial-scale=1",
});
type LoaderData = {
user: Awaited<ReturnType<typeof getUser>>;
toastMessage: ToastMessage | null;
posthogProjectKey?: string;
};
export const loader: LoaderFunction = async ({ request }) => {
export const loader = async ({ request }: LoaderArgs) => {
const session = await getSession(request.headers.get("cookie"));
const toastMessage = session.get("toastMessage") as ToastMessage;
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
return json<LoaderData>(
return typedjson(
{
user: await getUser(request),
toastMessage,
@@ -58,7 +54,8 @@ export const loader: LoaderFunction = async ({ request }) => {
};
function App() {
const { toastMessage, posthogProjectKey, user } = useLoaderData<LoaderData>();
const { toastMessage, posthogProjectKey, user } =
useTypedLoaderData<typeof loader>();
const postHogInitialised = useRef<boolean>(false);
useEffect(() => {
-1
View File
@@ -21,7 +21,6 @@ export async function loader({ request }: LoaderArgs) {
export default function AppLayout() {
return (
<div className="flex h-screen flex-col overflow-auto">
<Header>Home</Header>
<Outlet />
</div>
);
+7 -3
View File
@@ -1,5 +1,6 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { Header } from "~/components/Header";
export const loader = async ({ request }: LoaderArgs) => {
return typedjson({});
@@ -7,8 +8,11 @@ export const loader = async ({ request }: LoaderArgs) => {
export default function AppLayout() {
return (
<div className="flex h-screen flex-col overflow-auto">
adsadsdasasd asads asa dsa ds ads
</div>
<>
<Header>Home</Header>
<div className="flex h-screen flex-col overflow-auto">
adsadsdasasd asads asa dsa ds ads
</div>
</>
);
}
@@ -9,7 +9,7 @@ const gitHubStrategy = new GitHubStrategy(
{
clientID: env.GITHUB_CLIENT_ID ?? "",
clientSecret: env.GITHUB_SECRET ?? "",
callbackURL: `${process.env.APP_ORIGIN}/auth/github/callback`,
callbackURL: `${env.APP_ORIGIN}/auth/github/callback`,
},
async ({ accessToken, extraParams, profile }) => {
const emails = profile.emails;
@@ -1,19 +1,17 @@
import { createCookieSessionStorage } from "@remix-run/node";
import invariant from "tiny-invariant";
import { z } from "zod";
import { env } from "~/env.server";
const ONE_DAY = 60 * 60 * 24;
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
export const { commitSession, getSession } = createCookieSessionStorage({
cookie: {
name: "__redirectTo",
path: "/",
httpOnly: true,
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
secrets: [env.SESSION_SECRET],
secure: env.NODE_ENV === "production",
maxAge: ONE_DAY,
},
});
@@ -14,6 +14,8 @@ export async function getUser(request: Request) {
const user = await getUserById(userId);
if (user) return user;
console.log("user", user);
throw await logout(request);
}
@@ -1,7 +1,5 @@
import { createCookieSessionStorage } from "@remix-run/node";
import invariant from "tiny-invariant";
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
import { env } from "~/env.server";
export const sessionStorage = createCookieSessionStorage({
cookie: {
@@ -9,8 +7,8 @@ export const sessionStorage = createCookieSessionStorage({
sameSite: "lax", // this helps with CSRF
path: "/", // remember to add this so the cookie will work in all routes
httpOnly: true, // for security reasons, make this cookie http only
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production", // enable this in prod only
secrets: [env.SESSION_SECRET],
secure: env.NODE_ENV === "production", // enable this in prod only
maxAge: 60 * 60 * 24 * 365, // 7 days
},
});
+1
View File
@@ -37,6 +37,7 @@ export function useMatchesData(
id: string
): Record<string, unknown> | undefined {
const matchingRoutes = useMatches();
console.log("matchingRoutes", matchingRoutes);
const route = useMemo(
() => matchingRoutes.find((route) => route.id === id),
[matchingRoutes, id]
+1 -1
View File
@@ -98,7 +98,7 @@
"@types/express": "^4.17.13",
"@types/jest": "^29.2.0",
"@types/morgan": "^1.9.3",
"@types/node": "^18.8.0",
"@types/node": "^18.11.11",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/slug": "^5.0.3",
+6 -5
View File
@@ -4,9 +4,10 @@ import compression from "compression";
import morgan from "morgan";
import { createRequestHandler as expressCreateRequestHandler } from "@remix-run/express";
import { wrapExpressCreateRequestHandler } from "@sentry/remix";
import { env } from "~/env.server";
const createRequestHandler =
process.env.NODE_ENV === "production"
env.NODE_ENV === "production"
? wrapExpressCreateRequestHandler(expressCreateRequestHandler)
: expressCreateRequestHandler;
@@ -14,7 +15,7 @@ const app = express();
app.use((req, res, next) => {
// helpful headers:
res.set("x-fly-region", process.env.FLY_REGION ?? "unknown");
res.set("x-fly-region", env.FLY_REGION ?? "unknown");
res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`);
// /clean-urls/ -> /clean-urls
@@ -33,7 +34,7 @@ app.use((req, res, next) => {
// learn more: https://fly.io/docs/getting-started/multi-region-databases/#replay-the-request
app.all("*", function getReplayResponse(req, res, next) {
const { method, path: pathname } = req;
const { PRIMARY_REGION, FLY_REGION } = process.env;
const { PRIMARY_REGION, FLY_REGION } = env;
const isMethodReplayable = !["GET", "OPTIONS", "HEAD"].includes(method);
const isReadOnlyRegion =
@@ -71,7 +72,7 @@ app.use(express.static("public", { maxAge: "1h" }));
app.use(morgan("tiny"));
const MODE = process.env.NODE_ENV;
const MODE = env.NODE_ENV;
const BUILD_DIR = path.join(process.cwd(), "build");
app.all(
@@ -88,7 +89,7 @@ app.all(
}
);
const port = process.env.REMIX_APP_PORT || 3000;
const port = env.REMIX_APP_PORT || 3000;
app.listen(port, () => {
// require the built app so we're ready when the first request comes in
+1 -1
View File
@@ -62,7 +62,7 @@ importers:
'@types/jest': ^29.2.0
'@types/mailgun-js': ^0.22.12
'@types/morgan': ^1.9.3
'@types/node': ^18.8.0
'@types/node': ^18.11.11
'@types/react': ^18.0.21
'@types/react-dom': ^18.0.6
'@types/slug': ^5.0.3