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
-
-
-
-
-
- )}
- />
@@ -104,7 +104,7 @@ export default function RootLayout({
{children}
- {signinDialog}
+ {loginDialog}
diff --git a/src/app/signin/page.tsx b/src/app/login/page.tsx
similarity index 100%
rename from src/app/signin/page.tsx
rename to src/app/login/page.tsx
diff --git a/src/components/layout/auth-form.tsx b/src/components/layout/auth-form.tsx
index 9c179bc..e274caa 100644
--- a/src/components/layout/auth-form.tsx
+++ b/src/components/layout/auth-form.tsx
@@ -1,81 +1,72 @@
"use client";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Label } from "@radix-ui/react-label";
-import { signIn } from "next-auth/react";
-import { useState } from "react";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
+import Link from "next/link";
import { buttonVariants } from "~/components/ui/button";
import { cn } from "~/lib/utils";
-import { saCheckEmailExists } from "~/server/actions";
-import Icons from "../shared/icons";
-import { Input } from "../ui/input";
-import { toast } from "../ui/use-toast";
-const userAuthSchema = z.object({
- email: z.string().email("Please enter a valid email address."),
-});
+// const userAuthSchema = z.object({
+// email: z.string().email("Please enter a valid email address."),
+// });
-type FormData = z.infer;
+// type FormData = z.infer;
export default function AuthForm() {
- const [isLoading, setIsLoading] = useState(false);
- const [isGithubLoading, setIsGithubLoading] = useState(false);
+ // const [isLoading, setIsLoading] = useState(false);
+ // const [isGithubLoading, setIsGithubLoading] = useState(false);
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(userAuthSchema),
- });
+ // const {
+ // register,
+ // handleSubmit,
+ // reset,
+ // formState: { errors },
+ // } = useForm({
+ // resolver: zodResolver(userAuthSchema),
+ // });
- async function onSubmit(data: FormData) {
- setIsLoading(true);
+ // async function onSubmit(data: FormData) {
+ // setIsLoading(true);
- saCheckEmailExists(data.email.toLowerCase())
- .then(async () => {
- try {
- const res = await signIn("email", {
- email: data.email.toLowerCase(),
- redirect: false,
- });
+ // saCheckEmailExists(data.email.toLowerCase())
+ // .then(async () => {
+ // try {
+ // const res = await signIn("email", {
+ // email: data.email.toLowerCase(),
+ // redirect: false,
+ // });
- if (!res?.ok) {
- throw new Error("Something went wrong.");
- }
+ // if (!res?.ok) {
+ // throw new Error("Something went wrong.");
+ // }
- toast({
- title: "Check your email",
- description:
- "We sent you a sign in link. Be sure to check your spam too.",
- });
- reset();
- } catch (err) {
- toast({
- title: "Something went wrong.",
- description: "Your signin request failed. Please try again.",
- variant: "destructive",
- });
- } finally {
- setIsLoading(false);
- }
- })
- .catch(() => {
- toast({
- title: "Account not found.",
- description:
- "You have to use the email already linked to your account.",
- variant: "destructive",
- });
- setIsLoading(false);
- });
- }
+ // toast({
+ // title: "Check your email",
+ // description:
+ // "We sent you a sign in link. Be sure to check your spam too.",
+ // });
+ // reset();
+ // } catch (err) {
+ // toast({
+ // title: "Something went wrong.",
+ // description: "Your signin request failed. Please try again.",
+ // variant: "destructive",
+ // });
+ // } finally {
+ // setIsLoading(false);
+ // }
+ // })
+ // .catch(() => {
+ // toast({
+ // title: "Account not found.",
+ // description:
+ // "You have to use the email already linked to your account.",
+ // variant: "destructive",
+ // });
+ // setIsLoading(false);
+ // });
+ // }
return (
@@ -78,11 +78,11 @@ export default function Navbar({
) : (
setIsModalOpen(false)}
>
- Sign In
+ Login
)}
diff --git a/src/components/layout/image-upload-modal.tsx b/src/components/layout/image-upload-modal.tsx
index 9612473..310a4f0 100644
--- a/src/components/layout/image-upload-modal.tsx
+++ b/src/components/layout/image-upload-modal.tsx
@@ -27,7 +27,7 @@ const fileTypes = ["image"];
export default function ImageUploadModal({
onChange,
}: {
- onChange: ControllerRenderProps["onChange"];
+ onChange: ControllerRenderProps["onChange"];
}) {
const [files, setFiles] = useState([]);
const [preview, setPreview] = useState(null);
diff --git a/src/components/layout/user-nav.tsx b/src/components/layout/user-nav.tsx
index 84127cd..a132366 100644
--- a/src/components/layout/user-nav.tsx
+++ b/src/components/layout/user-nav.tsx
@@ -1,7 +1,6 @@
"use client";
import { LayoutDashboardIcon, LogOut } from "lucide-react";
-import { signOut } from "next-auth/react";
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import {
@@ -13,13 +12,14 @@ import {
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { type CurrentUser } from "~/types";
+import { Button } from "../ui/button";
export default function UserNav({ user }: { user: CurrentUser }) {
return (
-
+
{user.name?.[0]}
@@ -39,9 +39,17 @@ export default function UserNav({ user }: { user: CurrentUser }) {
Dashboard
- signOut()}>
-
- Log out
+
+
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index b2d333f..260f2be 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,100 +1,36 @@
-import { PrismaAdapter } from "@next-auth/prisma-adapter";
-import { type NextAuthOptions } from "next-auth";
-import EmailProvider, {
- type SendVerificationRequestParams,
-} from "next-auth/providers/email";
-import GithubProvider from "next-auth/providers/github";
-import db from "~/lib/db";
-import { sendMail } from "./resend";
+import { prisma } from "@lucia-auth/adapter-prisma";
+import { github } from "@lucia-auth/oauth/providers";
+import { lucia } from "lucia";
+import { nextjs_future } from "lucia/middleware";
+import "lucia/polyfill/node";
+import db from "./db";
+import { cache } from "react";
+import * as context from "next/headers";
-const sendVerificationRequest = async ({
- identifier: email,
- url,
-}: SendVerificationRequestParams) => {
- const user = await db.user.findFirst({
- where: {
- email,
- },
- select: {
- name: true,
- },
- });
-
- try {
- await sendMail({
- toMail: email,
- type: "verification",
- data: {
- name: user?.name as string,
- url,
- },
- });
- } catch (error) {
- throw new Error(JSON.stringify(error));
- }
-};
-
-export const authOptions: NextAuthOptions = {
- adapter: PrismaAdapter(db),
- providers: [
- EmailProvider({
- sendVerificationRequest,
- }),
- GithubProvider({
- clientId: process.env.GITHUB_CLIENT_ID as string,
- clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
- }),
- ],
- pages: {
- signIn: "/signin",
+export const auth = lucia({
+ adapter: prisma(db),
+ env: process.env.NODE_ENV === "production" ? "PROD" : "DEV",
+ middleware: nextjs_future(),
+ sessionCookie: {
+ expires: false,
},
- session: {
- strategy: "jwt",
+ getUserAttributes: (user) => {
+ return {
+ name: user.name as string,
+ email: user.email as string,
+ picture: user.picture as string,
+ };
},
- callbacks: {
- async session({ token, session }) {
- if (token) {
- session.user.id = token.id;
- session.user.name = token.name;
- session.user.email = token.email;
- session.user.image = token.picture;
- }
+});
- return session;
- },
- async jwt({ token, user }) {
- const dbUser = await db.user.findFirst({
- where: {
- email: token.email,
- },
- });
+export const githubAuth = github(auth, {
+ clientId: process.env.GITHUB_CLIENT_ID ?? "",
+ clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
+});
- if (!dbUser) {
- if (user) {
- token.id = user.id;
- }
- return token;
- }
+export const getPageSession = cache(() => {
+ const authRequest = auth.handleRequest("GET", context);
+ return authRequest.validate();
+});
- return {
- id: dbUser.id,
- name: dbUser.name,
- email: dbUser.email,
- picture: dbUser.image,
- };
- },
- },
- events: {
- async signIn({ user, isNewUser }) {
- if (user && isNewUser) {
- await sendMail({
- toMail: user.email as string,
- type: "new-signin",
- data: {
- name: user.name as string,
- },
- });
- }
- },
- },
-};
+export type Auth = typeof auth;
diff --git a/src/middleware.ts b/src/middleware.ts
deleted file mode 100644
index 3c2da64..0000000
--- a/src/middleware.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { getToken } from "next-auth/jwt";
-import { withAuth } from "next-auth/middleware";
-import { NextResponse } from "next/server";
-
-export default withAuth(
- async function middleware(req) {
- const token = await getToken({ req });
- const isAuth = !!token;
-
- if (req.nextUrl.pathname === "/signin") {
- if (isAuth) {
- return NextResponse.redirect(new URL(`/dashboard`, req.url));
- }
- return null;
- }
-
- if (!isAuth) {
- return NextResponse.redirect(new URL(`/signin`, req.url));
- }
- },
- {
- callbacks: {
- authorized() {
- return true;
- },
- },
- }
-);
-
-export const config = {
- matcher: ["/dashboard/:path*", "/signin"],
-};
diff --git a/src/server/user.ts b/src/server/user.ts
index 9b6dfa8..d09b1af 100644
--- a/src/server/user.ts
+++ b/src/server/user.ts
@@ -1,15 +1,14 @@
-import { getServerSession } from "next-auth";
import { cache } from "react";
-import { authOptions } from "~/lib/auth";
import db from "~/lib/db";
+import { getPageSession } from "~/lib/auth";
import { type payload } from "~/types";
export const getUser = cache(async () => {
- const session = await getServerSession(authOptions);
+ const session = await getPageSession();
if (session) {
const user = await db.user.findFirst({
- where: { id: session.user.id },
+ where: { id: session.user.userId },
});
if (user) {
diff --git a/src/server/utils.ts b/src/server/utils.ts
index 8b8591a..c295fcc 100644
--- a/src/server/utils.ts
+++ b/src/server/utils.ts
@@ -12,10 +12,10 @@ const getImageKeyFromUrl = (url: string) => {
export async function removeUserOldImageCDN(id: string, newImageUrl: string) {
const user = await db.user.findFirst({
where: { id },
- select: { image: true },
+ select: { picture: true },
});
- const currentImageUrl = user?.image;
+ const currentImageUrl = user?.picture;
if (!currentImageUrl) return;
diff --git a/src/types/index.ts b/src/types/index.ts
index 3540d43..ede0499 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -5,19 +5,17 @@ export type CurrentUser = {
id: string;
name: string;
email: string;
- image: string;
- shortBio: string;
+ picture: string;
};
export interface payload {
name: string;
email: string;
- shortBio?: string;
- image?: string;
+ picture?: string;
}
export const settingsSchema = z.object({
- image: z.string().url(),
+ picture: z.string().url(),
name: z
.string({
required_error: "Please type your name.",
diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts
deleted file mode 100644
index a8cebdb..0000000
--- a/src/types/next-auth.d.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { type User } from "next-auth";
-import "next-auth/jwt";
-
-type UserId = string;
-
-declare module "next-auth/jwt" {
- interface JWT {
- id: UserId;
- }
-}
-
-declare module "next-auth" {
- interface Session {
- user: User & {
- id: UserId;
- };
- }
-}