Merge pull request #331 from linkwarden/feat/extra-login-providers

Feat/Added Authentic Support
This commit is contained in:
Daniel 2023-12-07 21:02:17 +03:30 committed by GitHub
commit add781451a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 215 additions and 86 deletions

View File

@ -12,6 +12,7 @@ PAGINATION_TAKE_COUNT=
STORAGE_FOLDER= STORAGE_FOLDER=
AUTOSCROLL_TIMEOUT= AUTOSCROLL_TIMEOUT=
NEXT_PUBLIC_DISABLE_REGISTRATION= NEXT_PUBLIC_DISABLE_REGISTRATION=
NEXT_PUBLIC_DISABLE_LOGIN=
RE_ARCHIVE_LIMIT= RE_ARCHIVE_LIMIT=
NEXT_PUBLIC_MAX_UPLOAD_SIZE= NEXT_PUBLIC_MAX_UPLOAD_SIZE=
@ -33,3 +34,9 @@ NEXT_PUBLIC_KEYCLOAK_ENABLED=
KEYCLOAK_ISSUER= KEYCLOAK_ISSUER=
KEYCLOAK_CLIENT_ID= KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET= KEYCLOAK_CLIENT_SECRET=
# Authentik
NEXT_PUBLIC_AUTHENTIK_ENABLED=
AUTHENTIK_ISSUER=
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=

View File

@ -3,7 +3,7 @@
<h1>Linkwarden</h1> <h1>Linkwarden</h1>
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a> <a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a>
<img alt="GitHub commits since latest release (by SemVer including pre-releases)" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/v1.1.0/dev"> <img alt="GitHub commits since latest release (by SemVer including pre-releases)" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/latest/dev">
<img src="https://img.shields.io/github/languages/top/linkwarden/linkwarden?style=flat-square" alt="Top Language"> <img src="https://img.shields.io/github/languages/top/linkwarden/linkwarden?style=flat-square" alt="Top Language">
<img src="https://img.shields.io/github/stars/linkwarden/linkwarden?style=flat-square" alt="Github Stars"> <img src="https://img.shields.io/github/stars/linkwarden/linkwarden?style=flat-square" alt="Github Stars">

View File

@ -0,0 +1,35 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
type Props = {
onClick?: Function;
icon?: IconProp;
label: string;
loading?: boolean;
className?: string;
type?: "button" | "submit" | "reset" | undefined;
};
export default function AccentSubmitButton({
onClick,
icon,
label,
loading,
className,
type,
}: Props) {
return (
<button
type={type ? type : undefined}
className={`border primary-btn-gradient select-none duration-200 bg-black border-[oklch(var(--p))] hover:border-[#0070b5] rounded-lg text-center px-4 py-2 text-white active:scale-95 tracking-wider w-fit flex justify-center items-center gap-2 ${
className || ""
}`}
onClick={() => {
if (loading !== undefined && !loading && onClick) onClick();
}}
>
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
<p className="font-bold">{label}</p>
</button>
);
}

View File

@ -166,7 +166,7 @@ export default function PreservedFormats() {
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center"> <div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
{link?.collection.ownerId === session.data?.user.id ? ( {link?.collection.ownerId === session.data?.user.id ? (
<div <div
className={`btn btn-accent text-white ${ className={`btn btn-accent dark:border-violet-400 text-white ${
link?.pdfPath && link?.pdfPath &&
link?.screenshotPath && link?.screenshotPath &&
link?.pdfPath !== "pending" && link?.pdfPath !== "pending" &&

View File

@ -111,7 +111,7 @@ export default function EditCollectionModal({
</div> </div>
<button <button
className="btn btn-accent text-white w-fit ml-auto" className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
onClick={submit} onClick={submit}
> >
Save Save

View File

@ -438,7 +438,7 @@ export default function EditCollectionSharingModal({
{permissions === true && ( {permissions === true && (
<button <button
className="btn btn-accent text-white w-fit ml-auto mt-3" className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto mt-3"
onClick={submit} onClick={submit}
> >
Save Save

View File

@ -159,7 +159,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
</div> </div>
<div className="flex justify-end items-center mt-5"> <div className="flex justify-end items-center mt-5">
<button className="btn btn-accent text-white" onClick={submit}> <button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Save Save
</button> </button>
</div> </div>

View File

@ -116,7 +116,7 @@ export default function NewCollectionModal({ onClose }: Props) {
</div> </div>
<button <button
className="btn btn-accent text-white w-fit ml-auto" className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
onClick={submit} onClick={submit}
> >
Create Collection Create Collection

View File

@ -192,7 +192,10 @@ export default function NewLinkModal({ onClose }: Props) {
<p>{optionsExpanded ? "Hide" : "More"} Options</p> <p>{optionsExpanded ? "Hide" : "More"} Options</p>
</div> </div>
<button className="btn btn-accent text-white" onClick={submit}> <button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Link Create Link
</button> </button>
</div> </div>

View File

@ -191,7 +191,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
<div className="flex flex-col-reverse sm:flex-row sm:gap-3 items-center justify-center"> <div className="flex flex-col-reverse sm:flex-row sm:gap-3 items-center justify-center">
{link?.collection.ownerId === session.data?.user.id ? ( {link?.collection.ownerId === session.data?.user.id ? (
<div <div
className={`btn btn-accent text-white ${ className={`btn btn-accent dark:border-violet-400 text-white ${
link?.pdfPath && link?.pdfPath &&
link?.screenshotPath && link?.screenshotPath &&
link?.pdfPath !== "pending" && link?.pdfPath !== "pending" &&

View File

@ -237,7 +237,10 @@ export default function UploadFileModal({ onClose }: Props) {
<p>{optionsExpanded ? "Hide" : "More"} Options</p> <p>{optionsExpanded ? "Hide" : "More"} Options</p>
</div> </div>
<button className="btn btn-accent text-white" onClick={submit}> <button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Link Create Link
</button> </button>
</div> </div>

View File

@ -68,7 +68,7 @@ export default function Navbar() {
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
className="flex items-center group btn btn-accent text-white btn-sm px-2" className="flex items-center group btn btn-accent dark:border-violet-400 text-white btn-sm px-2"
> >
<FontAwesomeIcon icon={faPlus} className="w-5 h-5" /> <FontAwesomeIcon icon={faPlus} className="w-5 h-5" />
<FontAwesomeIcon <FontAwesomeIcon

View File

@ -20,7 +20,7 @@ export default function NoLinksFound({ text }: Props) {
onClick={() => { onClick={() => {
setNewLinkModal(true); setNewLinkModal(true);
}} }}
className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent text-white group" className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent dark:border-violet-400 text-white group"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faPlus} icon={faPlus}

View File

@ -21,7 +21,7 @@ export default function SubmitButton({
return ( return (
<button <button
type={type ? type : undefined} type={type ? type : undefined}
className={`btn btn-accent text-white tracking-wider w-fit flex items-center gap-2 ${ className={`btn btn-accent dark:border-violet-400 text-white tracking-wider w-fit flex items-center gap-2 ${
className || "" className || ""
}`} }`}
onClick={() => { onClick={() => {

View File

@ -14,7 +14,7 @@ export default async function postLink(
userId: number userId: number
) { ) {
try { try {
if (link.url) new URL(link.url); new URL(link.url || "");
} catch (error) { } catch (error) {
return { return {
response: response:

View File

@ -10,11 +10,13 @@ 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"; import KeycloakProvider from "next-auth/providers/keycloak";
import AuthentikProvider from "next-auth/providers/authentik";
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 keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
const adapter = PrismaAdapter(prisma); const adapter = PrismaAdapter(prisma);
@ -103,6 +105,34 @@ if (keycloakEnabled) {
}; };
} }
if (authentikEnabled) {
console.log(authentikEnabled)
providers.push(
AuthentikProvider({
id: "authentik",
name: "Authentik",
clientId: process.env.AUTHENTIK_CLIENT_ID!,
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET!,
issuer: process.env.AUTHENTIK_ISSUER,
profile: (profile) => {
console.log(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: adapter as Adapter, adapter: adapter as Adapter,
session: { session: {

View File

@ -6,6 +6,7 @@ import { useSession } from "next-auth/react";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import AccentSubmitButton from "@/components/AccentSubmitButton";
export default function ChooseUsername() { export default function ChooseUsername() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
@ -72,10 +73,10 @@ export default function ChooseUsername() {
</p> </p>
</div> </div>
<SubmitButton <AccentSubmitButton
type="submit" type="submit"
label="Complete Registration" label="Complete Registration"
className="mt-2 w-full text-center" className="mt-2 w-full"
loading={submitLoader} loading={submitLoader}
/> />

View File

@ -192,7 +192,7 @@ export default function Dashboard() {
onClick={() => { onClick={() => {
setNewLinkModal(true); setNewLinkModal(true);
}} }}
className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent text-white group" className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent dark:border-violet-400 text-white group"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faPlus} icon={faPlus}

View File

@ -1,4 +1,4 @@
import SubmitButton from "@/components/SubmitButton"; import AccentSubmitButton from "@/components/AccentSubmitButton";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
@ -73,10 +73,10 @@ export default function Forgot() {
/> />
</div> </div>
<SubmitButton <AccentSubmitButton
type="submit" type="submit"
label="Send Login Link" label="Send Login Link"
className="mt-2 w-full text-center" className="mt-2 w-full"
loading={submitLoader} loading={submitLoader}
/> />
<div className="flex items-baseline gap-1 justify-center"> <div className="flex items-baseline gap-1 justify-center">

View File

@ -244,7 +244,9 @@ export default function Index() {
: undefined} : undefined}
</p> </p>
{link?.name ? <p>{link?.description}</p> : undefined} {link?.name ? (
<p>{unescapeString(link?.description)}</p>
) : undefined}
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import SubmitButton from "@/components/SubmitButton"; import AccentSubmitButton from "@/components/AccentSubmitButton";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
@ -13,6 +13,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; const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED;
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED;
export default function Login() { export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
@ -60,10 +61,24 @@ export default function Login() {
setSubmitLoader(false); setSubmitLoader(false);
} }
async function loginUserAuthentik() {
setSubmitLoader(true);
const load = toast.loading("Authenticating...");
const res = await signIn("authentik", {});
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}>
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content"> <div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
{process.env.NEXT_PUBLIC_DISABLE_LOGIN !== "true" ? (
<>
<p className="text-3xl text-center font-extralight"> <p className="text-3xl text-center font-extralight">
Enter your credentials Enter your credentials
</p> </p>
@ -81,7 +96,9 @@ export default function Login() {
placeholder="johnny" placeholder="johnny"
value={form.username} value={form.username}
className="bg-base-100" className="bg-base-100"
onChange={(e) => setForm({ ...form, username: e.target.value })} onChange={(e) =>
setForm({ ...form, username: e.target.value })
}
/> />
</div> </div>
@ -93,29 +110,45 @@ export default function Login() {
placeholder="••••••••••••••" placeholder="••••••••••••••"
value={form.password} value={form.password}
className="bg-base-100" className="bg-base-100"
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) =>
setForm({ ...form, password: e.target.value })
}
/> />
{emailEnabled && ( {emailEnabled && (
<div className="w-fit ml-auto mt-1"> <div className="w-fit ml-auto mt-1">
<Link href={"/forgot"} className="text-neutral font-semibold"> <Link
href={"/forgot"}
className="text-neutral font-semibold"
>
Forgot Password? Forgot Password?
</Link> </Link>
</div> </div>
)} )}
</div> </div>
<SubmitButton <AccentSubmitButton
type="submit" type="submit"
label="Login" label="Login"
className=" w-full text-center" className="w-full text-center"
loading={submitLoader} loading={submitLoader}
/> />
</>
) : undefined}
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true" ? ( {process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true" ? (
<SubmitButton <AccentSubmitButton
type="button" type="button"
onClick={loginUserKeycloak} onClick={loginUserKeycloak}
label="Sign in with Keycloak" label="Sign in with Keycloak"
className=" w-full text-center" className="w-full text-center"
loading={submitLoader}
/>
) : undefined}
{process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true" ? (
<AccentSubmitButton
type="button"
onClick={loginUserAuthentik}
label="Sign in with Authentiks"
className="w-full text-center"
loading={submitLoader} loading={submitLoader}
/> />
) : undefined} ) : undefined}

View File

@ -1,11 +1,11 @@
import Link from "next/link"; import Link from "next/link";
import { useState, FormEvent } from "react"; import { useState, FormEvent } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import AccentSubmitButton from "@/components/AccentSubmitButton";
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@ -220,12 +220,12 @@ export default function Register() {
</div> </div>
) : undefined} ) : undefined}
<button <AccentSubmitButton
type="submit" type="submit"
className={`border primary-btn-gradient select-none duration-100 bg-black border-[#0071B7] hover:border-[#059bf8] rounded-lg text-center px-4 py-2 text-slate-200 hover:text-white `} label="Sign Up"
> className="w-full"
<p className="text-center w-full font-bold">Sign Up</p> loading={submitLoader}
</button> />
<div className="flex items-baseline gap-1 justify-center"> <div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-neutral">Already have an account?</p> <p className="w-fit text-neutral">Already have an account?</p>
<Link href={"/login"} className="block font-bold"> <Link href={"/login"} className="block font-bold">

View File

@ -1,6 +1,10 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClose } from "@fortawesome/free-solid-svg-icons"; import {
faClose,
faFileExport,
faFileImport,
} from "@fortawesome/free-solid-svg-icons";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
@ -247,10 +251,14 @@ export default function Account() {
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
className="flex gap-2 text-sm btn btn-outline btn-neutral btn-xs" className="flex gap-2 text-sm btn btn-outline btn-neutral group"
id="import-dropdown" id="import-dropdown"
> >
Import From <FontAwesomeIcon
icon={faFileImport}
className="w-5 h-5 duration-100"
/>
<p>Import From</p>
</div> </div>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60"> <ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li> <li>
@ -300,8 +308,12 @@ export default function Account() {
<div> <div>
<p className="mb-2">Download your data instantly.</p> <p className="mb-2">Download your data instantly.</p>
<Link className="w-fit" href="/api/v1/migration"> <Link className="w-fit" href="/api/v1/migration">
<div className="btn btn-outline btn-neutral btn-xs"> <div className="flex w-fit gap-2 text-sm btn btn-outline btn-neutral group">
Export Data <FontAwesomeIcon
icon={faFileExport}
className="w-5 h-5 duration-100"
/>
<p>Export Data</p>
</div> </div>
</Link> </Link>
</div> </div>

View File

@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { Plan } from "@/types/global"; import { Plan } from "@/types/global";
import AccentSubmitButton from "@/components/AccentSubmitButton";
export default function Subscribe() { export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
@ -47,7 +48,7 @@ export default function Subscribe() {
</p> </p>
</div> </div>
<div className="flex text-white dark:text-black gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative"> <div className="flex gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
<button <button
onClick={() => setPlan(Plan.monthly)} onClick={() => setPlan(Plan.monthly)}
className={`w-full duration-100 text-sm rounded-lg p-1 ${ className={`w-full duration-100 text-sm rounded-lg p-1 ${
@ -95,14 +96,13 @@ export default function Subscribe() {
</fieldset> </fieldset>
</div> </div>
<button <AccentSubmitButton
className={`border primary-btn-gradient select-none duration-100 bg-black border-[#0071B7] hover:border-[#059bf8] rounded-lg text-center px-4 py-2 text-slate-200 hover:text-white `} type="button"
onClick={() => { label="Complete Subscription!"
if (!submitLoader) submit(); className="w-full"
}} onClick={submit}
> loading={submitLoader}
<p className="text-center w-full font-bold">Complete Subscription!</p> />
</button>
<div <div
onClick={() => signOut()} onClick={() => signOut()}

View File

@ -181,11 +181,11 @@ body {
} }
.primary-btn-gradient { .primary-btn-gradient {
box-shadow: inset 0px -10px 10px #0071b7; box-shadow: inset 0px -9px 10px oklch(var(--p));
} }
.primary-btn-gradient:hover { .primary-btn-gradient:hover {
box-shadow: inset 0px -15px 10px #059bf8; box-shadow: inset 0px -20px 40px #00436c;
} }
.line-break * { .line-break * {