-
Notifications
You must be signed in to change notification settings - Fork 3
Add PDF streaming viewer and token caching #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { getThumbnail } from "../../../../api/File"; | |
| import clientRoot from "../../../../api/server"; | ||
| import capitalise from "../../../../utils/capitalise.js"; | ||
| import Share from "../../../share"; | ||
| import API_BASE_URL from "../../../../api/server"; | ||
| import { verifyFile, unverifyFile, renameFile } from "../../../../api/File"; | ||
| import { | ||
| RemoveFileFromFolder, | ||
|
|
@@ -19,12 +20,13 @@ import { getFileDownloadLink } from "../../../../api/File"; | |
| import { fetchFolder } from "../../../../api/Folder.js"; | ||
|
|
||
| const FileDisplay = ({ file, path, code, isMobileView = false }) => { | ||
|
|
||
| const user = useSelector((state) => state.user?.user); | ||
| const fileSize = formatFileSize(file.size); | ||
| const fileType = formatFileType(file.name); | ||
| const [showDialog, setShowDialog] = useState(false); | ||
| const [dialogType, setDialogType] = useState("verify"); | ||
| const [onConfirmAction, setOnConfirmAction] = useState(() => () => {}); | ||
| const [onConfirmAction, setOnConfirmAction] = useState(() => () => { }); | ||
| const [isProcessing, setIsProcessing] = useState(false); | ||
|
|
||
| let name = file.name; | ||
|
|
@@ -139,7 +141,11 @@ const FileDisplay = ({ file, path, code, isMobileView = false }) => { | |
| toast.error("Please login to preview file."); | ||
| return; | ||
| } | ||
| window.open(preview_url, "_blank"); | ||
| window.open( | ||
| `${API_BASE_URL}/api/contribution/view/${file._id}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Functional Regression (Office Documents): Opening the backend streaming URL works great for PDFs and images (which browsers can render inline natively). However, browsers cannot render Office files (like Previously, clicking "View" on a |
||
| "_blank" | ||
| ); | ||
|
|
||
| }; | ||
|
|
||
|
|
||
|
|
@@ -187,9 +193,8 @@ const FileDisplay = ({ file, path, code, isMobileView = false }) => { | |
|
|
||
| return ( | ||
| <div | ||
| className={`file-display ${ | ||
| user?.isBR ? (file.isVerified ? "verified" : "unverified") : "" | ||
| }`} | ||
| className={`file-display ${user?.isBR ? (file.isVerified ? "verified" : "unverified") : "" | ||
| }`} | ||
| > | ||
| <img | ||
| src={thumbnailUrl} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,11 @@ import AppError from "../../utils/appError.js"; | |
| import validatePayload from "../../utils/validate.js"; | ||
| import UploadFile from "../../services/UploadFile.js"; | ||
| import fs from "fs"; | ||
| import { FolderModel } from "../course/course.model.js"; | ||
| import { FolderModel, FileModel } from "../course/course.model.js"; | ||
| import logger from "../../utils/logger.js"; | ||
| import { normalizeCourseCode, getCourseCodeCaseInsensitiveRegex } from "../../utils/course.js"; | ||
| import { getAccessToken } from "../onedrive/onedrive.controller.js"; | ||
| import axios from "axios"; | ||
|
|
||
| async function ContributionCreation(contributionId, data) { | ||
| const existingContribution = await Contribution.findOne({ contributionId }); | ||
|
|
@@ -50,7 +52,6 @@ async function HandleFileUpload(req, res, next) { | |
| logger.info("Handling File Upload"); | ||
| const contributionId = req.headers["contribution-id"]; | ||
| const files = req.files; | ||
|
|
||
| if (!files || files.length === 0) { | ||
| return res.status(400).json({ error: "No files were uploaded" }); | ||
| } | ||
|
|
@@ -157,12 +158,43 @@ async function GetBrContribution(req, res, next) { | |
| } | ||
| } | ||
|
|
||
| async function viewFile(req, res, next) { | ||
|
RsbhThakur marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Authorization Check & Resource Leaks:There are three issues with this controller:
|
||
| try { | ||
| const { id } = req.params; | ||
| const file = await FileModel.findById(id); | ||
| if (!file) { | ||
| return res.status(404).json({ message: "File not found" }); | ||
| } | ||
| const accessToken = await getAccessToken(); | ||
| if (!accessToken) { | ||
| return res.status(500).json({ message: "Access token not found" }); | ||
| } | ||
|
|
||
| const response = await axios.get(`https://graph.microsoft.com/v1.0/me/drive/items/${file.fileId}/content`, { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}` | ||
| }, | ||
| responseType: "stream" | ||
| }) | ||
| if (!response) { | ||
| return res.status(500).json({ message: "File not found" }); | ||
| } | ||
| res.setHeader("Content-Type", response.headers["content-type"]) | ||
|
|
||
| response.data.pipe(res) | ||
|
|
||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| export default { | ||
| GetAllContributions, | ||
| CreateNewContribution, | ||
| HandleFileUpload, | ||
| GetMyContributions, | ||
| DeleteContribution, | ||
| GetContributionsUpdatedSince, | ||
| GetBrContribution | ||
| GetBrContribution, | ||
| viewFile | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,5 +14,5 @@ router.post("/upload", upload.array("file"), catchAsync(ContributionController.H | |
| // router.get("/:id", catchAsync(ContributionController.CreateNewContribution)); | ||
| router.post("/updated", catchAsync(ContributionController.GetContributionsUpdatedSince)); | ||
| router.post("/br",isAuthenticated, ContributionController.GetBrContribution) | ||
|
|
||
| router.get('/view/:id',catchAsync(ContributionController.viewFile)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security Vulnerability: This route is currently unprotected. There is no |
||
| export default router; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,8 +37,6 @@ export async function generateDeviceCode(req, res) { | |
|
|
||
| if (!response.data) throw new AppError(500, "Something went wrong"); | ||
|
|
||
| console.log(response.data.user_code); | ||
|
|
||
| fs.writeFileSync("./onedrive-device-code.token", response.data.device_code, "utf-8"); | ||
| if (fs.existsSync("./onedrive-access-token.token")) { | ||
| fs.unlinkSync("./onedrive-access-token.token"); | ||
|
|
@@ -207,22 +205,19 @@ async function visitAllFiles() { | |
| const searchDocument = await SearchResults.findOne({ | ||
| code: getCourseCodeCaseInsensitiveRegex(courseCode), | ||
| }); | ||
| console.log(searchDocument); | ||
| if (!searchDocument) { | ||
| await SearchResults.create({ | ||
| name: courseName, | ||
| code: courseCode, | ||
| isAvailable: true, | ||
| }); | ||
| console.log("Created", folder.name); | ||
| } else { | ||
| await SearchResults.updateOne( | ||
| { code: getCourseCodeCaseInsensitiveRegex(courseCode) }, | ||
| { | ||
| isAvailable: true, | ||
| } | ||
| ); | ||
| console.log("Updated", folder.name); | ||
| } | ||
| })); | ||
| return "ok"; | ||
|
|
@@ -257,15 +252,13 @@ export async function visitCourseById(id) { | |
| code: courseCode, | ||
| isAvailable: true, | ||
| }); | ||
| console.log("Created", required_course.name); | ||
| } else { | ||
| await SearchResults.updateOne( | ||
| { code: getCourseCodeCaseInsensitiveRegex(courseCode) }, | ||
| { | ||
| isAvailable: true, | ||
| } | ||
| ); | ||
| console.log("Updated", required_course.name); | ||
| } | ||
|
|
||
| return "ok"; | ||
|
|
@@ -322,14 +315,53 @@ async function visitFile(file, currCourse) { | |
| return NewFile._id; | ||
| } | ||
|
|
||
| // export async function getAccessToken() { | ||
| // let data; | ||
| // if (fs.existsSync("./onedrive-refresh-token.token")) { | ||
| // data = await refreshAccessToken(); | ||
| // } else { | ||
| // data = await generateAccessToken(); | ||
| // } | ||
| // return data.access_token; | ||
| // } | ||
| let cachedAccessToken = null; | ||
| let tokenExpiry = 0; | ||
| let refreshPromise = null; | ||
| export async function getAccessToken() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Robustness Issue (No Cache Eviction on 401):Caching the token in memory to avoid duplicate Graph API authorization round-trips is a massive performance win. However, if the cached token is invalidated or revoked on Microsoft's end before the local expiration ( |
||
| let data; | ||
| if (fs.existsSync("./onedrive-refresh-token.token")) { | ||
| data = await refreshAccessToken(); | ||
| } else { | ||
| data = await generateAccessToken(); | ||
|
|
||
| if ( | ||
| cachedAccessToken && | ||
| Date.now() < tokenExpiry | ||
| ) { | ||
| return cachedAccessToken; | ||
| } | ||
| if (refreshPromise) { | ||
| return refreshPromise; | ||
| } | ||
|
|
||
| refreshPromise = (async () => { | ||
| let data; | ||
|
|
||
| if (fs.existsSync("./onedrive-refresh-token.token")) { | ||
| data = await refreshAccessToken(); | ||
| } else { | ||
| data = await generateAccessToken(); | ||
| } | ||
|
|
||
| cachedAccessToken = data.access_token; | ||
|
|
||
| tokenExpiry = | ||
| Date.now() + | ||
| (data.expires_in - 60) * 1000; | ||
|
|
||
| return cachedAccessToken; | ||
| })(); | ||
| try { | ||
| return await refreshPromise; | ||
| } finally { | ||
| refreshPromise = null; | ||
| } | ||
| return data.access_token; | ||
|
|
||
| } | ||
|
|
||
| async function refreshAccessToken() { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.