feat: basic support for Keycloak (OIDC)
This commit is contained in:
parent
518b94b1f4
commit
946eed3773
|
@ -23,3 +23,9 @@ EMAIL_SERVER=
|
|||
|
||||
# Docker postgres settings
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Keycloak
|
||||
NEXT_PUBLIC_KEYCLOAK_ENABLED=
|
||||
KEYCLOAK_ISSUER=
|
||||
KEYCLOAK_CLIENT=
|
||||
KEYCLOAK_CLIENT_SECRET=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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 (
|
||||
<CenteredForm text="Sign in to your account">
|
||||
<form onSubmit={loginUser}>
|
||||
|
@ -102,6 +115,15 @@ export default function Login() {
|
|||
className=" w-full text-center"
|
||||
loading={submitLoader}
|
||||
/>
|
||||
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true" ? (
|
||||
<SubmitButton
|
||||
type="button"
|
||||
onClick={loginUserKeycloak}
|
||||
label="Sign in with Keycloak"
|
||||
className=" w-full text-center"
|
||||
loading={submitLoader}
|
||||
/>
|
||||
) : undefined}
|
||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
|
||||
"true" ? undefined : (
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL;
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Ŝarĝante…
Reference in New Issue