mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-03-03 03:47:02 +00:00
init
This commit is contained in:
@@ -8,7 +8,9 @@ interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
|
||||
const STRIPE_ENABLED = process.env.NEXT_PUBLIC_STRIPE === "true";
|
||||
const TRIAL_PERIOD_DAYS = process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14;
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
|
||||
export default function AuthRedirect({ children }: Props) {
|
||||
const router = useRouter();
|
||||
@@ -22,11 +24,19 @@ export default function AuthRedirect({ children }: Props) {
|
||||
const isLoggedIn = status === "authenticated";
|
||||
const isUnauthenticated = status === "unauthenticated";
|
||||
const isPublicPage = router.pathname.startsWith("/public");
|
||||
|
||||
const trialEndTime =
|
||||
new Date(user?.createdAt || 0).getTime() +
|
||||
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
|
||||
|
||||
const daysLeft = Math.floor((trialEndTime - Date.now()) / 86400000);
|
||||
|
||||
const hasInactiveSubscription =
|
||||
user?.id &&
|
||||
!user?.subscription?.active &&
|
||||
!user.parentSubscription?.active &&
|
||||
stripeEnabled;
|
||||
STRIPE_ENABLED &&
|
||||
(REQUIRE_CC || daysLeft <= 0);
|
||||
|
||||
// There are better ways of doing this... but this one works for now
|
||||
const routes = [
|
||||
|
||||
@@ -2,6 +2,10 @@ import verifySubscription from "./stripe/verifySubscription";
|
||||
import { prisma } from "@linkwarden/prisma";
|
||||
import stripeSDK from "./stripe/stripeSDK";
|
||||
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
const MANAGED_PAYMENTS_ENABLED =
|
||||
process.env.MANAGED_PAYMENTS_ENABLED === "true";
|
||||
|
||||
export default async function paymentCheckout(email: string, priceId: string) {
|
||||
const stripe = stripeSDK();
|
||||
|
||||
@@ -44,12 +48,16 @@ export default async function paymentCheckout(email: string, priceId: string) {
|
||||
customer_email: isExistingCustomer ? undefined : email.toLowerCase(),
|
||||
success_url: `${process.env.BASE_URL}/dashboard`,
|
||||
cancel_url: `${process.env.BASE_URL}/login`,
|
||||
subscription_data: {
|
||||
trial_period_days: NEXT_PUBLIC_TRIAL_PERIOD_DAYS
|
||||
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS)
|
||||
: 14,
|
||||
},
|
||||
...(process.env.MANAGED_PAYMENTS_ENABLED === "true"
|
||||
...(REQUIRE_CC
|
||||
? {
|
||||
subscription_data: {
|
||||
trial_period_days: NEXT_PUBLIC_TRIAL_PERIOD_DAYS
|
||||
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS)
|
||||
: 14,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(MANAGED_PAYMENTS_ENABLED
|
||||
? {
|
||||
managed_payments: {
|
||||
enabled: true,
|
||||
|
||||
@@ -7,10 +7,25 @@ interface UserIncludingSubscription extends User {
|
||||
parentSubscription: Subscription | null;
|
||||
}
|
||||
|
||||
const TRIAL_PERIOD_DAYS = process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14;
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
|
||||
export default async function verifySubscription(
|
||||
user?: UserIncludingSubscription | null
|
||||
) {
|
||||
if (!user || (!user.subscriptions && !user.parentSubscription)) {
|
||||
if (!user) return null;
|
||||
|
||||
const trialEndTime =
|
||||
new Date(user.createdAt).getTime() +
|
||||
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
|
||||
|
||||
const daysLeft = Math.floor((trialEndTime - Date.now()) / 86400000);
|
||||
|
||||
if (
|
||||
!user.subscriptions &&
|
||||
!user.parentSubscription &&
|
||||
(REQUIRE_CC || daysLeft <= 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -19,8 +34,9 @@ export default async function verifySubscription(
|
||||
}
|
||||
|
||||
if (
|
||||
!user.subscriptions?.active ||
|
||||
new Date() > user.subscriptions.currentPeriodEnd
|
||||
(!user.subscriptions?.active ||
|
||||
new Date() > user.subscriptions.currentPeriodEnd) &&
|
||||
(REQUIRE_CC || daysLeft <= 0)
|
||||
) {
|
||||
const subscription = await checkSubscriptionByEmail(user.email as string);
|
||||
|
||||
|
||||
@@ -46,6 +46,17 @@ export default async function verifyUser({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!user.emailVerified &&
|
||||
process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"
|
||||
) {
|
||||
res.status(401).json({
|
||||
response:
|
||||
"Email not verified, please verify your email to continue using Linkwarden.",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (STRIPE_SECRET_KEY) {
|
||||
const subscribedUser = await verifySubscription(user);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@linkwarden/web",
|
||||
"version": "v2.12.2",
|
||||
"version": "v2.13.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/linkwarden/linkwarden.git",
|
||||
"author": "Daniel31X13 <daniel31x13@gmail.com>",
|
||||
@@ -74,7 +74,7 @@
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^6.9.3",
|
||||
"papaparse": "^5.5.3",
|
||||
"playwright": "^1.45.0",
|
||||
"playwright": "^1.55.0",
|
||||
"react": "18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/dompurify": "^3.0.4",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
|
||||
@@ -32,6 +32,8 @@ type UserModal = {
|
||||
userId: number | null;
|
||||
};
|
||||
|
||||
const TRIAL_PERIOD_DAYS = process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14;
|
||||
|
||||
export default function Billing() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
@@ -40,9 +42,20 @@ export default function Billing() {
|
||||
const { data: users = [] } = useUsers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE || account?.parentSubscriptionId)
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE || account?.parentSubscriptionId) {
|
||||
router.push("/settings/account");
|
||||
}, []);
|
||||
} else if (account?.createdAt) {
|
||||
const trialEndTime =
|
||||
new Date(account.createdAt).getTime() +
|
||||
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
|
||||
|
||||
const daysLeft = Math.floor((trialEndTime - Date.now()) / 86400000);
|
||||
|
||||
if (daysLeft > 0 && !account.subscription?.active) {
|
||||
router.push("/subscribe");
|
||||
}
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filteredUsers, setFilteredUsers] = useState<User[]>();
|
||||
|
||||
@@ -9,6 +9,11 @@ import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import { useUser } from "@linkwarden/router/user";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
|
||||
const TRIAL_PERIOD_DAYS =
|
||||
Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS) || 14;
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
|
||||
export default function Subscribe() {
|
||||
const { t } = useTranslation();
|
||||
@@ -21,6 +26,18 @@ export default function Subscribe() {
|
||||
|
||||
const { data: user } = useUser();
|
||||
|
||||
const [daysLeft, setDaysLeft] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.createdAt) {
|
||||
const trialEndTime =
|
||||
new Date(user.createdAt).getTime() +
|
||||
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
|
||||
|
||||
setDaysLeft(Math.floor((trialEndTime - Date.now()) / 86400000));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
session.status === "authenticated" &&
|
||||
@@ -45,9 +62,13 @@ export default function Subscribe() {
|
||||
|
||||
return (
|
||||
<CenteredForm
|
||||
text={`Start with a ${
|
||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||
}-day free trial, cancel anytime!`}
|
||||
text={
|
||||
daysLeft <= 0
|
||||
? "Your free trial has ended, subscribe to continue."
|
||||
: `You have ${REQUIRE_CC ? 14 : daysLeft || 0} ${
|
||||
!REQUIRE_CC && daysLeft === 1 ? "day" : "days"
|
||||
} left in your free trial.`
|
||||
}
|
||||
>
|
||||
<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-xl shadow-md border border-neutral-content">
|
||||
<p className="sm:text-3xl text-xl text-center font-extralight">
|
||||
@@ -116,11 +137,11 @@ export default function Subscribe() {
|
||||
<p className="text-sm">
|
||||
{plan === Plan.monthly
|
||||
? t("total_monthly_desc", {
|
||||
count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS),
|
||||
count: REQUIRE_CC ? 14 : daysLeft,
|
||||
monthlyPrice: "4",
|
||||
})
|
||||
: t("total_annual_desc", {
|
||||
count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS),
|
||||
count: REQUIRE_CC ? 14 : daysLeft,
|
||||
annualPrice: "36",
|
||||
})}
|
||||
</p>
|
||||
@@ -138,12 +159,21 @@ export default function Subscribe() {
|
||||
{t("complete_subscription")}
|
||||
</Button>
|
||||
|
||||
<div
|
||||
onClick={() => signOut()}
|
||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||
>
|
||||
{t("sign_out")}
|
||||
</div>
|
||||
{REQUIRE_CC ? (
|
||||
<div
|
||||
onClick={() => signOut()}
|
||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||
>
|
||||
{t("sign_out")}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||
href="/dashboard"
|
||||
>
|
||||
{t("subscribe_later")}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CenteredForm>
|
||||
);
|
||||
|
||||
@@ -513,5 +513,6 @@
|
||||
"rename_tag_instruction": "Please provide a name for the final tag.",
|
||||
"merging": "Merging...",
|
||||
"delete_tags": "Delete {{count}} Tags",
|
||||
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links."
|
||||
"tags_deletion_confirmation_message": "Are you sure you want to delete {{count}} Tags? This will remove the tags from all links.",
|
||||
"subscribe_later": "Subscribe Later?"
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export default async function autoTagLink(
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
aiGenerated: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@ type PickLinksOptions = {
|
||||
maxBatchLinks: number;
|
||||
};
|
||||
|
||||
const TRIAL_PERIOD_DAYS = process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14;
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
|
||||
export default async function getLinkBatchFairly({
|
||||
maxBatchLinks,
|
||||
}: PickLinksOptions) {
|
||||
@@ -38,9 +41,26 @@ export default async function getLinkBatchFairly({
|
||||
OR: [
|
||||
{ subscriptions: { is: { active: true } } },
|
||||
{ parentSubscription: { is: { active: true } } },
|
||||
...(REQUIRE_CC
|
||||
? []
|
||||
: [
|
||||
{
|
||||
createdAt: {
|
||||
gte: new Date(
|
||||
new Date().getTime() -
|
||||
Number(TRIAL_PERIOD_DAYS) * 86400000
|
||||
),
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
...(process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"
|
||||
? {
|
||||
emailVerified: { not: null },
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
orderBy: [{ lastPickedAt: { sort: "asc", nulls: "first" } }, { id: "asc" }],
|
||||
select: { id: true, lastPickedAt: true },
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
"meilisearch": "^0.48.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ollama-ai-provider": "^1.2.0",
|
||||
"playwright": "^1.45.0",
|
||||
"playwright": "^1.55.0",
|
||||
"rss-parser": "^3.13.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tsx": "^4.19.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"nodemon": "^3.1.9",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -2,6 +2,8 @@ import { prisma } from "@linkwarden/prisma";
|
||||
|
||||
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
||||
const stripeEnabled = process.env.STRIPE_SECRET_KEY;
|
||||
const TRIAL_PERIOD_DAYS = process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14;
|
||||
const REQUIRE_CC = process.env.NEXT_PUBLIC_REQUIRE_CC === "true";
|
||||
|
||||
export const hasPassedLimit = async (
|
||||
userId: number,
|
||||
@@ -22,6 +24,7 @@ export const hasPassedLimit = async (
|
||||
select: {
|
||||
parentSubscriptionId: true,
|
||||
subscriptions: { select: { id: true, quantity: true } },
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,6 +32,22 @@ export const hasPassedLimit = async (
|
||||
return true;
|
||||
}
|
||||
|
||||
const trialEndTime =
|
||||
new Date(user.createdAt).getTime() +
|
||||
(1 + Number(TRIAL_PERIOD_DAYS)) * 86400000; // Add 1 to account for the current day
|
||||
|
||||
const daysLeft = Math.floor((trialEndTime - Date.now()) / 86400000);
|
||||
|
||||
if (!REQUIRE_CC && daysLeft > 0) {
|
||||
const totalLinks = await prisma.link.count({
|
||||
where: {
|
||||
createdById: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return MAX_LINKS_PER_USER - (numberOfImports + totalLinks) < 0;
|
||||
}
|
||||
|
||||
const subscriptionId = user?.parentSubscriptionId ?? user?.subscriptions?.id;
|
||||
let quantity = user?.subscriptions?.quantity;
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ const useCollections = (auth?: MobileAuth) => {
|
||||
: undefined
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.response;
|
||||
|
||||
if (Array.isArray(data.response)) return data.response;
|
||||
else return [];
|
||||
},
|
||||
enabled: status === "authenticated",
|
||||
});
|
||||
|
||||
28
yarn.lock
28
yarn.lock
@@ -3221,12 +3221,12 @@
|
||||
tiny-glob "^0.2.9"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@playwright/test@^1.45.0":
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.0.tgz#790a66165a46466c0d7099dd260881802f5aba7e"
|
||||
integrity sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==
|
||||
"@playwright/test@^1.55.0":
|
||||
version "1.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.55.0.tgz#080fa6d9ee6d749ff523b1c18259572d0268b963"
|
||||
integrity sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==
|
||||
dependencies:
|
||||
playwright "1.45.0"
|
||||
playwright "1.55.0"
|
||||
|
||||
"@prisma/client@^6.10.1":
|
||||
version "6.10.1"
|
||||
@@ -11188,17 +11188,17 @@ pkg-dir@^4.2.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
playwright-core@1.45.0:
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.0.tgz#5741a670b7c9060ce06852c0051d84736fb94edc"
|
||||
integrity sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==
|
||||
playwright-core@1.55.0:
|
||||
version "1.55.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.0.tgz#ec8a9f8ef118afb3e86e0f46f1393e3bea32adf4"
|
||||
integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
|
||||
|
||||
playwright@1.45.0, playwright@^1.45.0:
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.0.tgz#400c709c64438690f13705cb9c88ef93089c5c27"
|
||||
integrity sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==
|
||||
playwright@1.55.0, playwright@^1.55.0:
|
||||
version "1.55.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.0.tgz#7aca7ac3ffd9e083a8ad8b2514d6f9ba401cc78b"
|
||||
integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
|
||||
dependencies:
|
||||
playwright-core "1.45.0"
|
||||
playwright-core "1.55.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user