diff --git a/.env.sample b/.env.sample index c43e252..e6faff3 100644 --- a/.env.sample +++ b/.env.sample @@ -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= \ No newline at end of file +SPACES_REGION= + +# SMTP Settings (Optional) +NEXT_PUBLIC_EMAIL_PROVIDER= +EMAIL_FROM= +EMAIL_SERVER= \ No newline at end of file diff --git a/components/Modal/EmailConfirmaion.tsx b/components/Modal/EmailConfirmaion.tsx new file mode 100644 index 0000000..966287b --- /dev/null +++ b/components/Modal/EmailConfirmaion.tsx @@ -0,0 +1,24 @@ +import { signIn } from "next-auth/react"; +import React from "react"; + +export default function EmailConfirmaion({ email }: { email: string }) { + return ( +
+
+

Please check your email

+

A sign in link has been sent to your email address.

+
+ signIn("email", { + email, + redirect: false, + }) + } + className="mx-auto font-semibold mt-2 cursor-pointer w-fit" + > + Resend? +
+
+
+ ); +} diff --git a/components/Modal/User/ChangePassword.tsx b/components/Modal/User/ChangePassword.tsx index cab0518..0cb2168 100644 --- a/components/Modal/User/ChangePassword.tsx +++ b/components/Modal/User/ChangePassword.tsx @@ -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 (
-

Old Password

- - 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" - />

New Password

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" />

Re-enter New Password

@@ -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" /> diff --git a/components/Modal/User/PrivacySettings.tsx b/components/Modal/User/PrivacySettings.tsx index 9ce6197..8957d5f 100644 --- a/components/Modal/User/PrivacySettings.tsx +++ b/components/Modal/User/PrivacySettings.tsx @@ -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(); } }; diff --git a/components/Modal/User/ProfileSettings.tsx b/components/Modal/User/ProfileSettings.tsx index 376668e..19d364f 100644 --- a/components/Modal/User/ProfileSettings.tsx +++ b/components/Modal/User/ProfileSettings.tsx @@ -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" />
+ + {EmailProvider ? ( +
+

Email

+ 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" + /> +
+ ) : undefined}
diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 67e234d..916dc03 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -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}; } diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts index d3098a8..62fcf3e 100644 --- a/lib/api/controllers/users/updateUser.ts +++ b/lib/api/controllers/users/updateUser.ts @@ -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, }, }); diff --git a/lib/api/sendVerificationRequest.ts b/lib/api/sendVerificationRequest.ts new file mode 100644 index 0000000..4cb85a2 --- /dev/null +++ b/lib/api/sendVerificationRequest.ts @@ -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, "​."); + + const brandColor = theme.brandColor || "#346df1"; + const color = { + background: "#f9f9f9", + text: "#444", + mainBackground: "#fff", + buttonBackground: brandColor, + buttonBorder: brandColor, + buttonText: theme.buttonText || "#fff", + }; + + return ` + + + + + + + + + + + +
+ Sign in to ${escapedHost} +
+ + + + +
Sign + in
+
+ If you did not request this email you can safely ignore it. +
+ +`; +} + +/** 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`; +} diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index fdd7d95..3f2d8ef 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -5,64 +5,80 @@ 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"; + +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 = { + adapter: PrismaAdapter(prisma) as Adapter, session: { strategy: "jwt", }, - 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; - }, - }), - ], + 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; diff --git a/pages/api/auth/register.ts b/pages/api/auth/register.ts index 488dd42..e9632f8 100644 --- a/pages/api/auth/register.ts +++ b/pages/api/auth/register.ts @@ -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,17 +22,43 @@ 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: { - 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) { const saltRounds = 10; @@ -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." }); } } diff --git a/pages/forgot.tsx b/pages/forgot.tsx new file mode 100644 index 0000000..2ae2029 --- /dev/null +++ b/pages/forgot.tsx @@ -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({ + 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 ? ( + + ) : undefined} +

+ Linkwarden +

+
+
+

Password reset

+
+ +

Email

+ + 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" + /> + +

+ Make sure to change your password in the profile settings afterwards. +

+ + +
+
+ + Go back + +
+ + ); +} diff --git a/pages/login.tsx b/pages/login.tsx index a4a88f9..89a9b85 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -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 ( <> -

+

Linkwarden

@@ -54,11 +56,14 @@ export default function Login() {

-

Username

+

+ Username + {EmailProvider ? " or Email" : undefined} +

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} /> -
+

New here?

Sign Up
+ {EmailProvider && ( +
+

Forgot your password?

+ + Send login link + +
+ )} ); } diff --git a/pages/register.tsx b/pages/register.tsx index 40dee63..8e22b76 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -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({ name: "", username: "", + email: EmailProvider ? "" : undefined, password: "", passwordConfirmation: "", }); async function registerUser() { - if ( - form.name !== "" && - form.username !== "" && - form.password !== "" && - form.passwordConfirmation !== "" - ) { + 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 ( <> -

+ {showConfirmation && form.email ? ( + + ) : undefined} +

Linkwarden

@@ -100,12 +127,26 @@ export default function Register() { 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 ? ( + <> +

Email

+ + 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} +

Password

-
-

Have an account?

+
+

Already have an account?

Login diff --git a/prisma/migrations/20230704065656_renamed_email_to_username/migration.sql b/prisma/migrations/20230704065656_renamed_email_to_username/migration.sql deleted file mode 100644 index 8261148..0000000 --- a/prisma/migrations/20230704065656_renamed_email_to_username/migration.sql +++ /dev/null @@ -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"); diff --git a/prisma/migrations/20230625202057_init/migration.sql b/prisma/migrations/20230711222012_init/migration.sql similarity index 68% rename from prisma/migrations/20230625202057_init/migration.sql rename to prisma/migrations/20230711222012_init/migration.sql index 989f508..408f99f 100644 --- a/prisma/migrations/20230625202057_init/migration.sql +++ b/prisma/migrations/20230711222012_init/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a365dcd..13aec07 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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[] diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts index 20fe67c..62a004b 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -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; } } } diff --git a/types/global.ts b/types/global.ts index 350cba2..95b2640 100644 --- a/types/global.ts +++ b/types/global.ts @@ -35,7 +35,6 @@ export interface CollectionIncludingMembersAndLinkCount export interface AccountSettings extends User { profilePic: string; - oldPassword?: string; newPassword?: string; }