From 772fe1574c902e209bf259fd04387204e1d92b6a Mon Sep 17 00:00:00 2001 From: Liam <67254223+RealFascinated@users.noreply.github.com> Date: Sun, 13 Nov 2022 22:45:01 +0000 Subject: [PATCH] Add file model --- src/models/FileModel.js | 10 +++++ src/models/UserModel.js | 12 ++++-- src/pages/api/auth/[...nextauth].js | 34 +++++++++++++-- src/pages/api/hello.js | 5 --- src/pages/api/upload/sharex.js | 52 ++++++++++++++++++++++ src/pages/files/[fileId].js | 37 ++++++++++++++++ src/utils/helpers/fileHelpers.js | 44 +++++++++++++++++++ src/utils/helpers/ioHelpers.js | 64 ++++++++++++++++++++++++++++ src/utils/helpers/mongoHelpers.js | 9 ++++ src/utils/helpers/passwordHelpers.js | 11 ++--- src/utils/helpers/stringHelpers.js | 2 +- src/utils/helpers/userHelpers.js | 57 ++++++++++++++++++++++--- 12 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 src/models/FileModel.js delete mode 100644 src/pages/api/hello.js create mode 100644 src/pages/api/upload/sharex.js create mode 100644 src/pages/files/[fileId].js create mode 100644 src/utils/helpers/fileHelpers.js create mode 100644 src/utils/helpers/ioHelpers.js create mode 100644 src/utils/helpers/mongoHelpers.js diff --git a/src/models/FileModel.js b/src/models/FileModel.js new file mode 100644 index 0000000..d8e225f --- /dev/null +++ b/src/models/FileModel.js @@ -0,0 +1,10 @@ +import mongoose, { Schema } from "mongoose"; + +const schema = new Schema({ + uploader: mongoose.Types.ObjectId, + fileId: String, + uploadDate: Date, + contentType: String, +}); + +export default mongoose.models.File || mongoose.model("File", schema); diff --git a/src/models/UserModel.js b/src/models/UserModel.js index 1778ea9..2e81959 100644 --- a/src/models/UserModel.js +++ b/src/models/UserModel.js @@ -1,9 +1,15 @@ import mongoose, { Schema } from "mongoose"; const schema = new Schema({ - email: String, - username: String, - password: String, + // The username of the user + username: { + type: String, + index: true, + }, + password: String, // The hashed password of the user + salt: String, // The salt the password was hashed with + uploadKey: String, // The users upload key for ShareX + lastLoginDate: Date, // The last time the user logged in }); export default mongoose.models.User || mongoose.model("User", schema); diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js index f3f4af6..125ef02 100644 --- a/src/pages/api/auth/[...nextauth].js +++ b/src/pages/api/auth/[...nextauth].js @@ -1,19 +1,47 @@ import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; +import { connectMongo } from "../../../utils/helpers/mongoHelpers"; +import { + generateRandomPassword, + isValidPassword, +} from "../../../utils/helpers/passwordHelpers"; +import { createUser, getUser } from "../../../utils/helpers/userHelpers"; + +// Create admin account if one doesn't exist yet +const pass = generateRandomPassword(); +createUser("admin", pass).then((returned) => { + if (returned === true) { + console.log(`Created admin account. Username: admin, Password: ${pass}`); + } +}); export const authOptions = { providers: [ CredentialsProvider({ name: "Credentials", credentials: { - username: { label: "Username", type: "text", placeholder: "admin" }, + username: { + label: "Username", + type: "text", + placeholder: "admin", + }, password: { label: "Password", type: "password" }, }, async authorize(credentials, req) { - console.log(credentials); - const user = { id: "1", name: "J Smith", email: "admin@example.com" }; + await connectMongo(); + const { username, password } = credentials; + const user = await getUser(username); + if (user == null) { + return null; + } if (user) { + const validPassword = isValidPassword(password, user.password); + if (validPassword === false) { + return null; + } + user.lastLoginDate = new Date(); + await user.save(); return user; } else { return null; diff --git a/src/pages/api/hello.js b/src/pages/api/hello.js deleted file mode 100644 index df63de8..0000000 --- a/src/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction - -export default function handler(req, res) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/src/pages/api/upload/sharex.js b/src/pages/api/upload/sharex.js new file mode 100644 index 0000000..82cd9f3 --- /dev/null +++ b/src/pages/api/upload/sharex.js @@ -0,0 +1,52 @@ +import multer from "multer"; +import nextConnect from "next-connect"; +import { createFile } from "../../../utils/helpers/fileHelpers"; +import { getUserByUploadKey } from "../../../utils/helpers/userHelpers"; + +const apiRoute = nextConnect({ + onError(error, req, res) { + res.status(501).json({ + message: `An internal server error has occured. Please check console.`, + }); + console.log(error); + }, + onNoMatch(req, res) { + res.status(405).json({ message: `Method "${req.method}" Not Allowed` }); + }, +}); + +apiRoute.use(multer().any()); + +apiRoute.post(async (req, res) => { + const file = req.files[0]; + if (!file) { + return res.status(200).json({ + status: "OK", + message: `No file provided`, + }); + } + const { originalname: filename, mimetype, buffer, size } = file; + const { secret } = req.body; + console.log(secret); + + const user = await getUserByUploadKey(secret); + if (user == null) { + return res.status(200).json({ + status: "OK", + message: `Unauthorized`, + }); + } + + const id = await createFile(user, filename, buffer, mimetype); + res.status(200).json({ + message: `${process.env.NEXT_PUBLIC_SITE_URL}/files/${id}`, + }); +}); + +export default apiRoute; + +export const config = { + api: { + bodyParser: false, // Disallow body parsing, consume as stream + }, +}; diff --git a/src/pages/files/[fileId].js b/src/pages/files/[fileId].js new file mode 100644 index 0000000..7a3e623 --- /dev/null +++ b/src/pages/files/[fileId].js @@ -0,0 +1,37 @@ +import FileModel from "../../models/FileModel"; +import UserModel from "../../models/UserModel"; + +export default function File(props) { + const { isValidFile, fileData } = props; + const file = JSON.parse(fileData); + + console.log(file); +} + +export async function getServerSideProps(ctx) { + let { fileId } = ctx.query; + fileId = fileId.split(".")[0]; + + const file = await FileModel.aggregate([ + { + $match: { + fileId: fileId, + }, + }, + { + $lookup: { + from: UserModel.collection.name, + localField: "uploader", + foreignField: "_id", + as: "uploader", + }, + }, + ]).exec(); + console.log(file.uploader); + return { + props: { + isValidFile: file !== null, + fileData: JSON.stringify(file || []), + }, + }; +} diff --git a/src/utils/helpers/fileHelpers.js b/src/utils/helpers/fileHelpers.js new file mode 100644 index 0000000..e43f0eb --- /dev/null +++ b/src/utils/helpers/fileHelpers.js @@ -0,0 +1,44 @@ +import path from "path"; +import { FILE_STORAGE_LOCATION } from "../../consts/filePaths"; +import FileModel from "../../models/FileModel"; +import { createFileIO } from "./ioHelpers"; +import { connectMongo } from "./mongoHelpers"; +import { randomString } from "./stringHelpers"; + +connectMongo(); + +/** + * Returns the the files object in mongo for the given id + * + * @param {string} fileId The files id + * @return The file object or null if not found + */ +export async function getFile(fileId) { + return await FileModel.findOne({ fileId: fileId }); +} + +/** + * Creates the file object in mongo and stores it to the storage location + * + * @param {UserModel} uploader The user who uploaded the file + * @param {[]} fileData The file data for the upload + */ +export async function createFile(uploader, fileName, buffer, contentType) { + const fileId = randomString(process.env.FILE_ID_LENGTH); + const extention = fileName.split(".")[1].toLowerCase(); + // Todo: Check if the file was actually saved to + // disk and create a return type so we can notify the user what happened + await createFileIO( + `${FILE_STORAGE_LOCATION}${path.sep}${uploader.uploadKey}`, + `${fileId}.${extention}`, + buffer + ); + const file = await FileModel.create({ + uploader: uploader._id, + fileId: fileId, + uploadDate: new Date(), + contentType: contentType, + }); + await file.save(); + return `${fileId}.${extention}`; +} diff --git a/src/utils/helpers/ioHelpers.js b/src/utils/helpers/ioHelpers.js new file mode 100644 index 0000000..c5edffc --- /dev/null +++ b/src/utils/helpers/ioHelpers.js @@ -0,0 +1,64 @@ +import fs from "fs"; +import NodeCache from "node-cache"; +import path from "path"; + +const existsCache = new NodeCache({ + stdTTL: 300, // 5 minutes +}); + +/** + * Checks if the given file/directory exists + * + * @param {string} path The path to the file/directory + * @returns If the file/directory exists + */ +export function exists(path) { + if (existsCache.has(path)) { + return existsCache.get(path); + } + const exists = fs.existsSync(path); + existsCache.set(path, exists); + return exists; +} + +/** + * Creates a file in the given directory + * + * @param {string} path The path to the file + * @param {Buffer} bytes The bytes of the file + */ +export function createFileIO(dir, fileName, bytes) { + if (!exists(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const fileLocation = dir + path.sep + fileName; + console.log(fileLocation); + fs.writeFile( + fileLocation, + bytes, + { + encoding: "utf-8", + }, + (err) => { + console.log(err); + } + ); +} + +/** + * Creates a file in the given directory + * + * @param {string} path The path to the file + * @return The file + */ +export function readFileIO(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, (err, data) => { + if (err) { + return reject(err); + } + return resolve(data); + }); + }); +} diff --git a/src/utils/helpers/mongoHelpers.js b/src/utils/helpers/mongoHelpers.js new file mode 100644 index 0000000..ec03782 --- /dev/null +++ b/src/utils/helpers/mongoHelpers.js @@ -0,0 +1,9 @@ +import mongoose from "mongoose"; + +export async function connectMongo() { + try { + await mongoose.connect(process.env.MONGODB_CONNECTION_STRING); + } catch (e) { + console.log(`Mongo connection failed: ${e.message}`); + } +} diff --git a/src/utils/helpers/passwordHelpers.js b/src/utils/helpers/passwordHelpers.js index 49471fb..ed879da 100644 --- a/src/utils/helpers/passwordHelpers.js +++ b/src/utils/helpers/passwordHelpers.js @@ -1,4 +1,5 @@ import bcrypt from "bcrypt"; +import { randomString } from "./stringHelpers"; /** * Generates a random salt for a password @@ -6,7 +7,7 @@ import bcrypt from "bcrypt"; * @return The random salt */ export function generateSalt() { - return randomString(16); + return bcrypt.genSaltSync(10); } /** @@ -15,7 +16,7 @@ export function generateSalt() { * @return The password */ export function generateRandomPassword() { - return randomString(8); + return randomString(16); } /** @@ -32,10 +33,10 @@ export function hashPassword(salt, password) { /** * Checks if the password is valid with the salt * - * @param {string} salt The salt * @param {string} password The password that the user gave + * @param {string} hashedPassword The password in the users account * @return If the password is valid or not */ -export function isValidPassword(salt, password) { - return bcrypt.compareSync(password, salt); +export function isValidPassword(password, hashedPassword) { + return bcrypt.compareSync(password, hashedPassword); } diff --git a/src/utils/helpers/stringHelpers.js b/src/utils/helpers/stringHelpers.js index 8f7dab0..a7e6e03 100644 --- a/src/utils/helpers/stringHelpers.js +++ b/src/utils/helpers/stringHelpers.js @@ -4,7 +4,7 @@ * @param {Number} length * @returns The random string */ -function randomString(length) { +export function randomString(length) { var result = ""; var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; diff --git a/src/utils/helpers/userHelpers.js b/src/utils/helpers/userHelpers.js index 34aab85..5292647 100644 --- a/src/utils/helpers/userHelpers.js +++ b/src/utils/helpers/userHelpers.js @@ -1,10 +1,55 @@ +import UserModel from "../../models/UserModel"; +import { connectMongo } from "./mongoHelpers"; +import { generateSalt, hashPassword } from "./passwordHelpers"; +import { randomString } from "./stringHelpers"; + +connectMongo(); + /** - * Returns the user with the given email address + * Returns the user with the given username * - * @param {string} email The users email address - * @return The users object in mongo or null if not found + * @param {string} username The users username + * @return The user object in mongo or null if not found */ -export async function getUser(email) { - const user = await UserModel.find({ email: email }); - return user; +export async function getUser(username) { + return await UserModel.findOne({ username: username }); +} + +/** + * Returns the user with the given upload key + * + * @param {string} uploadKey The users uploadKey + * @return The user object in mongo or null if not found + */ +export async function getUserByUploadKey(uploadKey) { + return await UserModel.findOne({ uploadKey: uploadKey }); +} + +/** + * Creates a new user and returns the user object + * + * @param {string} username The username of the account + * @param {string} password The non hashed password of the account + * + * @return null if user already exists, true if success, false if fail + */ +export async function createUser(username, password) { + let user = await getUser(username); + if (user !== null) { + return null; + } + + try { + const salt = generateSalt(); + user = await UserModel.create({ + username: username, + password: hashPassword(salt, password), + salt: salt, + uploadKey: randomString(16), + }); + user.save(); + } catch (e) { + return false; + } + return true; }