Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolve merge conflict in lockfile #232

Merged
merged 3 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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