fully implemented email authentication

This commit is contained in:
Daniel 2023-07-12 13:26:34 -05:00 committed by GitHub
parent 8513ab7688
commit 0050e14e7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 520 additions and 196 deletions

View File

@ -2,11 +2,17 @@ NEXTAUTH_SECRET=very_sensitive_secret
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
PAGINATION_TAKE_COUNT=20 PAGINATION_TAKE_COUNT=20
# Don't define this if you're defining the "AWS S3 Settings" below
STORAGE_FOLDER=data STORAGE_FOLDER=data
# Linkwarden Cloud specific configs (Ignore - Not applicable for self-hosted version) # AWS S3 Settings (Optional)
IS_CLOUD_INSTANCE=
SPACES_KEY= SPACES_KEY=
SPACES_SECRET= SPACES_SECRET=
SPACES_ENDPOINT= SPACES_ENDPOINT=
SPACES_REGION= SPACES_REGION=
# SMTP Settings (Optional)
NEXT_PUBLIC_EMAIL_PROVIDER=
EMAIL_FROM=
EMAIL_SERVER=

View File

@ -0,0 +1,24 @@
import { signIn } from "next-auth/react";
import React from "react";
export default function EmailConfirmaion({ email }: { email: string }) {
return (
<div className="overflow-y-auto py-2 fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
<div className="mx-auto p-3 rounded-xl border border-sky-100 shadow-lg bg-gray-100 text-sky-800">
<p className="text-center text-2xl mb-2">Please check your email</p>
<p>A sign in link has been sent to your email address.</p>
<div
onClick={() =>
signIn("email", {
email,
redirect: false,
})
}
className="mx-auto font-semibold mt-2 cursor-pointer w-fit"
>
Resend?
</div>
</div>
</div>
);
}

View File

@ -17,7 +17,6 @@ export default function ChangePassword({
setUser, setUser,
user, user,
}: Props) { }: Props) {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword1] = useState(""); const [newPassword, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState(""); const [newPassword2, setNewPassword2] = useState("");
@ -28,15 +27,15 @@ export default function ChangePassword({
useEffect(() => { useEffect(() => {
if ( if (
!(oldPassword == "" || newPassword == "" || newPassword2 == "") && !(newPassword == "" || newPassword2 == "") &&
newPassword === newPassword2 newPassword === newPassword2
) { ) {
setUser({ ...user, oldPassword, newPassword }); setUser({ ...user, newPassword });
} }
}, [oldPassword, newPassword, newPassword2]); }, [newPassword, newPassword2]);
const submit = async () => { const submit = async () => {
if (oldPassword == "" || newPassword == "" || newPassword2 == "") { if (newPassword == "" || newPassword2 == "") {
toast.error("Please fill all the fields."); toast.error("Please fill all the fields.");
} else if (newPassword === newPassword2) { } else if (newPassword === newPassword2) {
setSubmitLoader(true); setSubmitLoader(true);
@ -56,11 +55,15 @@ export default function ChangePassword({
setSubmitLoader(false); setSubmitLoader(false);
if (user.username !== account.username || user.name !== account.name) if (
(user.username !== account.username || user.name !== account.name) &&
user.username &&
user.email
)
update({ username: user.username, name: user.name }); update({ username: user.username, name: user.name });
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, newPassword: undefined });
togglePasswordFormModal(); togglePasswordFormModal();
} }
} else { } else {
@ -71,20 +74,13 @@ export default function ChangePassword({
return ( return (
<div className="mx-auto sm:w-[35rem] w-80"> <div className="mx-auto sm:w-[35rem] w-80">
<div className="max-w-[25rem] w-full mx-auto flex flex-col gap-3 justify-between"> <div className="max-w-[25rem] w-full mx-auto flex flex-col gap-3 justify-between">
<p className="text-sm text-sky-500">Old Password</p>
<input
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
type="password"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-sm text-sky-500">New Password</p> <p className="text-sm text-sky-500">New Password</p>
<input <input
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword1(e.target.value)} onChange={(e) => setNewPassword1(e.target.value)}
type="password" type="password"
placeholder="*****************"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
<p className="text-sm text-sky-500">Re-enter New Password</p> <p className="text-sm text-sky-500">Re-enter New Password</p>
@ -93,6 +89,7 @@ export default function ChangePassword({
value={newPassword2} value={newPassword2}
onChange={(e) => setNewPassword2(e.target.value)} onChange={(e) => setNewPassword2(e.target.value)}
type="password" type="password"
placeholder="*****************"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />

View File

@ -35,7 +35,7 @@ export default function PrivacySettings({
}, [whitelistedUsersTextbox]); }, [whitelistedUsersTextbox]);
useEffect(() => { useEffect(() => {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, newPassword: undefined });
}, []); }, []);
const stringToArray = (str: string) => { const stringToArray = (str: string) => {
@ -68,7 +68,7 @@ export default function PrivacySettings({
update({ username: user.username, name: user.name }); update({ username: user.username, name: user.name });
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, newPassword: undefined });
toggleSettingsModal(); toggleSettingsModal();
} }
}; };

View File

@ -16,6 +16,8 @@ type Props = {
user: AccountSettings; user: AccountSettings;
}; };
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
export default function ProfileSettings({ export default function ProfileSettings({
toggleSettingsModal, toggleSettingsModal,
setUser, setUser,
@ -59,7 +61,7 @@ export default function ProfileSettings({
}; };
useEffect(() => { useEffect(() => {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, newPassword: undefined });
}, []); }, []);
const submit = async () => { const submit = async () => {
@ -80,7 +82,11 @@ export default function ProfileSettings({
setSubmitLoader(false); setSubmitLoader(false);
if (user.username !== account.username || user.name !== account.name) if (
user.username !== account.username ||
user.name !== account.name ||
user.email !== account.email
)
update({ update({
username: user.username, username: user.username,
email: user.username, email: user.username,
@ -88,7 +94,7 @@ export default function ProfileSettings({
}); });
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, newPassword: undefined });
toggleSettingsModal(); toggleSettingsModal();
} }
}; };
@ -158,6 +164,18 @@ export default function ProfileSettings({
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
</div> </div>
{EmailProvider ? (
<div>
<p className="text-sm text-sky-500 mb-2">Email</p>
<input
type="text"
value={user.email || ""}
onChange={(e) => setUser({ ...user, email: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
) : undefined}
</div> </div>
</div> </div>

View File

@ -39,7 +39,7 @@ export default function AuthRedirect({ children }: Props) {
} }
}, [status]); }, [status]);
return <>{children}</>; if (status !== "loading" && !redirect) return <>{children}</>;
// if (status !== "loading" && !redirect) return <>{children}</>; else return <></>;
// else return <></>; // return <>{children}</>;
} }

View File

@ -8,33 +8,11 @@ export default async function updateUser(
user: AccountSettings, user: AccountSettings,
userId: number userId: number
) { ) {
// Password Settings if (!user.username || !user.email)
if (user.newPassword && user.oldPassword) { return {
const targetUser = await prisma.user.findUnique({ response: "Username/Email invalid.",
where: { status: 400,
id: user.id, };
},
});
if (
targetUser &&
bcrypt.compareSync(user.oldPassword, targetUser.password)
) {
const saltRounds = 10;
const newHashedPassword = bcrypt.hashSync(user.newPassword, saltRounds);
await prisma.user.update({
where: {
id: userId,
},
data: {
password: newHashedPassword,
},
});
} else {
return { response: "Old password is incorrect.", status: 400 };
}
}
// Avatar Settings // Avatar Settings
@ -66,6 +44,9 @@ export default async function updateUser(
// Other settings // Other settings
const saltRounds = 10;
const newHashedPassword = bcrypt.hashSync(user.newPassword || "", saltRounds);
const updatedUser = await prisma.user.update({ const updatedUser = await prisma.user.update({
where: { where: {
id: userId, id: userId,
@ -73,8 +54,13 @@ export default async function updateUser(
data: { data: {
name: user.name, name: user.name,
username: user.username.toLowerCase(), username: user.username.toLowerCase(),
email: user.email?.toLowerCase(),
isPrivate: user.isPrivate, isPrivate: user.isPrivate,
whitelistedUsers: user.whitelistedUsers, whitelistedUsers: user.whitelistedUsers,
password:
user.newPassword && user.newPassword !== ""
? newHashedPassword
: undefined,
}, },
}); });

View File

@ -0,0 +1,77 @@
import { Theme } from "next-auth";
import { SendVerificationRequestParams } from "next-auth/providers";
import { createTransport } from "nodemailer";
export default async function sendVerificationRequest(
params: SendVerificationRequestParams
) {
console.log(params);
const { identifier, url, provider, theme } = params;
const { host } = new URL(url);
const transport = createTransport(provider.server);
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: `Sign in to ${host}`,
text: text({ url, host }),
html: html({ url, host, theme }),
});
const failed = result.rejected.concat(result.pending).filter(Boolean);
if (failed.length) {
throw new Error(`Email (${failed.join(", ")}) could not be sent`);
}
}
function html(params: { url: string; host: string; theme: Theme }) {
const { url, host, theme } = params;
const escapedHost = host.replace(/\./g, "&#8203;.");
const brandColor = theme.brandColor || "#346df1";
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) */
function text({ url, host }: { url: string; host: string }) {
return `Sign in to ${host}\n${url}\n\n`;
}

View File

@ -5,64 +5,80 @@ import { AuthOptions, Session } from "next-auth";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import EmailProvider from "next-auth/providers/email"; import EmailProvider from "next-auth/providers/email";
import { JWT } from "next-auth/jwt"; import { JWT } from "next-auth/jwt";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { Adapter } from "next-auth/adapters";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import { Provider } from "next-auth/providers";
const providers: Provider[] = [
CredentialsProvider({
type: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials, req) {
if (!credentials) return null;
const findUser = await prisma.user.findFirst({
where: {
OR: [
{
username: credentials.username.toLowerCase(),
},
{
email: credentials.username.toLowerCase(),
},
],
emailVerified: { not: null },
},
});
let passwordMatches: boolean = false;
if (findUser?.password) {
passwordMatches = bcrypt.compareSync(
credentials.password,
findUser.password
);
}
if (passwordMatches) {
return findUser;
} else return null as any;
},
}),
];
if (process.env.EMAIL_SERVER && process.env.EMAIL_FROM)
providers.push(
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
maxAge: 600,
sendVerificationRequest(params) {
sendVerificationRequest(params);
},
})
);
export const authOptions: AuthOptions = { export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma) as Adapter,
session: { session: {
strategy: "jwt", strategy: "jwt",
}, },
providers: [ providers,
// EmailProvider({
// server: process.env.EMAIL_SERVER,
// from: process.env.EMAIL_FROM,
// }),
CredentialsProvider({
type: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials, req) {
if (!credentials) return null;
// const { username, password } = credentials as {
// id: number;
// username: string;
// password: string;
// };
const findUser = await prisma.user.findFirst({
where: {
username: credentials.username.toLowerCase(),
},
});
let passwordMatches: boolean = false;
if (findUser?.password) {
passwordMatches = bcrypt.compareSync(
credentials.password,
findUser.password
);
}
if (passwordMatches) {
return findUser;
} else return null as any;
},
}),
],
pages: { pages: {
signIn: "/login", signIn: "/login",
}, },
callbacks: { callbacks: {
session: async ({ session, token }: { session: Session; token: JWT }) => { session: async ({ session, token }: { session: Session; token: JWT }) => {
console.log(token);
session.user.id = parseInt(token.id as string); session.user.id = parseInt(token.id as string);
session.user.username = token.username as string; session.user.username = token.username as string;
@ -70,7 +86,6 @@ export const authOptions: AuthOptions = {
}, },
// Using the `...rest` parameter to be able to narrow down the type based on `trigger` // Using the `...rest` parameter to be able to narrow down the type based on `trigger`
jwt({ token, trigger, session, user }) { jwt({ token, trigger, session, user }) {
console.log(user);
if (trigger === "signIn") { if (trigger === "signIn") {
token.id = user.id; token.id = user.id;
token.username = (user as any).username; token.username = (user as any).username;

View File

@ -2,6 +2,9 @@ import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
const EmailProvider =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
interface Data { interface Data {
response: string | object; response: string | object;
} }
@ -9,6 +12,7 @@ interface Data {
interface User { interface User {
name: string; name: string;
username: string; username: string;
email?: string;
password: string; password: string;
} }
@ -18,17 +22,43 @@ export default async function Index(
) { ) {
const body: User = req.body; const body: User = req.body;
if (!body.username || !body.password || !body.name) const checkHasEmptyFields = EmailProvider
? !body.username || !body.password || !body.name || !body.email
: !body.username || !body.password || !body.name;
if (checkHasEmptyFields)
return res return res
.status(400) .status(400)
.json({ response: "Please fill out all the fields." }); .json({ response: "Please fill out all the fields." });
const checkIfUserExists = await prisma.user.findFirst({ const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
// Remove user's who aren't verified for more than 10 minutes
await prisma.user.deleteMany({
where: { where: {
username: body.username.toLowerCase(), createdAt: {
lt: tenMinutesAgo,
},
emailVerified: null,
}, },
}); });
const checkIfUserExists = await prisma.user.findFirst({
where: EmailProvider
? {
OR: [
{ username: body.username.toLowerCase() },
{
email: body.email?.toLowerCase(),
},
],
emailVerified: { not: null },
}
: {
username: body.username.toLowerCase(),
},
});
if (!checkIfUserExists) { if (!checkIfUserExists) {
const saltRounds = 10; const saltRounds = 10;
@ -38,12 +68,13 @@ export default async function Index(
data: { data: {
name: body.name, name: body.name,
username: body.username.toLowerCase(), username: body.username.toLowerCase(),
email: body.email?.toLowerCase(),
password: hashedPassword, password: hashedPassword,
}, },
}); });
res.status(201).json({ response: "User successfully created." }); res.status(201).json({ response: "User successfully created." });
} else if (checkIfUserExists) { } else if (checkIfUserExists) {
res.status(400).json({ response: "User already exists." }); res.status(400).json({ response: "Username and/or Email already exists." });
} }
} }

86
pages/forgot.tsx Normal file
View File

@ -0,0 +1,86 @@
import EmailConfirmaion from "@/components/Modal/EmailConfirmaion";
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "react-hot-toast";
interface FormData {
email: string;
}
export default function Forgot() {
const [submitLoader, setSubmitLoader] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [form, setForm] = useState<FormData>({
email: "",
});
async function loginUser() {
if (form.email !== "") {
setSubmitLoader(true);
const load = toast.loading("Sending login link...");
const res = await signIn("email", {
email: form.email,
callbackUrl: window.location.origin,
});
setShowConfirmation(true);
toast.dismiss(load);
setSubmitLoader(false);
if (!res?.ok) {
toast.error("Invalid login.");
}
} else {
toast.error("Please fill out all the fields.");
}
}
return (
<>
{showConfirmation && form.email ? (
<EmailConfirmaion email={form.email} />
) : undefined}
<p className="text-xl font-bold text-center text-sky-500 mt-10 mb-3">
Linkwarden
</p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="my-5 text-center">
<p className="text-3xl font-bold text-sky-500">Password reset</p>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
<input
type="text"
placeholder="johnny@example.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-md text-gray-500">
Make sure to change your password in the profile settings afterwards.
</p>
<SubmitButton
onClick={loginUser}
label="Send Login Link"
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div>
<div className="flex items-baseline gap-1 justify-center my-3">
<Link href={"/login"} className="block text-sky-500 font-bold">
Go back
</Link>
</div>
</>
);
}

View File

@ -9,6 +9,8 @@ interface FormData {
password: string; password: string;
} }
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
export default function Login() { export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
@ -43,7 +45,7 @@ export default function Login() {
return ( return (
<> <>
<p className="text-xl font-bold text-center text-sky-500 my-10"> <p className="text-xl font-bold text-center text-sky-500 mt-10 mb-3">
Linkwarden Linkwarden
</p> </p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100"> <div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
@ -54,11 +56,14 @@ export default function Login() {
</p> </p>
</div> </div>
<p className="text-sm text-sky-500 w-fit font-semibold">Username</p> <p className="text-sm text-sky-500 w-fit font-semibold">
Username
{EmailProvider ? " or Email" : undefined}
</p>
<input <input
type="text" type="text"
placeholder="johnny@example.com" placeholder="johnny"
value={form.username} value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })} onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
@ -80,12 +85,20 @@ export default function Login() {
loading={submitLoader} loading={submitLoader}
/> />
</div> </div>
<div className="flex items-baseline gap-1 justify-center mt-10"> <div className="flex items-baseline gap-1 justify-center my-3">
<p className="w-fit text-gray-500">New here?</p> <p className="w-fit text-gray-500">New here?</p>
<Link href={"/register"} className="block text-sky-500 font-bold"> <Link href={"/register"} className="block text-sky-500 font-bold">
Sign Up Sign Up
</Link> </Link>
</div> </div>
{EmailProvider && (
<div className="flex items-baseline gap-1 justify-center mb-3">
<p className="w-fit text-gray-500">Forgot your password?</p>
<Link href={"/forgot"} className="block text-sky-500 font-bold">
Send login link
</Link>
</div>
)}
</> </>
); );
} }

View File

@ -1,35 +1,60 @@
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import EmailConfirmaion from "@/components/Modal/EmailConfirmaion";
interface FormData { const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
type FormData = {
name: string; name: string;
username: string; username: string;
email?: string;
password: string; password: string;
passwordConfirmation: string; passwordConfirmation: string;
} };
export default function Register() { export default function Register() {
const router = useRouter();
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [form, setForm] = useState<FormData>({ const [form, setForm] = useState<FormData>({
name: "", name: "",
username: "", username: "",
email: EmailProvider ? "" : undefined,
password: "", password: "",
passwordConfirmation: "", passwordConfirmation: "",
}); });
async function registerUser() { async function registerUser() {
if ( const checkHasEmptyFields = () => {
form.name !== "" && if (EmailProvider) {
form.username !== "" && return (
form.password !== "" && form.name !== "" &&
form.passwordConfirmation !== "" form.username !== "" &&
) { form.email !== "" &&
form.password !== "" &&
form.passwordConfirmation !== ""
);
} else {
return (
form.name !== "" &&
form.username !== "" &&
form.password !== "" &&
form.passwordConfirmation !== ""
);
}
};
const sendConfirmation = async () => {
await signIn("email", {
email: form.email,
redirect: false,
});
};
if (checkHasEmptyFields()) {
if (form.password === form.passwordConfirmation) { if (form.password === form.passwordConfirmation) {
const { passwordConfirmation, ...request } = form; const { passwordConfirmation, ...request } = form;
@ -48,20 +73,19 @@ export default function Register() {
const data = await response.json(); const data = await response.json();
toast.dismiss(load); toast.dismiss(load);
setSubmitLoader(false); setSubmitLoader(false);
if (response.ok) { if (response.ok) {
setForm({ if (form.email) {
name: "", await sendConfirmation();
username: "", setShowConfirmation(true);
password: "", }
passwordConfirmation: "",
});
toast.success("User Created!"); toast.success(
EmailProvider
router.push("/login"); ? "User Created! Please check you email."
: "User Created!"
);
} else { } else {
toast.error(data.response); toast.error(data.response);
} }
@ -75,7 +99,10 @@ export default function Register() {
return ( return (
<> <>
<p className="text-xl font-bold text-center my-10 text-sky-500"> {showConfirmation && form.email ? (
<EmailConfirmaion email={form.email} />
) : undefined}
<p className="text-xl font-bold text-center my-10 mb-3 text-sky-500">
Linkwarden Linkwarden
</p> </p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100"> <div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
@ -100,12 +127,26 @@ export default function Register() {
<input <input
type="text" type="text"
placeholder="johnny@example.com" placeholder="john"
value={form.username} value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })} onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
{EmailProvider ? (
<>
<p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
<input
type="email"
placeholder="johnny@example.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</>
) : undefined}
<p className="text-sm text-sky-500 w-fit font-semibold">Password</p> <p className="text-sm text-sky-500 w-fit font-semibold">Password</p>
<input <input
@ -136,8 +177,8 @@ export default function Register() {
loading={submitLoader} loading={submitLoader}
/> />
</div> </div>
<div className="flex items-baseline gap-1 justify-center mt-10"> <div className="flex items-baseline gap-1 justify-center my-3">
<p className="w-fit text-gray-500">Have an account?</p> <p className="w-fit text-gray-500">Already have an account?</p>
<Link href={"/login"} className="block w-min text-sky-500 font-bold"> <Link href={"/login"} className="block w-min text-sky-500 font-bold">
Login Login
</Link> </Link>

View File

@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the column `email` on the `User` table. All the data in the column will be lost.
- A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "User_email_key";
ALTER TABLE "User" RENAME COLUMN "email" TO "username";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT;
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

View File

@ -1,8 +1,39 @@
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"email" TEXT NOT NULL, "username" TEXT NOT NULL,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"password" TEXT NOT NULL, "password" TEXT NOT NULL,
"isPrivate" BOOLEAN NOT NULL DEFAULT false, "isPrivate" BOOLEAN NOT NULL DEFAULT false,
"whitelistedUsers" TEXT[] DEFAULT ARRAY[]::TEXT[], "whitelistedUsers" TEXT[] DEFAULT ARRAY[]::TEXT[],
@ -11,6 +42,13 @@ CREATE TABLE "User" (
CONSTRAINT "User_pkey" PRIMARY KEY ("id") CONSTRAINT "User_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable -- CreateTable
CREATE TABLE "Collection" ( CREATE TABLE "Collection" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
@ -68,9 +106,24 @@ CREATE TABLE "_LinkToTag" (
"B" INTEGER NOT NULL "B" INTEGER NOT NULL
); );
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Collection_name_ownerId_key" ON "Collection"("name", "ownerId"); CREATE UNIQUE INDEX "Collection_name_ownerId_key" ON "Collection"("name", "ownerId");
@ -89,6 +142,12 @@ CREATE UNIQUE INDEX "_LinkToTag_AB_unique" ON "_LinkToTag"("A", "B");
-- CreateIndex -- CreateIndex
CREATE INDEX "_LinkToTag_B_index" ON "_LinkToTag"("B"); CREATE INDEX "_LinkToTag_B_index" ON "_LinkToTag"("B");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Collection" ADD CONSTRAINT "Collection_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Collection" ADD CONSTRAINT "Collection_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -7,32 +7,32 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
// model Account { model Account {
// id String @id @default(cuid()) id String @id @default(cuid())
// userId Int userId Int
// type String type String
// provider String provider String
// providerAccountId String providerAccountId String
// refresh_token String? @db.Text refresh_token String? @db.Text
// access_token String? @db.Text access_token String? @db.Text
// expires_at Int? expires_at Int?
// token_type String? token_type String?
// scope String? scope String?
// id_token String? @db.Text id_token String? @db.Text
// session_state String? session_state String?
// user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// @@unique([provider, providerAccountId]) @@unique([provider, providerAccountId])
// } }
// model Session { model Session {
// id String @id @default(cuid()) id String @id @default(cuid())
// sessionToken String @unique sessionToken String @unique
// userId Int userId Int
// expires DateTime expires DateTime
// user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// } }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
@ -40,12 +40,12 @@ model User {
username String @unique username String @unique
// email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
image String? image String?
// accounts Account[] accounts Account[]
// sessions Session[] sessions Session[]
password String password String
collections Collection[] collections Collection[]

View File

@ -6,12 +6,16 @@ declare global {
NEXTAUTH_URL: string; NEXTAUTH_URL: string;
PAGINATION_TAKE_COUNT: string; PAGINATION_TAKE_COUNT: string;
STORAGE_FOLDER?: string; STORAGE_FOLDER?: string;
IS_CLOUD_INSTANCE?: true;
SPACES_KEY?: string; SPACES_KEY?: string;
SPACES_SECRET?: string; SPACES_SECRET?: string;
SPACES_ENDPOINT?: string; SPACES_ENDPOINT?: string;
BUCKET_NAME?: string; BUCKET_NAME?: string;
SPACES_REGION?: string; SPACES_REGION?: string;
NEXT_PUBLIC_EMAIL_PROVIDER?: true;
EMAIL_FROM?: string;
EMAIL_SERVER?: string;
} }
} }
} }

View File

@ -35,7 +35,6 @@ export interface CollectionIncludingMembersAndLinkCount
export interface AccountSettings extends User { export interface AccountSettings extends User {
profilePic: string; profilePic: string;
oldPassword?: string;
newPassword?: string; newPassword?: string;
} }