diff --git a/.env.sample b/.env.sample index 384929e..386c603 100644 --- a/.env.sample +++ b/.env.sample @@ -14,7 +14,9 @@ RE_ARCHIVE_LIMIT= SPACES_KEY= SPACES_SECRET= SPACES_ENDPOINT= +SPACES_BUCKET_NAME= SPACES_REGION= +SPACES_FORCE_PATH_STYLE= # SMTP Settings NEXT_PUBLIC_EMAIL_PROVIDER= @@ -23,3 +25,9 @@ EMAIL_SERVER= # Docker postgres settings POSTGRES_PASSWORD= + +# Keycloak +NEXT_PUBLIC_KEYCLOAK_ENABLED= +KEYCLOAK_ISSUER= +KEYCLOAK_CLIENT_ID= +KEYCLOAK_CLIENT_SECRET= diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts index f072e65..c32b1fb 100644 --- a/lib/api/controllers/users/userId/deleteUserById.ts +++ b/lib/api/controllers/users/userId/deleteUserById.ts @@ -21,14 +21,19 @@ export default async function deleteUserById( }; } - // Then, we check if the provided password matches the one stored in the database - const isPasswordValid = bcrypt.compareSync(body.password, user.password); + // Then, we check if the provided password matches the one stored in the database (disabled in Keycloak integration) + if (!process.env.KEYCLOAK_CLIENT_SECRET) { + const isPasswordValid = bcrypt.compareSync( + body.password, + user.password as string + ); - if (!isPasswordValid) { - return { - response: "Invalid credentials.", - status: 401, // Unauthorized - }; + if (!isPasswordValid) { + return { + response: "Invalid credentials.", + status: 401, // Unauthorized + }; + } } // Delete the user and all related data within a transaction diff --git a/lib/api/storage/createFile.ts b/lib/api/storage/createFile.ts index 8f55b45..31554fb 100644 --- a/lib/api/storage/createFile.ts +++ b/lib/api/storage/createFile.ts @@ -14,7 +14,7 @@ export default async function createFile({ }) { if (s3Client) { const bucketParams: PutObjectCommandInput = { - Bucket: process.env.BUCKET_NAME, + Bucket: process.env.SPACES_BUCKET_NAME, Key: filePath, Body: isBase64 ? Buffer.from(data as string, "base64") : data, }; diff --git a/lib/api/storage/moveFile.ts b/lib/api/storage/moveFile.ts index bbd8887..c86728f 100644 --- a/lib/api/storage/moveFile.ts +++ b/lib/api/storage/moveFile.ts @@ -5,7 +5,7 @@ import removeFile from "./removeFile"; export default async function moveFile(from: string, to: string) { if (s3Client) { - const Bucket = process.env.BUCKET_NAME; + const Bucket = process.env.SPACES_BUCKET_NAME; const copyParams = { Bucket: Bucket, diff --git a/lib/api/storage/readFile.ts b/lib/api/storage/readFile.ts index 01e6fda..64ff8d7 100644 --- a/lib/api/storage/readFile.ts +++ b/lib/api/storage/readFile.ts @@ -20,7 +20,7 @@ export default async function readFile(filePath: string) { if (s3Client) { const bucketParams: GetObjectCommandInput = { - Bucket: process.env.BUCKET_NAME, + Bucket: process.env.SPACES_BUCKET_NAME, Key: filePath, }; diff --git a/lib/api/storage/removeFile.ts b/lib/api/storage/removeFile.ts index 491e24c..e332829 100644 --- a/lib/api/storage/removeFile.ts +++ b/lib/api/storage/removeFile.ts @@ -6,7 +6,7 @@ import { PutObjectCommandInput, DeleteObjectCommand } from "@aws-sdk/client-s3"; export default async function removeFile({ filePath }: { filePath: string }) { if (s3Client) { const bucketParams: PutObjectCommandInput = { - Bucket: process.env.BUCKET_NAME, + Bucket: process.env.SPACES_BUCKET_NAME, Key: filePath, }; diff --git a/lib/api/storage/removeFolder.ts b/lib/api/storage/removeFolder.ts index 7383b88..e9f7d3b 100644 --- a/lib/api/storage/removeFolder.ts +++ b/lib/api/storage/removeFolder.ts @@ -40,7 +40,7 @@ async function emptyS3Directory(bucket: string, dir: string) { export default async function removeFolder({ filePath }: { filePath: string }) { if (s3Client) { try { - await emptyS3Directory(process.env.BUCKET_NAME as string, filePath); + await emptyS3Directory(process.env.SPACES_BUCKET_NAME as string, filePath); } catch (err) { console.log("Error", err); } diff --git a/lib/api/storage/s3Client.ts b/lib/api/storage/s3Client.ts index 8b0ccd5..cebba5a 100644 --- a/lib/api/storage/s3Client.ts +++ b/lib/api/storage/s3Client.ts @@ -6,7 +6,7 @@ const s3Client: S3 | undefined = process.env.SPACES_KEY && process.env.SPACES_SECRET ? new S3({ - forcePathStyle: false, + forcePathStyle: !!process.env.SPACES_FORCE_PATH_STYLE, endpoint: process.env.SPACES_ENDPOINT, region: process.env.SPACES_REGION, credentials: { diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index 0b4ab7e..4b5dd59 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -9,10 +9,15 @@ import { Adapter } from "next-auth/adapters"; import sendVerificationRequest from "@/lib/api/sendVerificationRequest"; import { Provider } from "next-auth/providers"; import verifySubscription from "@/lib/api/verifySubscription"; +import KeycloakProvider from "next-auth/providers/keycloak"; const emailEnabled = process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; +const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true"; + +const adapter = PrismaAdapter(prisma); + const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const providers: Provider[] = [ @@ -59,7 +64,7 @@ const providers: Provider[] = [ }), ]; -if (emailEnabled) +if (emailEnabled) { providers.push( EmailProvider({ server: process.env.EMAIL_SERVER, @@ -70,9 +75,36 @@ if (emailEnabled) }, }) ); +} + +if (keycloakEnabled) { + providers.push( + KeycloakProvider({ + id: "keycloak", + name: "Keycloak", + clientId: process.env.KEYCLOAK_CLIENT_ID!, + clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, + issuer: process.env.KEYCLOAK_ISSUER, + profile: (profile) => { + return { + id: profile.sub, + username: profile.preferred_username, + name: profile.name ?? profile.preferred_username, + email: profile.email, + image: profile.picture, + }; + }, + }) + ); + const _linkAccount = adapter.linkAccount; + adapter.linkAccount = (account) => { + const { "not-before-policy": _, refresh_expires_in, ...data } = account; + return _linkAccount ? _linkAccount(data) : undefined; + }; +} export const authOptions: AuthOptions = { - adapter: PrismaAdapter(prisma) as Adapter, + adapter: adapter as Adapter, session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 days @@ -85,7 +117,8 @@ export const authOptions: AuthOptions = { callbacks: { async jwt({ token, trigger, user }) { token.sub = token.sub ? Number(token.sub) : undefined; - if (trigger === "signIn") token.id = user?.id as number; + if (trigger === "signIn" || trigger === "signUp") + token.id = user?.id as number; return token; }, diff --git a/pages/login.tsx b/pages/login.tsx index b66f4e0..b1a7ce4 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -12,6 +12,7 @@ interface FormData { } const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; +const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED; export default function Login() { const [submitLoader, setSubmitLoader] = useState(false); @@ -47,6 +48,18 @@ export default function Login() { } } + async function loginUserKeycloak() { + setSubmitLoader(true); + + const load = toast.loading("Authenticating..."); + + const res = await signIn("keycloak", {}); + + toast.dismiss(load); + + setSubmitLoader(false); + } + return (
@@ -102,6 +115,15 @@ export default function Login() { className=" w-full text-center" loading={submitLoader} /> + {process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true" ? ( + + ) : undefined} {process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? undefined : (
diff --git a/pages/settings/delete.tsx b/pages/settings/delete.tsx index a901400..3718c61 100644 --- a/pages/settings/delete.tsx +++ b/pages/settings/delete.tsx @@ -25,7 +25,7 @@ export default function Password() { }, }; - if (password == "") { + if (process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" && password == "") { return toast.error("Please fill the required fields."); } @@ -78,18 +78,20 @@ export default function Password() { . This action is irreversible!

-
-

- Confirm Your Password -

+ {process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? ( +
+

+ Confirm Your Password +

- setPassword(e.target.value)} - placeholder="••••••••••••••" - type="password" - /> -
+ setPassword(e.target.value)} + placeholder="••••••••••••••" + type="password" + /> +
+ ) : undefined} {process.env.NEXT_PUBLIC_STRIPE ? (
diff --git a/prisma/migrations/20231105202241_modify_user_password/migration.sql b/prisma/migrations/20231105202241_modify_user_password/migration.sql new file mode 100644 index 0000000..0b600a7 --- /dev/null +++ b/prisma/migrations/20231105202241_modify_user_password/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; diff --git a/prisma/migrations/20231108232127_recreate_table_account/migration.sql b/prisma/migrations/20231108232127_recreate_table_account/migration.sql new file mode 100644 index 0000000..e9c9a7a --- /dev/null +++ b/prisma/migrations/20231108232127_recreate_table_account/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" INTEGER 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") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 269ea53..c4d72c1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,25 @@ datasource db { url = env("DATABASE_URL") } +model Account { + id String @id @default(cuid()) + userId Int + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + model User { id Int @id @default(autoincrement()) name String @@ -17,7 +36,9 @@ model User { emailVerified DateTime? image String? - password String + accounts Account[] + + password String? collections Collection[] tags Tag[] @@ -101,13 +122,13 @@ model Link { name String url String description String @default("") - + pinnedBy User[] - + collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int tags Tag[] - + textContent String? screenshotPath String? @@ -115,7 +136,7 @@ model Link { readabilityPath String? lastPreserved DateTime? - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) } @@ -135,7 +156,7 @@ model Tag { model Subscription { id Int @id @default(autoincrement()) - active Boolean + active Boolean stripeSubscriptionId String @unique currentPeriodStart DateTime currentPeriodEnd DateTime diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts index df4b4ff..d2bdbb8 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -13,8 +13,14 @@ declare global { SPACES_KEY?: string; SPACES_SECRET?: string; SPACES_ENDPOINT?: string; - BUCKET_NAME?: string; + SPACES_BUCKET_NAME?: string; SPACES_REGION?: string; + SPACES_FORCE_PATH_STYLE?: string; + + NEXT_PUBLIC_KEYCLOAK_ENABLED?: string; + KEYCLOAK_ISSUER?: string; + KEYCLOAK_CLIENT_ID?: string; + KEYCLOAK_CLIENT_SECRET?: string; NEXT_PUBLIC_EMAIL_PROVIDER?: string; EMAIL_FROM?: string;