51 | |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Book
63 |
64 |
69 |
73 | ID:
74 |
75 | {bookInfo.book.id}
76 |
81 |
91 |
92 |
93 |
94 |
95 |
96 | |
97 | {/**
98 | * Cover
99 | */}
100 |
101 | {bookInfo.book.cover ? (
102 | <>
103 |
109 |
117 |
118 | >
119 | ) : (
120 | <>
121 | None
122 | >
123 | )}
124 | |
125 |
126 | {bookInfo.book.title}
127 | |
128 |
129 | {bookInfo.book.description?.length
130 | ? bookInfo.book.description.length > 256
131 | ? bookInfo.book.description.slice(0, 256) + "..."
132 | : bookInfo.book.description
133 | : "---"}
134 | |
135 | {bookInfo.author.name} |
136 |
140 | {bookInfo.book.ISBN}
141 | |
142 |
143 |
149 | {bookInfo.book.categories.length > 0 ? (
150 | bookInfo.book.categories.map((category) => (
151 | <>
152 |
158 | {category}
159 |
160 | >
161 | ))
162 | ) : (
163 | <>
164 |
169 | None
170 |
171 | >
172 | )}
173 |
174 | |
175 |
179 | {bookInfo.book.amount}
180 | |
181 |
185 | {bookInfo.book.available}
186 | |
187 |
191 | {bookInfo.book.borrows}
192 | |
193 |
197 | {bookInfo.book.borrowedTimes}
198 | |
199 |
203 | {bookInfo.book.publicationDate
204 | ? typeof bookInfo.book.publicationDate === "string"
205 | ? moment(bookInfo.book.publicationDate).format("DD/MM/YYYY")
206 | : moment(
207 | new Date(bookInfo.book.publicationDate).toISOString()
208 | ).format("DD/MM/YYYY")
209 | : "---"}
210 | |
211 |
215 | {bookInfo.book.updatedAt
216 | ? typeof bookInfo.book.updatedAt === "string"
217 | ? moment(bookInfo.book.updatedAt).format("DD/MM/YYYY")
218 | : moment(new Date(bookInfo.book.updatedAt).toISOString()).format(
219 | "DD/MM/YYYY"
220 | )
221 | : "---"}
222 | |
223 |
227 | {bookInfo.book.createdAt
228 | ? typeof bookInfo.book.createdAt === "string"
229 | ? moment(bookInfo.book.createdAt).format("DD/MM/YYYY")
230 | : moment(new Date(bookInfo.book.createdAt).toISOString()).format(
231 | "DD/MM/YYYY"
232 | )
233 | : "---"}
234 | |
235 |
239 |
246 |
259 |
272 |
273 | |
274 |
275 | >
276 | );
277 | };
278 |
279 | export default BookItem;
280 |
--------------------------------------------------------------------------------
/pages/api/auth/signin.ts:
--------------------------------------------------------------------------------
1 | import { comparePasswords } from "@/server/bcrypt";
2 | import authDb from "@/server/mongo/authDb";
3 | import { EmailRegex, PasswordRegex } from "@/utils/regex";
4 | import { NextApiRequest, NextApiResponse } from "next";
5 | import JWT from "jsonwebtoken";
6 | import { getTimeDifference } from "@/utils/functions/date";
7 | import { UserAuth } from "@/utils/models/auth";
8 | import { jwtConfig } from "@/utils/site";
9 | import { SiteUser } from "@/utils/models/user";
10 | import userDb from "@/server/mongo/userDb";
11 |
12 | export interface APIEndpointSignInParameters {
13 | email: string;
14 | username: string;
15 | password: string;
16 | sessionToken: string;
17 | }
18 |
19 | export default async function handler(
20 | req: NextApiRequest,
21 | res: NextApiResponse
22 | ) {
23 | try {
24 | const { authCollection } = await authDb();
25 |
26 | const { usersCollection } = await userDb();
27 |
28 | const {
29 | email,
30 | username,
31 | password,
32 | sessionToken,
33 | }: APIEndpointSignInParameters = req.body;
34 |
35 | if (!authCollection) {
36 | return res.status(500).json({
37 | statusCode: 500,
38 | error: {
39 | type: "Database Connection Error",
40 | message: "Could not connect to authentication database",
41 | },
42 | });
43 | }
44 |
45 | const requestDate = new Date();
46 |
47 | switch (req.method) {
48 | case "POST": {
49 | if (sessionToken) {
50 | const previousSession = (await authCollection.findOne({
51 | "session.token": sessionToken,
52 | })) as unknown as UserAuth;
53 |
54 | if (!previousSession) {
55 | return res.status(400).json({
56 | statusCode: 400,
57 | error: {
58 | type: "Invalid Session",
59 | message: "Session is invalid",
60 | },
61 | });
62 | }
63 |
64 | const timeDifference = getTimeDifference(
65 | requestDate.toISOString(),
66 | previousSession.session!.expiresAt
67 | );
68 |
69 | if (timeDifference <= 0) {
70 | return res.status(400).json({
71 | statusCode: 400,
72 | error: {
73 | type: "Invalid Session",
74 | message: "Session expired",
75 | },
76 | });
77 | }
78 |
79 | const updatedUserAuth = {
80 | lastSignIn: requestDate.toISOString(),
81 | updatedAt: requestDate.toISOString(),
82 | "session.expiresAt": new Date(
83 | Date.now() + 30 * 24 * 60 * 60 * 1000
84 | ).toISOString(),
85 | "session.updatedAt": requestDate.toISOString(),
86 | };
87 |
88 | const {
89 | ok,
90 | value: { password: excludedPassword, ...userSessionData },
91 | }: {
92 | ok: 0 | 1;
93 | value: any;
94 | } = await authCollection.findOneAndUpdate(
95 | {
96 | "session.token": sessionToken,
97 | },
98 | {
99 | $set: updatedUserAuth,
100 | },
101 | {
102 | returnDocument: "after",
103 | }
104 | );
105 |
106 | const userData = (await usersCollection.findOne({
107 | email: userSessionData.email,
108 | })) as unknown as SiteUser;
109 |
110 | return res.status(200).json({
111 | statusCode: 200,
112 | success: {
113 | status: ok ? 1 : 0,
114 | type: "User Signed In",
115 | message: "Successfully signed in",
116 | },
117 | userAuth: userSessionData,
118 | user: userData,
119 | });
120 | } else {
121 | if ((!email && !username) || !password) {
122 | return res.status(400).json({
123 | statusCode: 400,
124 | error: {
125 | type: "Missing Parameters",
126 | message: "Email and password are required",
127 | },
128 | });
129 | }
130 |
131 | if (!EmailRegex.test(email) && email) {
132 | return res.status(400).json({
133 | statusCode: 400,
134 | error: {
135 | type: "Invalid Email",
136 | message: "Email is invalid",
137 | },
138 | });
139 | }
140 |
141 | if (!PasswordRegex.test(password)) {
142 | return res.status(400).json({
143 | statusCode: 400,
144 | error: {
145 | type: "Invalid Password",
146 | message: "Password is invalid",
147 | },
148 | });
149 | }
150 |
151 | const userAuth = (await authCollection.findOne({
152 | $or: [
153 | {
154 | email,
155 | },
156 | {
157 | username,
158 | },
159 | ],
160 | })) as unknown as UserAuth;
161 |
162 | if (!userAuth) {
163 | return res.status(400).json({
164 | statusCode: 400,
165 | error: {
166 | type: "Invalid Credentials",
167 | message: "Email or password is incorrect",
168 | },
169 | });
170 | }
171 |
172 | const isPasswordValid = await comparePasswords(
173 | password,
174 | userAuth.password
175 | );
176 |
177 | if (!isPasswordValid) {
178 | return res.status(400).json({
179 | statusCode: 400,
180 | error: {
181 | type: "Invalid Credentials",
182 | message: "Email or password is incorrect",
183 | },
184 | });
185 | }
186 |
187 | const updatedUserAuth = {
188 | lastSignIn: requestDate.toISOString(),
189 | updatedAt: requestDate.toISOString(),
190 | "session.token":
191 | userAuth.session?.token &&
192 | getTimeDifference(
193 | requestDate.toISOString(),
194 | userAuth.session.expiresAt
195 | ) > 0
196 | ? userAuth.session.token
197 | : JWT.sign(
198 | {
199 | userId: userAuth._id.toHexString(),
200 | },
201 | jwtConfig.secretKey,
202 | {
203 | expiresIn: "30d",
204 | }
205 | ),
206 | "session.updatedAt": requestDate.toISOString(),
207 | "session.expiresAt": new Date(
208 | Date.now() + 30 * 24 * 60 * 60 * 1000
209 | ).toISOString(),
210 | "session.createdAt":
211 | userAuth.session?.expiresAt &&
212 | getTimeDifference(
213 | requestDate.toISOString(),
214 | userAuth.session.expiresAt
215 | ) > 0
216 | ? userAuth.session.createdAt
217 | : requestDate.toISOString(),
218 | };
219 |
220 | const userData = (await usersCollection.findOne({
221 | $or: [
222 | {
223 | email,
224 | },
225 | {
226 | username,
227 | },
228 | ],
229 | })) as unknown as SiteUser;
230 |
231 | const {
232 | ok,
233 | value: { password: excludedPassword, ...userSessionData },
234 | }: {
235 | ok: 0 | 1;
236 | value: any;
237 | } = await authCollection.findOneAndUpdate(
238 | {
239 | $or: [
240 | {
241 | email,
242 | },
243 | {
244 | username,
245 | },
246 | ],
247 | },
248 | {
249 | $set: updatedUserAuth,
250 | },
251 | {
252 | returnDocument: "after",
253 | }
254 | );
255 |
256 | return res.status(200).json({
257 | statusCode: 200,
258 | success: {
259 | status: ok ? 1 : 0,
260 | type: "User Signed In",
261 | message: "Successfully signed in",
262 | },
263 | userAuth: userSessionData,
264 | user: userData,
265 | });
266 | }
267 |
268 | break;
269 | }
270 |
271 | default: {
272 | return res.status(405).json({
273 | statusCode: 405,
274 | error: {
275 | type: "Method Not Allowed",
276 | message: "Only POST requests are allowed",
277 | },
278 | });
279 |
280 | break;
281 | }
282 | }
283 | } catch (error: any) {
284 | return res.status(500).json({
285 | statusCode: 500,
286 | error: {
287 | type: "Server Error",
288 | ...error,
289 | },
290 | });
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/components/Book/BookCard.tsx:
--------------------------------------------------------------------------------
1 | import { BookInfo } from "@/utils/models/book";
2 | import { Box, Button, Grid, Icon, Text, Tooltip } from "@chakra-ui/react";
3 | import moment from "moment";
4 | import Image from "next/image";
5 | import React from "react";
6 | import { MdBrokenImage } from "react-icons/md";
7 | import CategoryTagsList from "../Category/CategoryTagsList";
8 | import { IoBookSharp } from "react-icons/io5";
9 | import { FaHandHolding } from "react-icons/fa";
10 | import { ImBooks } from "react-icons/im";
11 | import { SiBookstack } from "react-icons/si";
12 | import { AiOutlineEye } from "react-icons/ai";
13 | import { HiOutlineClock } from "react-icons/hi";
14 |
15 | type BookCardProps = {
16 | bookData: BookInfo;
17 | onViewBook: (bookData: BookInfo) => void;
18 | };
19 |
20 | const BookCard: React.FC