diff --git a/backend/app.ts b/backend/app.ts index 78d7980f..ff9d6aca 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import 'express-async-errors'; -const { updateMetrics } = require('./metrics'); +import routes from './routes'; import { json } from 'body-parser'; import express from 'express'; @@ -9,30 +9,23 @@ import cors from 'cors'; import path from 'path'; import { NotFoundError } from './errors/not-found-error'; import { errorHandler } from './middleware/error-handler'; +import { updateMetrics } from './metrics'; const app = express(); + app.set('trust proxy', true); app.use(json()); -const books = require('./routes/api/books'); -const user = require('./routes/api/user'); -const messages = require('./routes/api/messages'); -const subscription = require('./routes/api/subscription'); - app.use(express.json()); -const corsOptions = { - origin: '*', - credentials: true, //access-control-allow-credentials:true - optionSuccessStatus: 200, -}; - -app.use(cors(corsOptions)); +app.use( + cors({ + origin: 'http://localhost:3000', + credentials: true, + }) +); -app.use('/api/books', books); -app.use('/api/user', user); -app.use('/api/messages', messages); -app.use('/api/subscription', subscription); +app.use('/api', routes); app.use(updateMetrics); diff --git a/backend/controllers/books.controller.ts b/backend/controllers/books.controller.ts new file mode 100644 index 00000000..447889c0 --- /dev/null +++ b/backend/controllers/books.controller.ts @@ -0,0 +1,109 @@ +import { NextFunction, Request, Response } from 'express'; +import { + addNewBook, + deleteBook, + getAllBooksService, + getBookById, + getRecentlyAddedBooks, + searchBooksService, + updateBook, + updateCategories, +} from '../services/book'; + +export const getAllBooksController = async (req: Request, res: Response) => { + try { + const books = await getAllBooksService(req); + res.json(books); + } catch (err) { + if (err instanceof Error) { + res.status(500).json({ error: err.message }); + } else { + res.status(500).json({ error: 'An unknown error occurred' }); + } + } +}; + +export const searchBooksController = async (req: Request, res: Response) => { + try { + const books = await searchBooksService(req); + res.json(books); + } catch (err) { + if (err instanceof Error) { + res.status(500).json({ error: err.message }); + } else { + res.status(500).json({ error: 'An unknown error occurred' }); + } + } +}; +export const addBookController = async (req: Request, res: Response) => { + try { + const books = await addNewBook(req); + + res.status(201).json(books); + } catch (err) { + if (err instanceof Error) { + res.status(500).json({ error: err.message }); + } else { + res.status(500).json({ error: 'An unknown error occurred' }); + } + } +}; + +export const recentlyAddedBooksController = async ( + req: Request, + res: Response +) => { + try { + const books = await getRecentlyAddedBooks(); + res.json(books); + } catch (err) { + console.log(err); + res.status(500).json({ error: 'An unknown error occurred' }); + } +}; + +export const deleteBookController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const book = await deleteBook(req); + res.status(201).json(book); + } catch (err) { + next(err); + } +}; + +export const getBookByIdController = async (req: Request, res: Response) => { + try { + const book = await getBookById(req); + res.status(201).json(book); + } catch (err) { + console.log(err); + res.status(500).json({ error: 'An unknown error occurred' }); + } +}; + +export const updateCategoriesController = async ( + req: Request, + res: Response +) => { + try { + await updateCategories(); + res.status(200).send('Categories updated'); + } catch (err) { + console.log(err); + res.status(500).json({ error: 'An unknown error occurred' }); + } +}; + +export const updateBookController = async (req: Request, res: Response) => { + try { + const book = await updateBook(req); + res.status(201).json(book); + } catch (err) { + console.log(err); + res.status(500).json({ error: 'An unknown error occurred' }); + } +}; diff --git a/backend/controllers/messages.controller.ts b/backend/controllers/messages.controller.ts new file mode 100644 index 00000000..bd0ea43f --- /dev/null +++ b/backend/controllers/messages.controller.ts @@ -0,0 +1,33 @@ +import { Request, Response } from 'express'; +import { + createUserMessage, + deleteMessage, + getUserMessages, +} from '../services/message/userMessages.service'; + +export const getUserMessagesController = async ( + req: Request, + res: Response +) => { + const userMessages = await getUserMessages(); + res.json(userMessages); +}; + +export const createUserMessageController = async ( + req: Request, + res: Response +) => { + const { text, sender } = req.body; + try { + const userMessages = await createUserMessage(text, sender); + res.status(201).json(userMessages); + } catch (error) { + console.log(error); + } +}; + +export const deleteMessageController = async (req: Request, res: Response) => { + const { id } = req.body; + const result = await deleteMessage(id, req.body.user.isAdmin); + res.status(201).json(result); +}; diff --git a/backend/controllers/user.controller.ts b/backend/controllers/user.controller.ts new file mode 100644 index 00000000..2a20e344 --- /dev/null +++ b/backend/controllers/user.controller.ts @@ -0,0 +1,38 @@ +import { Request, Response } from 'express'; +import { + authenticateUser, + loginUser, + logoutUser, + registerUser, +} from '../services/user'; + +export const loginController = async (req: Request, res: Response) => { + const { username, password } = req.body; + const result = await loginUser(username, password); + res.status(201).json(result); +}; + +export const registerController = async (req: Request, res: Response) => { + const { username, email, password, isAdmin = false } = req.body; + const result = await registerUser(username, email, password, isAdmin); + res.status(201).json(result); +}; + +export const authController = async (req: Request, res: Response) => { + try { + const token = req.header('Authorization')?.split(' ')[1] || ''; + const result = await authenticateUser(req.body.user.id, token); + res.json(result); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}; + +export const logoutController = async (req: Request, res: Response) => { + try { + await logoutUser(req.body.user.id); + res.status(201).json({}); + } catch (err: any) { + res.status(500).json({ message: 'User logged out successfully' }); + } +}; diff --git a/backend/metrics.ts b/backend/metrics.ts index 63458318..daa8903e 100644 --- a/backend/metrics.ts +++ b/backend/metrics.ts @@ -27,6 +27,4 @@ app.get( } ); -module.exports = { - updateMetrics, -}; +export { app, updateMetrics }; diff --git a/backend/routes/api/__test__/books.test.ts b/backend/routes/api/__test__/books.test.ts index e3213fdb..358e646e 100644 --- a/backend/routes/api/__test__/books.test.ts +++ b/backend/routes/api/__test__/books.test.ts @@ -33,6 +33,7 @@ it('should not unauthorized users can upload new book', async () => { it('should authorized users can upload new book', async () => { const { token, sender } = await global.signin(); + await request(app) .post('/api/books/addNewBook') .set('Authorization', `Bearer ${token}`) @@ -114,9 +115,7 @@ it('should get all the books paginated', async () => { const allBooks = await request(app) .get(`/api/books/allBooks/?page=0&language=`) .set('Authorization', `Bearer ${token}`) - .expect(201); - - console.log(allBooks.body); + .expect(200); expect(allBooks.body.total).toEqual(2); expect(allBooks.body.results[0].name).toEqual(book2.body.name); diff --git a/backend/routes/api/books.ts b/backend/routes/api/books.ts index 092c61ab..e7622683 100644 --- a/backend/routes/api/books.ts +++ b/backend/routes/api/books.ts @@ -1,127 +1,25 @@ -import { Error } from 'mongoose'; -import axios from 'axios'; -import express, { NextFunction, Request, Response } from 'express'; +import express from 'express'; import { auth, isAdmin } from '../../middleware/auth'; -import { NotFoundError } from '../../errors/not-found-error'; + import { body } from 'express-validator'; import { validateRequest } from '../../middleware/validate-request'; -import Bottleneck from 'bottleneck'; -import { Books } from '../../models/Books'; -import { BooksData, Item } from './books.types'; import { - getUserSubscriptionsExcludingUser, - removeSubscription, -} from '../../web-push'; -import * as webpush from 'web-push'; + addBookController, + deleteBookController, + getAllBooksController, + getBookByIdController, + recentlyAddedBooksController, + searchBooksController, + updateBookController, + updateCategoriesController, +} from '../../controllers/books.controller'; const router = express.Router(); -const NodeCache = require('node-cache'); -const cache = new NodeCache(); - -router.get('/allBooks', async (req: Request, res: Response) => { - try { - const page = parseInt(String(req.query.page)) || 1; - const limit = parseInt(String(req.query.limit)) || 10; - const language = String(req.query.language) || 'all'; - - const startIndex = (page - 1) * limit; - - let query: { language?: string } = {}; - if (language !== 'all') { - query.language = language; - } - - const total = await Books.countDocuments(query); - const results: BooksData = { - results: await Books.find( - query, - 'name path size date url uploader category language description imageLinks' - ) - .populate('uploader', 'username email') - .sort({ date: -1 }) - .skip(startIndex) - .limit(limit) - .lean() - .skip(startIndex) - .limit(limit), - total: total, - page: page, - next: total > startIndex + limit ? { page: page + 1 } : undefined, - previous: startIndex > 0 ? { page: page - 1 } : undefined, - }; - - res.status(201).json(results); - } catch (err) { - if (err instanceof Error) { - throw new Error(err.message); - } else { - throw new Error(String(err)); - } - } -}); - -router.get('/searchBooks', async (req: Request, res: Response) => { - const cacheKey = JSON.stringify(req.query); - const cachedResult = cache.get(cacheKey); - if (cachedResult) { - return res.json(cachedResult); - } - const query = { - name: { - $regex: req.query.name, - $options: 'i', - }, - }; - - const page = parseInt(String(req.query.page)) || 1; - const limit = parseInt(String(req.query.limit)) || 10; - const startIndex = (page - 1) * limit; - - const [count, results] = await Promise.all([ - Books.countDocuments(query, { collation: { locale: 'tr', strength: 2 } }), - Books.find(query) - .select( - 'name path size date url uploader category language description imageLinks' - ) - .populate('uploader', 'username email') - .skip(startIndex) - .limit(limit) - .lean(), - ]); - - const endIndex = Math.min(startIndex + limit, count); - const pagination: { - next?: { - page: number; - limit: number; - }; - total?: number; - previous?: { - page: number; - limit: number; - }; - results?: any; - } = {}; - if (endIndex < count) { - pagination.next = { - page: page + 1, - limit: limit, - }; - } - pagination.total = count; - if (startIndex > 0) { - pagination.previous = { - page: page - 1, - limit: limit, - }; - } - pagination.results = results; - cache.set(cacheKey, pagination); // store the result in the cache +router.get('/allBooks', getAllBooksController); - res.json(pagination); -}); +router.get('/searchBooks', searchBooksController); router.post( '/addNewBook', @@ -133,206 +31,17 @@ router.post( ], validateRequest, auth, - async (req: Request, res: Response) => { - let responseData; - - try { - const response = await axios.get( - `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent( - req.body.name - )}` - ); - responseData = await response?.data?.items; - } catch (error) { - throw new Error('Could not find any categories'); - } - - const books = new Books({ - name: req.body.name, - url: req.body.url, - size: req.body.size, - date: new Date(), - uploader: req.body.uploader, - }); - - if (responseData) { - const categories: Set = new Set( - responseData - .slice(0, 10) - .filter((item: Item) => item.volumeInfo.categories) - .flatMap((item: Item) => item.volumeInfo.categories) - .map((category: string) => category.toLowerCase()) - ); - - const convertedCategories = Array.from(categories); - - const { description, imageLinks } = responseData[0].volumeInfo; - books.category = convertedCategories; - books.description = description; - books.imageLinks = imageLinks; - } - - await books.save(); - - const user = req.body.user; - - const payload = JSON.stringify({ - title: 'New Book Added', - body: `A new book "${req.body.name}" has been added!`, - }); - - const subscriptions = await getUserSubscriptionsExcludingUser(user.id); - - subscriptions.forEach((subscription) => { - if (subscription?.subscription?.endpoint) { - webpush - .sendNotification( - subscription.subscription as webpush.PushSubscription, - payload - ) - .catch((error) => { - if (error.statusCode === 410) { - removeSubscription(subscription.subscription); - } else { - console.error('Error sending push notification:', error); - } - }); - } else { - console.error('Invalid subscription endpoint:', subscription); - } - }); - - res.status(201).json(books); - } -); - -router.get('/recently-added', (req: Request, res: Response) => { - Books.find({}) - .sort({ date: -1 }) - .limit(50) - .exec((err, data) => { - if (err) console.log(err); - res.json(data); - }); -}); - -router.post( - '/deleteBook/:id', - validateRequest, - isAdmin, - async (req: Request, res: Response, next: NextFunction) => { - const id = req.params.id; - try { - const data = await Books.findByIdAndRemove(id); - - if (!data) { - throw new NotFoundError('Book not found'); - } - - res.status(201).json(data); - } catch (err) { - next(err); // Pass the error to the next error-handling middleware - } - - cache.flushAll(); - } + addBookController ); -router.post('/updateBook/:id', (req: Request, res: Response) => { - const id = req.params.id; - const name = req.body.name; - const language = req.body.language; - - Books.findById( - id, - ( - err: Error, - data: { - name: string; - language: string; - save: (arg0: (err: any, data: any) => void) => void; - } - ) => { - if (err) console.log(err); - if (data) { - data.name = name; - data.language = language; - - data.save((err, data) => { - if (err) console.log(err); - res.status(201).json(data); - }); - } - } - ); -}); - -router.get('/getBookById/:id', (req: Request, res: Response) => { - const id = req.params.id; - - Books.findById( - id, - ( - err: Error, - data: { - name: string; - url: string; - size: string; - uploader: string; - category: string[]; - language: string; - description: string; - imageLinks: { - smallThumbnail: string; - thumbnail: string; - }; - } - ) => { - if (err) console.log(err); - res.status(201).json(data); - } - ); -}); - -const limiter = new Bottleneck({ - minTime: 200, // Minimum time between subsequent tasks in ms. Adjust this value to fit the rate limit of the API. -}); - -router.post('/updateCategories', async (req: Request, res: Response) => { - const books = await Books.find({}).lean(); - - const updatePromises = books.map((book) => { - return limiter.schedule(async () => { - if (book.name) { - const response = await axios.get( - `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent( - book.name - )}` - ); - - const categories = new Set( - response.data.items - .slice(0, 10) - .filter((item: Item) => item.volumeInfo.categories) - .flatMap((item: Item) => item.volumeInfo.categories) - .map((category: string) => category.toLowerCase()) - ); +router.get('/recently-added', recentlyAddedBooksController); - const convertedCategories = Array.from(categories); - const { description, imageLinks } = response.data.items[0].volumeInfo; +router.post('/deleteBook/:id', validateRequest, isAdmin, deleteBookController); - return Books.findByIdAndUpdate(book._id, { - category: convertedCategories, - description, - imageLinks, - }); - } - }); - }); +router.post('/updateBook/:id', isAdmin, updateBookController); - await Promise.all(updatePromises); +router.get('/getBookById/:id', getBookByIdController); - res.status(200).send('Categories updated'); -}); +router.post('/updateCategories', isAdmin, updateCategoriesController); -module.exports = router; +export { router as booksRouter }; diff --git a/backend/routes/api/messages.ts b/backend/routes/api/messages.ts index ae6517e6..d3a6e076 100644 --- a/backend/routes/api/messages.ts +++ b/backend/routes/api/messages.ts @@ -1,20 +1,18 @@ -import express, { Response, Request } from 'express'; +import express from 'express'; import { auth } from '../../middleware/auth'; -import { Messages } from '../../models/Messages'; + import { body } from 'express-validator'; import { validateRequest } from '../../middleware/validate-request'; -import { NotAuthorizedError } from '../../errors/not-authorized-error'; -import { NotFoundError } from '../../errors/not-found-error'; + +import { + createUserMessageController, + deleteMessageController, + getUserMessagesController, +} from '../../controllers/messages.controller'; const router = express.Router(); -router.get('/userMessages', auth, async (req: Request, res: Response) => { - const userMessages = await Messages.find({}).populate( - 'sender', - 'username email _id isAdmin createdAt updatedAt messages' - ); - res.json(userMessages); -}); +router.get('/userMessages', auth, getUserMessagesController); router.post( '/userMessages', @@ -24,21 +22,7 @@ router.post( body('sender').not().isEmpty().isString().withMessage('Sender is required'), ], validateRequest, - async (req: Request, res: Response) => { - const { text, sender } = req.body; - - try { - const userMessages = new Messages({ - text, - date: new Date(), - sender, - }); - await userMessages.save(); - res.status(201).json(userMessages); - } catch (error) { - console.log(error); - } - } + createUserMessageController ); router.delete( @@ -46,20 +30,7 @@ router.delete( auth, [body('id').not().isEmpty().withMessage('Id is required')], validateRequest, - async (req: Request, res: Response) => { - const { id } = req.body; - if (!req.body.user.isAdmin) { - throw new NotAuthorizedError(); - } - - const data = await Messages.findByIdAndRemove(id); - - if (!data) { - throw new NotFoundError('Message not found'); - } - - res.status(201).json({ message: 'Message deleted!' }); - } + deleteMessageController ); -module.exports = router; +export { router as messagesRouter }; diff --git a/backend/routes/api/subscription.ts b/backend/routes/api/subscription.ts index 3bddc140..ff6f0922 100644 --- a/backend/routes/api/subscription.ts +++ b/backend/routes/api/subscription.ts @@ -20,4 +20,4 @@ router.post('/', async (req: Request, res: Response) => { res.status(201).json({ message: 'Subscription added successfully.' }); }); -module.exports = router; +export { router as subscriptionRouter }; diff --git a/backend/routes/api/user.ts b/backend/routes/api/user.ts index 1f0b41e8..794cc21a 100644 --- a/backend/routes/api/user.ts +++ b/backend/routes/api/user.ts @@ -1,14 +1,14 @@ import express, { Request, Response } from 'express'; - -import jwt from 'jsonwebtoken'; import { body } from 'express-validator'; -import { User } from '../../models/User'; -import bcrypt from 'bcrypt'; import { auth } from '../../middleware/auth'; -import { Error } from 'mongoose'; -import { NotFoundError } from '../../errors/not-found-error'; import { validateRequest } from '../../middleware/validate-request'; -import { BadRequestError } from '../../errors/bad-request-error'; + +import { + authController, + loginController, + logoutController, + registerController, +} from '../../controllers/user.controller'; const router = express.Router(); router.post( @@ -18,32 +18,7 @@ router.post( body('password', 'Please enter a valid password').isLength({ min: 6 }), ], validateRequest, - async (req: Request, res: Response) => { - const { username, password } = req.body; - - const user = await User.findOne({ username }); - - if (!user) throw new BadRequestError('Invalid credentials'); - const isMatch = await bcrypt.compare(password, user.password); - if (!isMatch) throw new NotFoundError('Invalid credentials'); - const token = jwt.sign( - { id: user._id, isAdmin: user.isAdmin }, - process.env.JWT_SECRET || '', - { expiresIn: '4h' } - ); - - res.status(201).json({ - token, - user: { - _id: user._id, - username: user.username, - isAdmin: user.isAdmin, - email: user.email, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }); - } + loginController ); router.post( @@ -60,92 +35,11 @@ router.post( body('isAdmin', 'Please enter a valid isAdmin').isBoolean().optional(), ], validateRequest, - async (req: Request, res: Response) => { - const { username, email, password, isAdmin = false } = req.body; - - const user = await User.findOne({ email }); - if (user) throw new BadRequestError('User already exists'); - - const existingUserByUsername = await User.findOne({ username }); - if (existingUserByUsername) - throw new BadRequestError('User with this username already exists'); - - const salt = await bcrypt.genSalt(10); - if (!salt) throw new Error('Something went wrong with bcrypt'); - - const hash = await bcrypt.hash(password, salt); - if (!hash) throw new Error('Something went wrong hashing the password'); - - const newUser = new User({ - username, - email, - password: hash, - isAdmin, - }); - - try { - const savedUser = await newUser.save(); - - if (!savedUser) throw new Error('Something went wrong saving the user'); - const token = jwt.sign( - { id: savedUser._id, isAdmin: savedUser.isAdmin }, - process.env.JWT_SECRET || '', - { expiresIn: '4h' } - ); - res.status(201).json({ - token, - user: { - id: savedUser.id, - username: savedUser.username, - email: savedUser.email, - }, - }); - } catch (error) { - console.log({ error }); - } - } + registerController ); -router.get('/auth', auth, async (req: Request, res: Response) => { - try { - const user = await User.findById(req.body.user.id).select('-password'); - const token = req.header('Authorization')?.split(' ')[1]; - - res.json({ - user: { - _id: user?._id, - username: user?.username, - isAdmin: user?.isAdmin, - email: user?.email, - createdAt: user?.createdAt, - updatedAt: user?.updatedAt, - }, - token: token, - }); - } catch (err: any) { - res.status(500).json({ error: err.message }); - } -}); +router.get('/auth', auth, authController); -router.post('/logout', auth, async (req: Request, res: Response) => { - try { - const user = await User.findById(req.body.user.id); - if (user && user.subscription) { - user.subscription = undefined; - await user.save(); - } - res.status(201).json({}); - } catch (err: any) { - res.status(500).json({ message: 'User logged out successfully' }); - } -}); -// router.post('/updateUser', auth, async (req, res) => { -// Books.find({}).then((books) => { -// User.findOne({ username: 'mehmesayin' }).then((user) => { -// user.booksUploaded = books; -// user.save(); -// }); -// }); -// }); +router.post('/logout', auth, logoutController); -module.exports = router; +export { router as userRouter }; diff --git a/backend/routes/index.ts b/backend/routes/index.ts new file mode 100644 index 00000000..9d210b33 --- /dev/null +++ b/backend/routes/index.ts @@ -0,0 +1,12 @@ +import express from 'express'; +import { booksRouter } from './api/books'; +import { userRouter } from './api/user'; +import { messagesRouter } from './api/messages'; + +const router = express.Router(); + +router.use('/books', booksRouter); +router.use('/user', userRouter); +router.use('/messages', messagesRouter); + +export default router; diff --git a/backend/services/book/addNewBook.service.ts b/backend/services/book/addNewBook.service.ts new file mode 100644 index 00000000..602b83ad --- /dev/null +++ b/backend/services/book/addNewBook.service.ts @@ -0,0 +1,84 @@ +// addNewBook.service.ts +import { Request } from 'express'; +import { Books } from '../../models/Books'; +import axios from 'axios'; +import * as webpush from 'web-push'; +import { Item } from '../../routes/api/books.types'; +import { + getUserSubscriptionsExcludingUser, + removeSubscription, +} from '../../web-push'; + +const addNewBook = async (req: Request) => { + let responseData; + + try { + const response = await axios.get( + `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent( + req.body.name + )}` + ); + responseData = await response?.data?.items; + } catch (error) { + throw new Error('Could not find any categories'); + } + + const books = new Books({ + name: req.body.name, + url: req.body.url, + size: req.body.size, + date: new Date(), + uploader: req.body.uploader, + }); + + if (responseData) { + const categories: Set = new Set( + responseData + .slice(0, 10) + .filter((item: Item) => item.volumeInfo.categories) + .flatMap((item: Item) => item.volumeInfo.categories) + .map((category: string) => category.toLowerCase()) + ); + + const convertedCategories = Array.from(categories); + + const { description, imageLinks } = responseData[0].volumeInfo; + books.category = convertedCategories; + books.description = description; + books.imageLinks = imageLinks; + } + + await books.save(); + + const user = req.body.user; + + const payload = JSON.stringify({ + title: 'New Book Added', + body: `A new book "${req.body.name}" has been added!`, + }); + + const subscriptions = await getUserSubscriptionsExcludingUser(user.id); + + subscriptions.forEach((subscription) => { + if (subscription?.subscription?.endpoint) { + webpush + .sendNotification( + subscription.subscription as webpush.PushSubscription, + payload + ) + .catch((error) => { + if (error.statusCode === 410) { + removeSubscription(subscription.subscription); + } else { + console.error('Error sending push notification:', error); + } + }); + } else { + console.error('Invalid subscription endpoint:', subscription); + } + }); + + return books; +}; + +export { addNewBook }; diff --git a/backend/services/book/deleteBook.service.ts b/backend/services/book/deleteBook.service.ts new file mode 100644 index 00000000..c9de3cfa --- /dev/null +++ b/backend/services/book/deleteBook.service.ts @@ -0,0 +1,17 @@ +// deleteBook.service.ts +import { Request } from 'express'; +import { Books } from '../../models/Books'; +import { NotFoundError } from '../../errors/not-found-error'; + +const deleteBook = async (req: Request) => { + const id = req.params.id; + const book = await Books.findByIdAndRemove(id); + + if (!book) { + throw new NotFoundError('Book not found'); + } + + return book; +}; + +export { deleteBook }; diff --git a/backend/services/book/getAllBooks.service.ts b/backend/services/book/getAllBooks.service.ts new file mode 100644 index 00000000..159a5d4a --- /dev/null +++ b/backend/services/book/getAllBooks.service.ts @@ -0,0 +1,48 @@ +// getAllBooksService.ts +import { Request } from 'express'; +import { Books } from '../../models/Books'; +import { BooksData } from '../../routes/api/books.types'; + +const getAllBooksService = async (req: Request) => { + try { + const page = parseInt(String(req.query.page)) || 1; + const limit = parseInt(String(req.query.limit)) || 10; + const language = String(req.query.language) || 'all'; + + const startIndex = (page - 1) * limit; + + let query: { language?: string } = {}; + if (language !== 'all') { + query.language = language; + } + + const total = await Books.countDocuments(query); + const results: BooksData = { + results: await Books.find( + query, + 'name path size date url uploader category language description imageLinks' + ) + .populate('uploader', 'username email') + .sort({ date: -1 }) + .skip(startIndex) + .limit(limit) + .lean() + .skip(startIndex) + .limit(limit), + total: total, + page: page, + next: total > startIndex + limit ? { page: page + 1 } : undefined, + previous: startIndex > 0 ? { page: page - 1 } : undefined, + }; + + return results; + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(String(err)); + } + } +}; + +export { getAllBooksService }; diff --git a/backend/services/book/getBookById.service.ts b/backend/services/book/getBookById.service.ts new file mode 100644 index 00000000..84a194b7 --- /dev/null +++ b/backend/services/book/getBookById.service.ts @@ -0,0 +1,12 @@ +// getBookById.service.ts +import { Request } from 'express'; +import { Books } from '../../models/Books'; + +const getBookById = async (req: Request) => { + const id = req.params.id; + const book = await Books.findById(id); + + return book; +}; + +export { getBookById }; diff --git a/backend/services/book/index.ts b/backend/services/book/index.ts new file mode 100644 index 00000000..cf5e798b --- /dev/null +++ b/backend/services/book/index.ts @@ -0,0 +1,8 @@ +export * from './getAllBooks.service'; +export * from './searchBooks.service'; +export * from './addNewBook.service'; +export * from './deleteBook.service'; +export * from './updateBook.service'; +export * from './recentlyAddedBooks.service'; +export * from './updateCategories.service'; +export * from './getBookById.service'; diff --git a/backend/services/book/recentlyAddedBooks.service.ts b/backend/services/book/recentlyAddedBooks.service.ts new file mode 100644 index 00000000..c6eb512b --- /dev/null +++ b/backend/services/book/recentlyAddedBooks.service.ts @@ -0,0 +1,10 @@ +// recentlyAddedBooks.service.ts +import { Books } from '../../models/Books'; + +const getRecentlyAddedBooks = async () => { + const books = await Books.find({}).sort({ date: -1 }).limit(50).exec(); + + return books; +}; + +export { getRecentlyAddedBooks }; diff --git a/backend/services/book/searchBooks.service.ts b/backend/services/book/searchBooks.service.ts new file mode 100644 index 00000000..c8a967c5 --- /dev/null +++ b/backend/services/book/searchBooks.service.ts @@ -0,0 +1,69 @@ +// searchBooksService.ts +import NodeCache from 'node-cache'; +import { Request } from 'express'; +import { Books } from '../../models/Books'; +const cache = new NodeCache(); + +const searchBooksService = async (req: Request) => { + const cacheKey = JSON.stringify(req.query); + const cachedResult = cache.get(cacheKey); + if (cachedResult) { + return cachedResult; + } + const query = { + name: { + $regex: req.query.name, + $options: 'i', + }, + }; + + const page = parseInt(String(req.query.page)) || 1; + const limit = parseInt(String(req.query.limit)) || 10; + const startIndex = (page - 1) * limit; + + const [count, results] = await Promise.all([ + Books.countDocuments(query, { collation: { locale: 'tr', strength: 2 } }), + Books.find(query) + .select( + 'name path size date url uploader category language description imageLinks' + ) + .populate('uploader', 'username email') + .skip(startIndex) + .limit(limit) + .lean(), + ]); + + const endIndex = Math.min(startIndex + limit, count); + + const pagination: { + next?: { + page: number; + limit: number; + }; + total?: number; + previous?: { + page: number; + limit: number; + }; + results?: any; + } = {}; + if (endIndex < count) { + pagination.next = { + page: page + 1, + limit: limit, + }; + } + pagination.total = count; + if (startIndex > 0) { + pagination.previous = { + page: page - 1, + limit: limit, + }; + } + pagination.results = results; + cache.set(cacheKey, pagination); // store the result in the cache + + return pagination; +}; + +export { searchBooksService }; diff --git a/backend/services/book/updateBook.service.ts b/backend/services/book/updateBook.service.ts new file mode 100644 index 00000000..3b2ae91f --- /dev/null +++ b/backend/services/book/updateBook.service.ts @@ -0,0 +1,21 @@ +// updateBook.service.ts +import { Request } from 'express'; +import { Books } from '../../models/Books'; + +const updateBook = async (req: Request) => { + const id = req.params.id; + const name = req.body.name; + const language = req.body.language; + + const book = await Books.findById(id); + + if (book) { + book.name = name; + book.language = language; + await book.save(); + } + + return book; +}; + +export { updateBook }; diff --git a/backend/services/book/updateCategories.service.ts b/backend/services/book/updateCategories.service.ts new file mode 100644 index 00000000..81d1d649 --- /dev/null +++ b/backend/services/book/updateCategories.service.ts @@ -0,0 +1,45 @@ +// updateCategories.service.ts +import { Books } from '../../models/Books'; +import axios from 'axios'; +import Bottleneck from 'bottleneck'; + +const limiter = new Bottleneck({ + minTime: 200, // Minimum time between subsequent tasks in ms. Adjust this value to fit the rate limit of the API. +}); + +const updateCategories = async () => { + const books = await Books.find({}).lean(); + + const updatePromises = books.map((book) => { + return limiter.schedule(async () => { + if (book.name) { + const response = await axios.get( + `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent( + book.name + )}` + ); + + const categories = new Set( + response.data.items + .slice(0, 10) + .filter((item: any) => item.volumeInfo.categories) + .flatMap((item: any) => item.volumeInfo.categories) + .map((category: string) => category.toLowerCase()) + ); + + const convertedCategories = Array.from(categories); + const { description, imageLinks } = response.data.items[0].volumeInfo; + + return Books.findByIdAndUpdate(book._id, { + category: convertedCategories, + description, + imageLinks, + }); + } + }); + }); + + await Promise.all(updatePromises); +}; + +export { updateCategories }; diff --git a/backend/services/index.ts b/backend/services/index.ts new file mode 100644 index 00000000..ee47330e --- /dev/null +++ b/backend/services/index.ts @@ -0,0 +1 @@ +export * from './book'; diff --git a/backend/services/message/userMessages.service.ts b/backend/services/message/userMessages.service.ts new file mode 100644 index 00000000..609f4795 --- /dev/null +++ b/backend/services/message/userMessages.service.ts @@ -0,0 +1,34 @@ +import { NotAuthorizedError } from '../../errors/not-authorized-error'; +import { NotFoundError } from '../../errors/not-found-error'; +import { Messages } from '../../models/Messages'; + +export const getUserMessages = async () => { + return await Messages.find({}).populate( + 'sender', + 'username email _id isAdmin createdAt updatedAt messages' + ); +}; + +export const createUserMessage = async (text: string, sender: string) => { + const userMessages = new Messages({ + text, + date: new Date(), + sender, + }); + await userMessages.save(); + return userMessages; +}; + +export const deleteMessage = async (id: string, isAdmin: boolean) => { + if (!isAdmin) { + throw new NotAuthorizedError(); + } + + const data = await Messages.findByIdAndRemove(id); + + if (!data) { + throw new NotFoundError('Message not found'); + } + + return { message: 'Message deleted!' }; +}; diff --git a/backend/services/user/auth.service.ts b/backend/services/user/auth.service.ts new file mode 100644 index 00000000..dae2bcff --- /dev/null +++ b/backend/services/user/auth.service.ts @@ -0,0 +1,17 @@ +import { User } from '../../models/User'; + +export const authenticateUser = async (userId: string, token: string) => { + const user = await User.findById(userId).select('-password'); + + return { + user: { + _id: user?._id, + username: user?.username, + isAdmin: user?.isAdmin, + email: user?.email, + createdAt: user?.createdAt, + updatedAt: user?.updatedAt, + }, + token: token, + }; +}; diff --git a/backend/services/user/index.ts b/backend/services/user/index.ts new file mode 100644 index 00000000..28286762 --- /dev/null +++ b/backend/services/user/index.ts @@ -0,0 +1,4 @@ +export * from './auth.service'; +export * from './login.service'; +export * from './logout.service'; +export * from './register.service'; diff --git a/backend/services/user/login.service.ts b/backend/services/user/login.service.ts new file mode 100644 index 00000000..fc14872a --- /dev/null +++ b/backend/services/user/login.service.ts @@ -0,0 +1,33 @@ +import { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; +import { User } from '../../models/User'; +import { BadRequestError } from '../../errors/bad-request-error'; +import { NotFoundError } from '../../errors/not-found-error'; + +const loginUser = async (username: string, password: string) => { + const user = await User.findOne({ username }); + + if (!user) throw new BadRequestError('Invalid credentials'); + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) throw new NotFoundError('Invalid credentials'); + const token = jwt.sign( + { id: user._id, isAdmin: user.isAdmin }, + process.env.JWT_SECRET || '', + { expiresIn: '4h' } + ); + + return { + token, + user: { + _id: user._id, + username: user.username, + isAdmin: user.isAdmin, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }; +}; + +export { loginUser }; diff --git a/backend/services/user/logout.service.ts b/backend/services/user/logout.service.ts new file mode 100644 index 00000000..94379ac1 --- /dev/null +++ b/backend/services/user/logout.service.ts @@ -0,0 +1,9 @@ +import { User } from '../../models/User'; + +export const logoutUser = async (userId: string) => { + const user = await User.findById(userId); + if (user && user.subscription) { + user.subscription = undefined; + await user.save(); + } +}; diff --git a/backend/services/user/register.service.ts b/backend/services/user/register.service.ts new file mode 100644 index 00000000..c4893cbf --- /dev/null +++ b/backend/services/user/register.service.ts @@ -0,0 +1,45 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { User } from '../../models/User'; +import { BadRequestError } from '../../errors/bad-request-error'; + +export const registerUser = async ( + username: string, + email: string, + password: string, + isAdmin: boolean = false +) => { + const user = await User.findOne({ email }); + if (user) throw new BadRequestError('User already exists'); + + const existingUserByUsername = await User.findOne({ username }); + if (existingUserByUsername) + throw new BadRequestError('User with this username already exists'); + + const salt = await bcrypt.genSalt(10); + const hash = await bcrypt.hash(password, salt); + + const newUser = new User({ + username, + email, + password: hash, + isAdmin, + }); + + const savedUser = await newUser.save(); + + const token = jwt.sign( + { id: savedUser._id, isAdmin: savedUser.isAdmin }, + process.env.JWT_SECRET || '', + { expiresIn: '4h' } + ); + + return { + token, + user: { + id: savedUser.id, + username: savedUser.username, + email: savedUser.email, + }, + }; +}; diff --git a/client/src/components/privateRoute.tsx b/client/src/components/privateRoute.tsx index 1894bcc8..51c98e19 100644 --- a/client/src/components/privateRoute.tsx +++ b/client/src/components/privateRoute.tsx @@ -11,7 +11,7 @@ export const PrivateRoute = ({ children, }: PrivateRouteProps) => { const isLoggedIn = useSelector((state: any) => state.authSlice.isLoggedIn); - console.log(isLoggedIn); + if (!isLoggedIn) { return ; } diff --git a/client/src/redux/services/book.api.ts b/client/src/redux/services/book.api.ts index a7e21001..99097e5f 100644 --- a/client/src/redux/services/book.api.ts +++ b/client/src/redux/services/book.api.ts @@ -6,6 +6,7 @@ export const bookApi = commonApi.injectEndpoints({ fetchAllBooks: build.query({ query: ({ page, language }) => ({ url: `/books/allBooks/`, + credentials: 'include', params: { page, language, diff --git a/client/src/redux/services/messages.api.ts b/client/src/redux/services/messages.api.ts index 8d0e54a0..a60345ed 100644 --- a/client/src/redux/services/messages.api.ts +++ b/client/src/redux/services/messages.api.ts @@ -6,6 +6,7 @@ export const messagesApi = commonApi.injectEndpoints({ getAllMessages: build.query({ query: () => ({ url: '/messages/userMessages', + credentials: 'include', }), providesTags: (result) => [{ type: 'Messages', id: 'List' }], }),