diff --git a/.eslintignore b/.eslintignore index 3bf2237..d5ca4be 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -ui/ \ No newline at end of file +ui/ +*.d.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 7e37e8c..0001d9d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,7 @@ } ], "@typescript-eslint/no-unused-vars": [ - "error", + "warn", { "argsIgnorePattern": "^_" } diff --git a/auth.d.ts b/auth.d.ts new file mode 100644 index 0000000..9ab4f66 --- /dev/null +++ b/auth.d.ts @@ -0,0 +1,10 @@ +/// +declare namespace Lucia { + type Auth = import("./src/lib/auth").Auth; + type DatabaseUserAttributes = { + name: string; + email: string; + picture: string; + }; + type DatabaseSessionAttributes = {}; +} diff --git a/package.json b/package.json index 912e720..0487bbb 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "dependencies": { "@ducanh2912/next-pwa": "^9.7.1", "@hookform/resolvers": "^3.3.2", - "@next-auth/prisma-adapter": "^1.0.7", + "@lucia-auth/adapter-prisma": "^3.0.2", + "@lucia-auth/oauth": "^3.5.0", "@prisma/client": "^5.4.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", @@ -28,10 +29,10 @@ "@react-email/components": "^0.0.7", "@uploadthing/react": "^5.6.2", "date-fns": "^2.30.0", + "lucia": "^2.7.4", "lucide-react": "^0.286.0", "nanoid": "^5.0.1", "next": "14.0.2", - "next-auth": "^4.24.5", "next-themes": "^0.2.1", "nodemailer": "^6.9.6", "postcss": "8.4.31", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f85a6f..e52e138 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,12 @@ dependencies: '@hookform/resolvers': specifier: ^3.3.2 version: 3.3.2(react-hook-form@7.47.0) - '@next-auth/prisma-adapter': - specifier: ^1.0.7 - version: 1.0.7(@prisma/client@5.4.2)(next-auth@4.24.5) + '@lucia-auth/adapter-prisma': + specifier: ^3.0.2 + version: 3.0.2(@prisma/client@5.4.2)(lucia@2.7.4) + '@lucia-auth/oauth': + specifier: ^3.5.0 + version: 3.5.0(lucia@2.7.4) '@prisma/client': specifier: ^5.4.2 version: 5.4.2(prisma@5.4.2) @@ -56,6 +59,9 @@ dependencies: date-fns: specifier: ^2.30.0 version: 2.30.0 + lucia: + specifier: ^2.7.4 + version: 2.7.4 lucide-react: specifier: ^0.286.0 version: 0.286.0(react@18.2.0) @@ -65,9 +71,6 @@ dependencies: next: specifier: 14.0.2 version: 14.0.2(@babel/core@7.23.2)(@opentelemetry/api@1.6.0)(react-dom@18.2.0)(react@18.2.0) - next-auth: - specifier: ^4.24.5 - version: 4.24.5(next@14.0.2)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 version: 0.2.1(next@14.0.2)(react-dom@18.2.0)(react@18.2.0) @@ -1891,6 +1894,24 @@ packages: tslib: 2.6.2 dev: true + /@lucia-auth/adapter-prisma@3.0.2(@prisma/client@5.4.2)(lucia@2.7.4): + resolution: {integrity: sha512-EyJWZene1/zasPwPctv8wwNErZt5mwwm5JATbhg+kXr3R8pbC7lJfVzDTAeeFClVH5k/FywRcsBl3JkPaNIcow==} + peerDependencies: + '@prisma/client': ^4.2.0 || ^5.0.0 + lucia: ^2.0.0 + dependencies: + '@prisma/client': 5.4.2(prisma@5.4.2) + lucia: 2.7.4 + dev: false + + /@lucia-auth/oauth@3.5.0(lucia@2.7.4): + resolution: {integrity: sha512-JSwAMVwlDJtbvfcJV1nbkv41OD830pgICrx8zFT71SYd5I1MnEQ+GqMTXBGRyxzc5XlLatT8ZS1Jt0k81487xg==} + peerDependencies: + lucia: ^2.0.0 + dependencies: + lucia: 2.7.4 + dev: false + /@mdx-js/esbuild@2.3.0(esbuild@0.19.5): resolution: {integrity: sha512-r/vsqsM0E+U4Wr0DK+0EfmABE/eg+8ITW4DjvYdh3ve/tK2safaqHArNnaqbOk1DjYGrhxtoXoGaM3BY8fGBTA==} peerDependencies: @@ -1928,16 +1949,6 @@ packages: - supports-color dev: true - /@next-auth/prisma-adapter@1.0.7(@prisma/client@5.4.2)(next-auth@4.24.5): - resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3' - next-auth: ^4 - dependencies: - '@prisma/client': 5.4.2(prisma@5.4.2) - next-auth: 4.24.5(next@14.0.2)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0) - dev: false - /@next/env@14.0.2: resolution: {integrity: sha512-HAW1sljizEaduEOes/m84oUqeIDAUYBR1CDwu2tobNlNDFP3cSm9d6QsOsGeNlIppU1p/p1+bWbYCbvwjFiceA==} @@ -2250,10 +2261,6 @@ packages: engines: {node: '>=14'} dev: true - /@panva/hkdf@1.1.1: - resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==} - dev: false - /@prisma/client@5.4.2(prisma@5.4.2): resolution: {integrity: sha512-2xsPaz4EaMKj1WS9iW6MlPhmbqtBsXAOeVttSePp8vTFTtvzh2hZbDgswwBdSCgPzmmwF+tLB259QzggvCmJqA==} engines: {node: '>=16.13'} @@ -4374,11 +4381,6 @@ packages: /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: false - /core-js-compat@3.33.0: resolution: {integrity: sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==} dependencies: @@ -6139,10 +6141,6 @@ packages: hasBin: true dev: true - /jose@4.15.3: - resolution: {integrity: sha512-RZJdL9Qjd1sqNdyiVteRGV/bnWtik/+PJh1JP4kT6+x1QQMn+7ryueRys5BEueuayvSVY8CWGCisCDazeRLTuw==} - dev: false - /js-beautify@1.14.9: resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==} engines: {node: '>=12'} @@ -6453,6 +6451,10 @@ packages: dependencies: yallist: 4.0.0 + /lucia@2.7.4: + resolution: {integrity: sha512-do6Aah5kX2DUl7N0CvELWV1/b+qZGF0AUmUrkthYeNfXfgx4RAkWi4BkAlgGDBB/0c1WApb18mUD1pA5qTaWmw==} + dev: false + /lucide-react@0.286.0(react@18.2.0): resolution: {integrity: sha512-0+AOFa/uiXlXJJTqcKto1gqbU9XflYgYZbS9DN2ytSIhSBQaF5xfRKAq/k0okBInpgu5P6i7dhCcgbHV4OMkHQ==} peerDependencies: @@ -7000,32 +7002,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false - /next-auth@4.24.5(next@14.0.2)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==} - peerDependencies: - next: ^12.2.5 || ^13 || ^14 - nodemailer: ^6.6.5 - react: ^17.0.2 || ^18 - react-dom: ^17.0.2 || ^18 - peerDependenciesMeta: - nodemailer: - optional: true - dependencies: - '@babel/runtime': 7.23.2 - '@panva/hkdf': 1.1.1 - cookie: 0.5.0 - jose: 4.15.3 - next: 14.0.2(@babel/core@7.23.2)(@opentelemetry/api@1.6.0)(react-dom@18.2.0)(react@18.2.0) - nodemailer: 6.9.6 - oauth: 0.9.15 - openid-client: 5.6.1 - preact: 10.18.1 - preact-render-to-string: 5.2.6(preact@10.18.1) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - uuid: 8.3.2 - dev: false - /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.19.5)(next@14.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} peerDependencies: @@ -7155,19 +7131,10 @@ packages: engines: {node: '>=0.10.0'} dev: true - /oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} - dev: false - /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - /object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - dev: false - /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -7235,11 +7202,6 @@ packages: es-abstract: 1.22.3 dev: true - /oidc-token-hash@5.0.3: - resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} - engines: {node: ^10.13.0 || >=12.0.0} - dev: false - /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -7250,15 +7212,6 @@ packages: engines: {node: '>= 14.17.0'} dev: true - /openid-client@5.6.1: - resolution: {integrity: sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==} - dependencies: - jose: 4.15.3 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.0.3 - dev: false - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -7543,19 +7496,6 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /preact-render-to-string@5.2.6(preact@10.18.1): - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} - peerDependencies: - preact: '>=10' - dependencies: - preact: 10.18.1 - pretty-format: 3.8.0 - dev: false - - /preact@10.18.1: - resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==} - dev: false - /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'} @@ -7646,10 +7586,6 @@ packages: engines: {node: '>=6'} dev: false - /pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - dev: false - /pretty@2.0.0: resolution: {integrity: sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==} engines: {node: '>=0.10.0'} @@ -8968,6 +8904,7 @@ packages: /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + dev: true /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} diff --git a/prisma/migrations/20231111061416_stripe/migration.sql b/prisma/migrations/20231208111437_/migration.sql similarity index 59% rename from prisma/migrations/20231111061416_stripe/migration.sql rename to prisma/migrations/20231208111437_/migration.sql index d1c139b..0ed9333 100644 --- a/prisma/migrations/20231111061416_stripe/migration.sql +++ b/prisma/migrations/20231208111437_/migration.sql @@ -1,29 +1,20 @@ -- CreateTable -CREATE TABLE "Account" ( +CREATE TABLE "Session" ( "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "type" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerAccountId" TEXT NOT NULL, - "refresh_token" TEXT, - "access_token" TEXT, - "expires_at" INTEGER, - "token_type" TEXT, - "scope" TEXT, - "id_token" TEXT, - "session_state" TEXT, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") + "user_id" TEXT NOT NULL, + "active_expires" BIGINT NOT NULL, + "idle_expires" BIGINT NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") ); -- CreateTable -CREATE TABLE "Session" ( +CREATE TABLE "Key" ( "id" TEXT NOT NULL, - "sessionToken" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, + "hashed_password" TEXT, + "user_id" TEXT NOT NULL, - CONSTRAINT "Session_pkey" PRIMARY KEY ("id") + CONSTRAINT "Key_pkey" PRIMARY KEY ("id") ); -- CreateTable @@ -31,9 +22,7 @@ CREATE TABLE "User" ( "id" TEXT NOT NULL, "name" TEXT, "email" TEXT, - "emailVerified" TIMESTAMP(3), - "image" TEXT, - "shortBio" TEXT, + "picture" TEXT, "stripe_customer_id" TEXT, "stripe_subscription_id" TEXT, "stripe_price_id" TEXT, @@ -61,16 +50,19 @@ CREATE TABLE "Project" ( ); -- CreateIndex -CREATE INDEX "Account_userId_idx" ON "Account"("userId"); +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_user_id_idx" ON "Session"("user_id"); -- CreateIndex -CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); +CREATE UNIQUE INDEX "Key_id_key" ON "Key"("id"); -- CreateIndex -CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); +CREATE INDEX "Key_user_id_idx" ON "Key"("user_id"); -- CreateIndex -CREATE INDEX "Session_userId_idx" ON "Session"("userId"); +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); -- CreateIndex CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); @@ -88,10 +80,10 @@ CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token" CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); -- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Session" ADD CONSTRAINT "Session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Key" ADD CONSTRAINT "Key_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20231208111448_/migration.sql b/prisma/migrations/20231208111448_/migration.sql new file mode 100644 index 0000000..74cc42d --- /dev/null +++ b/prisma/migrations/20231208111448_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "VerificationToken"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a8d5d8a..bf34b04 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,60 +11,38 @@ datasource db { directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection } -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) +model Session { + id String @id @unique @default(cuid()) + user_id String + active_expires BigInt + idle_expires BigInt + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) - @@unique([provider, providerAccountId]) - @@index([userId]) + @@index([user_id]) } -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) +model Key { + id String @id @unique @default(cuid()) + hashed_password String? + user_id String + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) - @@index([userId]) + @@index([user_id]) } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - shortBio String? + id String @id @unique @default(cuid()) + name String? + email String? @unique + picture String? + auth_session Session[] + keys Key[] + projects Project[] stripeCustomerId String? @unique @map(name: "stripe_customer_id") stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id") stripePriceId String? @map(name: "stripe_price_id") stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end") - - accounts Account[] - sessions Session[] - projects Project[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) } model Project { diff --git a/src/app/@signinDialog/(.)signin/page.tsx b/src/app/@loginDialog/(.)login/page.tsx similarity index 100% rename from src/app/@signinDialog/(.)signin/page.tsx rename to src/app/@loginDialog/(.)login/page.tsx diff --git a/src/app/@signinDialog/default.tsx b/src/app/@loginDialog/default.tsx similarity index 100% rename from src/app/@signinDialog/default.tsx rename to src/app/@loginDialog/default.tsx diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index aba56e2..0000000 --- a/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import NextAuth from "next-auth"; -import { authOptions } from "~/lib/auth"; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; diff --git a/src/app/api/auth/login/github/callback/route.ts b/src/app/api/auth/login/github/callback/route.ts new file mode 100644 index 0000000..1f0ad64 --- /dev/null +++ b/src/app/api/auth/login/github/callback/route.ts @@ -0,0 +1,64 @@ +import { OAuthRequestError } from "@lucia-auth/oauth"; +import { cookies, headers } from "next/headers"; +import { auth, githubAuth } from "~/lib/auth"; + +import type { NextRequest } from "next/server"; + +export const GET = async (request: NextRequest) => { + const storedState = cookies().get("github_oauth_state")?.value; + const url = new URL(request.url); + const state = url.searchParams.get("state"); + const code = url.searchParams.get("code"); + // validate state + if (!storedState || !state || storedState !== state || !code) { + return new Response(null, { + status: 400, + }); + } + try { + const { getExistingUser, githubUser, createUser } = + await githubAuth.validateCallback(code); + + const getUser = async () => { + const existingUser = await getExistingUser(); + if (existingUser) return existingUser; + const user = await createUser({ + attributes: { + name: githubUser.name!, + email: githubUser.email!, + picture: githubUser.avatar_url, + }, + }); + return user; + }; + + const user = await getUser(); + const session = await auth.createSession({ + userId: user.userId, + attributes: {}, + }); + const authRequest = auth.handleRequest(request.method, { + cookies, + headers, + }); + authRequest.setSession(session); + return new Response(null, { + status: 302, + headers: { + Location: "/dashboard", // redirect to profile page + }, + }); + } catch (e) { + console.log(e); + + if (e instanceof OAuthRequestError) { + // invalid code + return new Response(null, { + status: 400, + }); + } + return new Response(null, { + status: 500, + }); + } +}; diff --git a/src/app/api/auth/login/github/route.ts b/src/app/api/auth/login/github/route.ts new file mode 100644 index 0000000..813d3b8 --- /dev/null +++ b/src/app/api/auth/login/github/route.ts @@ -0,0 +1,19 @@ +import * as context from "next/headers"; +import { githubAuth } from "~/lib/auth"; + +export const GET = async () => { + const [url, state] = await githubAuth.getAuthorizationUrl(); + // store state + context.cookies().set("github_oauth_state", state, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60, + }); + return new Response(null, { + status: 302, + headers: { + Location: url.toString(), + }, + }); +}; diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..b83f173 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,25 @@ +import * as context from "next/headers"; +import { auth } from "~/lib/auth"; + +import type { NextRequest } from "next/server"; + +export const POST = async (request: NextRequest) => { + const authRequest = auth.handleRequest(request.method, context); + // check if user is authenticated + const session = await authRequest.validate(); + if (!session) { + return new Response(null, { + status: 401, + }); + } + // make sure to invalidate the current session! + await auth.invalidateSession(session.sessionId); + // delete session cookie + authRequest.setSession(null); + return new Response(null, { + status: 302, + headers: { + Location: "/login", // redirect to login page + }, + }); +}; diff --git a/src/app/api/stripe/route.ts b/src/app/api/stripe/route.ts index b6d96e7..4b03cea 100644 --- a/src/app/api/stripe/route.ts +++ b/src/app/api/stripe/route.ts @@ -1,9 +1,6 @@ -// /api/stripe - -import { getServerSession } from "next-auth"; import { z } from "zod"; import { proPlan } from "~/config/subscription"; -import { authOptions } from "~/lib/auth"; +import { getPageSession } from "~/lib/auth"; import { stripe } from "~/lib/stripe"; import { getUserSubscriptionPlan } from "~/lib/subscription"; @@ -11,13 +8,13 @@ const billingUrl = process.env.NEXT_PUBLIC_APP_URL + "/dashboard/billing"; export async function GET() { try { - const session = await getServerSession(authOptions); + const session = await getPageSession(); if (!session?.user || !session?.user.email) { return new Response("Unauthorized", { status: 401 }); } - const subscriptionPlan = await getUserSubscriptionPlan(session.user.id); + const subscriptionPlan = await getUserSubscriptionPlan(session.user.userId); // The user is on the pro plan. // Create a portal session to manage subscription. @@ -46,7 +43,7 @@ export async function GET() { }, ], metadata: { - userId: session.user.id, + userId: session.user.userId, }, }); diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts index fbcb403..68946a9 100644 --- a/src/app/api/uploadthing/core.ts +++ b/src/app/api/uploadthing/core.ts @@ -1,5 +1,5 @@ -import { getToken } from "next-auth/jwt"; import { createUploadthing, type FileRouter } from "uploadthing/next"; +import { getPageSession } from "~/lib/auth"; const f = createUploadthing(); @@ -8,15 +8,15 @@ export const ourFileRouter = { // Define as many FileRoutes as you like, each with a unique routeSlug imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) // Set permissions and file types for this FileRoute - .middleware(async ({ req }) => { + .middleware(async () => { // This code runs on your server before upload - const token = await getToken({ req }); + const session = await getPageSession(); // If you throw, the user will not be able to upload - if (!token) throw new Error("Unauthorized!"); + if (!session) throw new Error("Unauthorized!"); // Whatever is returned here is accessible in onUploadComplete as `metadata` - return { userId: token.id }; + return { userId: session.user.userId }; }) .onUploadComplete(async () => { // This code runs on your server after upload diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index c12f2c3..3bf4c68 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,10 +1,16 @@ +import { redirect } from "next/navigation"; import SidebarNav from "~/components/layout/sidebar-nav"; +import { getPageSession } from "~/lib/auth"; interface DashboardLayoutProps { children: React.ReactNode; } -export default function DashboardLayout({ children }: DashboardLayoutProps) { +export default async function DashboardLayout({ + children, +}: DashboardLayoutProps) { + const session = await getPageSession(); + if (!session) redirect("/login"); return (
diff --git a/src/app/dashboard/projects/action.ts b/src/app/dashboard/projects/action.ts index e7d76d7..3483e66 100644 --- a/src/app/dashboard/projects/action.ts +++ b/src/app/dashboard/projects/action.ts @@ -1,12 +1,11 @@ "use server"; import { type Project } from "@prisma/client"; -import { getServerSession, type Session } from "next-auth"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; -import { authOptions } from "~/lib/auth"; import db from "~/lib/db"; import { getUserSubscriptionPlan } from "~/lib/subscription"; +import { getUser } from "~/server/user"; interface Payload { name: string; @@ -21,7 +20,7 @@ export async function createProject(payload: Payload) { ...payload, user: { connect: { - id: user.id, + id: user?.id, }, }, }, @@ -32,7 +31,7 @@ export async function createProject(payload: Payload) { export async function checkIfFreePlanLimitReached() { const user = await getUser(); - const subscriptionPlan = await getUserSubscriptionPlan(user.id); + const subscriptionPlan = await getUserSubscriptionPlan(user?.id as string); // If user is on a free plan. // Check if user has reached limit of 3 projects. @@ -40,7 +39,7 @@ export async function checkIfFreePlanLimitReached() { const count = await db.project.count({ where: { - userId: user.id, + userId: user?.id, }, }); @@ -51,7 +50,7 @@ export async function getProjects() { const user = await getUser(); const projects = await db.project.findMany({ where: { - userId: user.id, + userId: user?.id, }, orderBy: { createdAt: "desc", @@ -65,7 +64,7 @@ export async function getProjectById(id: string) { const project = await db.project.findFirst({ where: { id, - userId: user.id, + userId: user?.id, }, }); return project as Project; @@ -76,7 +75,7 @@ export async function updateProjectById(id: string, payload: Payload) { await db.project.update({ where: { id, - userId: user.id, + userId: user?.id, }, data: payload, }); @@ -88,15 +87,9 @@ export async function deleteProjectById(id: string) { await db.project.delete({ where: { id, - userId: user.id, + userId: user?.id, }, }); revalidatePath(`/dashboard/projects`); redirect("/dashboard/projects"); } - -export async function getUser() { - const session = (await getServerSession(authOptions)) as Session; - - return session.user; -} diff --git a/src/app/dashboard/settings/settings-form.tsx b/src/app/dashboard/settings/settings-form.tsx index 44fb54b..7e85372 100644 --- a/src/app/dashboard/settings/settings-form.tsx +++ b/src/app/dashboard/settings/settings-form.tsx @@ -17,7 +17,6 @@ import { FormMessage, } from "~/components/ui/form"; import { Input } from "~/components/ui/input"; -import { Textarea } from "~/components/ui/textarea"; import { toast } from "~/components/ui/use-toast"; import { saRemoveNewImageFromCDN, @@ -48,28 +47,27 @@ export default function SettingsForm({ values: { name: currentUser.name, email: currentUser.email, - shortBio: currentUser.shortBio, - image: currentUser.image, + picture: currentUser.picture, }, }); const { formState, getFieldState } = form; - const { isDirty: isImageChanged } = getFieldState("image"); + const { isDirty: isImageChanged } = getFieldState("picture"); const [showConfirmAlert, setShowConfirmAlert] = useState(false); useEffect(() => { - if (isImageChanged && currentUser.image !== oldImage.current) { - oldImage.current = currentUser.image; + if (isImageChanged && currentUser.picture !== oldImage.current) { + oldImage.current = currentUser.picture; } - }, [currentUser.image, isImageChanged]); + }, [currentUser.picture, isImageChanged]); function onSubmit(data: SettingsValues) { if (!formState.isDirty) return; if (isImageChanged) { startTransition(() => - saRemoveUserOldImageFromCDN(currentUser.id, data.image) + saRemoveUserOldImageFromCDN(currentUser.id, data.picture) .then(() => saUpdateUserInDb(currentUser.id, data)) .then(() => { toast({ @@ -103,7 +101,7 @@ export default function SettingsForm({ function handleReset() { if (isImageChanged) { - saRemoveNewImageFromCDN(form.getValues().image) + saRemoveNewImageFromCDN(form.getValues().picture) .then(() => form.reset()) .catch((error) => console.error(error)); } else { @@ -119,7 +117,7 @@ export default function SettingsForm({ > ( Picture @@ -169,23 +167,6 @@ export default function SettingsForm({ )} /> - ( - - Short Bio - -