From 946eed3773c4a1e1fc4a53b14cbb06ad35881e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hru=C5=A1ka?= Date: Thu, 9 Nov 2023 11:41:08 +0100 Subject: [PATCH 1/2] feat: basic support for Keycloak (OIDC) --- .env.sample | 6 +++ .../users/userId/deleteUserById.ts | 2 +- pages/api/v1/auth/[...nextauth].ts | 39 +++++++++++++++++-- pages/login.tsx | 22 +++++++++++ .../migration.sql | 2 + .../migration.sql | 23 +++++++++++ prisma/schema.prisma | 33 +++++++++++++--- types/enviornment.d.ts | 5 +++ 8 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20231105202241_modify_user_password/migration.sql create mode 100644 prisma/migrations/20231108232127_recreate_table_account/migration.sql diff --git a/.env.sample b/.env.sample index 384929e..ee04f7d 100644 --- a/.env.sample +++ b/.env.sample @@ -23,3 +23,9 @@ EMAIL_SERVER= # Docker postgres settings POSTGRES_PASSWORD= + +# Keycloak +NEXT_PUBLIC_KEYCLOAK_ENABLED= +KEYCLOAK_ISSUER= +KEYCLOAK_CLIENT= +KEYCLOAK_CLIENT_SECRET= diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts index f072e65..19090b5 100644 --- a/lib/api/controllers/users/userId/deleteUserById.ts +++ b/lib/api/controllers/users/userId/deleteUserById.ts @@ -22,7 +22,7 @@ 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); + const isPasswordValid = bcrypt.compareSync(body.password, user.password || ""); if (!isPasswordValid) { return { diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index 0b4ab7e..46976a5 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -9,10 +9,16 @@ 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; + +const adapter = PrismaAdapter(prisma); + const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const providers: Provider[] = [ @@ -59,7 +65,7 @@ const providers: Provider[] = [ }), ]; -if (emailEnabled) +if (emailEnabled) { providers.push( EmailProvider({ server: process.env.EMAIL_SERVER, @@ -70,9 +76,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 +118,7 @@ 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/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 13b755f..5b283df 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[] @@ -98,13 +119,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? @@ -112,7 +133,7 @@ model Link { readabilityPath String? lastPreserved DateTime? - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) } @@ -132,7 +153,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..24c1c44 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -16,6 +16,11 @@ declare global { BUCKET_NAME?: string; SPACES_REGION?: 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; EMAIL_SERVER?: string; From 836dc10c2b7d6c18c2aaf1fec708c573151a1f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hru=C5=A1ka?= Date: Thu, 9 Nov 2023 11:41:29 +0100 Subject: [PATCH 2/2] fix: s3 integration + custom s3 (minio) support --- .env.sample | 2 ++ lib/api/storage/createFile.ts | 2 +- lib/api/storage/moveFile.ts | 2 +- lib/api/storage/readFile.ts | 2 +- lib/api/storage/removeFile.ts | 2 +- lib/api/storage/removeFolder.ts | 2 +- lib/api/storage/s3Client.ts | 2 +- types/enviornment.d.ts | 3 ++- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.env.sample b/.env.sample index ee04f7d..2ca5830 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= 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/types/enviornment.d.ts b/types/enviornment.d.ts index 24c1c44..d2bdbb8 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -13,8 +13,9 @@ 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;