Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5303d63e4b | |||
| 05a30e1ec6 | |||
| b1a55785b5 | |||
| 24b47e9d4b | |||
| 34d19f9dbe | |||
| 95dddd7da0 | |||
| 1a949ecdc6 | |||
| 2e6f1c207c | |||
| 6aa0fa9465 | |||
| 8677df0340 | |||
| 125f6ac619 | |||
| 89ecf5c529 | |||
| fa78d6057f | |||
| cfc28be898 | |||
| c8efd4f9db | |||
| ada4e53b46 | |||
| 91494b0188 | |||
| e9fd6ec4d5 | |||
| f08f4058dc | |||
| d60200205a | |||
| de38eb2963 | |||
| f22dd4535d | |||
| 043589b301 | |||
| 4556827d79 | |||
| 98ebd6d7bc | |||
| 0a3ca4a1d4 | |||
| 106410f55a | |||
| 1ffe1b68a9 | |||
| 91ab0e609b | |||
| cbb7a666cd | |||
| e8cf14334f | |||
| 019790791b | |||
| e41ba2668f | |||
| 66a09fdc4b | |||
| e50143ca7e | |||
| 162b120e55 | |||
| b4dd47aa37 | |||
| 256c232a85 | |||
| b7ddf22662 | |||
| 5f60e9833e | |||
| ceed23ff51 |
@@ -36,6 +36,7 @@ PREVIEW_MAX_BUFFER=
|
||||
IMPORT_LIMIT=
|
||||
PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH=
|
||||
MAX_WORKERS=
|
||||
DISABLE_PRESERVATION=
|
||||
|
||||
# AWS S3 Settings
|
||||
SPACES_KEY=
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Check pull request source branch
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- edited
|
||||
jobs:
|
||||
check-branches:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check branches
|
||||
run: |
|
||||
if [ ${{ github.head_ref }} != "dev" ] && [ ${{ github.base_ref }} == "main" ]; then
|
||||
echo "Merge requests to main branch are only allowed from dev branch. Please rebase your changes to dev branch."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Create and publish a container image on release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
|
||||
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a> <a href="https://news.ycombinator.com/item?id=36942308"><img src="https://img.shields.io/badge/Hacker%20News-280-%23FF6600"></img></a>
|
||||
|
||||
<a href="https://github.com/linkwarden/linkwarden/actions/workflows/release-container.yml"><img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/linkwarden/linkwarden/release-container.yml"></a>
|
||||
<a href="https://opencollective.com/linkwarden"><img src="https://img.shields.io/opencollective/all/linkwarden" alt="Open Collective"></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div align='center'>
|
||||
@@ -69,16 +72,19 @@ We've forked the old version from the current repository into [this repo](https:
|
||||
- 📱 Responsive design and supports most modern browsers.
|
||||
- 🌓 Dark/Light mode support.
|
||||
- 🧩 Browser extension, managed by the community. [Star it here!](https://github.com/linkwarden/browser-extension)
|
||||
- 🔄 Browser Synchronization (using [Floccus](https://floccus.org)!)
|
||||
- ⬇️ Import and export your bookmarks.
|
||||
- 🔐 SSO integration. (Enterprise and Self-hosted users only)
|
||||
- 📦 Installable Progressive Web App (PWA).
|
||||
- 🍏 iOS and MacOS Apps, maintained by [JGeek00](https://github.com/JGeek00).
|
||||
- 🍎 iOS Shortcut to save links to Linkwarden.
|
||||
- 🍎 iOS Shortcut to save Links to Linkwarden.
|
||||
- 🔑 API keys.
|
||||
- ✅ Bulk actions.
|
||||
- 👥 User administration.
|
||||
- 🌐 Support for Other Languages (i18n).
|
||||
- 📁 Image and PDF Uploads.
|
||||
- 🎨 Custom Icons for Links and Collections.
|
||||
- ⚙️ Customizable View and Adjustable Columns.
|
||||
- ✨ And many more features. (Literally!)
|
||||
|
||||
## Like what we're doing? Give us a Star ⭐
|
||||
|
||||
@@ -8,7 +8,7 @@ const InstallApp = (props: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return isOpen && !isPWA() ? (
|
||||
<div className="fixed left-0 right-0 bottom-10 w-full">
|
||||
<div className="fixed left-0 right-0 bottom-10 w-full px-5">
|
||||
<div className="mx-auto w-fit p-2 flex justify-between gap-2 items-center border border-neutral-content rounded-xl bg-base-300 backdrop-blur-md bg-opacity-80 max-w-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -21,6 +21,19 @@ type LinksAndCollectionAndOwner = Link & {
|
||||
const BROWSER_TIMEOUT = Number(process.env.BROWSER_TIMEOUT) || 5;
|
||||
|
||||
export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||
if (process.env.DISABLE_PRESERVATION === "true")
|
||||
return await prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: {
|
||||
lastPreserved: new Date().toISOString(),
|
||||
readable: "unavailable",
|
||||
image: "unavailable",
|
||||
monolith: "unavailable",
|
||||
pdf: "unavailable",
|
||||
preview: "unavailable",
|
||||
},
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
@@ -44,7 +57,8 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||
};
|
||||
}
|
||||
if (process.env.PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH) {
|
||||
browserOptions.executablePath = process.env.PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH;
|
||||
browserOptions.executablePath =
|
||||
process.env.PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH;
|
||||
}
|
||||
|
||||
const browser = await chromium.launch(browserOptions);
|
||||
|
||||
@@ -76,7 +76,7 @@ export default async function postLink(
|
||||
let linkType = "url";
|
||||
let imageExtension = "png";
|
||||
|
||||
if (!link.url) linkType = link.type;
|
||||
if (!link.url) linkType = link.type || "url";
|
||||
else if (contentType === "application/pdf") linkType = "pdf";
|
||||
else if (contentType?.startsWith("image")) {
|
||||
linkType = "image";
|
||||
|
||||
@@ -66,6 +66,7 @@ export default async function importFromLinkwarden(
|
||||
url: link.url?.trim().slice(0, 254),
|
||||
name: link.name?.trim().slice(0, 254),
|
||||
description: link.description?.trim().slice(0, 254),
|
||||
importDate: new Date(link.importDate || link.createdAt),
|
||||
collection: {
|
||||
connect: {
|
||||
id: newCollection.id,
|
||||
|
||||
@@ -184,27 +184,20 @@ export default async function deleteUserById(
|
||||
|
||||
try {
|
||||
if (user.subscriptions?.id) {
|
||||
const listByEmail = await stripe.customers.list({
|
||||
email: user.email?.toLowerCase(),
|
||||
expand: ["data.subscriptions"],
|
||||
});
|
||||
const deleted = await stripe.subscriptions.cancel(
|
||||
user.subscriptions.stripeSubscriptionId,
|
||||
{
|
||||
cancellation_details: {
|
||||
comment: body.cancellation_details?.comment,
|
||||
feedback: body.cancellation_details?.feedback,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
||||
const deleted = await stripe.subscriptions.cancel(
|
||||
listByEmail.data[0].subscriptions?.data[0].id,
|
||||
{
|
||||
cancellation_details: {
|
||||
comment: body.cancellation_details?.comment,
|
||||
feedback: body.cancellation_details?.feedback,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
response: deleted,
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: deleted,
|
||||
status: 200,
|
||||
};
|
||||
} else if (user.parentSubscription?.id && user && user.emailVerified) {
|
||||
await updateSeats(
|
||||
user.parentSubscription.stripeSubscriptionId,
|
||||
|
||||
@@ -7,6 +7,10 @@ type Data = {
|
||||
quantity: number;
|
||||
periodStart: number;
|
||||
periodEnd: number;
|
||||
action:
|
||||
| "customer.subscription.created"
|
||||
| "customer.subscription.updated"
|
||||
| "customer.subscription.deleted";
|
||||
};
|
||||
|
||||
export default async function handleSubscription({
|
||||
@@ -15,6 +19,7 @@ export default async function handleSubscription({
|
||||
quantity,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
action,
|
||||
}: Data) {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: {
|
||||
@@ -57,7 +62,13 @@ export default async function handleSubscription({
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throw new Error("User not found");
|
||||
if (!user) {
|
||||
if (action === "customer.subscription.deleted") {
|
||||
return "User not found or deleted";
|
||||
} else {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const PostUserSchema = () => {
|
||||
password: z.string().min(8).max(2048).optional(),
|
||||
email: emailEnabled
|
||||
? z.string().trim().email().toLowerCase()
|
||||
: z.string().optional(),
|
||||
: z.string().nullish(),
|
||||
username: emailEnabled
|
||||
? z.string().optional()
|
||||
: z
|
||||
@@ -59,7 +59,7 @@ export const UpdateUserSchema = () => {
|
||||
name: z.string().trim().min(1).max(50).optional(),
|
||||
email: emailEnabled
|
||||
? z.string().trim().email().toLowerCase()
|
||||
: z.string().optional(),
|
||||
: z.string().nullish(),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -92,7 +92,7 @@ export const PostSessionSchema = z.object({
|
||||
});
|
||||
|
||||
export const PostLinkSchema = z.object({
|
||||
type: z.enum(["url", "pdf", "image"]),
|
||||
type: z.enum(["url", "pdf", "image"]).nullish(),
|
||||
url: z.string().trim().max(2048).url().optional(),
|
||||
name: z.string().trim().max(2048).optional(),
|
||||
description: z.string().trim().max(2048).optional(),
|
||||
|
||||
@@ -5,6 +5,7 @@ const { i18n } = require("./next-i18next.config");
|
||||
const nextConfig = {
|
||||
i18n,
|
||||
reactStrictMode: true,
|
||||
staticPageGenerationTimeout: 1000,
|
||||
images: {
|
||||
// For fetching the favicons
|
||||
domains: ["t2.gstatic.com"],
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkwarden",
|
||||
"version": "v2.8.0",
|
||||
"version": "v2.8.3",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/linkwarden/linkwarden.git",
|
||||
"author": "Daniel31X13 <daniel31x13@gmail.com>",
|
||||
|
||||
@@ -72,6 +72,7 @@ export default async function webhook(
|
||||
quantity: data?.quantity ?? 1,
|
||||
periodStart: data.current_period_start,
|
||||
periodEnd: data.current_period_end,
|
||||
action: "customer.subscription.created",
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -82,6 +83,7 @@ export default async function webhook(
|
||||
quantity: data?.quantity ?? 1,
|
||||
periodStart: data.current_period_start,
|
||||
periodEnd: data.current_period_end,
|
||||
action: "customer.subscription.updated",
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -92,16 +94,7 @@ export default async function webhook(
|
||||
quantity: data?.lines?.data[0]?.quantity ?? 1,
|
||||
periodStart: data.current_period_start,
|
||||
periodEnd: data.current_period_end,
|
||||
});
|
||||
break;
|
||||
|
||||
case "customer.subscription.cancelled":
|
||||
await handleSubscription({
|
||||
id: data.id,
|
||||
active: !(data.current_period_end * 1000 < Date.now()),
|
||||
quantity: data?.lines?.data[0]?.quantity ?? 1,
|
||||
periodStart: data.current_period_start,
|
||||
periodEnd: data.current_period_end,
|
||||
action: "customer.subscription.deleted",
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function Account() {
|
||||
|
||||
const emailSchema = z.string().trim().email().toLowerCase();
|
||||
const emailValidation = emailSchema.safeParse(user.email || "");
|
||||
if (!emailValidation.success) {
|
||||
if (emailEnabled && !emailValidation.success) {
|
||||
return toast.error(t("email_invalid"));
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
"pwa_install_prompt": "Installiere Linkwarden auf deinem Startbildschirm für einen schnelleren Zugriff und ein besseres Erlebnis. <0>Mehr erfahren</0>",
|
||||
"full_content": "Vollständiger Inhalt",
|
||||
"slower": "Langsamer",
|
||||
"new_version_announcement": "Schau, was neu ist in <0>Linkwarden {{Version}}!</0>",
|
||||
"new_version_announcement": "Schau, was neu ist in <0>Linkwarden {{version}}!</0>",
|
||||
"creating": "Erstellen...",
|
||||
"upload_file": "Datei hochladen",
|
||||
"file": "Datei",
|
||||
@@ -343,8 +343,8 @@
|
||||
"shift_key_tip": "Halte die Umschalttaste gedrückt, während du auf „Löschen“ klickst, um diese Bestätigung in Zukunft zu umgehen.",
|
||||
"deleting_collection": "Lösche...",
|
||||
"collection_deleted": "Sammlung gelöscht.",
|
||||
"confirm_deletion_prompt": "Zur Bestätigung tippe „{{Name}}“ in das Feld unten:",
|
||||
"type_name_placeholder": "Tippe „{{Name}}“ hier.",
|
||||
"confirm_deletion_prompt": "Zur Bestätigung tippe „{{name}}“ in das Feld unten:",
|
||||
"type_name_placeholder": "Tippe „{{name}}“ hier.",
|
||||
"deletion_warning": "Wenn Du diese Sammlung löschst, wird ihr gesamter Inhalt unwiderruflich gelöscht und sie wird für jeden unzugänglich, auch für Mitglieder mit vorherigem Zugriff.",
|
||||
"leave_prompt": "Klicke auf die Schaltfläche unten, um die aktuelle Sammlung zu verlassen.",
|
||||
"leave": "Verlassen",
|
||||
|
||||
@@ -393,5 +393,41 @@
|
||||
"change_icon": "Змінити піктограму",
|
||||
"upload_preview_image": "Завантажте зображення для попереднього перегляду",
|
||||
"columns": "Стовпці",
|
||||
"default": "За замовчуванням"
|
||||
"default": "За замовчуванням",
|
||||
"invalid_url_guide":"Будь ласка, введіть дійсну адресу для посилання. (Він має починатися з http/https)",
|
||||
"email_invalid": "Введіть дійсну електронну адресу.",
|
||||
"username_invalid_guide": "Ім'я користувача має складатися щонайменше з 3 символів, пробіли та спеціальні символи не допускаються.",
|
||||
"team_management": "Управління командою",
|
||||
"invite_user": "Запросити користувача",
|
||||
"invite_users": "Запросіть користувачів",
|
||||
"invite_user_desc": "Щоб запросити когось до своєї команди, введіть адресу електронної пошти нижче:",
|
||||
"invite_user_note": "Зауважте, що після прийняття запрошення буде придбано додаткове місце, і з вашого облікового запису буде автоматично виставлено рахунок за це додавання.",
|
||||
"invite_user_price": "Вартість кожного місця становить ${{price}} на місяць або ${{priceAnnual}} на рік, залежно від вашого поточного плану підписки.",
|
||||
"send_invitation": "Надіслати запрошення",
|
||||
"learn_more": "Дізнайтесь більше",
|
||||
"invitation_desc": "{{owner}} запросив вас приєднатися до Linkwarden. \nЩоб продовжити, завершіть налаштування облікового запису.",
|
||||
"invitation_accepted": "Запрошення прийнято!",
|
||||
"status": "Статус",
|
||||
"pending": "В очікуванні",
|
||||
"active": "Активний",
|
||||
"manage_seats": "Керуйте місцями",
|
||||
"seats_purchased": "Придбано {{count}} місць",
|
||||
"seat_purchased": "Придбано {{count}} місце",
|
||||
"date_added": "Дата додавання",
|
||||
"resend_invite": "Повторно надіслати запрошення",
|
||||
"resend_invite_success": "Запрошення повторно надіслано!",
|
||||
"remove_user": "Видалити користувача",
|
||||
"continue_to_dashboard": "Перейдіть до панелі інструментів",
|
||||
"confirm_user_removal_desc": "Щоб знову отримати доступ до Linkwarden, їм потрібно буде підписатися.",
|
||||
"click_out_to_apply": "Натисніть зовні, щоб застосувати",
|
||||
"submit": "Надіслати",
|
||||
"thanks_for_feedback": "Дякуємо за ваш відгук!",
|
||||
"quick_survey": "Швидке опитування",
|
||||
"how_did_you_discover_linkwarden": "Як ви дізналися про Linkwarden?",
|
||||
"rather_not_say": "Скоріше не скажу",
|
||||
"search_engine": "Пошукова система (Google, Bing тощо)",
|
||||
"reddit": "Reddit",
|
||||
"lemmy": "Lemmy",
|
||||
"people_recommendation": "Рекомендація (друг, родина тощо)",
|
||||
"open_all_links": "Відкрити всі посилання"
|
||||
}
|
||||
Reference in New Issue
Block a user