Skip to content

Commit

Permalink
Merge pull request #232 from omsimos/partners
Browse files Browse the repository at this point in the history
resolve merge conflict in lockfile
  • Loading branch information
joshxfi committed Aug 18, 2024
2 parents 54acf73 + 8ce4195 commit 015f679
Show file tree
Hide file tree
Showing 15 changed files with 663 additions and 19 deletions.
3 changes: 3 additions & 0 deletions apps/partners/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"],
experimental: {
serverComponentsExternalPackages: ["@node-rs/argon2"],
},
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
Expand Down
31 changes: 16 additions & 15 deletions apps/partners/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,46 @@
"@graphql-yoga/plugin-persisted-operations": "^3.6.2",
"@graphql-yoga/plugin-response-cache": "^3.8.2",
"@lucia-auth/adapter-drizzle": "^1.0.7",
"@node-rs/argon2": "^1.8.3",
"@umamin/db": "workspace:*",
"@umamin/gql": "workspace:*",
"@umamin/ui": "workspace:*",
"@urql/core": "^5.0.5",
"@urql/exchange-graphcache": "^7.1.1",
"@urql/exchange-persisted": "^4.3.0",
"@urql/next": "^1.1.1",
"@whatwg-node/server": "^0.9.46",
"arctic": "^1.9.2",
"date-fns": "^3.6.0",
"geist": "^1.3.1",
"arctic": "^1.9.2",
"gql.tada": "^1.8.5",
"graphql": "^16.9.0",
"graphql-yoga": "^5.6.2",
"lucia": "^3.2.0",
"lucide-react": "^0.407.0",
"modern-screenshot": "^4.4.39",
"react-intersection-observer": "^9.10.2",
"urql": "^4.1.0",
"zod": "^3.22.4",
"sonner": "^1.5.0",
"nanoid": "^5.0.7",
"next": "14.2.5",
"nextjs-toploader": "^1.6.12",
"@whatwg-node/server": "^0.9.46",
"@umamin/db": "workspace:*",
"@umamin/gql": "workspace:*",
"@umamin/ui": "workspace:*",
"react": "^18",
"react-dom": "^18",
"next": "14.2.5"
"react-intersection-observer": "^9.10.2",
"sonner": "^1.5.0",
"urql": "^4.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.12.12",
"@umamin/eslint-config": "workspace:*",
"@umamin/tsconfig": "workspace:*",
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@umamin/eslint-config": "workspace:*",
"@umamin/tsconfig": "workspace:*",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.5"
"typescript": "^5"
}
}
20 changes: 20 additions & 0 deletions apps/partners/src/app/dashboard/components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from "next/link";
import { Badge } from "@umamin/ui/components/badge";
import { SignOutButton } from "./sign-out-btn";

export async function Navbar() {
return (
<nav className="fixed left-0 right-0 top-0 z-50 w-full bg-background bg-opacity-40 bg-clip-padding py-5 backdrop-blur-xl backdrop-filter lg:z-40 container max-w-screen-xl flex justify-between items-center">
<div className="space-x-2 flex items-center">
<Link href="/" aria-label="logo">
<span className="font-semibold text-foreground">umamin</span>
<span className="text-muted-foreground font-medium">.link</span>
</Link>

<Badge variant="outline">partners</Badge>
</div>

<SignOutButton />
</nav>
);
}
21 changes: 21 additions & 0 deletions apps/partners/src/app/dashboard/components/sign-out-btn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { Loader2 } from "lucide-react";
import { useFormStatus } from "react-dom";
import { Button } from "@umamin/ui/components/button";

export function SignOutButton() {
const { pending } = useFormStatus();

return (
<Button
data-testid="logout-btn"
type="submit"
disabled={pending}
variant="outline"
>
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Sign Out
</Button>
);
}
14 changes: 14 additions & 0 deletions apps/partners/src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Navbar } from "./components/navbar";

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main>
<Navbar />
{children}
</main>
);
}
10 changes: 10 additions & 0 deletions apps/partners/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getSession } from "@/lib/auth";

export default async function Dashboard() {
const { user } = await getSession();
return (
<div className="max-w-screen-xl mx-auto mt-32 container">
<h1 className="text-4xl">Hello, {user?.displayName || user?.username}</h1>
</div>
);
}
43 changes: 43 additions & 0 deletions apps/partners/src/app/login/components/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { login } from "@/lib/actions";
import { useFormState } from "react-dom";

import { LoginButton } from "./login-button";
import { Input } from "@umamin/ui/components/input";
import { Label } from "@umamin/ui/components/label";

export function LoginForm() {
const [state, formAction] = useFormState(login, { error: "" });

return (
<form action={formAction} className="space-y-6">
<div>
<Label htmlFor="username">Username</Label>
<Input
required
id="username"
name="username"
placeholder="umamin"
className="mt-2"
/>
</div>

<div>
<Label htmlFor="password">Password</Label>
<Input
required
id="password"
name="password"
type="password"
className="mt-2"
/>
{!!state?.error && (
<p className="text-red-500 text-sm mt-2 font-medium">{state.error}</p>
)}
</div>

<LoginButton />
</form>
);
}
25 changes: 25 additions & 0 deletions apps/partners/src/app/login/components/login-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import Link from "next/link";
import { Loader2 } from "lucide-react";
import { useFormStatus } from "react-dom";
import { Button } from "@umamin/ui/components/button";

export function LoginButton() {
const { pending } = useFormStatus();

return (
<div>
<Button disabled={pending} type="submit" className="w-full">
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Login
</Button>

<Button disabled={pending} variant="outline" asChild>
<Link href="/login/google" className="mt-4 w-full">
Continue with Google
</Link>
</Button>
</div>
);
}
158 changes: 158 additions & 0 deletions apps/partners/src/app/login/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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;
}
Loading

0 comments on commit 015f679

Please sign in to comment.