refactored email verification
This commit is contained in:
parent
db446d450f
commit
f68ca100a1
|
@ -36,6 +36,7 @@ SPACES_FORCE_PATH_STYLE=
|
||||||
NEXT_PUBLIC_EMAIL_PROVIDER=
|
NEXT_PUBLIC_EMAIL_PROVIDER=
|
||||||
EMAIL_FROM=
|
EMAIL_FROM=
|
||||||
EMAIL_SERVER=
|
EMAIL_SERVER=
|
||||||
|
BASE_URL=
|
||||||
|
|
||||||
# Proxy settings
|
# Proxy settings
|
||||||
PROXY=
|
PROXY=
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
onSubmit: Function;
|
||||||
|
oldEmail: string;
|
||||||
|
newEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmailChangeVerificationModal({
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
oldEmail,
|
||||||
|
newEmail,
|
||||||
|
}: Props) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Confirm Password</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<p>
|
||||||
|
Please confirm your password before changing your email address.{" "}
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE === "true"
|
||||||
|
? "Updating this field will change your billing email on Stripe as well."
|
||||||
|
: undefined}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Old Email</p>
|
||||||
|
<p className="text-neutral">{oldEmail}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>New Email</p>
|
||||||
|
<p className="text-neutral">{newEmail}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Password</p>
|
||||||
|
<TextInput
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••••••••"
|
||||||
|
className="bg-base-200"
|
||||||
|
type="password"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end items-center">
|
||||||
|
<button
|
||||||
|
className="btn btn-accent dark:border-violet-400 text-white"
|
||||||
|
onClick={() => onSubmit(password)}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ import { AccountSettings } from "@/types/global";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import removeFile from "@/lib/api/storage/removeFile";
|
import removeFile from "@/lib/api/storage/removeFile";
|
||||||
import createFile from "@/lib/api/storage/createFile";
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
import updateCustomerEmail from "@/lib/api/updateCustomerEmail";
|
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
|
import sendChangeEmailVerificationRequest from "@/lib/api/sendChangeEmailVerificationRequest";
|
||||||
|
|
||||||
const emailEnabled =
|
const emailEnabled =
|
||||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||||
|
@ -13,17 +13,6 @@ export default async function updateUserById(
|
||||||
userId: number,
|
userId: number,
|
||||||
data: AccountSettings
|
data: AccountSettings
|
||||||
) {
|
) {
|
||||||
const ssoUser = await prisma.account.findFirst({
|
|
||||||
where: {
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emailEnabled && !data.email)
|
if (emailEnabled && !data.email)
|
||||||
return {
|
return {
|
||||||
response: "Email invalid.",
|
response: "Email invalid.",
|
||||||
|
@ -39,6 +28,7 @@ export default async function updateUserById(
|
||||||
response: "Password must be at least 8 characters.",
|
response: "Password must be at least 8 characters.",
|
||||||
status: 400,
|
status: 400,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check email (if enabled)
|
// Check email (if enabled)
|
||||||
const checkEmail =
|
const checkEmail =
|
||||||
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||||
|
@ -126,11 +116,42 @@ export default async function updateUserById(
|
||||||
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousEmail = (
|
// Email Settings
|
||||||
await prisma.user.findUnique({ where: { id: userId } })
|
|
||||||
)?.email;
|
|
||||||
|
|
||||||
// Other settings
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { email: true, password: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user && user.email && data.email && data.email !== user.email) {
|
||||||
|
if (!data.password) {
|
||||||
|
return {
|
||||||
|
response: "Invalid password.",
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
if (!user.password) {
|
||||||
|
return {
|
||||||
|
response: "User has no password.",
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatch = bcrypt.compareSync(data.password, user.password);
|
||||||
|
|
||||||
|
if (!passwordMatch) {
|
||||||
|
return {
|
||||||
|
response: "Password is incorrect.",
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sendChangeEmailVerificationRequest(user.email, data.email, data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other settings / Apply changes
|
||||||
|
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds);
|
const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds);
|
||||||
|
@ -142,7 +163,6 @@ export default async function updateUserById(
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
username: data.username?.toLowerCase().trim(),
|
username: data.username?.toLowerCase().trim(),
|
||||||
email: data.email?.toLowerCase().trim(),
|
|
||||||
isPrivate: data.isPrivate,
|
isPrivate: data.isPrivate,
|
||||||
image:
|
image:
|
||||||
data.image && data.image.startsWith("http")
|
data.image && data.image.startsWith("http")
|
||||||
|
@ -211,15 +231,6 @@ export default async function updateUserById(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
|
||||||
|
|
||||||
if (STRIPE_SECRET_KEY && emailEnabled && previousEmail !== data.email)
|
|
||||||
await updateCustomerEmail(
|
|
||||||
STRIPE_SECRET_KEY,
|
|
||||||
previousEmail as string,
|
|
||||||
data.email as string
|
|
||||||
);
|
|
||||||
|
|
||||||
const response: Omit<AccountSettings, "password"> = {
|
const response: Omit<AccountSettings, "password"> = {
|
||||||
...userInfo,
|
...userInfo,
|
||||||
whitelistedUsers: newWhitelistedUsernames,
|
whitelistedUsers: newWhitelistedUsernames,
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { prisma } from "./db";
|
||||||
|
import transporter from "./transporter";
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default async function sendChangeEmailVerificationRequest(
|
||||||
|
oldEmail: string,
|
||||||
|
newEmail: string,
|
||||||
|
user: string
|
||||||
|
) {
|
||||||
|
const token = randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
await prisma.$transaction(async () => {
|
||||||
|
await prisma.verificationToken.create({
|
||||||
|
data: {
|
||||||
|
identifier: oldEmail?.toLowerCase(),
|
||||||
|
token,
|
||||||
|
expires: new Date(Date.now() + 24 * 3600 * 1000), // 1 day
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: oldEmail?.toLowerCase(),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
unverifiedNewEmail: newEmail?.toLowerCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailsDir = path.resolve(process.cwd(), "templates");
|
||||||
|
|
||||||
|
const templateFile = readFileSync(
|
||||||
|
path.join(emailsDir, "verifyEmailChange.html"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailTemplate = Handlebars.compile(templateFile);
|
||||||
|
|
||||||
|
transporter.sendMail({
|
||||||
|
from: process.env.EMAIL_FROM,
|
||||||
|
to: newEmail,
|
||||||
|
subject: "Verify your new Linkwarden email address",
|
||||||
|
html: emailTemplate({
|
||||||
|
user,
|
||||||
|
baseUrl: process.env.BASE_URL,
|
||||||
|
oldEmail,
|
||||||
|
newEmail,
|
||||||
|
verifyUrl: `${process.env.BASE_URL}/auth/verify-email?token=${token}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,19 +1,33 @@
|
||||||
import { Theme } from "next-auth";
|
import { readFileSync } from "fs";
|
||||||
import { SendVerificationRequestParams } from "next-auth/providers";
|
import { SendVerificationRequestParams } from "next-auth/providers";
|
||||||
import { createTransport } from "nodemailer";
|
import path from "path";
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import transporter from "./transporter";
|
||||||
|
|
||||||
export default async function sendVerificationRequest(
|
export default async function sendVerificationRequest(
|
||||||
params: SendVerificationRequestParams
|
params: SendVerificationRequestParams
|
||||||
) {
|
) {
|
||||||
const { identifier, url, provider, theme } = params;
|
const emailsDir = path.resolve(process.cwd(), "templates");
|
||||||
|
|
||||||
|
const templateFile = readFileSync(
|
||||||
|
path.join(emailsDir, "verifyEmail.html"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailTemplate = Handlebars.compile(templateFile);
|
||||||
|
|
||||||
|
const { identifier, url, provider, token } = params;
|
||||||
const { host } = new URL(url);
|
const { host } = new URL(url);
|
||||||
const transport = createTransport(provider.server);
|
const result = await transporter.sendMail({
|
||||||
const result = await transport.sendMail({
|
|
||||||
to: identifier,
|
to: identifier,
|
||||||
from: provider.from,
|
from: provider.from,
|
||||||
subject: `Sign in to ${host}`,
|
subject: `Please verify your email address`,
|
||||||
text: text({ url, host }),
|
text: text({ url, host }),
|
||||||
html: html({ url, host, theme }),
|
html: emailTemplate({
|
||||||
|
url: `${
|
||||||
|
process.env.NEXTAUTH_URL
|
||||||
|
}/callback/email?token=${token}&email=${encodeURIComponent(identifier)}`,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const failed = result.rejected.concat(result.pending).filter(Boolean);
|
const failed = result.rejected.concat(result.pending).filter(Boolean);
|
||||||
if (failed.length) {
|
if (failed.length) {
|
||||||
|
@ -21,55 +35,6 @@ export default async function sendVerificationRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function html(params: { url: string; host: string; theme: Theme }) {
|
|
||||||
const { url, host, theme } = params;
|
|
||||||
|
|
||||||
const escapedHost = host.replace(/\./g, "​.");
|
|
||||||
|
|
||||||
const brandColor = theme.brandColor || "#0029cf";
|
|
||||||
const color = {
|
|
||||||
background: "#f9f9f9",
|
|
||||||
text: "#444",
|
|
||||||
mainBackground: "#fff",
|
|
||||||
buttonBackground: brandColor,
|
|
||||||
buttonBorder: brandColor,
|
|
||||||
buttonText: theme.buttonText || "#fff",
|
|
||||||
};
|
|
||||||
|
|
||||||
return `
|
|
||||||
<body style="background: ${color.background};">
|
|
||||||
<table width="100%" border="0" cellspacing="20" cellpadding="0"
|
|
||||||
style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
|
|
||||||
<tr>
|
|
||||||
<td align="center"
|
|
||||||
style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
|
||||||
Sign in to <strong>${escapedHost}</strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding: 20px 0;">
|
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}">
|
|
||||||
<a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">
|
|
||||||
Sign in
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"
|
|
||||||
style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
|
||||||
If you did not request this email you can safely ignore it.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
|
/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
|
||||||
function text({ url, host }: { url: string; host: string }) {
|
function text({ url, host }: { url: string; host: string }) {
|
||||||
return `Sign in to ${host}\n${url}\n\n`;
|
return `Sign in to ${host}\n${url}\n\n`;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createTransport } from "nodemailer";
|
||||||
|
|
||||||
|
export default createTransport({
|
||||||
|
url: process.env.EMAIL_SERVER,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_FROM,
|
||||||
|
},
|
||||||
|
});
|
|
@ -46,6 +46,7 @@
|
||||||
"eslint-config-next": "13.4.9",
|
"eslint-config-next": "13.4.9",
|
||||||
"formidable": "^3.5.1",
|
"formidable": "^3.5.1",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"himalaya": "^1.1.0",
|
"himalaya": "^1.1.0",
|
||||||
"jimp": "^0.22.10",
|
"jimp": "^0.22.10",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { prisma } from "@/lib/api/db";
|
||||||
|
import updateCustomerEmail from "@/lib/api/updateCustomerEmail";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function verifyEmail(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const token = req.query.token;
|
||||||
|
|
||||||
|
if (!token || typeof token !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "Invalid token.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token in db
|
||||||
|
const verifyToken = await prisma.verificationToken.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
expires: {
|
||||||
|
gte: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldEmail = verifyToken?.identifier;
|
||||||
|
|
||||||
|
if (!oldEmail) {
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "Invalid token.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure email isn't in use
|
||||||
|
const findNewEmail = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email: oldEmail,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
unverifiedNewEmail: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEmail = findNewEmail?.unverifiedNewEmail;
|
||||||
|
|
||||||
|
if (!newEmail) {
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "No unverified emails found.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailInUse = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email: newEmail,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(emailInUse);
|
||||||
|
|
||||||
|
if (emailInUse) {
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "Email is already in use.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove SSO provider
|
||||||
|
await prisma.account.deleteMany({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
email: oldEmail,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update email in db
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: oldEmail,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email: newEmail.toLowerCase().trim(),
|
||||||
|
unverifiedNewEmail: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply to Stripe
|
||||||
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||||
|
|
||||||
|
if (STRIPE_SECRET_KEY)
|
||||||
|
await updateCustomerEmail(STRIPE_SECRET_KEY, oldEmail, newEmail);
|
||||||
|
|
||||||
|
// Clean up existing tokens
|
||||||
|
await prisma.verificationToken.delete({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.verificationToken.deleteMany({
|
||||||
|
where: {
|
||||||
|
identifier: oldEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
response: token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://localhost:3000/api/v1/auth/verify-email?token=67b3d33491be1ba7b9ab60a8cb8caec5f248b72c0e890aafec979c0d33899279
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
const VerifyEmail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = router.query.token;
|
||||||
|
|
||||||
|
if (!token || typeof token !== "string") {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
|
||||||
|
fetch(`/api/v1/auth/verify-email?token=${token}`, {
|
||||||
|
method: "POST",
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success("Email verified. You can now login.");
|
||||||
|
} else {
|
||||||
|
toast.error("Invalid token.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(token);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div>Verify email...</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifyEmail;
|
|
@ -12,9 +12,13 @@ import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Checkbox from "@/components/Checkbox";
|
import Checkbox from "@/components/Checkbox";
|
||||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
|
import EmailChangeVerificationModal from "@/components/ModalContent/EmailChangeVerificationModal";
|
||||||
|
|
||||||
|
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
const [emailChangeVerificationModal, setEmailChangeVerificationModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
@ -30,6 +34,7 @@ export default function Account() {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
emailVerified: null,
|
emailVerified: null,
|
||||||
|
password: undefined,
|
||||||
image: "",
|
image: "",
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -68,19 +73,29 @@ export default function Account() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async (password?: string) => {
|
||||||
setSubmitLoader(true);
|
setSubmitLoader(true);
|
||||||
|
|
||||||
const load = toast.loading("Applying...");
|
const load = toast.loading("Applying...");
|
||||||
|
|
||||||
const response = await updateAccount({
|
const response = await updateAccount({
|
||||||
...user,
|
...user,
|
||||||
|
// @ts-ignore
|
||||||
|
password: password ? password : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
const emailChanged = account.email !== user.email;
|
||||||
|
|
||||||
|
if (emailChanged) {
|
||||||
toast.success("Settings Applied!");
|
toast.success("Settings Applied!");
|
||||||
|
toast.success(
|
||||||
|
"Email change request sent. Please verify the new email address."
|
||||||
|
);
|
||||||
|
setEmailChangeVerificationModal(false);
|
||||||
|
} else toast.success("Settings Applied!");
|
||||||
} else toast.error(response.data as string);
|
} else toast.error(response.data as string);
|
||||||
setSubmitLoader(false);
|
setSubmitLoader(false);
|
||||||
};
|
};
|
||||||
|
@ -177,12 +192,6 @@ export default function Account() {
|
||||||
{emailEnabled ? (
|
{emailEnabled ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">Email</p>
|
<p className="mb-2">Email</p>
|
||||||
{user.email !== account.email &&
|
|
||||||
process.env.NEXT_PUBLIC_STRIPE === "true" ? (
|
|
||||||
<p className="text-neutral mb-2 text-sm">
|
|
||||||
Updating this field will change your billing email as well
|
|
||||||
</p>
|
|
||||||
) : undefined}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={user.email || ""}
|
value={user.email || ""}
|
||||||
className="bg-base-200"
|
className="bg-base-200"
|
||||||
|
@ -230,6 +239,47 @@ export default function Account() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:-mt-3">
|
||||||
|
<Checkbox
|
||||||
|
label="Make profile private"
|
||||||
|
state={user.isPrivate}
|
||||||
|
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-neutral text-sm">
|
||||||
|
This will limit who can find and add you to new Collections.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{user.isPrivate && (
|
||||||
|
<div className="pl-5">
|
||||||
|
<p className="mt-2">Whitelisted Users</p>
|
||||||
|
<p className="text-neutral text-sm mb-3">
|
||||||
|
Please provide the Username of the users you wish to grant
|
||||||
|
visibility to your profile. Separated by comma.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||||
|
placeholder="Your profile is hidden from everyone right now..."
|
||||||
|
value={whitelistedUsersTextbox}
|
||||||
|
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubmitButton
|
||||||
|
onClick={() => {
|
||||||
|
if (account.email !== user.email) {
|
||||||
|
setEmailChangeVerificationModal(true);
|
||||||
|
} else {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loading={submitLoader}
|
||||||
|
label="Save Changes"
|
||||||
|
className="mt-2 w-full sm:w-fit"
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||||
<p className="truncate w-full pr-7 text-3xl font-thin">
|
<p className="truncate w-full pr-7 text-3xl font-thin">
|
||||||
|
@ -310,49 +360,6 @@ export default function Account() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
|
||||||
<p className="truncate w-full pr-7 text-3xl font-thin">
|
|
||||||
Profile Visibility
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider my-3"></div>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Make profile private"
|
|
||||||
state={user.isPrivate}
|
|
||||||
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-neutral text-sm">
|
|
||||||
This will limit who can find and add you to new Collections.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{user.isPrivate && (
|
|
||||||
<div className="pl-5">
|
|
||||||
<p className="mt-2">Whitelisted Users</p>
|
|
||||||
<p className="text-neutral text-sm mb-3">
|
|
||||||
Please provide the Username of the users you wish to grant
|
|
||||||
visibility to your profile. Separated by comma.
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
className="w-full resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
|
||||||
placeholder="Your profile is hidden from everyone right now..."
|
|
||||||
value={whitelistedUsersTextbox}
|
|
||||||
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SubmitButton
|
|
||||||
onClick={submit}
|
|
||||||
loading={submitLoader}
|
|
||||||
label="Save Changes"
|
|
||||||
className="mt-2 w-full sm:w-fit"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
<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 pr-7 text-3xl font-thin">
|
<p className="text-red-500 dark:text-red-500 truncate w-full pr-7 text-3xl font-thin">
|
||||||
|
@ -380,6 +387,15 @@ export default function Account() {
|
||||||
<p className="text-center w-full">Delete Your Account</p>
|
<p className="text-center w-full">Delete Your Account</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{emailChangeVerificationModal ? (
|
||||||
|
<EmailChangeVerificationModal
|
||||||
|
onClose={() => setEmailChangeVerificationModal(false)}
|
||||||
|
onSubmit={submit}
|
||||||
|
oldEmail={account.email || ""}
|
||||||
|
newEmail={user.email || ""}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "unverifiedNewEmail" TEXT;
|
|
@ -31,6 +31,7 @@ model User {
|
||||||
username String? @unique
|
username String? @unique
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
|
unverifiedNewEmail String?
|
||||||
image String?
|
image String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
password String?
|
password String?
|
||||||
|
|
|
@ -0,0 +1,413 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Email</title>
|
||||||
|
<style media="all" type="text/css">
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
.main p,
|
||||||
|
.main td,
|
||||||
|
.main span {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="body"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
bgcolor="#f8f8f8"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="container"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
"
|
||||||
|
width="600"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
style="
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span
|
||||||
|
class="preheader"
|
||||||
|
style="
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
"
|
||||||
|
>Please verify your email address by clicking the button
|
||||||
|
below.</span
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="main"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #eaebed;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="wrapper"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Verify your email address
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Thank you for signing up with Linkwarden! 🥳
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Just one small step left. Simply verify your email address,
|
||||||
|
and you’re all set!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="left"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: auto;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #00335a;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
align="center"
|
||||||
|
bgcolor="#0867ec"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{url}}"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
background-color: #00335a;
|
||||||
|
color: #ffffff;
|
||||||
|
"
|
||||||
|
>Verify Email</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr
|
||||||
|
style="
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #eaebed;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #868686;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
If you’re having trouble clicking the button, click on the
|
||||||
|
following link:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
word-break: break-all;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{url}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div
|
||||||
|
class="footer"
|
||||||
|
style="
|
||||||
|
clear: both;
|
||||||
|
padding-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="content-block"
|
||||||
|
style="vertical-align: top; text-align: center"
|
||||||
|
valign="top"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/public/linkwarden_light.png"
|
||||||
|
alt="logo"
|
||||||
|
style="width: 180px; height: auto"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,424 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Email</title>
|
||||||
|
<style media="all" type="text/css">
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
.main p,
|
||||||
|
.main td,
|
||||||
|
.main span {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="body"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
bgcolor="#f8f8f8"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="container"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
"
|
||||||
|
width="600"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
style="
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span
|
||||||
|
class="preheader"
|
||||||
|
style="
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
"
|
||||||
|
>You recently requested to change the email address associated
|
||||||
|
with your Linkwarden account. To verify your new email address,
|
||||||
|
please click the button below.</span
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="main"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #eaebed;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="wrapper"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 24px;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Verify your new Linkwarden email address
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Hi {{user}}!
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
You recently requested to change the email address
|
||||||
|
associated with your
|
||||||
|
<a href="{{baseUrl}}">Linkwarden</a> account. To verify your
|
||||||
|
new email address, please click the button below.
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b>Old Email:</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{oldEmail}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b>New Email:</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{newEmail}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="left"
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: auto;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #00335a;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
align="center"
|
||||||
|
bgcolor="#0867ec"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{verifyUrl}}"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
background-color: #00335a;
|
||||||
|
color: #ffffff;
|
||||||
|
"
|
||||||
|
>Verify Email</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div
|
||||||
|
class="footer"
|
||||||
|
style="
|
||||||
|
clear: both;
|
||||||
|
padding-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
role="presentation"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="content-block"
|
||||||
|
style="vertical-align: top; text-align: center"
|
||||||
|
valign="top"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://raw.githubusercontent.com/linkwarden/linkwarden/main/public/linkwarden_light.png"
|
||||||
|
alt="logo"
|
||||||
|
style="width: 180px; height: auto"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
"
|
||||||
|
valign="top"
|
||||||
|
>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -30,13 +30,14 @@ declare global {
|
||||||
EMAIL_FROM?: string;
|
EMAIL_FROM?: string;
|
||||||
EMAIL_SERVER?: string;
|
EMAIL_SERVER?: string;
|
||||||
|
|
||||||
|
BASE_URL?: string; // Used for email and stripe
|
||||||
|
|
||||||
NEXT_PUBLIC_STRIPE?: string;
|
NEXT_PUBLIC_STRIPE?: string;
|
||||||
STRIPE_SECRET_KEY?: string;
|
STRIPE_SECRET_KEY?: string;
|
||||||
MONTHLY_PRICE_ID?: string;
|
MONTHLY_PRICE_ID?: string;
|
||||||
YEARLY_PRICE_ID?: string;
|
YEARLY_PRICE_ID?: string;
|
||||||
NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL?: string;
|
NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL?: string;
|
||||||
NEXT_PUBLIC_TRIAL_PERIOD_DAYS?: string;
|
NEXT_PUBLIC_TRIAL_PERIOD_DAYS?: string;
|
||||||
BASE_URL?: string;
|
|
||||||
|
|
||||||
// Proxy settings
|
// Proxy settings
|
||||||
PROXY?: string;
|
PROXY?: string;
|
||||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -3709,6 +3709,18 @@ graphemer@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||||
|
|
||||||
|
handlebars@^4.7.8:
|
||||||
|
version "4.7.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9"
|
||||||
|
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
|
||||||
|
dependencies:
|
||||||
|
minimist "^1.2.5"
|
||||||
|
neo-async "^2.6.2"
|
||||||
|
source-map "^0.6.1"
|
||||||
|
wordwrap "^1.0.0"
|
||||||
|
optionalDependencies:
|
||||||
|
uglify-js "^3.1.4"
|
||||||
|
|
||||||
har-schema@^2.0.0:
|
har-schema@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||||
|
@ -4438,6 +4450,11 @@ minimist@^1.2.0, minimist@^1.2.6:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||||
|
|
||||||
|
minimist@^1.2.5:
|
||||||
|
version "1.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||||
|
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||||
|
|
||||||
minipass@^3.0.0:
|
minipass@^3.0.0:
|
||||||
version "3.3.6"
|
version "3.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
|
||||||
|
@ -4508,6 +4525,11 @@ ndarray@^1.0.13:
|
||||||
iota-array "^1.0.0"
|
iota-array "^1.0.0"
|
||||||
is-buffer "^1.0.2"
|
is-buffer "^1.0.2"
|
||||||
|
|
||||||
|
neo-async@^2.6.2:
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
next-auth@^4.22.1:
|
next-auth@^4.22.1:
|
||||||
version "4.22.1"
|
version "4.22.1"
|
||||||
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.22.1.tgz#1ea5084e38867966dc6492a71c6729c8f5cfa96b"
|
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.22.1.tgz#1ea5084e38867966dc6492a71c6729c8f5cfa96b"
|
||||||
|
@ -5532,6 +5554,11 @@ source-map@^0.5.7:
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||||
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
|
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
|
||||||
|
|
||||||
|
source-map@^0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||||
|
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||||
|
|
||||||
spawn-command@0.0.2:
|
spawn-command@0.0.2:
|
||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
|
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
|
||||||
|
@ -5982,6 +6009,11 @@ typescript@4.9.4:
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
|
||||||
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
||||||
|
|
||||||
|
uglify-js@^3.1.4:
|
||||||
|
version "3.17.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
|
||||||
|
integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==
|
||||||
|
|
||||||
unbox-primitive@^1.0.2:
|
unbox-primitive@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||||
|
@ -6218,6 +6250,11 @@ wide-align@^1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width "^1.0.2 || 2 || 3 || 4"
|
string-width "^1.0.2 || 2 || 3 || 4"
|
||||||
|
|
||||||
|
wordwrap@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||||
|
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
|
Ŝarĝante…
Reference in New Issue