Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorpfiz committed Nov 25, 2023
1 parent 6f409c7 commit f93ff71
Show file tree
Hide file tree
Showing 15 changed files with 790 additions and 138 deletions.
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# These are supported funding model platforms

github: juliusmarminge
github: trevorpfiz
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

2 changes: 2 additions & 0 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"expo-constants": "~14.4.2",
"expo-linking": "~5.0.2",
"expo-router": "2.0.12",
"expo-secure-store": "^12.5.0",
"expo-splash-screen": "~0.22.0",
"expo-status-bar": "~1.7.1",
"expo-web-browser": "^12.5.0",
"nativewind": "^4.0.13",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
14 changes: 14 additions & 0 deletions apps/expo/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FlashList } from "@shopify/flash-list";

import { api } from "~/utils/api";
import type { RouterOutputs } from "~/utils/api";
import { useSignIn, useSignOut, useUser } from "~/utils/auth-hooks";

function PostCard(props: {
post: RouterOutputs["post"]["all"][number];
Expand Down Expand Up @@ -101,6 +102,10 @@ const Index = () => {

const postQuery = api.post.all.useQuery();

const user = useUser();
const signIn = useSignIn();
const signOut = useSignOut();

const deletePostMutation = api.post.delete.useMutation({
onSettled: () => utils.post.all.invalidate(),
});
Expand All @@ -120,6 +125,15 @@ const Index = () => {
color={"#f472b6"}
/>

<Text className="pb-2 text-center text-xl font-semibold text-white">
{user?.name ?? "Not logged in"}
</Text>
<Button
onPress={() => (user ? signOut() : signIn())}
title={user ? "Sign Out" : "Sign In With Discord"}
color={"#5B65E9"}
/>

<View className="py-2">
<Text className="font-semibold italic text-white">
Press on a post
Expand Down
8 changes: 7 additions & 1 deletion apps/expo/src/utils/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import superjson from "superjson";

import type { AppRouter } from "@acme/api";

import { getToken } from "./session-store";

/**
* A set of typesafe hooks for consuming your API.
*/
Expand Down Expand Up @@ -52,9 +54,13 @@ export function TRPCProvider(props: { children: React.ReactNode }) {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers() {
headers: async () => {
const headers = new Map<string, string>();
headers.set("x-trpc-source", "expo-react");

const token = await getToken();
if (token) headers.set("Authorization", `Bearer ${token}`);

return Object.fromEntries(headers);
},
}),
Expand Down
48 changes: 48 additions & 0 deletions apps/expo/src/utils/auth-hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as Linking from "expo-linking";
import * as Browser from "expo-web-browser";

import { api } from "./api";
import { deleteToken, setToken } from "./session-store";

export const signIn = async () => {
const signInUrl = "http://localhost:3000/api/auth/signin";
const redirectTo = "exp://192.168.10.181:8081/login";
const result = await Browser.openAuthSessionAsync(
`${signInUrl}?expo-redirect=${encodeURIComponent(redirectTo)}`,
redirectTo,
);
if (result.type !== "success") return;

const url = Linking.parse(result.url);
const sessionToken = String(url.queryParams?.session_token);
if (!sessionToken) return;

await setToken(sessionToken);
// ...
};

export const useUser = () => {
const { data: session } = api.auth.getSession.useQuery();
return session?.user ?? null;
};

export const useSignIn = () => {
const utils = api.useUtils();

return async () => {
await signIn();
await utils.invalidate();
};
};

export const useSignOut = () => {
const utils = api.useUtils();
const signOut = api.auth.signOut.useMutation();

return async () => {
const res = await signOut.mutateAsync();
if (!res.success) return;
await deleteToken();
await utils.invalidate();
};
};
6 changes: 6 additions & 0 deletions apps/expo/src/utils/session-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as SecureStore from "expo-secure-store";

const key = "session_token";
export const getToken = () => SecureStore.getItemAsync(key);
export const deleteToken = () => SecureStore.deleteItemAsync(key);
export const setToken = (v: string) => SecureStore.setItemAsync(key, v);
47 changes: 46 additions & 1 deletion apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
export { GET, POST } from "@acme/auth";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { GET as _GET, POST } from "@acme/auth";

export const runtime = "edge";

const expoRedirectCookieName = "__acme-expo-redirect-state";
const setCookieMatchPattern = /next-auth\.session-token=([^;]+)/;

export const GET = async (
req: NextRequest,
props: { params: { nextauth: string[] } },
) => {
const nextauthAction = props.params.nextauth[0];
const isExpoSignIn = req.nextUrl.searchParams.get("expo-redirect");
const isExpoCallback = cookies().get(expoRedirectCookieName);

if (nextauthAction === "signin" && !!isExpoSignIn) {
// set a cookie we can read in the callback
// to know to send the user back to expo
cookies().set({
name: expoRedirectCookieName,
value: isExpoSignIn,
maxAge: 60 * 10, // 10 min
path: "/",
});
}

if (nextauthAction === "callback" && !!isExpoCallback) {
cookies().delete(expoRedirectCookieName);

const authResponse = await _GET(req);
const setCookie = authResponse.headers.getSetCookie()[0];
const match = setCookie?.match(setCookieMatchPattern)?.[1];
if (!match) throw new Error("No session cookie found");

const url = new URL(isExpoCallback.value);
url.searchParams.set("session_token", match);
return NextResponse.redirect(url);
}

// Every other request just calls the default handler
return _GET(req);
};

export { POST };
5 changes: 5 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { authRouter } from "./router/auth";
import { postRouter } from "./router/post";
import { createTRPCRouter } from "./trpc";

/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
auth: authRouter,
post: postRouter,
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/router/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { invalidateSessionToken } from "@acme/auth";

import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";

export const authRouter = createTRPCRouter({
Expand All @@ -8,4 +10,11 @@ export const authRouter = createTRPCRouter({
// testing type validation of overridden next-auth Session in @acme/auth package
return "you can see this secret message!";
}),
signOut: protectedProcedure.mutation(async (opts) => {
if (!opts.ctx.token) {
return { success: false };
}
await invalidateSessionToken(opts.ctx.token);
return { success: true };
}),
});
13 changes: 9 additions & 4 deletions packages/api/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";

import { auth } from "@acme/auth";
import { auth, validateToken } from "@acme/auth";
import type { Session } from "@acme/auth";
import { db } from "@acme/db";

Expand All @@ -25,6 +25,7 @@ import { db } from "@acme/db";
*/
interface CreateContextOptions {
session: Session | null;
token: string | null;
}

/**
Expand All @@ -38,7 +39,7 @@ interface CreateContextOptions {
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
...opts,
db,
};
};
Expand All @@ -52,13 +53,17 @@ export const createTRPCContext = async (opts: {
req?: Request;
auth: Session | null;
}) => {
const session = opts.auth ?? (await auth());
const source = opts.req?.headers.get("x-trpc-source") ?? "unknown";
const authToken = opts.req?.headers.get("Authorization") ?? null;
const session = authToken
? await validateToken(authToken)
: opts.auth ?? (await auth());

const source = opts.req?.headers.get("x-trpc-source") ?? "unknown";
console.log(">>> tRPC Request from", source, "by", session?.user);

return createInnerTRPCContext({
session,
token: authToken,
});
};

Expand Down
23 changes: 21 additions & 2 deletions packages/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* @see https://github.com/nextauthjs/next-auth/pull/8932 */

import Discord from "@auth/core/providers/discord";
import type { DefaultSession } from "@auth/core/types";
import type { DefaultSession, Session } from "@auth/core/types";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import NextAuth from "next-auth";

Expand All @@ -18,13 +18,15 @@ declare module "next-auth" {
}
}

const adapter = DrizzleAdapter(db, tableCreator);

export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
adapter: DrizzleAdapter(db, tableCreator),
adapter,
providers: [Discord],
callbacks: {
session: ({ session, user }) => ({
Expand All @@ -36,3 +38,20 @@ export const {
}),
},
});

export const validateToken = async (token: string): Promise<Session | null> => {
const sessionToken = token.slice("Bearer ".length);
const session = await adapter.getSessionAndUser?.(sessionToken);
return session
? {
user: {
...session.user,
},
expires: session.session.expires.toISOString(),
}
: null;
};

export const invalidateSessionToken = async (token: string) => {
await adapter.deleteSession?.(token);
};
2 changes: 1 addition & 1 deletion packages/db/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default {
schema: "./schema",
driver: "mysql2",
dbCredentials: {
connectionString: process.env.DATABASE_URL,
uri: process.env.DATABASE_URL,
},
tablesFilter: ["t3turbo_*"],
} satisfies Config;
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@acme/prettier-config": "workspace:^0.1.0",
"@acme/tsconfig": "workspace:^0.1.0",
"dotenv-cli": "^7.3.0",
"drizzle-kit": "^0.19.13",
"drizzle-kit": "^0.20.4",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2"
Expand Down
Loading

0 comments on commit f93ff71

Please sign in to comment.