Compare commits

...

26 Commits

Author SHA1 Message Date
Daniel ae6656e0ec Merge pull request #386 from treyg/global-theming-2.4
Global theming support
2024-01-02 07:30:01 -05:00
daniel31x13 fbca98984b Revert "updated README.md"
This reverts commit 4da2310e95.
2024-01-02 07:16:11 -05:00
daniel31x13 4da2310e95 updated README.md 2024-01-02 07:12:18 -05:00
daniel31x13 93bcfc67fe updated SECURITY.md 2024-01-02 07:01:04 -05:00
Daniel d16b296b15 Merge pull request #390 from QAComet/qacomet/worker-environment-variables
fix: load environment variables in the worker script
2024-01-01 17:12:46 -05:00
QAComet 3fc61ac5ce fix: load environment variables in the worker script 2024-01-01 15:09:55 -07:00
daniel31x13 ced51e4801 minor fix 2024-01-01 10:37:20 -05:00
Daniel 254c090605 Merge pull request #387 from linkwarden/dev
Dev
2023-12-31 16:05:41 -05:00
daniel31x13 2a83ced9d8 updated README 2023-12-31 16:05:18 -05:00
daniel31x13 52d333f085 updated README 2023-12-31 16:03:19 -05:00
Daniel fbbb97b4cd Merge pull request #385 from linkwarden/dev
added an extra environment variable
2023-12-31 10:46:50 -05:00
daniel31x13 4e29330472 added an extra environment variable 2023-12-31 10:46:09 -05:00
Daniel 44c82ff426 Merge pull request #384 from linkwarden/dev
replaced link outer component with <Link> tag for better accessibility
2023-12-31 10:06:06 -05:00
daniel31x13 29e0370808 replaced link outer component with <Link> tag for better accessibility 2023-12-31 10:05:30 -05:00
Daniel 74399c1708 Merge pull request #383 from linkwarden/dev
update version number
2023-12-31 08:03:36 -05:00
daniel31x13 1dde8a6088 update version number 2023-12-31 08:03:15 -05:00
daniel31x13 e872c25332 small improvement + better error handling 2023-12-31 07:55:45 -05:00
Daniel dea1e12700 Merge pull request #379 from linkwarden/dev
bugs fixed
2023-12-30 09:33:39 -05:00
daniel31x13 055869883a bugs fixed 2023-12-30 09:31:53 -05:00
Daniel a5d3926d84 Merge pull request #376 from linkwarden/dev
hotfix
2023-12-30 00:05:30 -05:00
daniel31x13 eee6a807da hotfix 2023-12-30 00:03:45 -05:00
Daniel e24ae15a73 Merge pull request #375 from linkwarden/dev
Dev
2023-12-29 23:59:34 -05:00
daniel31x13 f1dadf1546 throw error if a link was blocking the queue for too long 2023-12-29 23:59:00 -05:00
Trey Gordon 7e9eae0ef2 style: change to neutral to handle new themes 2023-12-29 12:29:10 -05:00
Trey Gordon 6b28abc405 feat: add new theming options 2023-12-29 12:28:43 -05:00
daniel31x13 ee6dcdcc5b added prettier settings 2023-12-29 12:21:22 -05:00
64 changed files with 1487 additions and 1051 deletions
+2
View File
@@ -18,6 +18,8 @@ RE_ARCHIVE_LIMIT=
NEXT_PUBLIC_MAX_FILE_SIZE=
MAX_LINKS_PER_USER=
ARCHIVE_TAKE_COUNT=
BROWSER_TIMEOUT=
IGNORE_UNAUTHORIZED_CA=
# AWS S3 Settings
SPACES_KEY=
+12 -10
View File
@@ -1,17 +1,19 @@
# Security Policy
# Security
## Supported Versions
The Linkwarden team and community take security bugs in Linkwarden seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
| Version | Supported |
| ------- | --------- |
| 1.x.x | ✅ |
# Reporting Security Issues
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
First off, we really appreciate the time you spent!
Instead, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/linkwarden/linkwarden/security/advisories/new) tab.
If you found a vulnerability, these are the ways you can reach us:
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message:
[security@linkwarden.app](mailto:security@linkwarden.app)
Email: [security@linkwarden.app](mailto:security@linkwarden.app)
Or you can directly DM me via Twitter: [@daniel31x13](https://twitter.com/Daniel31X13).
After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
# Preferred Languages
We prefer all communications to be in English.
+11
View File
@@ -0,0 +1,11 @@
node_modules
.next
public
*.lock
*.log
.github
data
pgdata
+4
View File
@@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"tabWidth": 2
}
+10 -6
View File
@@ -27,17 +27,21 @@ Additionally, Linkwarden is designed with collaboration in mind, sharing links w
<img src="./assets/dashboard.png" />
<div align="center">
<img src="./assets/all_links.png" width="32%" />
<img src="./assets/all_links.jpg" width="23%" />
<img src="./assets/all_collections.png" width="32%" />
<img src="./assets/list_view.jpg" width="23%" />
<img src="./assets/manage_team.png" width="32%" />
<img src="./assets/all_collections.jpg" width="23%" />
<img src="./assets/readable_view.png" width="32%" />
<img src="./assets/manage_team.jpg" width="23%" />
<img src="./assets/public_page.png" width="32%" />
<img src="./assets/readable_view.jpg" width="23%" />
<img src="./assets/light_mode.png" width="32%" />
<img src="./assets/preserved_formats.jpg" width="23%" />
<img src="./assets/public_page.jpg" width="23%" />
<img src="./assets/light_dashboard.jpg" width="23%" />
</div>
<details>
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 KiB

+1 -3
View File
@@ -22,9 +22,7 @@ export default function FilterSearchDropdown({
role="button"
className="btn btn-sm btn-square btn-ghost"
>
<i
className="bi-funnel text-neutral text-2xl"
></i>
<i className="bi-funnel text-neutral text-2xl"></i>
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1">
<li>
+8 -1
View File
@@ -80,6 +80,11 @@ export default function LinkGrid({ link, count, className }: Props) {
<div
ref={ref}
className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative"
>
<Link
href={link.url || ""}
target="_blank"
className="rounded-2xl cursor-pointer"
>
<div className="relative rounded-t-2xl h-40 overflow-hidden">
{previewAvailable(link) ? (
@@ -127,7 +132,7 @@ export default function LinkGrid({ link, count, className }: Props) {
title={link.url || ""}
className="w-fit"
>
<div className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-60 duration-100">
<div className="flex gap-1 item-center select-none text-neutral mt-1">
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</div>
@@ -135,6 +140,7 @@ export default function LinkGrid({ link, count, className }: Props) {
</div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between text-xs text-neutral px-3 pb-1">
<div className="cursor-pointer w-fit">
{collection ? (
@@ -143,6 +149,7 @@ export default function LinkGrid({ link, count, className }: Props) {
</div>
<LinkDate link={link} />
</div>
</Link>
{showInfo ? (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
@@ -17,7 +17,7 @@ export default function LinkCollection({
return (
<div
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
router.push(`/collections/${link.collection.id}`);
}}
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
@@ -1,7 +1,9 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import React from "react";
export default function LinkDate({ link }: {
export default function LinkDate({
link,
}: {
link: LinkIncludingShortenedCollectionAndTags;
}) {
const formattedDate = new Date(link.createdAt as string).toLocaleString(
@@ -10,7 +12,7 @@ export default function LinkDate({ link }: {
year: "numeric",
month: "short",
day: "numeric",
},
}
);
return (
+4 -3
View File
@@ -55,8 +55,9 @@ export default function LinkCardCompact({ link, count, className }: Props) {
!showInfo ? "hover:bg-base-300" : ""
} duration-200 rounded-lg`}
>
<div
onClick={() => link.url && window.open(link.url || "", "_blank")}
<Link
href={link.url || ""}
target="_blank"
className="flex items-center cursor-pointer py-3 px-3"
>
<div className="shrink-0">
@@ -91,7 +92,7 @@ export default function LinkCardCompact({ link, count, className }: Props) {
<LinkDate link={link} />
</div>
</div>
</div>
</Link>
<LinkActions
link={link}
+2 -2
View File
@@ -49,7 +49,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
<p>Are you sure you want to delete this Link?</p>
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl"/>
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>Warning:</b> This action is irreversible!
</span>
@@ -64,7 +64,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
onClick={deleteLink}
>
<i className="bi-trash text-xl"/>
<i className="bi-trash text-xl" />
Delete
</button>
</div>
+1 -1
View File
@@ -86,7 +86,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
title={link.url}
target="_blank"
>
<i className="bi-link-45deg text-xl"/>
<i className="bi-link-45deg text-xl" />
<p>{shortendURL}</p>
</Link>
) : undefined}
+6 -6
View File
@@ -26,13 +26,13 @@ export default function Navbar() {
const { width } = useWindowDimensions();
const handleToggle = () => {
if (settings.theme === "dark") {
updateSettings({ theme: "light" });
} else {
updateSettings({ theme: "dark" });
}
const [colorTheme, mode] = (settings.theme || "default-light").split('-');
const newMode = mode === "dark" ? "light" : "dark";
const newTheme = `${colorTheme}-${newMode}`;
updateSettings({ theme: newTheme });
};
useEffect(() => {
setSidebar(false);
}, [width]);
@@ -143,7 +143,7 @@ export default function Navbar() {
tabIndex={0}
role="button"
>
Switch to {settings.theme === "light" ? "Dark" : "Light"}
Switch to {(settings.theme || "default-light").endsWith("-dark") ? "Light" : "Dark"}
</div>
</li>
<li>
+3 -3
View File
@@ -102,9 +102,9 @@ export default function PreservedFormatRow({
) : undefined}
<Link
href={`${isPublic ? "/public" : ""}/preserved/${
link?.id
}?format=${format}`}
href={`${
isPublic ? "/public" : ""
}/preserved/${link?.id}?format=${format}`}
target="_blank"
className="btn btn-sm btn-square"
>
+2 -2
View File
@@ -47,11 +47,11 @@ export default function SearchBar({ placeholder }: Props) {
"/public/collections/" +
router.query.id +
"?q=" +
encodeURIComponent(searchQuery || ""),
encodeURIComponent(searchQuery || "")
);
} else {
return router.push(
"/search?q=" + encodeURIComponent(searchQuery),
"/search?q=" + encodeURIComponent(searchQuery)
);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.4.0";
const LINKWARDEN_VERSION = "v2.4.7";
const { collections } = useCollectionStore();
+29 -22
View File
@@ -6,32 +6,39 @@ type Props = {
};
export default function ToggleDarkMode({ className }: Props) {
const { settings, updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState(localStorage.getItem("theme"));
const handleToggle = (e: any) => {
setTheme(e.target.checked ? "dark" : "light");
};
const { updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState('default-light');
useEffect(() => {
updateSettings({ theme: theme as string });
}, [theme]);
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
setTheme(storedTheme);
} else {
// Default theme if not set in localStorage
localStorage.setItem("theme", "default-light");
setTheme("default-light");
}
console.log("Initial theme from localStorage:", storedTheme || "default-light");
}, []);
const handleToggle = () => {
const [currentColorTheme, currentMode] = theme.split('-');
const newMode = currentMode === 'light' ? 'dark' : 'light';
const newTheme = `${currentColorTheme}-${newMode}`;
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
console.log("New theme set:", newTheme);
};
const isDarkMode = theme.endsWith('-dark');
return (
<div
className="tooltip tooltip-bottom"
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`}
>
<label
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}
>
<input
type="checkbox"
onChange={handleToggle}
className="theme-controller"
checked={localStorage.getItem("theme") === "light" ? false : true}
/>
<div className="tooltip tooltip-bottom" data-tip={`Switch to ${isDarkMode ? "Light" : "Dark"}`}>
<label className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}>
<input type="checkbox" onChange={handleToggle} className="theme-controller" checked={isDarkMode} />
<i className="bi-sun-fill text-xl swap-on"></i>
<i className="bi-moon-fill text-xl swap-off"></i>
</label>
+2 -1
View File
@@ -11,7 +11,8 @@ services:
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
restart: always
image: ghcr.io/linkwarden/linkwarden:latest
# build: . # uncomment this line to build from source
image: ghcr.io/linkwarden/linkwarden:latest # comment this line to build from source
ports:
- 3000:3000
volumes:
+2 -2
View File
@@ -8,7 +8,7 @@ import { SetStateAction, useEffect } from "react";
type Props<
T extends
| CollectionIncludingMembersAndLinkCount
| LinkIncludingShortenedCollectionAndTags
| LinkIncludingShortenedCollectionAndTags,
> = {
sortBy: Sort;
@@ -19,7 +19,7 @@ type Props<
export default function useSort<
T extends
| CollectionIncludingMembersAndLinkCount
| LinkIncludingShortenedCollectionAndTags
| LinkIncludingShortenedCollectionAndTags,
>({ sortBy, data, setData }: Props<T>) {
useEffect(() => {
const dataArray = [...data];
+35 -7
View File
@@ -17,13 +17,31 @@ type LinksAndCollectionAndOwner = Link & {
};
};
const BROWSER_TIMEOUT = Number(process.env.BROWSER_TIMEOUT) || 5;
export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
const browser = await chromium.launch();
const context = await browser.newContext(devices["Desktop Chrome"]);
const page = await context.newPage();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(
() =>
reject(
new Error(
`Browser has been open for more than ${BROWSER_TIMEOUT} minutes.`
)
),
BROWSER_TIMEOUT * 60000
);
});
try {
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
await Promise.race([
(async () => {
const validatedUrl = link.url
? await validateUrlSize(link.url)
: undefined;
if (validatedUrl === null) throw "File is too large to be stored.";
@@ -32,11 +50,11 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
let imageExtension = "png";
if (!link.url) linkType = link.type;
else if (contentType === "application/pdf") linkType = "pdf";
else if (contentType?.includes("application/pdf")) linkType = "pdf";
else if (contentType?.startsWith("image")) {
linkType = "image";
if (contentType === "image/jpeg") imageExtension = "jpeg";
else if (contentType === "image/png") imageExtension = "png";
if (contentType.includes("image/jpeg")) imageExtension = "jpeg";
else if (contentType.includes("image/png")) imageExtension = "png";
}
const user = link.collection?.owner;
@@ -56,8 +74,12 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
user.archiveAsPDF && !link.pdf?.startsWith("archive")
? "pending"
: undefined,
readable: !link.readable?.startsWith("archive") ? "pending" : undefined,
preview: !link.readable?.startsWith("archive") ? "pending" : undefined,
readable: !link.readable?.startsWith("archive")
? "pending"
: undefined,
preview: !link.readable?.startsWith("archive")
? "pending"
: undefined,
lastPreserved: new Date().toISOString(),
},
});
@@ -202,7 +224,10 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
if (linkExists) {
const processingPromises = [];
if (user.archiveAsScreenshot && !link.image?.startsWith("archive")) {
if (
user.archiveAsScreenshot &&
!link.image?.startsWith("archive")
) {
processingPromises.push(
page.screenshot({ fullPage: true }).then((screenshot) => {
return createFile({
@@ -243,6 +268,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
});
}
}
})(),
timeoutPromise,
]);
} catch (err) {
console.log(err);
console.log("Failed Link details:", link);
@@ -14,7 +14,7 @@ export default async function getDashboardData(
else if (query.sort === Sort.DescriptionZA) order = { description: "desc" };
const pinnedLinks = await prisma.link.findMany({
take: 6,
take: 8,
where: {
AND: [
{
+4 -1
View File
@@ -68,7 +68,10 @@ export default async function postLink(
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
if (validatedUrl === null)
return { response: "File is too large to be stored.", status: 400 };
return {
response: "Something went wrong while retrieving the file size.",
status: 400,
};
const contentType = validatedUrl?.get("content-type");
let linkType = "url";
@@ -5,6 +5,9 @@ import Stripe from "stripe";
import { DeleteUserBody } from "@/types/global";
import removeFile from "@/lib/api/storage/removeFile";
const keycloakEnabled = process.env.KEYCLOAK_CLIENT_SECRET;
const authentikEnabled = process.env.AUTHENTIK_CLIENT_SECRET;
export default async function deleteUserById(
userId: number,
body: DeleteUserBody
@@ -22,7 +25,7 @@ export default async function deleteUserById(
}
// Then, we check if the provided password matches the one stored in the database (disabled in Keycloak integration)
if (!process.env.KEYCLOAK_CLIENT_SECRET) {
if (!keycloakEnabled && !authentikEnabled) {
const isPasswordValid = bcrypt.compareSync(
body.password,
user.password as string
@@ -23,6 +23,7 @@ export default async function updateUserById(
id: userId,
},
});
if (ssoUser) {
// deny changes to SSO-defined properties
if (data.email !== user?.email) {
@@ -49,7 +50,7 @@ export default async function updateUserById(
status: 400,
};
}
if (data.image !== "") {
if (data.image?.startsWith("data:image/jpeg;base64")) {
return {
response: "SSO Users cannot change their avatar.",
status: 400,
+4 -1
View File
@@ -40,7 +40,10 @@ async function emptyS3Directory(bucket: string, dir: string) {
export default async function removeFolder({ filePath }: { filePath: string }) {
if (s3Client) {
try {
await emptyS3Directory(process.env.SPACES_BUCKET_NAME as string, filePath);
await emptyS3Directory(
process.env.SPACES_BUCKET_NAME as string,
filePath
);
} catch (err) {
console.log("Error", err);
}
+12 -1
View File
@@ -1,6 +1,17 @@
import fetch from "node-fetch";
import https from "https";
export default async function validateUrlSize(url: string) {
try {
const response = await fetch(url, { method: "HEAD" });
const httpsAgent = new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
const response = await fetch(url, {
method: "HEAD",
agent: httpsAgent,
});
const totalSizeMB =
Number(response.headers.get("content-length")) / Math.pow(1024, 2);
+2 -1
View File
@@ -42,7 +42,8 @@ export default async function verifyUser({
return null;
}
if (!user.username && !ssoUser) { // SSO users don't need a username
if (!user.username && !ssoUser) {
// SSO users don't need a username
res.status(401).json({
response: "Username not found.",
});
+10 -1
View File
@@ -1,6 +1,15 @@
import fetch from "node-fetch";
import https from "https";
export default async function getTitle(url: string) {
try {
const response = await fetch(url);
const httpsAgent = new https.Agent({
rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
});
const response = await fetch(url, {
agent: httpsAgent,
});
const text = await response.text();
// regular expression to find the <title> tag
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "linkwarden",
"version": "2.4.0",
"version": "2.4.7",
"main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>",
@@ -15,7 +15,8 @@
"worker:prod": "ts-node --transpile-only --skip-project scripts/worker.ts",
"start": "concurrently \"next start\" \"yarn worker:prod\"",
"build": "next build",
"lint": "next lint"
"lint": "next lint",
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\""
},
"dependencies": {
"@auth/prisma-adapter": "^1.0.1",
@@ -38,6 +39,7 @@
"crypto-js": "^4.2.0",
"csstype": "^3.1.2",
"dompurify": "^3.0.6",
"dotenv": "^16.3.1",
"eslint": "8.46.0",
"eslint-config-next": "13.4.9",
"formidable": "^3.5.1",
@@ -64,11 +66,13 @@
"@types/bcrypt": "^5.0.0",
"@types/dompurify": "^3.0.4",
"@types/jsdom": "^21.1.3",
"@types/node-fetch": "^2.6.10",
"@types/shelljs": "^0.8.15",
"autoprefixer": "^10.4.14",
"daisyui": "^4.4.2",
"nodemon": "^3.0.2",
"postcss": "^8.4.26",
"prettier": "3.1.1",
"prisma": "^5.1.0",
"tailwindcss": "^3.3.3",
"ts-node": "^10.9.2",
+14 -2
View File
@@ -1,6 +1,6 @@
import React from "react";
import React, { useEffect } from "react";
import "@/styles/globals.css";
import 'bootstrap-icons/font/bootstrap-icons.css';
import "bootstrap-icons/font/bootstrap-icons.css";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
import Head from "next/head";
@@ -14,6 +14,18 @@ export default function App({
}: AppProps<{
session: Session;
}>) {
useEffect(() => {
let theme = localStorage.getItem("theme");
if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
}
document.documentElement.setAttribute('data-theme', theme);
}, []);
return (
<SessionProvider
session={pageProps.session}
+5 -2
View File
@@ -249,7 +249,8 @@ if (process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true") {
profile: (profile) => {
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
username: profile.preferred_username,
name: profile.name || "",
email: profile.email,
image: profile.picture,
};
@@ -589,13 +590,15 @@ if (process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true") {
profile: (profile) => {
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
username: profile.preferred_username,
name: profile.name || "",
email: profile.email,
image: profile.picture,
};
},
})
);
const _linkAccount = adapter.linkAccount;
adapter.linkAccount = (account) => {
const { "not-before-policy": _, refresh_expires_in, ...data } = account;
+290 -120
View File
@@ -1,238 +1,408 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiRequest, NextApiResponse } from "next";
import * as process from "process";
export type ResponseData = {
credentialsEnabled: string|undefined
emailEnabled: string|undefined
registrationDisabled: string|undefined
credentialsEnabled: string | undefined;
emailEnabled: string | undefined;
registrationDisabled: string | undefined;
buttonAuths: {
method: string
name: string
}[]
}
export default function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
method: string;
name: string;
}[];
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.json(getLogins());
}
export function getLogins() {
const buttonAuths = []
const buttonAuths = [];
// 42 School
if (process.env.NEXT_PUBLIC_FORTYTWO_ENABLED === 'true') {
buttonAuths.push({method: '42-school', name: process.env.FORTYTWO_CUSTOM_NAME ?? '42 School'});
if (process.env.NEXT_PUBLIC_FORTYTWO_ENABLED === "true") {
buttonAuths.push({
method: "42-school",
name: process.env.FORTYTWO_CUSTOM_NAME ?? "42 School",
});
}
// Apple
if (process.env.NEXT_PUBLIC_APPLE_ENABLED === 'true') {
buttonAuths.push({method: 'apple', name: process.env.APPLE_CUSTOM_NAME ?? 'Apple'});
if (process.env.NEXT_PUBLIC_APPLE_ENABLED === "true") {
buttonAuths.push({
method: "apple",
name: process.env.APPLE_CUSTOM_NAME ?? "Apple",
});
}
// Atlassian
if (process.env.NEXT_PUBLIC_ATLASSIAN_ENABLED === 'true') {
buttonAuths.push({method: 'atlassian', name: process.env.ATLASSIAN_CUSTOM_NAME ?? 'Atlassian'});
if (process.env.NEXT_PUBLIC_ATLASSIAN_ENABLED === "true") {
buttonAuths.push({
method: "atlassian",
name: process.env.ATLASSIAN_CUSTOM_NAME ?? "Atlassian",
});
}
// Auth0
if (process.env.NEXT_PUBLIC_AUTH0_ENABLED === 'true') {
buttonAuths.push({method: 'auth0', name: process.env.AUTH0_CUSTOM_NAME ?? 'Auth0'});
if (process.env.NEXT_PUBLIC_AUTH0_ENABLED === "true") {
buttonAuths.push({
method: "auth0",
name: process.env.AUTH0_CUSTOM_NAME ?? "Auth0",
});
}
// Authentik
if (process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === 'true') {
buttonAuths.push({method: 'authentik', name: process.env.AUTHENTIK_CUSTOM_NAME ?? 'Authentik'});
if (process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true") {
buttonAuths.push({
method: "authentik",
name: process.env.AUTHENTIK_CUSTOM_NAME ?? "Authentik",
});
}
// Battle.net
if (process.env.NEXT_PUBLIC_BATTLENET_ENABLED === 'true') {
buttonAuths.push({method: 'battlenet', name: process.env.BATTLENET_CUSTOM_NAME ?? 'Battle.net'});
if (process.env.NEXT_PUBLIC_BATTLENET_ENABLED === "true") {
buttonAuths.push({
method: "battlenet",
name: process.env.BATTLENET_CUSTOM_NAME ?? "Battle.net",
});
}
// Box
if (process.env.NEXT_PUBLIC_BOX_ENABLED === 'true') {
buttonAuths.push({method: 'box', name: process.env.BOX_CUSTOM_NAME ?? 'Box'});
if (process.env.NEXT_PUBLIC_BOX_ENABLED === "true") {
buttonAuths.push({
method: "box",
name: process.env.BOX_CUSTOM_NAME ?? "Box",
});
}
// Cognito
if (process.env.NEXT_PUBLIC_COGNITO_ENABLED === 'true') {
buttonAuths.push({method: 'cognito', name: process.env.COGNITO_CUSTOM_NAME ?? 'Cognito'});
if (process.env.NEXT_PUBLIC_COGNITO_ENABLED === "true") {
buttonAuths.push({
method: "cognito",
name: process.env.COGNITO_CUSTOM_NAME ?? "Cognito",
});
}
// Coinbase
if (process.env.NEXT_PUBLIC_COINBASE_ENABLED === 'true') {
buttonAuths.push({method: 'coinbase', name: process.env.COINBASE_CUSTOM_NAME ?? 'Coinbase'});
if (process.env.NEXT_PUBLIC_COINBASE_ENABLED === "true") {
buttonAuths.push({
method: "coinbase",
name: process.env.COINBASE_CUSTOM_NAME ?? "Coinbase",
});
}
// Discord
if (process.env.NEXT_PUBLIC_DISCORD_ENABLED === 'true') {
buttonAuths.push({method: 'discord', name: process.env.DISCORD_CUSTOM_NAME ?? 'Discord'});
if (process.env.NEXT_PUBLIC_DISCORD_ENABLED === "true") {
buttonAuths.push({
method: "discord",
name: process.env.DISCORD_CUSTOM_NAME ?? "Discord",
});
}
// Dropbox
if (process.env.NEXT_PUBLIC_DROPBOX_ENABLED === 'true') {
buttonAuths.push({method: 'dropbox', name: process.env.DROPBOX_CUSTOM_NAME ?? 'Dropbox'});
if (process.env.NEXT_PUBLIC_DROPBOX_ENABLED === "true") {
buttonAuths.push({
method: "dropbox",
name: process.env.DROPBOX_CUSTOM_NAME ?? "Dropbox",
});
}
// Duende IdentityServer6
if (process.env.NEXT_PUBLIC_DUENDE_IDS6_ENABLED === 'true') {
buttonAuths.push({method: 'duende-identityserver6', name: process.env.DUENDE_IDS6_CUSTOM_NAME ?? 'DuendeIdentityServer6'});
if (process.env.NEXT_PUBLIC_DUENDE_IDS6_ENABLED === "true") {
buttonAuths.push({
method: "duende-identityserver6",
name: process.env.DUENDE_IDS6_CUSTOM_NAME ?? "DuendeIdentityServer6",
});
}
// EVE Online
if (process.env.NEXT_PUBLIC_EVEONLINE_ENABLED === 'true') {
buttonAuths.push({method: 'eveonline', name: process.env.EVEONLINE_CUSTOM_NAME ?? 'EVE Online'});
if (process.env.NEXT_PUBLIC_EVEONLINE_ENABLED === "true") {
buttonAuths.push({
method: "eveonline",
name: process.env.EVEONLINE_CUSTOM_NAME ?? "EVE Online",
});
}
// Facebook
if (process.env.NEXT_PUBLIC_FACEBOOK_ENABLED === 'true') {
buttonAuths.push({method: 'facebook', name: process.env.FACEBOOK_CUSTOM_NAME ?? 'Facebook'});
if (process.env.NEXT_PUBLIC_FACEBOOK_ENABLED === "true") {
buttonAuths.push({
method: "facebook",
name: process.env.FACEBOOK_CUSTOM_NAME ?? "Facebook",
});
}
// FACEIT
if (process.env.NEXT_PUBLIC_FACEIT_ENABLED === 'true') {
buttonAuths.push({method: 'faceit', name: process.env.FACEIT_CUSTOM_NAME ?? 'FACEIT'});
if (process.env.NEXT_PUBLIC_FACEIT_ENABLED === "true") {
buttonAuths.push({
method: "faceit",
name: process.env.FACEIT_CUSTOM_NAME ?? "FACEIT",
});
}
// Foursquare
if (process.env.NEXT_PUBLIC_FOURSQUARE_ENABLED === 'true') {
buttonAuths.push({method: 'foursquare', name: process.env.FOURSQUARE_CUSTOM_NAME ?? 'Foursquare'});
if (process.env.NEXT_PUBLIC_FOURSQUARE_ENABLED === "true") {
buttonAuths.push({
method: "foursquare",
name: process.env.FOURSQUARE_CUSTOM_NAME ?? "Foursquare",
});
}
// Freshbooks
if (process.env.NEXT_PUBLIC_FRESHBOOKS_ENABLED === 'true') {
buttonAuths.push({method: 'freshbooks', name: process.env.FRESHBOOKS_CUSTOM_NAME ?? 'Freshbooks'});
if (process.env.NEXT_PUBLIC_FRESHBOOKS_ENABLED === "true") {
buttonAuths.push({
method: "freshbooks",
name: process.env.FRESHBOOKS_CUSTOM_NAME ?? "Freshbooks",
});
}
// FusionAuth
if (process.env.NEXT_PUBLIC_FUSIONAUTH_ENABLED === 'true') {
buttonAuths.push({method: 'fusionauth', name: process.env.FUSIONAUTH_CUSTOM_NAME ?? 'FusionAuth'});
if (process.env.NEXT_PUBLIC_FUSIONAUTH_ENABLED === "true") {
buttonAuths.push({
method: "fusionauth",
name: process.env.FUSIONAUTH_CUSTOM_NAME ?? "FusionAuth",
});
}
// GitHub
if (process.env.NEXT_PUBLIC_GITHUB_ENABLED === 'true') {
buttonAuths.push({method: 'github', name: process.env.GITHUB_CUSTOM_NAME ?? 'GitHub'});
if (process.env.NEXT_PUBLIC_GITHUB_ENABLED === "true") {
buttonAuths.push({
method: "github",
name: process.env.GITHUB_CUSTOM_NAME ?? "GitHub",
});
}
// GitLab
if (process.env.NEXT_PUBLIC_GITLAB_ENABLED === 'true') {
buttonAuths.push({method: 'gitlab', name: process.env.GITLAB_CUSTOM_NAME ?? 'GitLab'});
if (process.env.NEXT_PUBLIC_GITLAB_ENABLED === "true") {
buttonAuths.push({
method: "gitlab",
name: process.env.GITLAB_CUSTOM_NAME ?? "GitLab",
});
}
// Google
if (process.env.NEXT_PUBLIC_GOOGLE_ENABLED === 'true') {
buttonAuths.push({method: 'google', name: process.env.GOOGLE_CUSTOM_NAME ?? 'Google'});
if (process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true") {
buttonAuths.push({
method: "google",
name: process.env.GOOGLE_CUSTOM_NAME ?? "Google",
});
}
// HubSpot
if (process.env.NEXT_PUBLIC_HUBSPOT_ENABLED === 'true') {
buttonAuths.push({method: 'hubspot', name: process.env.HUBSPOT_CUSTOM_NAME ?? 'HubSpot'});
if (process.env.NEXT_PUBLIC_HUBSPOT_ENABLED === "true") {
buttonAuths.push({
method: "hubspot",
name: process.env.HUBSPOT_CUSTOM_NAME ?? "HubSpot",
});
}
// IdentityServer4
if (process.env.NEXT_PUBLIC_IDS4_ENABLED === 'true') {
buttonAuths.push({method: 'identity-server4', name: process.env.IDS4_CUSTOM_NAME ?? 'IdentityServer4'});
if (process.env.NEXT_PUBLIC_IDS4_ENABLED === "true") {
buttonAuths.push({
method: "identity-server4",
name: process.env.IDS4_CUSTOM_NAME ?? "IdentityServer4",
});
}
// Kakao
if (process.env.NEXT_PUBLIC_KAKAO_ENABLED === 'true') {
buttonAuths.push({method: 'kakao', name: process.env.KAKAO_CUSTOM_NAME ?? 'Kakao'});
if (process.env.NEXT_PUBLIC_KAKAO_ENABLED === "true") {
buttonAuths.push({
method: "kakao",
name: process.env.KAKAO_CUSTOM_NAME ?? "Kakao",
});
}
// Keycloak
if (process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === 'true') {
buttonAuths.push({method: 'keycloak', name: process.env.KEYCLOAK_CUSTOM_NAME ?? 'Keycloak'});
if (process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true") {
buttonAuths.push({
method: "keycloak",
name: process.env.KEYCLOAK_CUSTOM_NAME ?? "Keycloak",
});
}
// LINE
if (process.env.NEXT_PUBLIC_LINE_ENABLED === 'true') {
buttonAuths.push({method: 'line', name: process.env.LINE_CUSTOM_NAME ?? 'LINE'});
if (process.env.NEXT_PUBLIC_LINE_ENABLED === "true") {
buttonAuths.push({
method: "line",
name: process.env.LINE_CUSTOM_NAME ?? "LINE",
});
}
// LinkedIn
if (process.env.NEXT_PUBLIC_LINKEDIN_ENABLED === 'true') {
buttonAuths.push({method: 'linkedin', name: process.env.LINKEDIN_CUSTOM_NAME ?? 'LinkedIn'});
if (process.env.NEXT_PUBLIC_LINKEDIN_ENABLED === "true") {
buttonAuths.push({
method: "linkedin",
name: process.env.LINKEDIN_CUSTOM_NAME ?? "LinkedIn",
});
}
// MailChimp
if (process.env.NEXT_PUBLIC_MAILCHIMP_ENABLED === 'true') {
buttonAuths.push({method: 'mailchimp', name: process.env.MAILCHIMP_CUSTOM_NAME ?? 'Mailchimp'});
if (process.env.NEXT_PUBLIC_MAILCHIMP_ENABLED === "true") {
buttonAuths.push({
method: "mailchimp",
name: process.env.MAILCHIMP_CUSTOM_NAME ?? "Mailchimp",
});
}
// Mail.ru
if (process.env.NEXT_PUBLIC_MAILRU_ENABLED === 'true') {
buttonAuths.push({method: 'mailru', name: process.env.MAILRU_CUSTOM_NAME ?? 'Mail.ru'});
if (process.env.NEXT_PUBLIC_MAILRU_ENABLED === "true") {
buttonAuths.push({
method: "mailru",
name: process.env.MAILRU_CUSTOM_NAME ?? "Mail.ru",
});
}
// Naver
if (process.env.NEXT_PUBLIC_NAVER_ENABLED === 'true') {
buttonAuths.push({method: 'naver', name: process.env.NAVER_CUSTOM_NAME ?? 'Naver'});
if (process.env.NEXT_PUBLIC_NAVER_ENABLED === "true") {
buttonAuths.push({
method: "naver",
name: process.env.NAVER_CUSTOM_NAME ?? "Naver",
});
}
// Netlify
if (process.env.NEXT_PUBLIC_NETLIFY_ENABLED === 'true') {
buttonAuths.push({method: 'netlify', name: process.env.NETLIFY_CUSTOM_NAME ?? 'Netlify'});
if (process.env.NEXT_PUBLIC_NETLIFY_ENABLED === "true") {
buttonAuths.push({
method: "netlify",
name: process.env.NETLIFY_CUSTOM_NAME ?? "Netlify",
});
}
// Okta
if (process.env.NEXT_PUBLIC_OKTA_ENABLED === 'true') {
buttonAuths.push({method: 'okta', name: process.env.OKTA_CUSTOM_NAME ?? 'Okta'});
if (process.env.NEXT_PUBLIC_OKTA_ENABLED === "true") {
buttonAuths.push({
method: "okta",
name: process.env.OKTA_CUSTOM_NAME ?? "Okta",
});
}
// OneLogin
if (process.env.NEXT_PUBLIC_ONELOGIN_ENABLED === 'true') {
buttonAuths.push({method: 'onelogin', name: process.env.ONELOGIN_CUSTOM_NAME ?? 'OneLogin'});
if (process.env.NEXT_PUBLIC_ONELOGIN_ENABLED === "true") {
buttonAuths.push({
method: "onelogin",
name: process.env.ONELOGIN_CUSTOM_NAME ?? "OneLogin",
});
}
// Osso
if (process.env.NEXT_PUBLIC_OSSO_ENABLED === 'true') {
buttonAuths.push({method: 'osso', name: process.env.OSSO_CUSTOM_NAME ?? 'Osso'});
if (process.env.NEXT_PUBLIC_OSSO_ENABLED === "true") {
buttonAuths.push({
method: "osso",
name: process.env.OSSO_CUSTOM_NAME ?? "Osso",
});
}
// osu!
if (process.env.NEXT_PUBLIC_OSU_ENABLED === 'true') {
buttonAuths.push({method: 'osu', name: process.env.OSU_CUSTOM_NAME ?? 'Osu!'});
if (process.env.NEXT_PUBLIC_OSU_ENABLED === "true") {
buttonAuths.push({
method: "osu",
name: process.env.OSU_CUSTOM_NAME ?? "Osu!",
});
}
// Patreon
if (process.env.NEXT_PUBLIC_PATREON_ENABLED === 'true') {
buttonAuths.push({method: 'patreon', name: process.env.PATREON_CUSTOM_NAME ?? 'Patreon'});
if (process.env.NEXT_PUBLIC_PATREON_ENABLED === "true") {
buttonAuths.push({
method: "patreon",
name: process.env.PATREON_CUSTOM_NAME ?? "Patreon",
});
}
// Pinterest
if (process.env.NEXT_PUBLIC_PINTEREST_ENABLED === 'true') {
buttonAuths.push({method: 'pinterest', name: process.env.PINTEREST_CUSTOM_NAME ?? 'Pinterest'});
if (process.env.NEXT_PUBLIC_PINTEREST_ENABLED === "true") {
buttonAuths.push({
method: "pinterest",
name: process.env.PINTEREST_CUSTOM_NAME ?? "Pinterest",
});
}
// Pipedrive
if (process.env.NEXT_PUBLIC_PIPEDRIVE_ENABLED === 'true') {
buttonAuths.push({method: 'pipedrive', name: process.env.PIPEDRIVE_CUSTOM_NAME ?? 'Pipedrive'});
if (process.env.NEXT_PUBLIC_PIPEDRIVE_ENABLED === "true") {
buttonAuths.push({
method: "pipedrive",
name: process.env.PIPEDRIVE_CUSTOM_NAME ?? "Pipedrive",
});
}
// Reddit
if (process.env.NEXT_PUBLIC_REDDIT_ENABLED === 'true') {
buttonAuths.push({method: 'reddit', name: process.env.REDDIT_CUSTOM_NAME ?? 'Reddit'});
if (process.env.NEXT_PUBLIC_REDDIT_ENABLED === "true") {
buttonAuths.push({
method: "reddit",
name: process.env.REDDIT_CUSTOM_NAME ?? "Reddit",
});
}
// Salesforce
if (process.env.NEXT_PUBLIC_SALESFORCE_ENABLED === 'true') {
buttonAuths.push({method: 'salesforce', name: process.env.SALESFORCE_CUSTOM_NAME ?? 'Salesforce'});
if (process.env.NEXT_PUBLIC_SALESFORCE_ENABLED === "true") {
buttonAuths.push({
method: "salesforce",
name: process.env.SALESFORCE_CUSTOM_NAME ?? "Salesforce",
});
}
// Slack
if (process.env.NEXT_PUBLIC_SLACK_ENABLED === 'true') {
buttonAuths.push({method: 'slack', name: process.env.SLACK_CUSTOM_NAME ?? 'Slack'});
if (process.env.NEXT_PUBLIC_SLACK_ENABLED === "true") {
buttonAuths.push({
method: "slack",
name: process.env.SLACK_CUSTOM_NAME ?? "Slack",
});
}
// Spotify
if (process.env.NEXT_PUBLIC_SPOTIFY_ENABLED === 'true') {
buttonAuths.push({method: 'spotify', name: process.env.SPOTIFY_CUSTOM_NAME ?? 'Spotify'});
if (process.env.NEXT_PUBLIC_SPOTIFY_ENABLED === "true") {
buttonAuths.push({
method: "spotify",
name: process.env.SPOTIFY_CUSTOM_NAME ?? "Spotify",
});
}
// Strava
if (process.env.NEXT_PUBLIC_STRAVA_ENABLED === 'true') {
buttonAuths.push({method: 'strava', name: process.env.STRAVA_CUSTOM_NAME ?? 'Strava'});
if (process.env.NEXT_PUBLIC_STRAVA_ENABLED === "true") {
buttonAuths.push({
method: "strava",
name: process.env.STRAVA_CUSTOM_NAME ?? "Strava",
});
}
// Todoist
if (process.env.NEXT_PUBLIC_TODOIST_ENABLED === 'true') {
buttonAuths.push({method: 'todoist', name: process.env.TODOIST_CUSTOM_NAME ?? 'Todoist'});
if (process.env.NEXT_PUBLIC_TODOIST_ENABLED === "true") {
buttonAuths.push({
method: "todoist",
name: process.env.TODOIST_CUSTOM_NAME ?? "Todoist",
});
}
// Twitch
if (process.env.NEXT_PUBLIC_TWITCH_ENABLED === 'true') {
buttonAuths.push({method: 'twitch', name: process.env.TWITCH_CUSTOM_NAME ?? 'Twitch'});
if (process.env.NEXT_PUBLIC_TWITCH_ENABLED === "true") {
buttonAuths.push({
method: "twitch",
name: process.env.TWITCH_CUSTOM_NAME ?? "Twitch",
});
}
// United Effects
if (process.env.NEXT_PUBLIC_UNITED_EFFECTS_ENABLED === 'true') {
buttonAuths.push({method: 'united-effects', name: process.env.UNITED_EFFECTS_CUSTOM_NAME ?? 'United Effects'});
if (process.env.NEXT_PUBLIC_UNITED_EFFECTS_ENABLED === "true") {
buttonAuths.push({
method: "united-effects",
name: process.env.UNITED_EFFECTS_CUSTOM_NAME ?? "United Effects",
});
}
// VK
if (process.env.NEXT_PUBLIC_VK_ENABLED === 'true') {
buttonAuths.push({method: 'vk', name: process.env.VK_CUSTOM_NAME ?? 'VK'});
if (process.env.NEXT_PUBLIC_VK_ENABLED === "true") {
buttonAuths.push({
method: "vk",
name: process.env.VK_CUSTOM_NAME ?? "VK",
});
}
// Wikimedia
if (process.env.NEXT_PUBLIC_WIKIMEDIA_ENABLED === 'true') {
buttonAuths.push({method: 'wikimedia', name: process.env.WIKIMEDIA_CUSTOM_NAME ?? 'Wikimedia'});
if (process.env.NEXT_PUBLIC_WIKIMEDIA_ENABLED === "true") {
buttonAuths.push({
method: "wikimedia",
name: process.env.WIKIMEDIA_CUSTOM_NAME ?? "Wikimedia",
});
}
// Wordpress.com
if (process.env.NEXT_PUBLIC_WORDPRESS_ENABLED === 'true') {
buttonAuths.push({method: 'wordpress', name: process.env.WORDPRESS_CUSTOM_NAME ?? 'WordPress.com'});
if (process.env.NEXT_PUBLIC_WORDPRESS_ENABLED === "true") {
buttonAuths.push({
method: "wordpress",
name: process.env.WORDPRESS_CUSTOM_NAME ?? "WordPress.com",
});
}
// Yandex
if (process.env.NEXT_PUBLIC_YANDEX_ENABLED === 'true') {
buttonAuths.push({method: 'yandex', name: process.env.YANDEX_CUSTOM_NAME ?? 'Yandex'});
if (process.env.NEXT_PUBLIC_YANDEX_ENABLED === "true") {
buttonAuths.push({
method: "yandex",
name: process.env.YANDEX_CUSTOM_NAME ?? "Yandex",
});
}
// Zitadel
if (process.env.NEXT_PUBLIC_ZITADEL_ENABLED === 'true') {
buttonAuths.push({method: 'zitadel', name: process.env.ZITADEL_CUSTOM_NAME ?? 'ZITADEL'});
if (process.env.NEXT_PUBLIC_ZITADEL_ENABLED === "true") {
buttonAuths.push({
method: "zitadel",
name: process.env.ZITADEL_CUSTOM_NAME ?? "ZITADEL",
});
}
// Zoho
if (process.env.NEXT_PUBLIC_ZOHO_ENABLED === 'true') {
buttonAuths.push({method: 'zoho', name: process.env.ZOHO_CUSTOM_NAME ?? 'Zoho'});
if (process.env.NEXT_PUBLIC_ZOHO_ENABLED === "true") {
buttonAuths.push({
method: "zoho",
name: process.env.ZOHO_CUSTOM_NAME ?? "Zoho",
});
}
// Zoom
if (process.env.NEXT_PUBLIC_ZOOM_ENABLED === 'true') {
buttonAuths.push({method: 'zoom', name: process.env.ZOOM_CUSTOM_NAME ?? 'Zoom'});
if (process.env.NEXT_PUBLIC_ZOOM_ENABLED === "true") {
buttonAuths.push({
method: "zoom",
name: process.env.ZOOM_CUSTOM_NAME ?? "Zoom",
});
}
return {
credentialsEnabled: (process.env.NEXT_PUBLIC_CREDENTIALS_ENABLED === 'true' || process.env.NEXT_PUBLIC_CREDENTIALS_ENABLED === undefined) ? "true" : "false",
emailEnabled: (process.env.NEXT_PUBLIC_EMAIL_PROVIDER === 'true' ? 'true' : 'false'),
registrationDisabled: (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === 'true' ? 'true' : 'false'),
buttonAuths: buttonAuths
credentialsEnabled:
process.env.NEXT_PUBLIC_CREDENTIALS_ENABLED === "true" ||
process.env.NEXT_PUBLIC_CREDENTIALS_ENABLED === undefined
? "true"
: "false",
emailEnabled:
process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" ? "true" : "false",
registrationDisabled:
process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true"
? "true"
: "false",
buttonAuths: buttonAuths,
};
}
+4 -2
View File
@@ -104,8 +104,10 @@ export default function Index() {
className="h-[60rem] p-5 flex gap-3 flex-col"
style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
(settings.theme || "default-light").endsWith("-dark") ? "#262626" : "#f3f4f6"
} 13rem, ${
(settings.theme || "default-light").endsWith("-dark") ? "#171717" : "#ffffff"
} 100%)`,
}}
>
{activeCollection && (
+2 -2
View File
@@ -155,7 +155,7 @@ export default function Dashboard() {
</div>
<Link
href="/links"
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer"
className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm"></i>
@@ -264,7 +264,7 @@ export default function Dashboard() {
</div>
<Link
href="/links/pinned"
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer"
className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm "></i>
+3 -4
View File
@@ -3,7 +3,7 @@ import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useState, FormEvent } from "react";
import React, { useState, FormEvent } from "react";
import { toast } from "react-hot-toast";
import { getLogins } from "./api/v1/logins";
import { InferGetServerSidePropsType } from "next";
@@ -131,18 +131,17 @@ export default function Login({
const Buttons: any = [];
availableLogins.buttonAuths.forEach((value, index) => {
Buttons.push(
<>
<React.Fragment key={index}>
{index !== 0 ? <div className="divider my-1">OR</div> : undefined}
<AccentSubmitButton
key={index}
type="button"
onClick={() => loginUserButton(value.method)}
label={`Sign in with ${value.name}`}
className=" w-full text-center"
loading={submitLoader}
/>
</>
</React.Fragment>
);
});
return Buttons;
+58 -39
View File
@@ -1,16 +1,16 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() {
const { updateSettings } = useLocalSettingsStore();
const { account, updateAccount } = useAccountStore();
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
@@ -18,17 +18,12 @@ export default function Appearance() {
});
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
} else toast.error(response.data as string);
setSubmitLoader(false);
};
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>(
!objectIsEmpty(account)
? account
@@ -48,6 +43,9 @@ export default function Appearance() {
} as unknown as AccountSettings)
);
// Combine colorTheme and mode into a single state
const [theme, setTheme] = useState(localStorage.getItem("theme") || "default-dark");
function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
@@ -56,51 +54,72 @@ export default function Appearance() {
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
const handleThemeChange = (newThemePart: string, isColorTheme: boolean) => {
const currentTheme = localStorage.getItem("theme") || "default-light";
const [currentColorTheme, currentMode] = currentTheme.split('-');
const newTheme = isColorTheme ? `${newThemePart}-${currentMode}` : `${currentColorTheme}-${newThemePart}`;
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
// Update the theme state
setTheme(newTheme);
};
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Theme</p>
<div className="flex gap-3 w-full">
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
localStorage.getItem("theme") === "dark"
? "dark:outline-primary text-primary"
: "text-white"
}`}
onClick={() => updateSettings({ theme: "dark" })}
<p className="mb-3">Select Mode</p>
<div className="grid grid-cols-2 gap-3">
{["light", "dark"].map((modeOption) => (
<button
key={modeOption}
onClick={() => handleThemeChange(modeOption, false)}
className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.endsWith(modeOption) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
>
<i className="bi-moon-fill text-6xl"></i>
<p className="ml-2 text-2xl">Dark</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
localStorage.getItem("theme") === "light"
? "outline-primary text-primary"
: "text-black"
}`}
onClick={() => updateSettings({ theme: "light" })}
>
<i className="bi-sun-fill text-6xl"></i>
<p className="ml-2 text-2xl">Light</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
{modeOption === 'light' ?
<i className={`bi-sun-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i> :
<i className={`bi-moon-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i>}
<p className="ml-2 text-2xl">{modeOption.charAt(0).toUpperCase() + modeOption.slice(1)}</p>
</button>
))}
</div>
</div>
{/* <SubmitButton
<div>
<p className="mb-3">Select Color Theme</p>
<div className="grid grid-cols-4 gap-3">
{["default", "red", "green", "orange"].map((colorTheme) => (
<button
key={colorTheme}
onClick={() => handleThemeChange(colorTheme, true)}
className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.startsWith(colorTheme) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
>
{colorTheme.charAt(0).toUpperCase() + colorTheme.slice(1)}
</button>
))}
</div>
</div>
<button
onClick={submit}
loading={submitLoader}
label="Save"
className="mt-2 mx-auto lg:mx-0"
/> */}
disabled={submitLoader}
className="mt-2 mx-auto lg:mx-0 bg-primary text-white rounded-md px-4 py-2"
>
{submitLoader ? "Saving..." : "Save"}
</button>
</div>
</SettingsLayout>
);
}
+5 -4
View File
@@ -5,6 +5,9 @@ import CenteredForm from "@/layouts/CenteredForm";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
export default function Delete() {
const [password, setPassword] = useState("");
const [comment, setComment] = useState<string>();
@@ -23,7 +26,7 @@ export default function Delete() {
},
};
if (process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" && password == "") {
if (!keycloakEnabled && !authentikEnabled && password == "") {
return toast.error("Please fill the required fields.");
}
@@ -57,9 +60,7 @@ export default function Delete() {
href="/settings/account"
className="absolute top-4 left-4 btn btn-ghost btn-square btn-sm"
>
<i
className="bi-chevron-left text-neutral text-xl"
></i>
<i className="bi-chevron-left text-neutral text-xl"></i>
</Link>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-red-500 dark:text-red-500 truncate w-full text-3xl text-center">
+10 -10
View File
@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
@@ -10,7 +10,7 @@ import { defineConfig, devices } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
testDir: "./e2e",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -20,31 +20,31 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
+1 -1
View File
@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

+1
View File
@@ -1,3 +1,4 @@
import 'dotenv/config';
import { Collection, Link, User } from "@prisma/client";
import { prisma } from "../lib/api/db";
import archiveHandler from "../lib/api/archiveHandler";
+23 -18
View File
@@ -1,9 +1,9 @@
import { create } from "zustand";
import {ViewMode} from "@/types/global";
import { ViewMode } from "@/types/global";
type LocalSettings = {
theme?: string;
viewMode?: string
viewMode?: string;
};
type LocalSettingsStore = {
@@ -18,34 +18,39 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
viewMode: "",
},
updateSettings: async (newSettings) => {
if (
newSettings.theme &&
newSettings.theme !== localStorage.getItem("theme")
) {
if (newSettings.theme) {
localStorage.setItem("theme", newSettings.theme);
document.documentElement.setAttribute('data-theme', newSettings.theme);
const localTheme = localStorage.getItem("theme") || "";
document.querySelector("html")?.setAttribute("data-theme", localTheme);
if (newSettings.theme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
if (
newSettings.viewMode &&
newSettings.viewMode !== localStorage.getItem("viewMode")
) {
if (newSettings.viewMode) {
localStorage.setItem("viewMode", newSettings.viewMode);
// const localTheme = localStorage.getItem("viewMode") || "";
}
set((state) => ({ settings: { ...state.settings, ...newSettings } }));
},
setSettings: async () => {
if (!localStorage.getItem("theme")) {
localStorage.setItem("theme", "dark");
let theme = localStorage.getItem("theme");
if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
}
const localTheme = localStorage.getItem("theme") || "";
const localTheme = theme;
document.documentElement.setAttribute('data-theme', localTheme);
if (localTheme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
set((state) => ({
settings: { ...state.settings, theme: localTheme },
+106 -3
View File
@@ -5,7 +5,7 @@ module.exports = {
daisyui: {
themes: [
{
light: {
"default-light": {
primary: "#0369a1",
secondary: "#0891b2",
accent: "#6d28d9",
@@ -21,7 +21,7 @@ module.exports = {
},
},
{
dark: {
"default-dark": {
primary: "#7dd3fc",
secondary: "#22d3ee",
accent: "#6d28d9",
@@ -36,6 +36,108 @@ module.exports = {
error: "#f1293c",
},
},
// Red Light Theme
{
"red-light": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Red Dark Theme
{
"red-dark": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Green Light Theme
{
"green-light": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Green Dark Theme
{
"green-dark": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Light Theme
{
"orange-light": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Dark Theme
{
"orange-dark": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
],
},
darkMode: ["class", '[data-theme="dark"]'],
@@ -53,4 +155,5 @@ module.exports = {
addVariant("dark", '&[data-theme="dark"]');
}),
],
};
};
+1
View File
@@ -12,6 +12,7 @@ declare global {
NEXT_PUBLIC_MAX_FILE_SIZE?: string;
MAX_LINKS_PER_USER?: string;
ARCHIVE_TAKE_COUNT?: string;
IGNORE_UNAUTHORIZED_CA?: string;
SPACES_KEY?: string;
SPACES_SECRET?: string;
+18
View File
@@ -1795,6 +1795,14 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/node-fetch@^2.6.10":
version "2.6.10"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.10.tgz#ff5c1ceacab782f2b7ce69957d38c1c27b0dc469"
integrity sha512-PPpPK6F9ALFTn59Ka3BaL+qGuipRfxNE8qVgkp0bVixeiR2c2/L+IVOiBdu9JhhT22sWnQEp6YyHGI2b2+CMcA==
dependencies:
"@types/node" "*"
form-data "^4.0.0"
"@types/node@*", "@types/node@>=8.1.0":
version "20.4.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.4.tgz#c79c7cc22c9d0e97a7944954c9e663bcbd92b0cb"
@@ -2706,6 +2714,11 @@ dompurify@^3.0.6:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae"
integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==
dotenv@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -4751,6 +4764,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"
integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==
pretty-format@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"