Merge pull request #310 from linkwarden/keycloak-integration
Keycloak integration
This commit is contained in:
commit
c73f13a9b0
|
@ -14,7 +14,9 @@ RE_ARCHIVE_LIMIT=
|
||||||
SPACES_KEY=
|
SPACES_KEY=
|
||||||
SPACES_SECRET=
|
SPACES_SECRET=
|
||||||
SPACES_ENDPOINT=
|
SPACES_ENDPOINT=
|
||||||
|
SPACES_BUCKET_NAME=
|
||||||
SPACES_REGION=
|
SPACES_REGION=
|
||||||
|
SPACES_FORCE_PATH_STYLE=
|
||||||
|
|
||||||
# SMTP Settings
|
# SMTP Settings
|
||||||
NEXT_PUBLIC_EMAIL_PROVIDER=
|
NEXT_PUBLIC_EMAIL_PROVIDER=
|
||||||
|
@ -23,3 +25,9 @@ EMAIL_SERVER=
|
||||||
|
|
||||||
# Docker postgres settings
|
# Docker postgres settings
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Keycloak
|
||||||
|
NEXT_PUBLIC_KEYCLOAK_ENABLED=
|
||||||
|
KEYCLOAK_ISSUER=
|
||||||
|
KEYCLOAK_CLIENT_ID=
|
||||||
|
KEYCLOAK_CLIENT_SECRET=
|
||||||
|
|
|
@ -21,8 +21,12 @@ export default async function deleteUserById(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, we check if the provided password matches the one stored in the database
|
// Then, we check if the provided password matches the one stored in the database (disabled in Keycloak integration)
|
||||||
const isPasswordValid = bcrypt.compareSync(body.password, user.password);
|
if (!process.env.KEYCLOAK_CLIENT_SECRET) {
|
||||||
|
const isPasswordValid = bcrypt.compareSync(
|
||||||
|
body.password,
|
||||||
|
user.password as string
|
||||||
|
);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return {
|
return {
|
||||||
|
@ -30,6 +34,7 @@ export default async function deleteUserById(
|
||||||
status: 401, // Unauthorized
|
status: 401, // Unauthorized
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the user and all related data within a transaction
|
// Delete the user and all related data within a transaction
|
||||||
await prisma
|
await prisma
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default async function createFile({
|
||||||
}) {
|
}) {
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
const bucketParams: PutObjectCommandInput = {
|
const bucketParams: PutObjectCommandInput = {
|
||||||
Bucket: process.env.BUCKET_NAME,
|
Bucket: process.env.SPACES_BUCKET_NAME,
|
||||||
Key: filePath,
|
Key: filePath,
|
||||||
Body: isBase64 ? Buffer.from(data as string, "base64") : data,
|
Body: isBase64 ? Buffer.from(data as string, "base64") : data,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import removeFile from "./removeFile";
|
||||||
|
|
||||||
export default async function moveFile(from: string, to: string) {
|
export default async function moveFile(from: string, to: string) {
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
const Bucket = process.env.BUCKET_NAME;
|
const Bucket = process.env.SPACES_BUCKET_NAME;
|
||||||
|
|
||||||
const copyParams = {
|
const copyParams = {
|
||||||
Bucket: Bucket,
|
Bucket: Bucket,
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default async function readFile(filePath: string) {
|
||||||
|
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
const bucketParams: GetObjectCommandInput = {
|
const bucketParams: GetObjectCommandInput = {
|
||||||
Bucket: process.env.BUCKET_NAME,
|
Bucket: process.env.SPACES_BUCKET_NAME,
|
||||||
Key: filePath,
|
Key: filePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { PutObjectCommandInput, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||||
export default async function removeFile({ filePath }: { filePath: string }) {
|
export default async function removeFile({ filePath }: { filePath: string }) {
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
const bucketParams: PutObjectCommandInput = {
|
const bucketParams: PutObjectCommandInput = {
|
||||||
Bucket: process.env.BUCKET_NAME,
|
Bucket: process.env.SPACES_BUCKET_NAME,
|
||||||
Key: filePath,
|
Key: filePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ async function emptyS3Directory(bucket: string, dir: string) {
|
||||||
export default async function removeFolder({ filePath }: { filePath: string }) {
|
export default async function removeFolder({ filePath }: { filePath: string }) {
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
try {
|
try {
|
||||||
await emptyS3Directory(process.env.BUCKET_NAME as string, filePath);
|
await emptyS3Directory(process.env.SPACES_BUCKET_NAME as string, filePath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Error", err);
|
console.log("Error", err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ const s3Client: S3 | undefined =
|
||||||
process.env.SPACES_KEY &&
|
process.env.SPACES_KEY &&
|
||||||
process.env.SPACES_SECRET
|
process.env.SPACES_SECRET
|
||||||
? new S3({
|
? new S3({
|
||||||
forcePathStyle: false,
|
forcePathStyle: !!process.env.SPACES_FORCE_PATH_STYLE,
|
||||||
endpoint: process.env.SPACES_ENDPOINT,
|
endpoint: process.env.SPACES_ENDPOINT,
|
||||||
region: process.env.SPACES_REGION,
|
region: process.env.SPACES_REGION,
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|
|
@ -9,10 +9,15 @@ import { Adapter } from "next-auth/adapters";
|
||||||
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
|
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
|
||||||
import { Provider } from "next-auth/providers";
|
import { Provider } from "next-auth/providers";
|
||||||
import verifySubscription from "@/lib/api/verifySubscription";
|
import verifySubscription from "@/lib/api/verifySubscription";
|
||||||
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
|
|
||||||
const emailEnabled =
|
const emailEnabled =
|
||||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
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 STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
|
@ -59,7 +64,7 @@ const providers: Provider[] = [
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (emailEnabled)
|
if (emailEnabled) {
|
||||||
providers.push(
|
providers.push(
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
server: process.env.EMAIL_SERVER,
|
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 = {
|
export const authOptions: AuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma) as Adapter,
|
adapter: adapter as Adapter,
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
@ -85,7 +117,8 @@ export const authOptions: AuthOptions = {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, trigger, user }) {
|
async jwt({ token, trigger, user }) {
|
||||||
token.sub = token.sub ? Number(token.sub) : undefined;
|
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;
|
return token;
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||||
|
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED;
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
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 (
|
return (
|
||||||
<CenteredForm text="Sign in to your account">
|
<CenteredForm text="Sign in to your account">
|
||||||
<form onSubmit={loginUser}>
|
<form onSubmit={loginUser}>
|
||||||
|
@ -102,6 +115,15 @@ export default function Login() {
|
||||||
className=" w-full text-center"
|
className=" w-full text-center"
|
||||||
loading={submitLoader}
|
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 ===
|
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
|
||||||
"true" ? undefined : (
|
"true" ? undefined : (
|
||||||
<div className="flex items-baseline gap-1 justify-center">
|
<div className="flex items-baseline gap-1 justify-center">
|
||||||
|
|
|
@ -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.");
|
return toast.error("Please fill the required fields.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ export default function Password() {
|
||||||
. This action is irreversible!
|
. This action is irreversible!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-black dark:text-white">
|
<p className="mb-2 text-black dark:text-white">
|
||||||
Confirm Your Password
|
Confirm Your Password
|
||||||
|
@ -90,6 +91,7 @@ export default function Password() {
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||||
<fieldset className="border rounded-md p-2 border-sky-500">
|
<fieldset className="border rounded-md p-2 border-sky-500">
|
||||||
|
|
|
@ -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")
|
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 {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
|
@ -17,7 +36,9 @@ model User {
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
||||||
password String
|
accounts Account[]
|
||||||
|
|
||||||
|
password String?
|
||||||
|
|
||||||
collections Collection[]
|
collections Collection[]
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
|
|
|
@ -13,8 +13,14 @@ declare global {
|
||||||
SPACES_KEY?: string;
|
SPACES_KEY?: string;
|
||||||
SPACES_SECRET?: string;
|
SPACES_SECRET?: string;
|
||||||
SPACES_ENDPOINT?: string;
|
SPACES_ENDPOINT?: string;
|
||||||
BUCKET_NAME?: string;
|
SPACES_BUCKET_NAME?: string;
|
||||||
SPACES_REGION?: 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;
|
NEXT_PUBLIC_EMAIL_PROVIDER?: string;
|
||||||
EMAIL_FROM?: string;
|
EMAIL_FROM?: string;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue