mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
Standalone raw file redirects (#428)
This commit is contained in:
@@ -6,6 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { useClipboard } from 'use-clipboard-copy'
|
import { useClipboard } from 'use-clipboard-copy'
|
||||||
|
|
||||||
import { getBaseUrl } from '../utils/getBaseUrl'
|
import { getBaseUrl } from '../utils/getBaseUrl'
|
||||||
|
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||||
|
import { getReadablePath } from '../utils/getReadablePath'
|
||||||
|
|
||||||
export default function CustomEmbedLinkMenu({
|
export default function CustomEmbedLinkMenu({
|
||||||
path,
|
path,
|
||||||
@@ -18,6 +20,9 @@ export default function CustomEmbedLinkMenu({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
|
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
|
||||||
const closeMenu = () => setMenuOpen(false)
|
const closeMenu = () => setMenuOpen(false)
|
||||||
|
|
||||||
const filename = path.substring(path.lastIndexOf('/') + 1)
|
const filename = path.substring(path.lastIndexOf('/') + 1)
|
||||||
@@ -77,13 +82,13 @@ export default function CustomEmbedLinkMenu({
|
|||||||
/>
|
/>
|
||||||
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Default')}</h4>
|
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Default')}</h4>
|
||||||
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
|
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
|
||||||
{`${getBaseUrl()}/api?path=${path}&raw=true`}
|
{`${getBaseUrl()}/api/raw/?path=${getReadablePath(path)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Customised')}</h4>
|
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Customised')}</h4>
|
||||||
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
|
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
|
||||||
<span>{`${getBaseUrl()}/api/name/`}</span>
|
<span>{`${getBaseUrl()}/api/name/`}</span>
|
||||||
<span className="underline decoration-blue-400 decoration-wavy">{name}</span>
|
<span className="underline decoration-blue-400 decoration-wavy">{name}</span>
|
||||||
<span>{`?path=${path}&raw=true`}</span>
|
<span>{`/?path=${getReadablePath(path)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,7 +96,11 @@ export default function CustomEmbedLinkMenu({
|
|||||||
<button
|
<button
|
||||||
className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 px-4 py-2 text-center text-sm font-medium text-white hover:bg-gradient-to-bl focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800"
|
className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 px-4 py-2 text-center text-sm font-medium text-white hover:bg-gradient-to-bl focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(`${getBaseUrl()}/api/name/${name}?path=${path}&raw=true`)
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api/name/${name}/?path=${getReadablePath(path)}${
|
||||||
|
hashedToken ? `&odpt=${hashedToken}` : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
toast.success(t('Copied customised link to clipboard.'))
|
toast.success(t('Copied customised link to clipboard.'))
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useRouter } from 'next/router'
|
|||||||
|
|
||||||
import { getBaseUrl } from '../utils/getBaseUrl'
|
import { getBaseUrl } from '../utils/getBaseUrl'
|
||||||
import { getReadablePath } from '../utils/getReadablePath'
|
import { getReadablePath } from '../utils/getReadablePath'
|
||||||
|
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||||
import CustomEmbedLinkMenu from './CustomEmbedLinkMenu'
|
import CustomEmbedLinkMenu from './CustomEmbedLinkMenu'
|
||||||
|
|
||||||
const btnStyleMap = (btnColor?: string) => {
|
const btnStyleMap = (btnColor?: string) => {
|
||||||
@@ -62,8 +63,10 @@ export const DownloadButton = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl }) => {
|
const DownloadButtonGroup = () => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
|
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
@@ -74,22 +77,24 @@ const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl })
|
|||||||
<CustomEmbedLinkMenu menuOpen={menuOpen} setMenuOpen={setMenuOpen} path={asPath} />
|
<CustomEmbedLinkMenu menuOpen={menuOpen} setMenuOpen={setMenuOpen} path={asPath} />
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(downloadUrl)}
|
onClickCallback={() => window.open(`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`)}
|
||||||
btnColor="blue"
|
btnColor="blue"
|
||||||
btnText={t('Download')}
|
btnText={t('Download')}
|
||||||
btnIcon="file-download"
|
btnIcon="file-download"
|
||||||
btnTitle={t('Download the file directly through OneDrive')}
|
btnTitle={t('Download the file directly through OneDrive')}
|
||||||
/>
|
/>
|
||||||
{/* <DownloadButton
|
{/* <DownloadButton
|
||||||
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
|
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
|
||||||
btnColor="teal"
|
btnColor="teal"
|
||||||
btnText={t('Proxy download')}
|
btnText={t('Proxy download')}
|
||||||
btnIcon="download"
|
btnIcon="download"
|
||||||
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
|
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
|
||||||
/> */}
|
/> */}
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => {
|
onClickCallback={() => {
|
||||||
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`)
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api/raw/?path=${getReadablePath(asPath)}${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
)
|
||||||
toast.success(t('Copied direct link to clipboard.'))
|
toast.success(t('Copied direct link to clipboard.'))
|
||||||
}}
|
}}
|
||||||
btnColor="pink"
|
btnColor="pink"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types'
|
||||||
|
import { ParsedUrlQuery } from 'querystring'
|
||||||
|
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import toast, { Toaster } from 'react-hot-toast'
|
import toast, { Toaster } from 'react-hot-toast'
|
||||||
import emojiRegex from 'emoji-regex'
|
import emojiRegex from 'emoji-regex'
|
||||||
|
|
||||||
import { ParsedUrlQuery } from 'querystring'
|
|
||||||
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
@@ -13,6 +13,7 @@ import useLocalStorage from '../utils/useLocalStorage'
|
|||||||
import { getPreviewType, preview } from '../utils/getPreviewType'
|
import { getPreviewType, preview } from '../utils/getPreviewType'
|
||||||
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
||||||
import { getExtension, getFileIcon } from '../utils/getFileIcon'
|
import { getExtension, getFileIcon } from '../utils/getFileIcon'
|
||||||
|
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||||
import {
|
import {
|
||||||
DownloadingToast,
|
DownloadingToast,
|
||||||
downloadMultipleFiles,
|
downloadMultipleFiles,
|
||||||
@@ -36,7 +37,6 @@ import ImagePreview from './previews/ImagePreview'
|
|||||||
import DefaultPreview from './previews/DefaultPreview'
|
import DefaultPreview from './previews/DefaultPreview'
|
||||||
import { PreviewContainer } from './previews/Containers'
|
import { PreviewContainer } from './previews/Containers'
|
||||||
|
|
||||||
import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types'
|
|
||||||
import FolderListLayout from './FolderListLayout'
|
import FolderListLayout from './FolderListLayout'
|
||||||
import FolderGridLayout from './FolderGridLayout'
|
import FolderGridLayout from './FolderGridLayout'
|
||||||
|
|
||||||
@@ -134,9 +134,9 @@ export const Checkbox: FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Downloading: FC<{ title: string }> = ({ title }) => {
|
export const Downloading: FC<{ title: string; style: string }> = ({ title, style }) => {
|
||||||
return (
|
return (
|
||||||
<span title={title} className="rounded p-2" role="status">
|
<span title={title} className={`${style} rounded`} role="status">
|
||||||
<LoadingIcon
|
<LoadingIcon
|
||||||
// Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only
|
// Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only
|
||||||
// for consistent icon alignment, as class `align-*` cannot satisfy it
|
// for consistent icon alignment, as class `align-*` cannot satisfy it
|
||||||
@@ -155,6 +155,7 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const hashedToken = getStoredToken(router.asPath)
|
||||||
const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
|
const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -237,7 +238,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
const folder = folderName ? decodeURIComponent(folderName) : undefined
|
const folder = folderName ? decodeURIComponent(folderName) : undefined
|
||||||
const files = getFiles()
|
const files = getFiles()
|
||||||
.filter(c => selected[c.id])
|
.filter(c => selected[c.id])
|
||||||
.map(c => ({ name: c.name, url: c['@microsoft.graph.downloadUrl'] }))
|
.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
url: `/api/raw/?path=${path}/${c.name}${hashedToken ? `&odpt=${hashedToken}` : ''}`,
|
||||||
|
}))
|
||||||
|
|
||||||
if (files.length == 1) {
|
if (files.length == 1) {
|
||||||
const el = document.createElement('a')
|
const el = document.createElement('a')
|
||||||
@@ -278,9 +282,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
const hashedTokenForPath = getStoredToken(p)
|
||||||
yield {
|
yield {
|
||||||
name: c?.name,
|
name: c?.name,
|
||||||
url: c ? c['@microsoft.graph.downloadUrl'] : undefined,
|
url: `/api/raw/?path=${p}${hashedTokenForPath ? `&odpt=${hashedTokenForPath}` : ''}`,
|
||||||
path: p,
|
path: p,
|
||||||
isFolder,
|
isFolder,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import { getBaseUrl } from '../utils/getBaseUrl'
|
|||||||
import { formatModifiedDateTime } from '../utils/fileDetails'
|
import { formatModifiedDateTime } from '../utils/fileDetails'
|
||||||
import { getReadablePath } from '../utils/getReadablePath'
|
import { getReadablePath } from '../utils/getReadablePath'
|
||||||
import { Checkbox, ChildIcon, ChildName, Downloading } from './FileListing'
|
import { Checkbox, ChildIcon, ChildName, Downloading } from './FileListing'
|
||||||
|
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => {
|
const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => {
|
||||||
// We use the generated medium thumbnail for rendering preview images (excluding folders)
|
// We use the generated medium thumbnail for rendering preview images (excluding folders)
|
||||||
const thumbnailUrl = 'folder' in c ? null : `/api/thumbnail?path=${path}&size=medium`
|
const hashedToken = getStoredToken(path)
|
||||||
|
const thumbnailUrl =
|
||||||
|
'folder' in c ? null : `/api/thumbnail/?path=${path}&size=medium${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
|
||||||
// Some thumbnails are broken, so we check for onerror event in the image component
|
// Some thumbnails are broken, so we check for onerror event in the image component
|
||||||
const [brokenThumbnail, setBrokenThumbnail] = useState(false)
|
const [brokenThumbnail, setBrokenThumbnail] = useState(false)
|
||||||
@@ -66,6 +69,7 @@ const FolderGridLayout = ({
|
|||||||
toast,
|
toast,
|
||||||
}) => {
|
}) => {
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ const FolderGridLayout = ({
|
|||||||
title={t('Select all files')}
|
title={t('Select all files')}
|
||||||
/>
|
/>
|
||||||
{totalGenerating ? (
|
{totalGenerating ? (
|
||||||
<Downloading title={t('Downloading selected files, refresh page to cancel')} />
|
<Downloading title={t('Downloading selected files, refresh page to cancel')} style="p-1.5" />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
title={t('Download selected files')}
|
title={t('Download selected files')}
|
||||||
@@ -118,7 +122,7 @@ const FolderGridLayout = ({
|
|||||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
</span>
|
</span>
|
||||||
{folderGenerating[c.id] ? (
|
{folderGenerating[c.id] ? (
|
||||||
<Downloading title={t('Downloading folder, refresh page to cancel')} />
|
<Downloading title={t('Downloading folder, refresh page to cancel')} style="px-1.5 py-1" />
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
title={t('Download folder')}
|
title={t('Download folder')}
|
||||||
@@ -135,7 +139,11 @@ const FolderGridLayout = ({
|
|||||||
title={t('Copy raw file permalink')}
|
title={t('Copy raw file permalink')}
|
||||||
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(getItemPath(c.name))}&raw=true`)
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
|
||||||
|
hashedToken ? `&odpt=${hashedToken}` : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
toast.success(t('Copied raw file permalink.'))
|
toast.success(t('Copied raw file permalink.'))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -144,7 +152,9 @@ const FolderGridLayout = ({
|
|||||||
<a
|
<a
|
||||||
title={t('Download file')}
|
title={t('Download file')}
|
||||||
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
href={c['@microsoft.graph.downloadUrl']}
|
href={`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
|
||||||
|
hashedToken ? `&odpt=${hashedToken}` : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails'
|
|||||||
import { getReadablePath } from '../utils/getReadablePath'
|
import { getReadablePath } from '../utils/getReadablePath'
|
||||||
|
|
||||||
import { Downloading, Checkbox, ChildIcon, ChildName } from './FileListing'
|
import { Downloading, Checkbox, ChildIcon, ChildName } from './FileListing'
|
||||||
|
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => {
|
const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => {
|
||||||
return (
|
return (
|
||||||
@@ -45,9 +46,13 @@ const FolderListLayout = ({
|
|||||||
toast,
|
toast,
|
||||||
}) => {
|
}) => {
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Get item path from item name
|
||||||
|
const getItemPath = (name: string) => `${path === '/' ? '' : path}/${encodeURIComponent(name)}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
|
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
|
||||||
<div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30">
|
<div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30">
|
||||||
@@ -72,7 +77,7 @@ const FolderListLayout = ({
|
|||||||
title={t('Select files')}
|
title={t('Select files')}
|
||||||
/>
|
/>
|
||||||
{totalGenerating ? (
|
{totalGenerating ? (
|
||||||
<Downloading title={t('Downloading selected files, refresh page to cancel')} />
|
<Downloading title={t('Downloading selected files, refresh page to cancel')} style="p-1.5" />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
title={t('Download selected files')}
|
title={t('Download selected files')}
|
||||||
@@ -113,7 +118,7 @@ const FolderListLayout = ({
|
|||||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
</span>
|
</span>
|
||||||
{folderGenerating[c.id] ? (
|
{folderGenerating[c.id] ? (
|
||||||
<Downloading title={t('Downloading folder, refresh page to cancel')} />
|
<Downloading title={t('Downloading folder, refresh page to cancel')} style="px-1.5 py-1" />
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
title={t('Download folder')}
|
title={t('Download folder')}
|
||||||
@@ -134,9 +139,9 @@ const FolderListLayout = ({
|
|||||||
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(
|
clipboard.copy(
|
||||||
`${getBaseUrl()}/api?path=${getReadablePath(
|
`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
|
||||||
`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
|
hashedToken ? `&odpt=${hashedToken}` : ''
|
||||||
)}&raw=true`
|
}`
|
||||||
)
|
)
|
||||||
toast.success(t('Copied raw file permalink.'))
|
toast.success(t('Copied raw file permalink.'))
|
||||||
}}
|
}}
|
||||||
@@ -146,7 +151,7 @@ const FolderListLayout = ({
|
|||||||
<a
|
<a
|
||||||
title={t('Download file')}
|
title={t('Download file')}
|
||||||
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
href={c['@microsoft.graph.downloadUrl']}
|
href={`/api/raw/?path=${getItemPath(c.name)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export async function* traverseFolder(path: string): AsyncGenerator<TraverseItem
|
|||||||
i,
|
i,
|
||||||
path,
|
path,
|
||||||
data: await fetcher(
|
data: await fetcher(
|
||||||
next ? `/api?path=${path}&next=${next}` : `/api?path=${path}`,
|
next ? `/api/?path=${path}&next=${next}` : `/api?path=${path}`,
|
||||||
hashedToken ?? undefined
|
hashedToken ?? undefined
|
||||||
).catch(error => ({ i, path, error })),
|
).catch(error => ({ i, path, error })),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function mapAbsolutePath(path: string): string {
|
|||||||
function useDriveItemSearch() {
|
function useDriveItemSearch() {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const searchDriveItem = async (q: string) => {
|
const searchDriveItem = async (q: string) => {
|
||||||
const { data } = await axios.get<OdSearchResult>(`/api/search?q=${q}`)
|
const { data } = await axios.get<OdSearchResult>(`/api/search/?q=${q}`)
|
||||||
|
|
||||||
// Map parentReference to the absolute path of the search result
|
// Map parentReference to the absolute path of the search result
|
||||||
data.map(item => {
|
data.map(item => {
|
||||||
@@ -111,7 +111,7 @@ function SearchResultItemTemplate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) {
|
function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) {
|
||||||
const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item?id=${result.id}`, fetcher)
|
const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item/?id=${result.id}`, fetcher)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
|
|||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
import { LoadingIcon } from '../Loading'
|
import { LoadingIcon } from '../Loading'
|
||||||
import { formatModifiedDateTime } from '../../utils/fileDetails'
|
import { formatModifiedDateTime } from '../../utils/fileDetails'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
Loading,
|
Loading,
|
||||||
@@ -21,12 +22,13 @@ enum PlayerState {
|
|||||||
const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
|
|
||||||
const rapRef = useRef<ReactAudioPlayer>(null)
|
const rapRef = useRef<ReactAudioPlayer>(null)
|
||||||
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
|
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
|
||||||
|
|
||||||
// Render audio thumbnail, and also check for broken thumbnails
|
// Render audio thumbnail, and also check for broken thumbnails
|
||||||
const thumbnail = `/api/thumbnail?path=${asPath}&size=medium`
|
const thumbnail = `/api/thumbnail/?path=${asPath}&size=medium${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
const [brokenThumbnail, setBrokenThumbnail] = useState(false)
|
const [brokenThumbnail, setBrokenThumbnail] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,7 +94,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
|
|
||||||
<ReactAudioPlayer
|
<ReactAudioPlayer
|
||||||
className="h-11 w-full"
|
className="h-11 w-full"
|
||||||
src={file['@microsoft.graph.downloadUrl']}
|
src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||||
ref={rapRef}
|
ref={rapRef}
|
||||||
controls
|
controls
|
||||||
preload="auto"
|
preload="auto"
|
||||||
@@ -102,7 +104,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
|
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import useSystemTheme from 'react-use-system-theme'
|
import useSystemTheme from 'react-use-system-theme'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import { tomorrowNightEighties, tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
|
import { tomorrowNightEighties, tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
|
||||||
|
|
||||||
import useAxiosGet from '../../utils/fetchOnMount'
|
import useFileContent from '../../utils/fetchOnMount'
|
||||||
import { getLanguageByFileName } from '../../utils/getPreviewType'
|
import { getLanguageByFileName } from '../../utils/getPreviewType'
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
@@ -13,7 +14,8 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
|
|||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const CodePreview: FC<{ file: any }> = ({ file }) => {
|
const CodePreview: FC<{ file: any }> = ({ file }) => {
|
||||||
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
const { asPath } = useRouter()
|
||||||
|
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
|
||||||
|
|
||||||
const theme = useSystemTheme('dark')
|
const theme = useSystemTheme('dark')
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -44,7 +46,7 @@ const CodePreview: FC<{ file: any }> = ({ file }) => {
|
|||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('MIME type')}</div>
|
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('MIME type')}</div>
|
||||||
<div>{file.file?.mimeType || t('Unavailable')}</div>
|
<div>{file.file?.mimeType ?? t('Unavailable')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -47,7 +47,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
Quick XOR
|
Quick XOR
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes?.quickXorHash || t('Unavailable')}
|
{file.file.hashes?.quickXorHash ?? t('Unavailable')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
||||||
@@ -55,7 +55,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
SHA1
|
SHA1
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes?.sha1Hash || t('Unavailable')}
|
{file.file.hashes?.sha1Hash ?? t('Unavailable')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
||||||
@@ -63,7 +63,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
SHA256
|
SHA256
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes?.sha256Hash || t('Unavailable')}
|
{file.file.hashes?.sha256Hash ?? t('Unavailable')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -73,7 +73,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
</div>
|
</div>
|
||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ import type { OdFileObject } from '../../types'
|
|||||||
|
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { ReactReader } from 'react-reader'
|
import { ReactReader } from 'react-reader'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import { DownloadBtnContainer } from './Containers'
|
import { DownloadBtnContainer } from './Containers'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
|
|
||||||
const [epubContainerWidth, setEpubContainerWidth] = useState(400)
|
const [epubContainerWidth, setEpubContainerWidth] = useState(400)
|
||||||
const epubContainer = useRef<HTMLDivElement>(null)
|
const epubContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -51,7 +56,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReactReader
|
<ReactReader
|
||||||
url={file['@microsoft.graph.downloadUrl']}
|
url={`/api/raw/?path=${asPath}${hashedToken ? '&token=' + hashedToken : ''}`}
|
||||||
getRendition={rendition => fixEpub(rendition)}
|
getRendition={rendition => fixEpub(rendition)}
|
||||||
loadingView={<Loading loadingText={t('Loading EPUB ...')} />}
|
loadingView={<Loading loadingText={t('Loading EPUB ...')} />}
|
||||||
location={location}
|
location={location}
|
||||||
@@ -63,7 +68,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
import type { OdFileObject } from '../../types'
|
import type { OdFileObject } from '../../types'
|
||||||
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { PreviewContainer, DownloadBtnContainer } from './Containers'
|
import { PreviewContainer, DownloadBtnContainer } from './Containers'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
src={file['@microsoft.graph.downloadUrl']}
|
src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
width={file.image?.width}
|
width={file.image?.width}
|
||||||
height={file.image?.height}
|
height={file.image?.height}
|
||||||
/>
|
/>
|
||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import useAxiosGet from '../../utils/fetchOnMount'
|
import useFileContent from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const MarkdownPreview: FC<{
|
const MarkdownPreview: FC<{
|
||||||
@@ -20,12 +20,12 @@ const MarkdownPreview: FC<{
|
|||||||
path: string
|
path: string
|
||||||
standalone?: boolean
|
standalone?: boolean
|
||||||
}> = ({ file, path, standalone = true }) => {
|
}> = ({ file, path, standalone = true }) => {
|
||||||
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
// The parent folder of the markdown file, which is also the relative image folder
|
// The parent folder of the markdown file, which is also the relative image folder
|
||||||
const parentPath = standalone ? path.substring(0, path.lastIndexOf('/')) : path
|
const parentPath = standalone ? path.substring(0, path.lastIndexOf('/')) : path
|
||||||
|
|
||||||
|
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${parentPath}/${file.name}`, path)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// Check if the image is relative path instead of a absolute url
|
// Check if the image is relative path instead of a absolute url
|
||||||
const isUrlAbsolute = (url: string | string[]) => url.indexOf('://') > 0 || url.indexOf('//') === 0
|
const isUrlAbsolute = (url: string | string[]) => url.indexOf('://') > 0 || url.indexOf('//') === 0
|
||||||
// Custom renderer to render images with relative path
|
// Custom renderer to render images with relative path
|
||||||
@@ -55,7 +55,7 @@ const MarkdownPreview: FC<{
|
|||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
alt={alt}
|
alt={alt}
|
||||||
src={`/api?path=${parentPath}/${src}&raw=true`}
|
src={`/api/?path=${parentPath}/${src}&raw=true`}
|
||||||
title={title}
|
title={title}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
@@ -100,7 +100,7 @@ const MarkdownPreview: FC<{
|
|||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
{standalone && (
|
{standalone && (
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { OdFileObject } from '../../types'
|
import type { OdFileObject } from '../../types'
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import Preview from 'preview-office-docs'
|
import Preview from 'preview-office-docs'
|
||||||
|
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import { DownloadBtnContainer } from './Containers'
|
import { DownloadBtnContainer } from './Containers'
|
||||||
|
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
|
|
||||||
const docContainer = useRef<HTMLDivElement>(null)
|
const docContainer = useRef<HTMLDivElement>(null)
|
||||||
const [docContainerWidth, setDocContainerWidth] = useState(600)
|
const [docContainerWidth, setDocContainerWidth] = useState(600)
|
||||||
|
|
||||||
@@ -18,13 +24,13 @@ const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="overflow-scroll" ref={docContainer} style={{ maxHeight: '90vh' }}>
|
<div className="overflow-scroll" ref={docContainer} style={{ maxHeight: '90vh' }}>
|
||||||
<Preview
|
<Preview
|
||||||
url={encodeURIComponent(file['@microsoft.graph.downloadUrl'])}
|
url={`${getBaseUrl()}/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||||
width={docContainerWidth.toString()}
|
width={docContainerWidth.toString()}
|
||||||
height="600"
|
height="600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import { DownloadBtnContainer } from './Containers'
|
import { DownloadBtnContainer } from './Containers'
|
||||||
|
|
||||||
const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
|
const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
|
||||||
// const url = `/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}&inline=true`
|
const { asPath } = useRouter()
|
||||||
const url = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(
|
const hashedToken = getStoredToken(asPath)
|
||||||
file['@microsoft.graph.downloadUrl']
|
|
||||||
)}`
|
// const url = `/api/proxy?url=${encodeURIComponent(...)}&inline=true`
|
||||||
|
const pdfPath = encodeURIComponent(
|
||||||
|
`${getBaseUrl()}/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
)
|
||||||
|
const url = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfPath}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -13,7 +20,7 @@ const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
|
|||||||
<iframe src={url} frameBorder="0" width="100%" height="100%"></iframe>
|
<iframe src={url} frameBorder="0" width="100%" height="100%"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import useAxiosGet from '../../utils/fetchOnMount'
|
import useFileContent from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const TextPreview = ({ file }) => {
|
const TextPreview = ({ file }) => {
|
||||||
|
const { asPath } = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
@@ -40,7 +42,7 @@ const TextPreview = ({ file }) => {
|
|||||||
<pre className="overflow-x-scroll p-0 text-sm md:p-3">{content}</pre>
|
<pre className="overflow-x-scroll p-0 text-sm md:p-3">{content}</pre>
|
||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
<DownloadButtonGroup />
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import { DownloadButton } from '../DownloadBtnGtoup'
|
import { DownloadButton } from '../DownloadBtnGtoup'
|
||||||
import useAxiosGet from '../../utils/fetchOnMount'
|
import useFileContent from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const parseDotUrl = (content: string): string | undefined => {
|
const parseDotUrl = (content: string): string | undefined => {
|
||||||
@@ -14,9 +15,10 @@ const parseDotUrl = (content: string): string | undefined => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TextPreview = ({ file }) => {
|
const TextPreview = ({ file }) => {
|
||||||
|
const { asPath } = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
@@ -49,11 +51,11 @@ const TextPreview = ({ file }) => {
|
|||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(parseDotUrl(content) || '')}
|
onClickCallback={() => window.open(parseDotUrl(content) ?? '')}
|
||||||
btnColor="blue"
|
btnColor="blue"
|
||||||
btnText={t('Open URL')}
|
btnText={t('Open URL')}
|
||||||
btnIcon="external-link-alt"
|
btnIcon="external-link-alt"
|
||||||
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) || '' })}
|
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) ?? '' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DownloadBtnContainer>
|
</DownloadBtnContainer>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useAsync } from 'react-async-hook'
|
|||||||
import { getBaseUrl } from '../../utils/getBaseUrl'
|
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||||
import { getExtension } from '../../utils/getFileIcon'
|
import { getExtension } from '../../utils/getFileIcon'
|
||||||
import { getReadablePath } from '../../utils/getReadablePath'
|
import { getReadablePath } from '../../utils/getReadablePath'
|
||||||
|
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||||
import { DownloadButton } from '../DownloadBtnGtoup'
|
import { DownloadButton } from '../DownloadBtnGtoup'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
@@ -18,16 +19,21 @@ import CustomEmbedLinkMenu from '../CustomEmbedLinkMenu'
|
|||||||
|
|
||||||
const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
const hashedToken = getStoredToken(asPath)
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution
|
// OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution
|
||||||
const thumbnail = `/api/thumbnail?path=${asPath}&size=large`
|
const thumbnail = `/api/thumbnail/?path=${asPath}&size=large${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
|
||||||
// We assume subtitle files are beside the video with the same name, only webvtt '.vtt' files are supported
|
// We assume subtitle files are beside the video with the same name, only webvtt '.vtt' files are supported
|
||||||
const subtitle = `/api?path=${asPath.substring(0, asPath.lastIndexOf('.'))}.vtt&raw=true`
|
const vtt = `${asPath.substring(0, asPath.lastIndexOf('.'))}.vtt`
|
||||||
|
const subtitle = `/api/raw/?path=${vtt}${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
|
||||||
|
// We also format the raw video file for the in-browser player as well as all other players
|
||||||
|
const videoUrl = `/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
|
||||||
const isFlv = getExtension(file.name) === 'flv'
|
const isFlv = getExtension(file.name) === 'flv'
|
||||||
const {
|
const {
|
||||||
@@ -42,7 +48,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CustomEmbedLinkMenu path={getReadablePath(asPath)} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
<CustomEmbedLinkMenu path={asPath} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
{error ? (
|
{error ? (
|
||||||
<FourOhFour errorMsg={error.message} />
|
<FourOhFour errorMsg={error.message} />
|
||||||
@@ -55,7 +61,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
video: {
|
video: {
|
||||||
url: file['@microsoft.graph.downloadUrl'],
|
url: videoUrl,
|
||||||
pic: thumbnail,
|
pic: thumbnail,
|
||||||
type: isFlv ? 'customFlv' : 'auto',
|
type: isFlv ? 'customFlv' : 'auto',
|
||||||
customType: {
|
customType: {
|
||||||
@@ -78,14 +84,14 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
<DownloadBtnContainer>
|
<DownloadBtnContainer>
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(file['@microsoft.graph.downloadUrl'])}
|
onClickCallback={() => window.open(videoUrl)}
|
||||||
btnColor="blue"
|
btnColor="blue"
|
||||||
btnText={t('Download')}
|
btnText={t('Download')}
|
||||||
btnIcon="file-download"
|
btnIcon="file-download"
|
||||||
/>
|
/>
|
||||||
{/* <DownloadButton
|
{/* <DownloadButton
|
||||||
onClickCallback={() =>
|
onClickCallback={() =>
|
||||||
window.open(`/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}`)
|
window.open(`/api/proxy?url=${encodeURIComponent(...)}`)
|
||||||
}
|
}
|
||||||
btnColor="teal"
|
btnColor="teal"
|
||||||
btnText={t('Proxy download')}
|
btnText={t('Proxy download')}
|
||||||
@@ -93,7 +99,9 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
/> */}
|
/> */}
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => {
|
onClickCallback={() => {
|
||||||
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`)
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api/raw/?path=${getReadablePath(asPath)}${hashedToken ? `&odpt=${hashedToken}` : ''}`
|
||||||
|
)
|
||||||
toast.success(t('Copied direct link to clipboard.'))
|
toast.success(t('Copied direct link to clipboard.'))
|
||||||
}}
|
}}
|
||||||
btnColor="pink"
|
btnColor="pink"
|
||||||
@@ -108,17 +116,17 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(`iina://weblink?url=${file['@microsoft.graph.downloadUrl']}`)}
|
onClickCallback={() => window.open(`iina://weblink?url=${getBaseUrl()}${videoUrl}`)}
|
||||||
btnText="IINA"
|
btnText="IINA"
|
||||||
btnImage="/players/iina.png"
|
btnImage="/players/iina.png"
|
||||||
/>
|
/>
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(`vlc://${file['@microsoft.graph.downloadUrl']}`)}
|
onClickCallback={() => window.open(`vlc://${getBaseUrl()}${videoUrl}`)}
|
||||||
btnText="VLC"
|
btnText="VLC"
|
||||||
btnImage="/players/vlc.png"
|
btnImage="/players/vlc.png"
|
||||||
/>
|
/>
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
onClickCallback={() => window.open(`potplayer://${file['@microsoft.graph.downloadUrl']}`)}
|
onClickCallback={() => window.open(`potplayer://${getBaseUrl()}/${videoUrl}`)}
|
||||||
btnText="PotPlayer"
|
btnText="PotPlayer"
|
||||||
btnImage="/players/potplayer.png"
|
btnImage="/players/potplayer.png"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ module.exports = {
|
|||||||
// unauthorised use of the proxied download feature - but that is disabled for now. So you can safely ignore this settings.
|
// unauthorised use of the proxied download feature - but that is disabled for now. So you can safely ignore this settings.
|
||||||
directLinkRegex: 'public[.].*[.]files[.]1drv[.]com',
|
directLinkRegex: 'public[.].*[.]files[.]1drv[.]com',
|
||||||
|
|
||||||
// Cache-Control header, check Vercel documentation for more details.
|
// Cache-Control header, check Vercel documentation for more details. The default settings imply:
|
||||||
|
// - max-age=0: no cache for your browser
|
||||||
|
// - s-maxage=0: cache is fresh for 60 seconds on the edge, after which it becomes stale
|
||||||
|
// - stale-while-revalidate: allow serving stale content while revalidating on the edge
|
||||||
// https://vercel.com/docs/concepts/edge-network/caching
|
// https://vercel.com/docs/concepts/edge-network/caching
|
||||||
cacheControlHeader: 'max-age=0, s-maxage=3540, stale-while-revalidate=60'
|
cacheControlHeader: 'max-age=0, s-maxage=60, stale-while-revalidate',
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-67
@@ -2,31 +2,17 @@ import { posix as pathPosix } from 'path'
|
|||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Cors from 'cors'
|
|
||||||
|
|
||||||
import apiConfig from '../../config/api.config'
|
import apiConfig from '../../config/api.config'
|
||||||
import siteConfig from '../../config/site.config'
|
import siteConfig from '../../config/site.config'
|
||||||
import { revealObfuscatedToken } from '../../utils/oAuthHandler'
|
import { revealObfuscatedToken } from '../../utils/oAuthHandler'
|
||||||
import { compareHashedToken } from '../../utils/protectedRouteHandler'
|
import { compareHashedToken } from '../../utils/protectedRouteHandler'
|
||||||
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
|
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
|
||||||
|
import { runCorsMiddleware } from './raw'
|
||||||
|
|
||||||
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
||||||
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
||||||
|
|
||||||
// CORS middleware for raw links: https://nextjs.org/docs/api-routes/api-middlewares
|
|
||||||
function runCorsMiddleware(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const cors = Cors({ methods: ['GET', 'HEAD'] })
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
cors(req, res, result => {
|
|
||||||
if (result instanceof Error) {
|
|
||||||
return reject(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode the path of the file relative to the base directory
|
* Encode the path of the file relative to the base directory
|
||||||
*
|
*
|
||||||
@@ -107,6 +93,64 @@ export function getAuthTokenPath(path: string) {
|
|||||||
return authTokenPath
|
return authTokenPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles protected route authentication:
|
||||||
|
* - Match the cleanPath against an array of user defined protected routes
|
||||||
|
* - If a match is found:
|
||||||
|
* - 1. Download the .password file stored inside the protected route and parse its contents
|
||||||
|
* - 2. Check if the od-protected-token header is present in the request
|
||||||
|
* - The request is continued only if these two contents are exactly the same
|
||||||
|
*
|
||||||
|
* @param cleanPath Sanitised directory path, used for matching whether route is protected
|
||||||
|
* @param accessToken OneDrive API access token
|
||||||
|
* @param req Next.js request object
|
||||||
|
* @param res Next.js response object
|
||||||
|
*/
|
||||||
|
export async function checkAuthRoute(
|
||||||
|
cleanPath: string,
|
||||||
|
accessToken: string,
|
||||||
|
odTokenHeader: string
|
||||||
|
): Promise<{ code: 200 | 401 | 404 | 500; message: string }> {
|
||||||
|
// Handle authentication through .password
|
||||||
|
const authTokenPath = getAuthTokenPath(cleanPath)
|
||||||
|
|
||||||
|
// Fetch password from remote file content
|
||||||
|
if (authTokenPath === '') {
|
||||||
|
return { code: 200, message: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
params: {
|
||||||
|
select: '@microsoft.graph.downloadUrl,file',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle request and check for header 'od-protected-token'
|
||||||
|
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
|
||||||
|
// console.log(odTokenHeader, odProtectedToken.data.trim())
|
||||||
|
|
||||||
|
if (
|
||||||
|
!compareHashedToken({
|
||||||
|
odTokenHeader: odTokenHeader,
|
||||||
|
dotPassword: odProtectedToken.data,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return { code: 401, message: 'Password required.' }
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Password file not found, fallback to 404
|
||||||
|
if (error?.response?.status === 404) {
|
||||||
|
return { code: 404, message: "You didn't set a password." }
|
||||||
|
} else {
|
||||||
|
return { code: 500, message: 'Internal server error.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 200, message: 'Authenticated.' }
|
||||||
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// If method is POST, then the API is called by the client to store acquired tokens
|
// If method is POST, then the API is called by the client to store acquired tokens
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
@@ -119,11 +163,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await storeOdAuthTokens({
|
await storeOdAuthTokens({ accessToken, accessTokenExpiry, refreshToken })
|
||||||
accessToken,
|
|
||||||
accessTokenExpiry,
|
|
||||||
refreshToken,
|
|
||||||
})
|
|
||||||
res.status(200).send('OK')
|
res.status(200).send('OK')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -155,43 +195,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle authentication through .password
|
// Handle protected routes authentication
|
||||||
const authTokenPath = getAuthTokenPath(cleanPath)
|
const { code, message } = await checkAuthRoute(cleanPath, accessToken, req.headers['od-protected-token'] as string)
|
||||||
|
// Status code other than 200 means user has not authenticated yet
|
||||||
// Fetch password from remote file content
|
if (code !== 200) {
|
||||||
if (authTokenPath !== '') {
|
res.status(code).json({ error: message })
|
||||||
// Don't server cached response for password protected folders
|
return
|
||||||
|
}
|
||||||
|
// If message is empty, then the path is not protected.
|
||||||
|
// Conversely, protected routes are not allowed to serve from cache.
|
||||||
|
if (message !== '') {
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
|
||||||
try {
|
|
||||||
const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, {
|
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
|
||||||
params: {
|
|
||||||
select: '@microsoft.graph.downloadUrl,file',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle request and check for header 'od-protected-token'
|
|
||||||
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
|
|
||||||
// console.log(req.headers['od-protected-token'], odProtectedToken.data.trim())
|
|
||||||
|
|
||||||
if (
|
|
||||||
!compareHashedToken({
|
|
||||||
odTokenHeader: req.headers['od-protected-token'] as string,
|
|
||||||
dotPassword: odProtectedToken.data,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
res.status(401).json({ error: 'Password required for this folder.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// Password file not found, fallback to 404
|
|
||||||
if (error.response.status === 404) {
|
|
||||||
res.status(404).json({ error: "You didn't set a password for your protected folder." })
|
|
||||||
}
|
|
||||||
res.status(500).end()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestPath = encodePath(cleanPath)
|
const requestPath = encodePath(cleanPath)
|
||||||
@@ -201,24 +215,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const isRoot = requestPath === ''
|
const isRoot = requestPath === ''
|
||||||
|
|
||||||
// Go for file raw download link, add CORS headers, and redirect to @microsoft.graph.downloadUrl
|
// Go for file raw download link, add CORS headers, and redirect to @microsoft.graph.downloadUrl
|
||||||
|
// (kept here for backwards compatibility, and cache headers will be reverted to no-cache)
|
||||||
if (raw) {
|
if (raw) {
|
||||||
await runCorsMiddleware(req, res)
|
await runCorsMiddleware(req, res)
|
||||||
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
|
||||||
const { data } = await axios.get(requestUrl, {
|
const { data } = await axios.get(requestUrl, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
params: {
|
params: {
|
||||||
select: '@microsoft.graph.downloadUrl,folder,file',
|
select: '@microsoft.graph.downloadUrl',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('folder' in data) {
|
if ('@microsoft.graph.downloadUrl' in data) {
|
||||||
res.status(400).json({ error: "Folders doesn't have raw download urls." })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ('file' in data) {
|
|
||||||
res.redirect(data['@microsoft.graph.downloadUrl'])
|
res.redirect(data['@microsoft.graph.downloadUrl'])
|
||||||
return
|
} else {
|
||||||
|
res.status(404).json({ error: 'No download url found.' })
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Querying current path identity (file or folder) and follow up query childrens in folder
|
// Querying current path identity (file or folder) and follow up query childrens in folder
|
||||||
@@ -226,7 +240,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { data: identityData } = await axios.get(requestUrl, {
|
const { data: identityData } = await axios.get(requestUrl, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
params: {
|
params: {
|
||||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -235,12 +249,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
params: next
|
params: next
|
||||||
? {
|
? {
|
||||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
|
||||||
top: siteConfig.maxItems,
|
top: siteConfig.maxItems,
|
||||||
$skipToken: next,
|
$skipToken: next,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
|
||||||
top: siteConfig.maxItems,
|
top: siteConfig.maxItems,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -261,7 +275,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
res.status(200).json({ file: identityData })
|
res.status(200).json({ file: identityData })
|
||||||
return
|
return
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(error.response.status).json({ error: error.response.data })
|
res.status(error?.response?.code ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
res.status(200).json(data)
|
res.status(200).json(data)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(error.response.status).json({ error: error.response.data })
|
res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ error: 'Invalid driveItem ID.' })
|
res.status(400).json({ error: 'Invalid driveItem ID.' })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { default as indexHandler } from '..'
|
import { default as rawFileHandler } from '../raw'
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
indexHandler(req, res)
|
rawFileHandler(req, res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { posix as pathPosix } from 'path'
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Cors from 'cors'
|
||||||
|
|
||||||
|
import { driveApi } from '../../config/api.config'
|
||||||
|
import { encodePath, getAccessToken, checkAuthRoute } from '.'
|
||||||
|
|
||||||
|
// CORS middleware for raw links: https://nextjs.org/docs/api-routes/api-middlewares
|
||||||
|
export function runCorsMiddleware(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const cors = Cors({ methods: ['GET', 'HEAD'] })
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cors(req, res, result => {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
return reject(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const accessToken = await getAccessToken()
|
||||||
|
if (!accessToken) {
|
||||||
|
res.status(403).json({ error: 'No access token.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path = '/', odpt = '' } = req.query
|
||||||
|
|
||||||
|
// Sometimes the path parameter is defaulted to '[...path]' which we need to handle
|
||||||
|
if (path === '[...path]') {
|
||||||
|
res.status(400).json({ error: 'No path specified.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If the path is not a valid path, return 400
|
||||||
|
if (typeof path !== 'string') {
|
||||||
|
res.status(400).json({ error: 'Path query invalid.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
|
||||||
|
|
||||||
|
// Handle protected routes authentication
|
||||||
|
const odTokenHeader = (req.headers['od-protected-token'] as string) ?? odpt
|
||||||
|
|
||||||
|
const { code, message } = await checkAuthRoute(cleanPath, accessToken, odTokenHeader)
|
||||||
|
// Status code other than 200 means user has not authenticated yet
|
||||||
|
if (code !== 200) {
|
||||||
|
res.status(code).json({ error: message })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If message is empty, then the path is not protected.
|
||||||
|
// Conversely, protected routes are not allowed to serve from cache.
|
||||||
|
if (message !== '') {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
}
|
||||||
|
|
||||||
|
await runCorsMiddleware(req, res)
|
||||||
|
try {
|
||||||
|
// Handle response from OneDrive API
|
||||||
|
const requestUrl = `${driveApi}/root${encodePath(cleanPath)}`
|
||||||
|
const { data } = await axios.get(requestUrl, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
params: {
|
||||||
|
select: '@microsoft.graph.downloadUrl',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('@microsoft.graph.downloadUrl' in data) {
|
||||||
|
res.redirect(data['@microsoft.graph.downloadUrl'])
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'No download url found.' })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
res.status(200).json(data.value)
|
res.status(200).json(data.value)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(error.response.status).json({ error: error.response.data })
|
res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(200).json([])
|
res.status(200).json([])
|
||||||
|
|||||||
+18
-12
@@ -5,19 +5,22 @@ import { posix as pathPosix } from 'path'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
import { encodePath, getAccessToken, getAuthTokenPath } from '.'
|
import { checkAuthRoute, encodePath, getAccessToken } from '.'
|
||||||
import apiConfig from '../../config/api.config'
|
import apiConfig from '../../config/api.config'
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Get access token from storage
|
|
||||||
const accessToken = await getAccessToken()
|
const accessToken = await getAccessToken()
|
||||||
|
if (!accessToken) {
|
||||||
|
res.status(403).json({ error: 'No access token.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get item thumbnails by its path since we will later check if it is protected
|
// Get item thumbnails by its path since we will later check if it is protected
|
||||||
const { path = '', size = 'medium' } = req.query
|
const { path = '', size = 'medium', odpt = '' } = req.query
|
||||||
|
|
||||||
// Set edge function caching for faster load times, check docs:
|
// Set edge function caching for faster load times, if route is not protected, check docs:
|
||||||
// https://vercel.com/docs/concepts/functions/edge-caching
|
// https://vercel.com/docs/concepts/functions/edge-caching
|
||||||
res.setHeader('Cache-Control', apiConfig.cacheControlHeader)
|
if (odpt === '') res.setHeader('Cache-Control', apiConfig.cacheControlHeader)
|
||||||
|
|
||||||
// Check whether the size is valid - must be one of 'large', 'medium', or 'small'
|
// Check whether the size is valid - must be one of 'large', 'medium', or 'small'
|
||||||
if (size !== 'large' && size !== 'medium' && size !== 'small') {
|
if (size !== 'large' && size !== 'medium' && size !== 'small') {
|
||||||
@@ -36,14 +39,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
|
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
|
||||||
|
|
||||||
// Check if the path is protected
|
const { code, message } = await checkAuthRoute(cleanPath, accessToken, odpt as string)
|
||||||
const authTokenPath = getAuthTokenPath(cleanPath)
|
// Status code other than 200 means user has not authenticated yet
|
||||||
|
if (code !== 200) {
|
||||||
// Currently protected paths are rejected to avoid file content leak
|
res.status(code).json({ error: message })
|
||||||
if (authTokenPath) {
|
|
||||||
res.status(404).json({ error: 'Protected pathes are not allowed.' })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// If message is empty, then the path is not protected.
|
||||||
|
// Conversely, protected routes are not allowed to serve from cache.
|
||||||
|
if (message !== '') {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
}
|
||||||
|
|
||||||
const requestPath = encodePath(cleanPath)
|
const requestPath = encodePath(cleanPath)
|
||||||
// Handle response from OneDrive API
|
// Handle response from OneDrive API
|
||||||
@@ -63,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
res.status(400).json({ error: "The item doesn't have a valid thumbnail." })
|
res.status(400).json({ error: "The item doesn't have a valid thumbnail." })
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(error.response.status).json({ error: error.response.data })
|
res.status(error?.response?.status).json({ error: error?.response?.data ?? 'Internal server error.' })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default function OAuthStep2() {
|
|||||||
|
|
||||||
<p className="py-1">{t('The authorisation code extracted is:')}</p>
|
<p className="py-1">{t('The authorisation code extracted is:')}</p>
|
||||||
<p className="my-2 overflow-hidden truncate rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800">
|
<p className="my-2 overflow-hidden truncate rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800">
|
||||||
{authCode || <span className="animate-pulse">{t('Waiting for code...')}</span>}
|
{authCode ?? <span className="animate-pulse">{t('Waiting for code...')}</span>}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
Vendored
+3
-9
@@ -1,4 +1,4 @@
|
|||||||
// API response object for /api?path=<path_to_file_or_folder>, this may return either a file or a folder.
|
// API response object for /api/?path=<path_to_file_or_folder>, this may return either a file or a folder.
|
||||||
// Pagination is also declared here with the 'next' parameter.
|
// Pagination is also declared here with the 'next' parameter.
|
||||||
export type OdAPIResponse = { file?: OdFileObject; folder?: OdFolderObject; next?: string }
|
export type OdAPIResponse = { file?: OdFileObject; folder?: OdFolderObject; next?: string }
|
||||||
// A folder object returned from the OneDrive API. This contains the parameter 'value', which is an array of items
|
// A folder object returned from the OneDrive API. This contains the parameter 'value', which is an array of items
|
||||||
@@ -8,7 +8,6 @@ export type OdFolderObject = {
|
|||||||
'@odata.context': string
|
'@odata.context': string
|
||||||
'@odata.nextLink'?: string
|
'@odata.nextLink'?: string
|
||||||
value: Array<{
|
value: Array<{
|
||||||
'@microsoft.graph.downloadUrl': string
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
@@ -17,14 +16,11 @@ export type OdFolderObject = {
|
|||||||
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
|
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
|
||||||
image?: OdImageFile
|
image?: OdImageFile
|
||||||
video?: OdVideoFile
|
video?: OdVideoFile
|
||||||
// 'thumbnails@odata.context'?: string
|
|
||||||
// thumbnails?: Array<OdThumbnail>
|
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
export type OdFolderChildren = OdFolderObject['value'][number]
|
export type OdFolderChildren = OdFolderObject['value'][number]
|
||||||
// A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.
|
// A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.
|
||||||
export type OdFileObject = {
|
export type OdFileObject = {
|
||||||
'@microsoft.graph.downloadUrl': string
|
|
||||||
'@odata.context': string
|
'@odata.context': string
|
||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
@@ -33,8 +29,6 @@ export type OdFileObject = {
|
|||||||
file: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } }
|
file: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } }
|
||||||
image?: OdImageFile
|
image?: OdImageFile
|
||||||
video?: OdVideoFile
|
video?: OdVideoFile
|
||||||
// 'thumbnails@odata.context'?: string
|
|
||||||
// thumbnails?: Array<OdThumbnail>
|
|
||||||
}
|
}
|
||||||
// A representation of a OneDrive image file. Some images do not return a width and height, so types are optional.
|
// A representation of a OneDrive image file. Some images do not return a width and height, so types are optional.
|
||||||
export type OdImageFile = {
|
export type OdImageFile = {
|
||||||
@@ -59,7 +53,7 @@ export type OdThumbnail = {
|
|||||||
medium: { height: number; width: number; url: string }
|
medium: { height: number; width: number; url: string }
|
||||||
small: { height: number; width: number; url: string }
|
small: { height: number; width: number; url: string }
|
||||||
}
|
}
|
||||||
// API response object for /api/search?q=<query>. Likewise, this array of items may also contain either files or folders.
|
// API response object for /api/search/?q=<query>. Likewise, this array of items may also contain either files or folders.
|
||||||
export type OdSearchResult = Array<{
|
export type OdSearchResult = Array<{
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -68,7 +62,7 @@ export type OdSearchResult = Array<{
|
|||||||
path: string
|
path: string
|
||||||
parentReference: { id: string; name: string; path: string }
|
parentReference: { id: string; name: string; path: string }
|
||||||
}>
|
}>
|
||||||
// API response object for /api/item?id={id}. This is primarily used for determining the path of the driveItem by ID.
|
// API response object for /api/item/?id={id}. This is primarily used for determining the path of the driveItem by ID.
|
||||||
export type OdDriveItem = {
|
export type OdDriveItem = {
|
||||||
'@odata.context': string
|
'@odata.context': string
|
||||||
'@odata.etag': string
|
'@odata.etag': string
|
||||||
|
|||||||
+16
-7
@@ -1,22 +1,31 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getStoredToken } from './protectedRouteHandler'
|
||||||
|
|
||||||
// Custom hook to axios get a URL or API endpoint on mount
|
/**
|
||||||
export default function useAxiosGet(fetchUrl: string): { response: any; error: string; validating: boolean } {
|
* Custom hook for axios to fetch raw file content on component mount
|
||||||
|
* @param fetchUrl The URL pointing to the raw file content
|
||||||
|
* @param path The path of the file, used for determining whether path is protected
|
||||||
|
*/
|
||||||
|
export default function useFileContent(
|
||||||
|
fetchUrl: string,
|
||||||
|
path: string
|
||||||
|
): { response: any; error: string; validating: boolean } {
|
||||||
const [response, setResponse] = useState('')
|
const [response, setResponse] = useState('')
|
||||||
const [validating, setValidating] = useState(true)
|
const [validating, setValidating] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
const url = fetchUrl + (hashedToken ? `&odpt=${hashedToken}` : '')
|
||||||
|
|
||||||
axios
|
axios
|
||||||
// Using 'blob' as response type to get the response as a raw file blob, which is later parsed as a string.
|
// Using 'blob' as response type to get the response as a raw file blob, which is later parsed as a string.
|
||||||
// Axios defaults response parsing to JSON, which causes issues when parsing JSON files.
|
// Axios defaults response parsing to JSON, which causes issues when parsing JSON files.
|
||||||
.get(fetchUrl, { responseType: 'blob' })
|
.get(url, { responseType: 'blob' })
|
||||||
.then(async res => setResponse(await res.data.text()))
|
.then(async res => setResponse(await res.data.text()))
|
||||||
.catch(e => setError(e.message))
|
.catch(e => setError(e.message))
|
||||||
.finally(() => {
|
.finally(() => setValidating(false))
|
||||||
setValidating(false)
|
}, [fetchUrl, path])
|
||||||
})
|
|
||||||
}, [fetchUrl])
|
|
||||||
return { response, error, validating }
|
return { response, error, validating }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export function useProtectedSWRInfinite(path: string = '') {
|
|||||||
if (previousPageData && !previousPageData.folder) return null
|
if (previousPageData && !previousPageData.folder) return null
|
||||||
|
|
||||||
// First page with no prevPageData
|
// First page with no prevPageData
|
||||||
if (pageIndex === 0) return [`/api?path=${path}`, hashedToken]
|
if (pageIndex === 0) return [`/api/?path=${path}`, hashedToken]
|
||||||
|
|
||||||
// Add nextPage token to API endpoint
|
// Add nextPage token to API endpoint
|
||||||
return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken]
|
return [`/api/?path=${path}&next=${previousPageData.next}`, hashedToken]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable auto-revalidate, these options are equivalent to useSWRImmutable
|
// Disable auto-revalidate, these options are equivalent to useSWRImmutable
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function extractAuthCodeFromRedirected(url: string): string {
|
|||||||
|
|
||||||
// New URL search parameter
|
// New URL search parameter
|
||||||
const params = new URLSearchParams(url.split('?')[1])
|
const params = new URLSearchParams(url.split('?')[1])
|
||||||
return params.get('code') || ''
|
return params.get('code') ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL
|
// After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL
|
||||||
|
|||||||
Reference in New Issue
Block a user