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
NEXTAUTH_URL=http://localhost:3000
PAGINATION_TAKE_COUNT=20
# Don't define this if you're defining the "AWS S3 Settings" below
STORAGE_FOLDER=data
# Linkwarden Cloud specific configs (Ignore - Not applicable for self-hosted version)
IS_CLOUD_INSTANCE=
# AWS S3 Settings (Optional)
SPACES_KEY=
SPACES_SECRET=
SPACES_ENDPOINT=
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,
user,
}: Props) {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState("");
@ -28,15 +27,15 @@ export default function ChangePassword({
useEffect(() => {
if (
!(oldPassword == "" || newPassword == "" || newPassword2 == "") &&
!(newPassword == "" || newPassword2 == "") &&
newPassword === newPassword2
) {
setUser({ ...user, oldPassword, newPassword });
setUser({ ...user, newPassword });
}
}, [oldPassword, newPassword, newPassword2]);
}, [newPassword, newPassword2]);
const submit = async () => {
if (oldPassword == "" || newPassword == "" || newPassword2 == "") {
if (newPassword == "" || newPassword2 == "") {
toast.error("Please fill all the fields.");
} else if (newPassword === newPassword2) {
setSubmitLoader(true);
@ -56,11 +55,15 @@ export default function ChangePassword({
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 });
if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
setUser({ ...user, newPassword: undefined });
togglePasswordFormModal();
}
} else {
@ -71,20 +74,13 @@ export default function ChangePassword({
return (
<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">
<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>
<input
value={newPassword}
onChange={(e) => setNewPassword1(e.target.value)}
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"
/>
<p className="text-sm text-sky-500">Re-enter New Password</p>
@ -93,6 +89,7 @@ export default function ChangePassword({
value={newPassword2}
onChange={(e) => setNewPassword2(e.target.value)}
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"
/>

View File

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

View File

@ -16,6 +16,8 @@ type Props = {
user: AccountSettings;
};
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
export default function ProfileSettings({
toggleSettingsModal,
setUser,
@ -59,7 +61,7 @@ export default function ProfileSettings({
};
useEffect(() => {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
setUser({ ...user, newPassword: undefined });
}, []);
const submit = async () => {
@ -80,7 +82,11 @@ export default function ProfileSettings({
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({
username: user.username,
email: user.username,
@ -88,7 +94,7 @@ export default function ProfileSettings({
});
if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
setUser({ ...user, newPassword: undefined });
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"
/>
</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>

View File

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

View File

@ -8,33 +8,11 @@ export default async function updateUser(
user: AccountSettings,
userId: number
) {
// Password Settings
if (user.newPassword && user.oldPassword) {
const targetUser = await prisma.user.findUnique({
where: {
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 };
}
}
if (!user.username || !user.email)
return {
response: "Username/Email invalid.",
status: 400,
};
// Avatar Settings
@ -66,6 +44,9 @@ export default async function updateUser(
// Other settings
const saltRounds = 10;
const newHashedPassword = bcrypt.hashSync(user.newPassword || "", saltRounds);
const updatedUser = await prisma.user.update({
where: {
id: userId,
@ -73,8 +54,13 @@ export default async function updateUser(
data: {
name: user.name,
username: user.username.toLowerCase(),
email: user.email?.toLowerCase(),
isPrivate: user.isPrivate,
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,16 +5,12 @@ import { AuthOptions, Session } from "next-auth";
import bcrypt from "bcrypt";
import EmailProvider from "next-auth/providers/email";
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";
export const authOptions: AuthOptions = {
session: {
strategy: "jwt",
},
providers: [
// EmailProvider({
// server: process.env.EMAIL_SERVER,
// from: process.env.EMAIL_FROM,
// }),
const providers: Provider[] = [
CredentialsProvider({
type: "credentials",
credentials: {
@ -30,16 +26,18 @@ export const authOptions: AuthOptions = {
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: {
OR: [
{
username: credentials.username.toLowerCase(),
},
{
email: credentials.username.toLowerCase(),
},
],
emailVerified: { not: null },
},
});
let passwordMatches: boolean = false;
@ -56,13 +54,31 @@ export const authOptions: AuthOptions = {
} 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 = {
adapter: PrismaAdapter(prisma) as Adapter,
session: {
strategy: "jwt",
},
providers,
pages: {
signIn: "/login",
},
callbacks: {
session: async ({ session, token }: { session: Session; token: JWT }) => {
console.log(token);
session.user.id = parseInt(token.id 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`
jwt({ token, trigger, session, user }) {
console.log(user);
if (trigger === "signIn") {
token.id = user.id;
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 bcrypt from "bcrypt";
const EmailProvider =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
interface Data {
response: string | object;
}
@ -9,6 +12,7 @@ interface Data {
interface User {
name: string;
username: string;
email?: string;
password: string;
}
@ -18,13 +22,39 @@ export default async function Index(
) {
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
.status(400)
.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: {
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(),
},
});
@ -38,12 +68,13 @@ export default async function Index(
data: {
name: body.name,
username: body.username.toLowerCase(),
email: body.email?.toLowerCase(),
password: hashedPassword,
},
});
res.status(201).json({ response: "User successfully created." });
} 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;
}
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false);
@ -43,7 +45,7 @@ export default function Login() {
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
</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">
@ -54,11 +56,14 @@ export default function Login() {
</p>
</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
type="text"
placeholder="johnny@example.com"
placeholder="johnny"
value={form.username}
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"
@ -80,12 +85,20 @@ export default function Login() {
loading={submitLoader}
/>
</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>
<Link href={"/register"} className="block text-sky-500 font-bold">
Sign Up
</Link>
</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 { useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
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;
username: string;
email?: string;
password: string;
passwordConfirmation: string;
}
};
export default function Register() {
const router = useRouter();
const [submitLoader, setSubmitLoader] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [form, setForm] = useState<FormData>({
name: "",
username: "",
email: EmailProvider ? "" : undefined,
password: "",
passwordConfirmation: "",
});
async function registerUser() {
if (
const checkHasEmptyFields = () => {
if (EmailProvider) {
return (
form.name !== "" &&
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) {
const { passwordConfirmation, ...request } = form;
@ -48,20 +73,19 @@ export default function Register() {
const data = await response.json();
toast.dismiss(load);
setSubmitLoader(false);
if (response.ok) {
setForm({
name: "",
username: "",
password: "",
passwordConfirmation: "",
});
if (form.email) {
await sendConfirmation();
setShowConfirmation(true);
}
toast.success("User Created!");
router.push("/login");
toast.success(
EmailProvider
? "User Created! Please check you email."
: "User Created!"
);
} else {
toast.error(data.response);
}
@ -75,7 +99,10 @@ export default function Register() {
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
</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">
@ -100,12 +127,26 @@ export default function Register() {
<input
type="text"
placeholder="johnny@example.com"
placeholder="john"
value={form.username}
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"
/>
{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>
<input
@ -136,8 +177,8 @@ export default function Register() {
loading={submitLoader}
/>
</div>
<div className="flex items-baseline gap-1 justify-center mt-10">
<p className="w-fit text-gray-500">Have an account?</p>
<div className="flex items-baseline gap-1 justify-center my-3">
<p className="w-fit text-gray-500">Already have an account?</p>
<Link href={"/login"} className="block w-min text-sky-500 font-bold">
Login
</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
CREATE TABLE "User" (
"id" SERIAL 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,
"isPrivate" BOOLEAN NOT NULL DEFAULT false,
"whitelistedUsers" TEXT[] DEFAULT ARRAY[]::TEXT[],
@ -11,6 +42,13 @@ CREATE TABLE "User" (
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "Collection" (
"id" SERIAL NOT NULL,
@ -68,9 +106,24 @@ CREATE TABLE "_LinkToTag" (
"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
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
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
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
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")
}
// model Account {
// id String @id @default(cuid())
// userId Int
// type String
// provider String
// providerAccountId String
// refresh_token String? @db.Text
// access_token String? @db.Text
// expires_at Int?
// token_type String?
// scope String?
// id_token String? @db.Text
// session_state String?
model Account {
id String @id @default(cuid())
userId Int
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
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 {
// id String @id @default(cuid())
// sessionToken String @unique
// userId Int
// expires DateTime
// user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// }
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId Int
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id Int @id @default(autoincrement())
@ -40,12 +40,12 @@ model User {
username String @unique
// email String? @unique
email String? @unique
emailVerified DateTime?
image String?
// accounts Account[]
// sessions Session[]
accounts Account[]
sessions Session[]
password String
collections Collection[]

View File

@ -6,12 +6,16 @@ declare global {
NEXTAUTH_URL: string;
PAGINATION_TAKE_COUNT: string;
STORAGE_FOLDER?: string;
IS_CLOUD_INSTANCE?: true;
SPACES_KEY?: string;
SPACES_SECRET?: string;
SPACES_ENDPOINT?: string;
BUCKET_NAME?: 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 {
profilePic: string;
oldPassword?: string;
newPassword?: string;
}