Skip to content

Commit

Permalink
feat: add shared package for reused pages
Browse files Browse the repository at this point in the history
  • Loading branch information
joshxfi committed Aug 25, 2024
1 parent 7b1ee67 commit 50f7cc5
Show file tree
Hide file tree
Showing 30 changed files with 979 additions and 364 deletions.
2 changes: 1 addition & 1 deletion apps/social/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const nextConfig = {
removeConsole: process.env.NODE_ENV === "production",
},

transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"],
transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql", "@umamin/shared"],
images: {
remotePatterns: [
{
Expand Down
1 change: 1 addition & 0 deletions apps/social/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@umamin/db": "workspace:*",
"@umamin/gql": "workspace:*",
"@umamin/ui": "workspace:*",
"@umamin/shared": "workspace:*",
"@urql/core": "^5.0.5",
"@urql/exchange-graphcache": "^7.1.1",
"@urql/exchange-persisted": "^4.3.0",
Expand Down
159 changes: 1 addition & 158 deletions apps/social/src/app/(authentication)/login/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,158 +1 @@
import { nanoid } from "nanoid";
import { generateId } from "lucia";
import { cookies } from "next/headers";
import { db, and, eq } from "@umamin/db";
import { OAuth2RequestError } from "arctic";
import {
user as userSchema,
account as accountSchema,
} from "@umamin/db/schema/user";

import { getSession, google, lucia } from "@/lib/auth";

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");

const storedState = cookies().get("google_oauth_state")?.value ?? null;
const storedCodeVerifier = cookies().get("code_verifier")?.value ?? null;

if (
!code ||
!state ||
!storedState ||
!storedCodeVerifier ||
state !== storedState
) {
return new Response(null, {
status: 400,
});
}

try {
const tokens = await google.validateAuthorizationCode(
code,
storedCodeVerifier,
);

const googleUserResponse = await fetch(
"https://openidconnect.googleapis.com/v1/userinfo",
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
},
);

const googleUser: GoogleUser = await googleUserResponse.json();

const { user } = await getSession();

const existingUser = await db.query.account.findFirst({
where: and(
eq(accountSchema.providerId, "google"),
eq(accountSchema.providerUserId, googleUser.sub),
),
});

if (user && existingUser) {
return new Response(null, {
status: 302,
headers: {
Location: "/settings?error=already_linked",
},
});
} else if (user) {
await db
.update(userSchema)
.set({
imageUrl: googleUser.picture,
})
.where(eq(userSchema.id, user.id));

await db.insert(accountSchema).values({
providerId: "google",
providerUserId: googleUser.sub,
userId: user.id,
picture: googleUser.picture,
email: googleUser.email,
});

return new Response(null, {
status: 302,
headers: {
Location: "/settings",
},
});
}

if (existingUser) {
const session = await lucia.createSession(existingUser.userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);

cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);

return new Response(null, {
status: 302,
headers: {
Location: "/login",
},
});
}

const usernameId = generateId(5);
const userId = nanoid();

await db.insert(userSchema).values({
id: userId,
imageUrl: googleUser.picture,
username: `umamin_${usernameId}`,
});

await db.insert(accountSchema).values({
providerId: "google",
providerUserId: googleUser.sub,
userId,
picture: googleUser.picture,
email: googleUser.email,
});

const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);

cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);

return new Response(null, {
status: 302,
headers: {
Location: "/login",
},
});
} catch (err: any) {
console.log(err);
if (err instanceof OAuth2RequestError) {
return new Response(null, {
status: 400,
});
}

return new Response(null, {
status: 500,
});
}
}

interface GoogleUser {
sub: string;
picture: string;
email: string;
}
export { GET } from "@umamin/shared/routes/google/callback/route";
32 changes: 1 addition & 31 deletions apps/social/src/app/(authentication)/login/google/route.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1 @@
import { generateState, generateCodeVerifier } from "arctic";
import { cookies } from "next/headers";
import { google } from "@/lib/auth";

export async function GET(): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = await google.createAuthorizationURL(state, codeVerifier, {
scopes: ["profile", "email"],
});

url.searchParams.set("access_type", "offline");

cookies().set("google_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax",
});

cookies().set("code_verifier", codeVerifier, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax",
});

return Response.redirect(url);
}
export { GET } from "@umamin/shared/routes/google/route";
29 changes: 1 addition & 28 deletions apps/social/src/app/(authentication)/login/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1 @@
import { Skeleton } from "@umamin/ui/components/skeleton";

export default function Loading() {
return (
<div className="max-w-lg md:max-w-md container mt-36 [&>div]:gap-3 [&>div]:flex [&>div]:flex-col flex gap-8 flex-col">
<div>
<Skeleton className="w-2/5 h-[25px] rounded-md" />
<Skeleton className="w-1/2 h-[10px] rounded-md" />
</div>

<div>
<Skeleton className="w-1/5 h-[10px] rounded-md" />
<Skeleton className="w-full h-[30px] rounded-md" />
</div>

<div>
<Skeleton className="w-1/5 h-[10px] rounded-md" />
<Skeleton className="w-full h-[30px] rounded-md" />
</div>

<div>
<Skeleton className="w-full h-[30px] rounded-md" />
<Skeleton className="w-full h-[30px] rounded-md" />
<Skeleton className="mx-auto w-1/2 h-[10px] rounded-md" />
</div>
</div>
);
}
export { default } from "@umamin/shared/app/login/loading";
50 changes: 1 addition & 49 deletions apps/social/src/app/(authentication)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import Link from "next/link";
import dynamic from "next/dynamic";
import { getSession } from "@/lib/auth";
import { redirect } from "next/navigation";
import { LoginForm } from "./components/form";

const BrowserWarning = dynamic(
() => import("@umamin/ui/components/browser-warning"),
{ ssr: false }
);

export const metadata = {
title: "Umamin Social — Login",
description:
Expand Down Expand Up @@ -37,41 +26,4 @@ export const metadata = {
},
};

export default async function Login() {
const { user } = await getSession();

if (user) {
redirect("/");
}

return (
<section className="container pt-10 flex flex-col items-center">
<BrowserWarning />
<div className="border-b-2 border-muted border-dashed pb-5 mb-10 sm:text-center inline-block">
<h1 className="font-bold md:text-6xl text-[10vw] leading-none dark:bg-gradient-to-b from-foreground dark:to-zinc-400 bg-clip-text bg-zinc-800 text-transparent tracking-tighter text-nowrap">
Umamin Social
</h1>
<p className="text-muted-foreground md:text-lg mt-2">
The <span className="text-foreground font-medium">Umamin v2.0</span>{" "}
Next generation open-source social platform
</p>
</div>

<div className="max-w-md w-full mx-auto">
<div className="mb-6">
<h2 className="text-2xl tracking-tight font-semibold">Account</h2>
<p className="text-sm text-muted-foreground">
Proceed with your Umamin v2.0 profile
</p>
</div>
<LoginForm />
<div className="mt-4 text-center text-sm w-full">
Don&apos;t have an account?{" "}
<Link href="/register" className="underline">
Sign up
</Link>
</div>
</div>
</section>
);
}
export { default } from "@umamin/shared/app/login/page";
45 changes: 1 addition & 44 deletions apps/social/src/app/(authentication)/register/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1 @@
import { Skeleton } from "@umamin/ui/components/skeleton";

export default function Loading() {
return (
<div className="container max-w-xl lg:mt-36 mt-28 mx-auto ">
<div className="flex flex-col gap-4">
<div className="flex gap-3">
<Skeleton className="size-16 md:size-20 rounded-full" />

<div className="flex flex-col gap-2">
<Skeleton className="h-[20px] w-[80px] rounded-md" />
<Skeleton className="h-[15px] w-[50px] rounded-md" />
</div>
</div>

<div className="flex flex-col gap-2">
<div className="flex gap-1 items-center">
<Skeleton className="size-[15px] rounded-full" />
<Skeleton className="h-[10px] w-[130px] rounded-md" />
</div>

<div className="flex gap-1 items-center">
<Skeleton className="size-[15px] rounded-full" />
<Skeleton className="h-[10px] w-[130px] rounded-md" />
</div>
</div>
</div>

<div className="space-y-5 mt-8">
<div>
<div className="flex justify-around">
<Skeleton className="h-[15px] w-[90px] rounded-md" />
<Skeleton className="h-[15px] w-[90px] rounded-md" />
</div>

<Skeleton className="w-full h-[2px] rounded-md mt-2" />
</div>

<Skeleton className="w-full h-[200px] rounded-md" />
<Skeleton className="w-full h-[200px] rounded-md" />
</div>
</div>
);
}
export { default } from "@umamin/shared/app/register/loading";
Loading

0 comments on commit 50f7cc5

Please sign in to comment.