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=
AUTOSCROLL_TIMEOUT=
NEXT_PUBLIC_DISABLE_REGISTRATION=
NEXT_PUBLIC_DISABLE_LOGIN=
RE_ARCHIVE_LIMIT=
NEXT_PUBLIC_MAX_UPLOAD_SIZE=
@ -33,3 +34,9 @@ NEXT_PUBLIC_KEYCLOAK_ENABLED=
KEYCLOAK_ISSUER=
KEYCLOAK_CLIENT_ID=
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>
<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/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">
{link?.collection.ownerId === session.data?.user.id ? (
<div
className={`btn btn-accent text-white ${
className={`btn btn-accent dark:border-violet-400 text-white ${
link?.pdfPath &&
link?.screenshotPath &&
link?.pdfPath !== "pending" &&

View File

@ -111,7 +111,7 @@ export default function EditCollectionModal({
</div>
<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}
>
Save

View File

@ -438,7 +438,7 @@ export default function EditCollectionSharingModal({
{permissions === true && (
<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}
>
Save

View File

@ -159,7 +159,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
</div>
<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
</button>
</div>

View File

@ -116,7 +116,7 @@ export default function NewCollectionModal({ onClose }: Props) {
</div>
<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}
>
Create Collection

View File

@ -192,7 +192,10 @@ export default function NewLinkModal({ onClose }: Props) {
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
</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
</button>
</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">
{link?.collection.ownerId === session.data?.user.id ? (
<div
className={`btn btn-accent text-white ${
className={`btn btn-accent dark:border-violet-400 text-white ${
link?.pdfPath &&
link?.screenshotPath &&
link?.pdfPath !== "pending" &&

View File

@ -237,7 +237,10 @@ export default function UploadFileModal({ onClose }: Props) {
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
</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
</button>
</div>

View File

@ -68,7 +68,7 @@ export default function Navbar() {
<div
tabIndex={0}
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

View File

@ -20,7 +20,7 @@ export default function NoLinksFound({ text }: Props) {
onClick={() => {
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
icon={faPlus}

View File

@ -21,7 +21,7 @@ export default function SubmitButton({
return (
<button
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 || ""
}`}
onClick={() => {

View File

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

View File

@ -10,11 +10,13 @@ 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";
import AuthentikProvider from "next-auth/providers/authentik";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
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 = {
adapter: adapter as Adapter,
session: {

View File

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

View File

@ -192,7 +192,7 @@ export default function Dashboard() {
onClick={() => {
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
icon={faPlus}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import SubmitButton from "@/components/SubmitButton";
import AccentSubmitButton from "@/components/AccentSubmitButton";
import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react";
@ -13,6 +13,7 @@ interface FormData {
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED;
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED;
export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false);
@ -60,10 +61,24 @@ export default function Login() {
setSubmitLoader(false);
}
async function loginUserAuthentik() {
setSubmitLoader(true);
const load = toast.loading("Authenticating...");
const res = await signIn("authentik", {});
toast.dismiss(load);
setSubmitLoader(false);
}
return (
<CenteredForm text="Sign in to your account">
<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">
{process.env.NEXT_PUBLIC_DISABLE_LOGIN !== "true" ? (
<>
<p className="text-3xl text-center font-extralight">
Enter your credentials
</p>
@ -81,7 +96,9 @@ export default function Login() {
placeholder="johnny"
value={form.username}
className="bg-base-100"
onChange={(e) => setForm({ ...form, username: e.target.value })}
onChange={(e) =>
setForm({ ...form, username: e.target.value })
}
/>
</div>
@ -93,25 +110,32 @@ export default function Login() {
placeholder="••••••••••••••"
value={form.password}
className="bg-base-100"
onChange={(e) => setForm({ ...form, password: e.target.value })}
onChange={(e) =>
setForm({ ...form, password: e.target.value })
}
/>
{emailEnabled && (
<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?
</Link>
</div>
)}
</div>
<SubmitButton
<AccentSubmitButton
type="submit"
label="Login"
className="w-full text-center"
loading={submitLoader}
/>
</>
) : undefined}
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true" ? (
<SubmitButton
<AccentSubmitButton
type="button"
onClick={loginUserKeycloak}
label="Sign in with Keycloak"
@ -119,6 +143,15 @@ export default function Login() {
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}
/>
) : undefined}
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
"true" ? undefined : (
<div className="flex items-baseline gap-1 justify-center">

View File

@ -1,11 +1,11 @@
import Link from "next/link";
import { useState, FormEvent } from "react";
import { toast } from "react-hot-toast";
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput";
import AccentSubmitButton from "@/components/AccentSubmitButton";
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@ -220,12 +220,12 @@ export default function Register() {
</div>
) : undefined}
<button
<AccentSubmitButton
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 `}
>
<p className="text-center w-full font-bold">Sign Up</p>
</button>
label="Sign Up"
className="w-full"
loading={submitLoader}
/>
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-neutral">Already have an account?</p>
<Link href={"/login"} className="block font-bold">

View File

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

View File

@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm";
import { Plan } from "@/types/global";
import AccentSubmitButton from "@/components/AccentSubmitButton";
export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false);
@ -47,7 +48,7 @@ export default function Subscribe() {
</p>
</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
onClick={() => setPlan(Plan.monthly)}
className={`w-full duration-100 text-sm rounded-lg p-1 ${
@ -95,14 +96,13 @@ export default function Subscribe() {
</fieldset>
</div>
<button
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 `}
onClick={() => {
if (!submitLoader) submit();
}}
>
<p className="text-center w-full font-bold">Complete Subscription!</p>
</button>
<AccentSubmitButton
type="button"
label="Complete Subscription!"
className="w-full"
onClick={submit}
loading={submitLoader}
/>
<div
onClick={() => signOut()}

View File

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