From 0bf480ef712e05de642b46b3cdcf47caae199ab5 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 14 Dec 2024 00:43:07 +0100 Subject: [PATCH] feat: github auth --- .env.example | 2 - .gitignore | 3 + dev/docker-compose.yml | 6 +- package.json | 5 +- .../20241213233617_github_init/migration.sql | 17 ++++ prisma/schema.prisma | 4 +- src/app/(protected)/layout.tsx | 2 +- .../(public)/auth/github/callback/route.ts | 84 +++++++++++++++++++ src/app/(public)/auth/github/route.ts | 18 ++++ src/app/(public)/auth/login/page.tsx | 47 ----------- src/app/(public)/auth/page.tsx | 15 ++++ src/app/(public)/auth/signUp/page.tsx | 46 ---------- src/components/app/NavBar/NavBar.tsx | 5 +- src/lib/auth/actions.ts | 2 +- src/lib/auth/index.ts | 4 + yarn.lock | 54 +++++++++++- 16 files changed, 202 insertions(+), 112 deletions(-) delete mode 100644 .env.example create mode 100644 prisma/migrations/20241213233617_github_init/migration.sql create mode 100644 src/app/(public)/auth/github/callback/route.ts create mode 100644 src/app/(public)/auth/github/route.ts delete mode 100644 src/app/(public)/auth/login/page.tsx create mode 100644 src/app/(public)/auth/page.tsx delete mode 100644 src/app/(public)/auth/signUp/page.tsx diff --git a/.env.example b/.env.example deleted file mode 100644 index f79672a..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# from the dev postgres containeer -DATABASE_URL=postgresql://postgres:dfsjhkdswkjntelsmldbfvsgknl5t@localhost:5555/postgres \ No newline at end of file diff --git a/.gitignore b/.gitignore index 00bba9b..7d5d2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +dev/ +!dev/docker-compose.yml \ No newline at end of file diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 7f29a72..a60c3a2 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -1,12 +1,10 @@ -volumes: - psql: services: psql: image: postgres environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: dfsjhkdswkjntelsmldbfvsgknl5t + POSTGRES_PASSWORD: S+xyPXPDcYNQtTy3hUNoC9eBwmsoGA volumes: - - psql:/var/lib/postgresql/data + - ./psql:/var/lib/postgresql/data ports: - 5555:5432 \ No newline at end of file diff --git a/package.json b/package.json index 2beb618..d4c79e3 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "docker compose --file dev/docker-compose.yml up -d && next dev", "build": "prisma generate && next build", "start": "next start", "lint": "next lint", @@ -13,11 +13,12 @@ "dependencies": { "@lucia-auth/adapter-prisma": "^4.0.1", "@node-rs/argon2": "^2.0.2", - "@prisma/client": "^5.12.1", + "@prisma/client": "^6.0.1", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", + "arctic": "^2.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "lucia": "^3.1.1", diff --git a/prisma/migrations/20241213233617_github_init/migration.sql b/prisma/migrations/20241213233617_github_init/migration.sql new file mode 100644 index 0000000..cd426b1 --- /dev/null +++ b/prisma/migrations/20241213233617_github_init/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `hashed_password` on the `User` table. All the data in the column will be lost. + - A unique constraint covering the columns `[githubId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `githubId` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "User_username_key"; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "hashed_password", +ADD COLUMN "githubId" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "User_githubId_key" ON "User"("githubId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 96bb15c..25cecb6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,8 +15,8 @@ datasource db { model User { id String @id @default(cuid()) - username String @unique - hashed_password String + githubId String @unique + username String sessions Session[] } diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx index ff22080..5633467 100644 --- a/src/app/(protected)/layout.tsx +++ b/src/app/(protected)/layout.tsx @@ -4,7 +4,7 @@ import { redirect } from "next/navigation"; export default async function Layout({ children }: { children: React.ReactNode }) { const { user } = await validateRequest() if (!user) { - return redirect('/auth/login') + return redirect('/auth') } return children } \ No newline at end of file diff --git a/src/app/(public)/auth/github/callback/route.ts b/src/app/(public)/auth/github/callback/route.ts new file mode 100644 index 0000000..e989b10 --- /dev/null +++ b/src/app/(public)/auth/github/callback/route.ts @@ -0,0 +1,84 @@ +import { github, lucia } from "@/lib/auth"; +import { cookies } from "next/headers"; +import { OAuth2RequestError } from "arctic"; +import { generateIdFromEntropySize } from "lucia"; +import prisma from "@/lib/db"; + +// TODO: maybe do the requests with octokit? +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const storedState = cookies().get("github_oauth_state")?.value ?? null; + if (!code || !state || !storedState || state !== storedState) { + return new Response(null, { + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + console.log(tokens); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken()}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + + const existingUser = await prisma.user.findUnique({ + where: { + githubId: githubUser.id.toString() + } + }); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } + + const userId = generateIdFromEntropySize(10); + + await prisma.user.create({ + data: { + id: userId, + githubId: githubUser.id.toString(), + username: githubUser.login + } + }) + + 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: "/" + } + }); + } catch (e) { + // the specific error message depends on the provider + console.error(e); + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); + } +} + +interface GitHubUser { + id: number; + login: string; +} diff --git a/src/app/(public)/auth/github/route.ts b/src/app/(public)/auth/github/route.ts new file mode 100644 index 0000000..6105127 --- /dev/null +++ b/src/app/(public)/auth/github/route.ts @@ -0,0 +1,18 @@ +import { generateState } from "arctic"; +import { github } from "@/lib/auth"; +import { cookies } from "next/headers"; + +export async function GET(): Promise { + const state = generateState(); + const url = github.createAuthorizationURL(state, []); + + cookies().set("github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + return Response.redirect(url); +} diff --git a/src/app/(public)/auth/login/page.tsx b/src/app/(public)/auth/login/page.tsx deleted file mode 100644 index 69e5e0f..0000000 --- a/src/app/(public)/auth/login/page.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import Link from "next/link"; -import { useFormState } from "react-dom"; -import { login } from "@/lib/auth/actions"; -import SubmitButton from "@/components/app/SubmitButton/SubmitButton"; -import { useEffect } from "react"; -import { toast } from "sonner"; - -export default function Page() { - const [formData, formAction] = useFormState(login, null); - useEffect(() => { - if (formData?.error) { - toast.error(formData.error) - } - }, [formData]) - - return ( -
-
-
-

Log In

-
-
-
-
- - -
-
- - -
- -
- No account? - - Create one! - -
-
-
-
-
- ); -} diff --git a/src/app/(public)/auth/page.tsx b/src/app/(public)/auth/page.tsx new file mode 100644 index 0000000..65c918b --- /dev/null +++ b/src/app/(public)/auth/page.tsx @@ -0,0 +1,15 @@ +"use client"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export default function Page() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/src/app/(public)/auth/signUp/page.tsx b/src/app/(public)/auth/signUp/page.tsx deleted file mode 100644 index a59c3e9..0000000 --- a/src/app/(public)/auth/signUp/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' -import { Label } from "@/components/ui/label" -import { Input } from "@/components/ui/input" -import Link from "next/link" -import { signup } from "@/lib/auth/actions"; -import SubmitButton from "@/components/app/SubmitButton/SubmitButton"; -import { useFormState } from "react-dom"; -import { toast } from "sonner"; -import { useEffect } from "react"; - -export default function Page() { - const [signupData, signupAction] = useFormState(signup, null) - useEffect(() => { - if (signupData?.error) { - toast.error(signupData.error) - } - }, [signupData]) - return ( -
-
-
-

Sign Up

-
-
-
-
- - -
-
- - -
- -
- Already have an account? - - Login - -
-
-
-
-
- ) -} \ No newline at end of file diff --git a/src/components/app/NavBar/NavBar.tsx b/src/components/app/NavBar/NavBar.tsx index 299003b..807fcf0 100644 --- a/src/components/app/NavBar/NavBar.tsx +++ b/src/components/app/NavBar/NavBar.tsx @@ -54,8 +54,7 @@ export default function Navbar() { - {/* TODO: Implement avatar system */} - {/**/} + {user.username} @@ -73,7 +72,7 @@ export default function Navbar() { ) : ( - + )} diff --git a/src/lib/auth/actions.ts b/src/lib/auth/actions.ts index 52cc150..736c11a 100644 --- a/src/lib/auth/actions.ts +++ b/src/lib/auth/actions.ts @@ -13,7 +13,7 @@ export async function logout() { await lucia.invalidateSession(session!.id); const sessionCookie = lucia.createBlankSessionCookie(); cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); - return redirect("/auth/login"); + return redirect("/auth"); } export async function login(prev: any, data: FormData) { diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 1dcdac4..6e221ab 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -3,7 +3,9 @@ import { Lucia, Session, User } from "lucia"; import prisma from "../db"; import { cache } from "react"; import { cookies } from "next/headers"; +import { GitHub } from "arctic"; +export const github = new GitHub(process.env.GITHUB_CLIENT!, process.env.GITHUB_SECRET!, 'http://localhost:3000/auth/github/callback'); const adapter = new PrismaAdapter(prisma.session, prisma.user); export const lucia = new Lucia(adapter, { @@ -18,6 +20,7 @@ export const lucia = new Lucia(adapter, { }, getUserAttributes: (attributes) => { return { + githubId: attributes.githubId, username: attributes.username }; } @@ -67,5 +70,6 @@ declare module "lucia" { } interface DatabaseUserAttributes { + githubId: string; username: string; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e348697..476ffc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -551,15 +551,52 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@oslojs/asn1@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@oslojs/asn1/-/asn1-1.0.0.tgz#25edb31585b369efdc103e9a1eb822df9c235174" + integrity sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA== + dependencies: + "@oslojs/binary" "1.0.0" + +"@oslojs/binary@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@oslojs/binary/-/binary-1.0.0.tgz#3e73f9cef0d06731d2aa528066666ccc00d610d6" + integrity sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ== + +"@oslojs/crypto@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@oslojs/crypto/-/crypto-1.0.1.tgz#74cf0d19d9fcda7cf5648cf3188dfeaf1d1b039f" + integrity sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ== + dependencies: + "@oslojs/asn1" "1.0.0" + "@oslojs/binary" "1.0.0" + +"@oslojs/encoding@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@oslojs/encoding/-/encoding-0.4.1.tgz#1489e560041533214511e9e03626962d24e58e9f" + integrity sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q== + +"@oslojs/encoding@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@oslojs/encoding/-/encoding-1.1.0.tgz#55f3d9a597430a01f2a5ef63c6b42f769f9ce34e" + integrity sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ== + +"@oslojs/jwt@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@oslojs/jwt/-/jwt-0.2.0.tgz#cdcd51e562eed2e536d273c840e90648c2d4a54a" + integrity sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg== + dependencies: + "@oslojs/encoding" "0.4.1" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@^5.12.1": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.13.0.tgz#b9f1d0983d714e982675201d8222a9ecb4bdad4a" - integrity sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg== +"@prisma/client@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-6.0.1.tgz#e24c5a44fb46d04a92426879bd9f8a2c28338420" + integrity sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg== "@prisma/debug@6.0.1": version "6.0.1" @@ -1044,6 +1081,15 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arctic@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/arctic/-/arctic-2.3.1.tgz#2cc9c5f2610ffdca0228b623a17cd9c8abf13b7f" + integrity sha512-bnmPYWbPtrQcneG/dmZIdvDeZ7pYhHqd4hYTbOR5LB9f429XLHiE4VnmKmJCPkw+G2CsG689WS+NDwa5UKsigA== + dependencies: + "@oslojs/crypto" "1.0.1" + "@oslojs/encoding" "1.1.0" + "@oslojs/jwt" "0.2.0" + arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"