replaced email with username

This commit is contained in:
Daniel 2023-07-08 14:05:43 +03:30
parent 3993615071
commit 97ca682c0a
32 changed files with 283 additions and 117 deletions

View File

@ -43,7 +43,7 @@ export default function TeamManagement({
canDelete: false, canDelete: false,
user: { user: {
name: "", name: "",
email: "", username: "",
}, },
}); });
@ -65,7 +65,7 @@ export default function TeamManagement({
canDelete: false, canDelete: false,
user: { user: {
name: "", name: "",
email: "", username: "",
}, },
}); });
}; };
@ -146,32 +146,32 @@ export default function TeamManagement({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
value={member.user.email} value={member.user.username}
onChange={(e) => { onChange={(e) => {
setMember({ setMember({
...member, ...member,
user: { ...member.user, email: e.target.value }, user: { ...member.user, username: e.target.value },
}); });
}} }}
onKeyDown={(e) => onKeyDown={(e) =>
e.key === "Enter" && e.key === "Enter" &&
addMemberToCollection( addMemberToCollection(
session.data?.user.email as string, session.data?.user.username as string,
member.user.email, member.user.username,
collection, collection,
setMemberState setMemberState
) )
} }
type="text" type="text"
placeholder="Email" placeholder="Username"
className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
<div <div
onClick={() => onClick={() =>
addMemberToCollection( addMemberToCollection(
session.data?.user.email as string, session.data?.user.username as string,
member.user.email, member.user.username,
collection, collection,
setMemberState setMemberState
) )
@ -206,7 +206,7 @@ export default function TeamManagement({
onClick={() => { onClick={() => {
const updatedMembers = collection.members.filter( const updatedMembers = collection.members.filter(
(member) => { (member) => {
return member.user.email !== e.user.email; return member.user.username !== e.user.username;
} }
); );
setCollection({ setCollection({
@ -225,7 +225,7 @@ export default function TeamManagement({
<p className="text-sm font-bold text-sky-500"> <p className="text-sm font-bold text-sky-500">
{e.user.name} {e.user.name}
</p> </p>
<p className="text-sky-900">{e.user.email}</p> <p className="text-sky-900">{e.user.username}</p>
</div> </div>
</div> </div>
<div className="flex sm:block items-center gap-5 min-w-[10rem]"> <div className="flex sm:block items-center gap-5 min-w-[10rem]">
@ -269,7 +269,9 @@ export default function TeamManagement({
if (permissions === true) { if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (
member.user.username === e.user.username
) {
return { return {
...member, ...member,
canCreate: !e.canCreate, canCreate: !e.canCreate,
@ -312,7 +314,9 @@ export default function TeamManagement({
if (permissions === true) { if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (
member.user.username === e.user.username
) {
return { return {
...member, ...member,
canUpdate: !e.canUpdate, canUpdate: !e.canUpdate,
@ -355,7 +359,9 @@ export default function TeamManagement({
if (permissions === true) { if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (
member.user.username === e.user.username
) {
return { return {
...member, ...member,
canDelete: !e.canDelete, canDelete: !e.canDelete,

View File

@ -56,8 +56,8 @@ export default function ChangePassword({
setSubmitLoader(false); setSubmitLoader(false);
if (user.email !== account.email || user.name !== account.name) if (user.username !== account.username || user.name !== account.name)
update({ email: user.email, name: user.name }); update({ username: user.username, name: user.name });
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, oldPassword: undefined, newPassword: undefined });

View File

@ -64,8 +64,8 @@ export default function PrivacySettings({
setSubmitLoader(false); setSubmitLoader(false);
if (user.email !== account.email || user.name !== account.name) if (user.username !== account.username || user.name !== account.name)
update({ email: user.email, name: user.name }); update({ username: user.username, name: user.name });
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, oldPassword: undefined, newPassword: undefined });
@ -93,7 +93,7 @@ export default function PrivacySettings({
<div> <div>
<p className="text-sm text-sky-500 my-2">Whitelisted Users</p> <p className="text-sm text-sky-500 my-2">Whitelisted Users</p>
<p className="text-gray-500 text-sm mb-3"> <p className="text-gray-500 text-sm mb-3">
Please provide the Email addresses of the users you wish to grant Please provide the Username of the users you wish to grant
visibility to your profile. Separated by comma. visibility to your profile. Separated by comma.
</p> </p>
<textarea <textarea

View File

@ -80,8 +80,12 @@ export default function ProfileSettings({
setSubmitLoader(false); setSubmitLoader(false);
if (user.email !== account.email || user.name !== account.name) if (user.username !== account.username || user.name !== account.name)
update({ email: user.email, name: user.name }); update({
username: user.username,
email: user.username,
name: user.name,
});
if (response.ok) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, oldPassword: undefined, newPassword: undefined });
@ -146,11 +150,11 @@ export default function ProfileSettings({
</div> </div>
<div> <div>
<p className="text-sm text-sky-500 mb-2">Email</p> <p className="text-sm text-sky-500 mb-2">Username</p>
<input <input
type="text" type="text"
value={user.email} value={user.username}
onChange={(e) => setUser({ ...user, email: e.target.value })} onChange={(e) => setUser({ ...user, username: e.target.value })}
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>

View File

@ -17,7 +17,7 @@ export default function useInitialData() {
setCollections(); setCollections();
setTags(); setTags();
// setLinks(); // setLinks();
setAccount(data.user.email as string); setAccount(data.user.username as string);
} }
}, [status]); }, [status]);
} }

View File

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

View File

@ -16,7 +16,7 @@ export default async function getCollection(userId: number) {
include: { include: {
user: { user: {
select: { select: {
email: true, username: true,
name: true, name: true,
}, },
}, },

View File

@ -42,7 +42,7 @@ export default async function postCollection(
color: collection.color, color: collection.color,
members: { members: {
create: collection.members.map((e) => ({ create: collection.members.map((e) => ({
user: { connect: { email: e.user.email.toLowerCase() } }, user: { connect: { username: e.user.username.toLowerCase() } },
canCreate: e.canCreate, canCreate: e.canCreate,
canUpdate: e.canUpdate, canUpdate: e.canUpdate,
canDelete: e.canDelete, canDelete: e.canDelete,
@ -57,7 +57,7 @@ export default async function postCollection(
include: { include: {
user: { user: {
select: { select: {
email: true, username: true,
name: true, name: true,
}, },
}, },

View File

@ -43,7 +43,7 @@ export default async function updateCollection(
isPublic: collection.isPublic, isPublic: collection.isPublic,
members: { members: {
create: collection.members.map((e) => ({ create: collection.members.map((e) => ({
user: { connect: { email: e.user.email.toLowerCase() } }, user: { connect: { username: e.user.username.toLowerCase() } },
canCreate: e.canCreate, canCreate: e.canCreate,
canUpdate: e.canUpdate, canUpdate: e.canUpdate,
canDelete: e.canDelete, canDelete: e.canDelete,
@ -58,7 +58,7 @@ export default async function updateCollection(
include: { include: {
user: { user: {
select: { select: {
email: true, username: true,
name: true, name: true,
id: true, id: true,
}, },

View File

@ -1,13 +1,13 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
export default async function getUser( export default async function getUser(
lookupEmail: string, lookupUsername: string,
isSelf: boolean, isSelf: boolean,
userEmail: string username: string
) { ) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
email: lookupEmail.toLowerCase(), username: lookupUsername.toLowerCase(),
}, },
}); });
@ -16,7 +16,7 @@ export default async function getUser(
if ( if (
!isSelf && !isSelf &&
user?.isPrivate && user?.isPrivate &&
!user.whitelistedUsers.includes(userEmail.toLowerCase()) !user.whitelistedUsers.includes(username.toLowerCase())
) { ) {
return { response: "This profile is private.", status: 401 }; return { response: "This profile is private.", status: 401 };
} }
@ -30,7 +30,7 @@ export default async function getUser(
// If user is requesting someone elses data // If user is requesting someone elses data
id: unsensitiveInfo.id, id: unsensitiveInfo.id,
name: unsensitiveInfo.name, name: unsensitiveInfo.name,
email: unsensitiveInfo.email, username: unsensitiveInfo.username,
}; };
return { response: data || null, status: 200 }; return { response: data || null, status: 200 };

View File

@ -72,7 +72,7 @@ export default async function updateUser(
}, },
data: { data: {
name: user.name, name: user.name,
email: user.email.toLowerCase(), username: user.username.toLowerCase(),
isPrivate: user.isPrivate, isPrivate: user.isPrivate,
whitelistedUsers: user.whitelistedUsers, whitelistedUsers: user.whitelistedUsers,
}, },

View File

@ -1,32 +1,32 @@
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
import getPublicUserDataByEmail from "./getPublicUserDataByEmail"; import getPublicUserDataByUsername from "./getPublicUserDataByUsername";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
const addMemberToCollection = async ( const addMemberToCollection = async (
ownerEmail: string, ownerUsername: string,
memberEmail: string, memberUsername: string,
collection: CollectionIncludingMembersAndLinkCount, collection: CollectionIncludingMembersAndLinkCount,
setMember: (newMember: Member) => null | undefined setMember: (newMember: Member) => null | undefined
) => { ) => {
const checkIfMemberAlreadyExists = collection.members.find((e) => { const checkIfMemberAlreadyExists = collection.members.find((e) => {
const email = e.user.email.toLowerCase(); const username = e.user.username.toLowerCase();
return email === memberEmail.toLowerCase(); return username === memberUsername.toLowerCase();
}); });
if ( if (
// no duplicate members // no duplicate members
!checkIfMemberAlreadyExists && !checkIfMemberAlreadyExists &&
// member can't be empty // member can't be empty
memberEmail.trim() !== "" && memberUsername.trim() !== "" &&
// member can't be the owner // member can't be the owner
memberEmail.trim() !== ownerEmail memberUsername.trim() !== ownerUsername
) { ) {
// Lookup, get data/err, list ... // Lookup, get data/err, list ...
const user = await getPublicUserDataByEmail( const user = await getPublicUserDataByUsername(
memberEmail.trim().toLowerCase() memberUsername.trim().toLowerCase()
); );
if (user.email) { if (user.username) {
setMember({ setMember({
collectionId: collection.id, collectionId: collection.id,
userId: user.id, userId: user.id,
@ -35,12 +35,12 @@ const addMemberToCollection = async (
canDelete: false, canDelete: false,
user: { user: {
name: user.name, name: user.name,
email: user.email, username: user.username,
}, },
}); });
} }
} else if (checkIfMemberAlreadyExists) toast.error("User already exists."); } else if (checkIfMemberAlreadyExists) toast.error("User already exists.");
else if (memberEmail.trim() === ownerEmail) else if (memberUsername.trim() === ownerUsername)
toast.error("You are already the collection owner."); toast.error("You are already the collection owner.");
}; };

View File

@ -1,8 +1,8 @@
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
export default async function getPublicUserDataByEmail(email: string) { export default async function getPublicUserDataByEmail(username: string) {
const response = await fetch( const response = await fetch(
`/api/routes/users?email=${email.toLowerCase()}` `/api/routes/users?username=${username.toLowerCase()}`
); );
const data = await response.json(); const data = await response.json();

View File

@ -13,6 +13,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^1.0.0",
"@aws-sdk/client-s3": "^3.363.0", "@aws-sdk/client-s3": "^3.363.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0", "@fortawesome/free-regular-svg-icons": "^6.3.0",

View File

@ -5,8 +5,14 @@ import type { AppProps } from "next/app";
import Head from "next/head"; import Head from "next/head";
import AuthRedirect from "@/layouts/AuthRedirect"; import AuthRedirect from "@/layouts/AuthRedirect";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { Session } from "next-auth";
export default function App({ Component, pageProps }: AppProps) { export default function App({
Component,
pageProps,
}: AppProps<{
session: Session;
}>) {
return ( return (
<SessionProvider session={pageProps.session}> <SessionProvider session={pageProps.session}>
<Head> <Head>

View File

@ -13,7 +13,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) if (!session?.user?.username)
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
const collectionIsAccessible = await getPermission( const collectionIsAccessible = await getPermission(

View File

@ -3,39 +3,66 @@ import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import { AuthOptions } from "next-auth"; import { AuthOptions } from "next-auth";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import EmailProvider from "next-auth/providers/email";
export const authOptions: AuthOptions = { export const authOptions: AuthOptions = {
session: { session: {
strategy: "jwt", strategy: "jwt",
}, },
providers: [ providers: [
// EmailProvider({
// server: process.env.EMAIL_SERVER,
// from: process.env.EMAIL_FROM,
// }),
CredentialsProvider({ CredentialsProvider({
type: "credentials", type: "credentials",
credentials: {}, credentials: {
username: {
label: "Username",
type: "text",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials, req) { async authorize(credentials, req) {
const { email, password } = credentials as { if (!credentials) return null;
id: number;
email: string; // const { username, password } = credentials as {
password: string; // id: number;
}; // username: string;
// password: string;
// };
console.log(credentials);
const findUser = await prisma.user.findFirst({ const findUser = await prisma.user.findFirst({
where: { where: {
email: email.toLowerCase(), username: credentials.username.toLowerCase(),
}, },
}); });
let passwordMatches: boolean = false; let passwordMatches: boolean = false;
if (findUser?.password) { if (findUser?.password) {
passwordMatches = bcrypt.compareSync(password, findUser.password); passwordMatches = bcrypt.compareSync(
credentials.password,
findUser.password
);
} }
console.log({
id: findUser?.id,
name: findUser?.name,
username: findUser?.username.toLowerCase(),
});
if (passwordMatches) { if (passwordMatches) {
return { return {
id: findUser?.id, id: findUser?.id,
name: findUser?.name, name: findUser?.name,
email: findUser?.email.toLowerCase(), email: findUser?.username.toLowerCase(),
}; };
} else return null as any; } else return null as any;
}, },
@ -46,15 +73,19 @@ export const authOptions: AuthOptions = {
}, },
callbacks: { callbacks: {
session: async ({ session, token }) => { session: async ({ session, token }) => {
console.log("TOKEN:", token);
session.user.id = parseInt(token?.sub as any); session.user.id = parseInt(token?.sub as any);
session.user.username = session.user.email;
console.log("SESSION:", session);
return session; return session;
}, },
// 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 }) { jwt({ token, trigger, session }) {
if (trigger === "update" && session?.name && session?.email) { if (trigger === "update" && session?.name && session?.username) {
// Note, that `session` can be any arbitrary object, remember to validate it! // Note, that `session` can be any arbitrary object, remember to validate it!
token.name = session.name; token.name = session.name;
token.username = session.username.toLowerCase();
token.email = session.email.toLowerCase(); token.email = session.email.toLowerCase();
} }
return token; return token;

View File

@ -8,7 +8,7 @@ interface Data {
interface User { interface User {
name: string; name: string;
email: string; username: string;
password: string; password: string;
} }
@ -18,14 +18,14 @@ export default async function Index(
) { ) {
const body: User = req.body; const body: User = req.body;
if (!body.email || !body.password || !body.name) if (!body.username || !body.password || !body.name)
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 checkIfUserExists = await prisma.user.findFirst({
where: { where: {
email: body.email.toLowerCase(), username: body.username.toLowerCase(),
}, },
}); });
@ -37,7 +37,7 @@ export default async function Index(
await prisma.user.create({ await prisma.user.create({
data: { data: {
name: body.name, name: body.name,
email: body.email.toLowerCase(), username: body.username.toLowerCase(),
password: hashedPassword, password: hashedPassword,
}, },
}); });

View File

@ -8,7 +8,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id; const userId = session?.user.id;
const userEmail = session?.user.email?.toLowerCase(); const userName = session?.user.username?.toLowerCase();
const queryId = Number(req.query.id); const queryId = Number(req.query.id);
if (!queryId) if (!queryId)
@ -17,7 +17,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
.status(401) .status(401)
.send("Invalid parameters."); .send("Invalid parameters.");
if (!userId || !userEmail) if (!userId || !userName)
return res return res
.setHeader("Content-Type", "text/plain") .setHeader("Content-Type", "text/plain")
.status(401) .status(401)
@ -32,7 +32,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if ( if (
targetUser?.isPrivate && targetUser?.isPrivate &&
!targetUser.whitelistedUsers.includes(userEmail) !targetUser.whitelistedUsers.includes(userName)
) { ) {
return res return res
.setHeader("Content-Type", "text/plain") .setHeader("Content-Type", "text/plain")

View File

@ -12,7 +12,7 @@ export default async function collections(
) { ) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) { if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
} }

View File

@ -9,7 +9,7 @@ import updateLink from "@/lib/api/controllers/links/updateLink";
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) { if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
} }

View File

@ -6,7 +6,7 @@ import getTags from "@/lib/api/controllers/tags/getTags";
export default async function tags(req: NextApiRequest, res: NextApiResponse) { export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) { if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
} }

View File

@ -7,15 +7,15 @@ import updateUser from "@/lib/api/controllers/users/updateUser";
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) { if (!session?.user.username) {
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
} }
const lookupEmail = req.query.email as string; const lookupUsername = req.query.username as string;
const isSelf = session.user.email === lookupEmail ? true : false; const isSelf = session.user.username === lookupUsername ? true : false;
if (req.method === "GET") { if (req.method === "GET") {
const users = await getUsers(lookupEmail, isSelf, session.user.email); const users = await getUsers(lookupUsername, isSelf, session.user.username);
return res.status(users.status).json({ response: users.response }); return res.status(users.status).json({ response: users.response });
} else if (req.method === "PUT" && !req.body.password) { } else if (req.method === "PUT" && !req.body.password) {
const updated = await updateUser(req.body, session.user.id); const updated = await updateUser(req.body, session.user.id);

View File

@ -22,18 +22,23 @@ export default function Dashboard() {
const [numberOfLinks, setNumberOfLinks] = useState(0); const [numberOfLinks, setNumberOfLinks] = useState(0);
const [tagPinDisclosure, setTagPinDisclosure] = useState<boolean>(() => { const [tagPinDisclosure, setTagPinDisclosure] = useState<boolean>(() => {
const storedValue = localStorage.getItem("tagPinDisclosure"); const storedValue =
typeof window !== "undefined" && localStorage.getItem("tagPinDisclosure");
return storedValue ? storedValue === "true" : true; return storedValue ? storedValue === "true" : true;
}); });
const [collectionPinDisclosure, setCollectionPinDisclosure] = const [collectionPinDisclosure, setCollectionPinDisclosure] =
useState<boolean>(() => { useState<boolean>(() => {
const storedValue = localStorage.getItem("collectionPinDisclosure"); const storedValue =
typeof window !== "undefined" &&
localStorage.getItem("collectionPinDisclosure");
return storedValue ? storedValue === "true" : true; return storedValue ? storedValue === "true" : true;
}); });
const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => { const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => {
const storedValue = localStorage.getItem("linkPinDisclosure"); const storedValue =
typeof window !== "undefined" &&
localStorage.getItem("linkPinDisclosure");
return storedValue ? storedValue === "true" : true; return storedValue ? storedValue === "true" : true;
}); });

View File

@ -5,7 +5,7 @@ import { useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
interface FormData { interface FormData {
email: string; username: string;
password: string; password: string;
} }
@ -13,18 +13,18 @@ export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const [form, setForm] = useState<FormData>({ const [form, setForm] = useState<FormData>({
email: "", username: "",
password: "", password: "",
}); });
async function loginUser() { async function loginUser() {
if (form.email !== "" && form.password !== "") { if (form.username !== "" && form.password !== "") {
setSubmitLoader(true); setSubmitLoader(true);
const load = toast.loading("Authenticating..."); const load = toast.loading("Authenticating...");
const res = await signIn("credentials", { const res = await signIn("credentials", {
email: form.email, username: form.username,
password: form.password, password: form.password,
redirect: false, redirect: false,
}); });
@ -54,13 +54,13 @@ export default function Login() {
</p> </p>
</div> </div>
<p className="text-sm text-sky-500 w-fit font-semibold">Email</p> <p className="text-sm text-sky-500 w-fit font-semibold">Username</p>
<input <input
type="text" type="text"
placeholder="johnny@example.com" placeholder="johnny@example.com"
value={form.email} value={form.username}
onChange={(e) => setForm({ ...form, email: 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"
/> />

View File

@ -6,7 +6,7 @@ import SubmitButton from "@/components/SubmitButton";
interface FormData { interface FormData {
name: string; name: string;
email: string; username: string;
password: string; password: string;
passwordConfirmation: string; passwordConfirmation: string;
} }
@ -18,7 +18,7 @@ export default function Register() {
const [form, setForm] = useState<FormData>({ const [form, setForm] = useState<FormData>({
name: "", name: "",
email: "", username: "",
password: "", password: "",
passwordConfirmation: "", passwordConfirmation: "",
}); });
@ -26,7 +26,7 @@ export default function Register() {
async function registerUser() { async function registerUser() {
if ( if (
form.name !== "" && form.name !== "" &&
form.email !== "" && form.username !== "" &&
form.password !== "" && form.password !== "" &&
form.passwordConfirmation !== "" form.passwordConfirmation !== ""
) { ) {
@ -54,7 +54,7 @@ export default function Register() {
if (response.ok) { if (response.ok) {
setForm({ setForm({
name: "", name: "",
email: "", username: "",
password: "", password: "",
passwordConfirmation: "", passwordConfirmation: "",
}); });
@ -96,13 +96,13 @@ export default function Register() {
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 w-fit font-semibold">Email</p> <p className="text-sm text-sky-500 w-fit font-semibold">Username</p>
<input <input
type="text" type="text"
placeholder="johnny@example.com" placeholder="johnny@example.com"
value={form.email} value={form.username}
onChange={(e) => setForm({ ...form, email: 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"
/> />

View File

@ -0,0 +1,32 @@
/*
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

@ -7,22 +7,69 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model User { // model Account {
id Int @id @default(autoincrement()) // id String @id @default(cuid())
name String // userId Int
email String @unique // type String
password String // provider String
collections Collection[] // providerAccountId String
tags Tag[] // 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?
pinnedLinks Link[] // user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// @@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 User {
id Int @id @default(autoincrement())
name String
username String @unique
// email String? @unique
emailVerified DateTime?
image String?
// accounts Account[]
// sessions Session[]
collectionsJoined UsersAndCollections[] password String
isPrivate Boolean @default(false) collections Collection[]
whitelistedUsers String[] @default([])
createdAt DateTime @default(now()) tags Tag[]
pinnedLinks Link[]
collectionsJoined UsersAndCollections[]
isPrivate Boolean @default(false)
whitelistedUsers String[] @default([])
createdAt DateTime @default(now())
} }
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Collection { model Collection {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String

View File

@ -8,14 +8,14 @@ type ResponseObject = {
type AccountStore = { type AccountStore = {
account: AccountSettings; account: AccountSettings;
setAccount: (email: string) => void; setAccount: (username: string) => void;
updateAccount: (user: AccountSettings) => Promise<ResponseObject>; updateAccount: (user: AccountSettings) => Promise<ResponseObject>;
}; };
const useAccountStore = create<AccountStore>()((set) => ({ const useAccountStore = create<AccountStore>()((set) => ({
account: {} as AccountSettings, account: {} as AccountSettings,
setAccount: async (email) => { setAccount: async (username) => {
const response = await fetch(`/api/routes/users?email=${email}`); const response = await fetch(`/api/routes/users?username=${username}`);
const data = await response.json(); const data = await response.json();

View File

@ -21,7 +21,7 @@ export interface Member {
canCreate: boolean; canCreate: boolean;
canUpdate: boolean; canUpdate: boolean;
canDelete: boolean; canDelete: boolean;
user: OptionalExcluding<User, "email" | "name">; user: OptionalExcluding<User, "username" | "name">;
} }
export interface CollectionIncludingMembersAndLinkCount export interface CollectionIncludingMembersAndLinkCount

View File

@ -1,9 +1,11 @@
import NextAuth, { DefaultSession } from "next-auth"; import NextAuth from "next-auth";
declare module "next-auth" { declare module "next-auth" {
interface Session { interface Session {
user: { user: {
id: number; id: number;
} & DefaultSession["user"]; username: string;
email: string;
};
} }
} }

View File

@ -12,6 +12,25 @@
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@auth/core@0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@auth/core/-/core-0.8.1.tgz#8fbfb7b11ed3e4b346857b033e454efb7c16df26"
integrity sha512-WudBmZudZ/cvykxHV5hIwrYsd7AlETQ535O7w3sSiiumT28+U9GvBb8oSRtfzxpW9rym3lAdfeTJqGA8U4FecQ==
dependencies:
"@panva/hkdf" "^1.0.4"
cookie "0.5.0"
jose "^4.11.1"
oauth4webapi "^2.0.6"
preact "10.11.3"
preact-render-to-string "5.2.3"
"@auth/prisma-adapter@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@auth/prisma-adapter/-/prisma-adapter-1.0.0.tgz#9107498921997b6174e0189553f29d0eb49ba2a0"
integrity sha512-+x+s5dgpNmqrcQC2ZRAXZIM6yhkWP/EXjIUgqUyMepLiX1OHi2AXIUAAbXsW4oG9OpYr/rvPIzPBpuGt6sPFwQ==
dependencies:
"@auth/core" "0.8.1"
"@aws-crypto/crc32@3.0.0": "@aws-crypto/crc32@3.0.0":
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
@ -1023,7 +1042,7 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@panva/hkdf@^1.0.2": "@panva/hkdf@^1.0.2", "@panva/hkdf@^1.0.4":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d" resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d"
integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA== integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
@ -2022,7 +2041,7 @@ convert-source-map@^1.5.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
cookie@^0.5.0: cookie@0.5.0, cookie@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
@ -3270,7 +3289,7 @@ jiti@^1.18.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
jose@^4.11.4, jose@^4.14.1: jose@^4.11.1, jose@^4.11.4, jose@^4.14.1:
version "4.14.4" version "4.14.4"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca" resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca"
integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g== integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==
@ -3663,6 +3682,11 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth4webapi@^2.0.6:
version "2.3.0"
resolved "https://registry.yarnpkg.com/oauth4webapi/-/oauth4webapi-2.3.0.tgz#d01aeb83b60dbe3ff9ef1c6ec4a39e29c7be7ff6"
integrity sha512-JGkb5doGrwzVDuHwgrR4nHJayzN4h59VCed6EW8Tql6iHDfZIabCJvg6wtbn5q6pyB2hZruI3b77Nudvq7NmvA==
oauth@^0.9.15: oauth@^0.9.15:
version "0.9.15" version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
@ -3961,6 +3985,13 @@ postcss@^8.4.23, postcss@^8.4.24:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
preact-render-to-string@5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz#23d17376182af720b1060d5a4099843c7fe92fe4"
integrity sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==
dependencies:
pretty-format "^3.8.0"
preact-render-to-string@^5.1.19: preact-render-to-string@^5.1.19:
version "5.2.6" version "5.2.6"
resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604" resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604"
@ -3968,7 +3999,7 @@ preact-render-to-string@^5.1.19:
dependencies: dependencies:
pretty-format "^3.8.0" pretty-format "^3.8.0"
preact@^10.6.3: preact@10.11.3, preact@^10.6.3:
version "10.11.3" version "10.11.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19" resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19"
integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg== integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==