diff --git a/components/FileListing.tsx b/components/FileListing.tsx
index 74a25d6..889a282 100644
--- a/components/FileListing.tsx
+++ b/components/FileListing.tsx
@@ -179,6 +179,14 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
if (error) {
+ console.log(error)
+
+ // If error includes 403 which means the user has not completed initial setup, redirect to OAuth page
+ if (error.message.includes('403')) {
+ router.push('/onedrive-vercel-index-oauth/step-1')
+ return
{error.message.includes('401') ?
:
}
diff --git a/config/api.json b/config/api.json
index bd4348e..5414703 100644
--- a/config/api.json
+++ b/config/api.json
@@ -1,7 +1,9 @@
{
"clientId": "d87bcc39-1750-4ca0-ad54-f8d0efbb2735",
+ "obfuscatedClientSecret": "U2FsdGVkX1830zo3/pFDqaBCVBb37iLw3WnBDWGF9GIB2f4apzv0roemp8Y+iIxI3Ih5ecyukqELQEGzZlYiWg==",
"redirectUri": "http://localhost",
"base": "/Public",
"authApi": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
- "driveApi": "https://graph.microsoft.com/v1.0/me/drive"
+ "driveApi": "https://graph.microsoft.com/v1.0/me/drive",
+ "scope": "Files.Read.All Files.ReadWrite.All offline_access"
}
diff --git a/next.config.js b/next.config.js
index 0f1844e..dfd9420 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,5 +1,7 @@
module.exports = {
webpack: (config) => {
+ config.resolve.fallback = { fs: false, path: false, stream: false, constants: false };
+
// load worker files as a urls with `file-loader`
config.module.rules.unshift({
test: /pdf\.worker\.(min\.)?js/,
diff --git a/package-lock.json b/package-lock.json
index 2473d4f..00b9f86 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,8 @@
"crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2",
"jszip": "^3.7.1",
+ "keyv": "^4.0.4",
+ "keyv-file": "^0.2.0",
"next": "^12.0.7",
"nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2",
@@ -39,6 +41,7 @@
},
"devDependencies": {
"@types/crypto-js": "^4.0.2",
+ "@types/keyv": "^3.1.3",
"@types/prismjs": "^1.16.5",
"@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0",
@@ -1132,6 +1135,15 @@
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg=="
},
+ "node_modules/@types/keyv": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz",
+ "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/localforage": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
@@ -3273,6 +3285,16 @@
"url": "https://www.patreon.com/infusion"
}
},
+ "node_modules/fs-extra": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
+ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4435,6 +4457,11 @@
"node": ">=4"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+ },
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -4474,6 +4501,14 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
@@ -4536,6 +4571,24 @@
"katex": "cli.js"
}
},
+ "node_modules/keyv": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz",
+ "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/keyv-file": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-0.2.0.tgz",
+ "integrity": "sha512-zUQ11eZRmilEUpV1gJSj8mBAHjyXpleQo1iCS0khb+GFRhiPfwavWgn4eDUKNlOyMZzmExnISl8HE1hNbim0gw==",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^4.0.1",
+ "tslib": "^1.9.3"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -7390,8 +7443,7 @@
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -7600,6 +7652,14 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -8643,6 +8703,15 @@
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg=="
},
+ "@types/keyv": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz",
+ "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/localforage": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
@@ -10308,6 +10377,16 @@
"integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==",
"dev": true
},
+ "fs-extra": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
+ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -11107,6 +11186,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"peer": true
},
+ "json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+ },
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -11140,6 +11224,14 @@
"minimist": "^1.2.5"
}
},
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"jsx-ast-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
@@ -11198,6 +11290,24 @@
"commander": "^6.0.0"
}
},
+ "keyv": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz",
+ "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==",
+ "requires": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "keyv-file": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-0.2.0.tgz",
+ "integrity": "sha512-zUQ11eZRmilEUpV1gJSj8mBAHjyXpleQo1iCS0khb+GFRhiPfwavWgn4eDUKNlOyMZzmExnISl8HE1hNbim0gw==",
+ "requires": {
+ "debug": "^4.1.1",
+ "fs-extra": "^4.0.1",
+ "tslib": "^1.9.3"
+ }
+ },
"language-subtag-registry": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -13341,8 +13451,7 @@
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"tsutils": {
"version": "3.21.0",
@@ -13484,6 +13593,11 @@
}
}
},
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index b05b829..a34a02c 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
"crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2",
"jszip": "^3.7.1",
+ "keyv": "^4.0.4",
+ "keyv-file": "^0.2.0",
"next": "^12.0.7",
"nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2",
@@ -40,6 +42,7 @@
},
"devDependencies": {
"@types/crypto-js": "^4.0.2",
+ "@types/keyv": "^3.1.3",
"@types/prismjs": "^1.16.5",
"@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index d4c1290..98fa744 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -20,6 +20,7 @@ import {
faArrowAltCircleDown,
faTrashAlt,
faEnvelope,
+ faCheckCircle,
} from '@fortawesome/free-regular-svg-icons'
import {
faPlus,
@@ -36,6 +37,10 @@ import {
faSignOutAlt,
faCloud,
faChevronCircleDown,
+ faExternalLinkAlt,
+ faExclamationCircle,
+ faExclamationTriangle,
+ faHome,
} from '@fortawesome/free-solid-svg-icons'
import * as Icons from '@fortawesome/free-brands-svg-icons'
@@ -78,7 +83,12 @@ library.add(
faEnvelope,
faCloud,
faChevronCircleDown,
- ...iconList,
+ faExternalLinkAlt,
+ faExclamationCircle,
+ faExclamationTriangle,
+ faHome,
+ faCheckCircle,
+ ...iconList
)
function MyApp({ Component, pageProps }: AppProps) {
diff --git a/pages/api/index.ts b/pages/api/index.ts
index 64ad414..77609a5 100644
--- a/pages/api/index.ts
+++ b/pages/api/index.ts
@@ -1,9 +1,13 @@
-import axios from 'axios'
-import type { NextApiRequest, NextApiResponse } from 'next'
import { posix as pathPosix } from 'path'
+import type { NextApiRequest, NextApiResponse } from 'next'
+import axios from 'axios'
+import Keyv from 'keyv'
+import { KeyvFile } from 'keyv-file'
+
import apiConfig from '../../config/api.json'
import siteConfig from '../../config/site.json'
+import { revealObfuscatedToken } from '../../utils/accessTokenHandler'
import { compareHashedToken } from '../../utils/protectedRouteHandler'
const basePath = pathPosix.resolve('/', apiConfig.base)
@@ -16,19 +20,28 @@ const encodePath = (path: string) => {
return `:${encodeURIComponent(encodedPath)}`
}
+const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
+
// Store access token in memory, cuz Vercel doesn't provide key-value storage natively
let _access_token = ''
+let _refresh_token = ''
const getAccessToken = async () => {
if (_access_token) {
- console.log('Fetch token from memory.')
+ console.log('Fetch access token from memory.')
return _access_token
}
+ // Return if refresh_token is empty
+ if (!_refresh_token) {
+ console.log('No refresh token, return empty access token.')
+ return ''
+ }
+
const body = new URLSearchParams()
body.append('client_id', apiConfig.clientId)
body.append('redirect_uri', apiConfig.redirectUri)
- body.append('client_secret', process.env.CLIENT_SECRET ? process.env.CLIENT_SECRET : '')
- body.append('refresh_token', process.env.REFRESH_TOKEN ? process.env.REFRESH_TOKEN : '')
+ body.append('client_secret', clientSecret)
+ body.append('refresh_token', _refresh_token)
body.append('grant_type', 'refresh_token')
const resp = await axios.post(apiConfig.authApi, body, {
@@ -45,124 +58,130 @@ const getAccessToken = async () => {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { path = '/', raw = false, next = '' } = 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 (typeof path === 'string') {
- const accessToken = await getAccessToken()
-
- // Handle authentication through .password
- const protectedRoutes = siteConfig.protectedRoutes
- let authTokenPath = ''
- for (const r of protectedRoutes) {
- if (path.startsWith(r)) {
- authTokenPath = `${r}/.password`
- break
- }
- }
-
- // Fetch password from remote file content
- if (authTokenPath !== '') {
- 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(path)
- // Handle response from OneDrive API
- const requestUrl = `${apiConfig.driveApi}/root${requestPath}`
- // Whether path is root, which requires some special treatment
- const isRoot = requestPath === ''
-
- // Go for file raw download link and query with only temporary link parameter
- if (raw) {
- const { data } = await axios.get(requestUrl, {
- headers: { Authorization: `Bearer ${accessToken}` },
- params: {
- select: '@microsoft.graph.downloadUrl,folder,file',
- },
- })
-
- if ('folder' 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'])
- return
- }
- }
-
- // Querying current path identity (file or folder) and follow up query childrens in folder
- // console.log(accessToken)
-
- const { data: identityData } = await axios.get(requestUrl, {
- headers: { Authorization: `Bearer ${accessToken}` },
- params: {
- select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
- },
- })
-
- if ('folder' in identityData) {
- const { data: folderData } = await axios.get(`${requestUrl}${isRoot ? '' : ':'}/children`, {
- headers: { Authorization: `Bearer ${accessToken}` },
- params: next
- ? {
- select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
- top: siteConfig.maxItems,
- $skipToken: next,
- }
- : {
- select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
- top: siteConfig.maxItems,
- },
- })
-
- // Extract next page token from full @odata.nextLink
- const nextPage = folderData['@odata.nextLink']
- ? folderData['@odata.nextLink'].match(/&\$skiptoken=(.+)/i)[1]
- : null
-
- // Return paging token if specified
- if (nextPage) {
- res.status(200).json({ folder: folderData, next: nextPage })
- } else {
- res.status(200).json({ folder: folderData })
- }
- return
- }
- res.status(200).json({ file: identityData })
+ // If the path is not a valid path, return 400
+ if (typeof path !== 'string') {
+ res.status(400).json({ error: 'Path query invalid.' })
return
}
- res.status(404).json({ error: 'Path query invalid.' })
+ const accessToken = await getAccessToken()
+
+ // Return error 403 if access_token is empty
+ if (!accessToken) {
+ res.status(403).json({ error: 'No access token.' })
+ return
+ }
+
+ // Handle authentication through .password
+ const protectedRoutes = siteConfig.protectedRoutes
+ let authTokenPath = ''
+ for (const r of protectedRoutes) {
+ if (path.startsWith(r)) {
+ authTokenPath = `${r}/.password`
+ break
+ }
+ }
+
+ // Fetch password from remote file content
+ if (authTokenPath !== '') {
+ 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(path)
+ // Handle response from OneDrive API
+ const requestUrl = `${apiConfig.driveApi}/root${requestPath}`
+ // Whether path is root, which requires some special treatment
+ const isRoot = requestPath === ''
+
+ // Go for file raw download link and query with only temporary link parameter
+ if (raw) {
+ const { data } = await axios.get(requestUrl, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ params: {
+ select: '@microsoft.graph.downloadUrl,folder,file',
+ },
+ })
+
+ if ('folder' 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'])
+ return
+ }
+ }
+
+ // Querying current path identity (file or folder) and follow up query childrens in folder
+ // console.log(accessToken)
+
+ const { data: identityData } = await axios.get(requestUrl, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ params: {
+ select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
+ },
+ })
+
+ if ('folder' in identityData) {
+ const { data: folderData } = await axios.get(`${requestUrl}${isRoot ? '' : ':'}/children`, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ params: next
+ ? {
+ select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
+ top: siteConfig.maxItems,
+ $skipToken: next,
+ }
+ : {
+ select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
+ top: siteConfig.maxItems,
+ },
+ })
+
+ // Extract next page token from full @odata.nextLink
+ const nextPage = folderData['@odata.nextLink'] ? folderData['@odata.nextLink'].match(/&\$skiptoken=(.+)/i)[1] : null
+
+ // Return paging token if specified
+ if (nextPage) {
+ res.status(200).json({ folder: folderData, next: nextPage })
+ } else {
+ res.status(200).json({ folder: folderData })
+ }
+ return
+ }
+ res.status(200).json({ file: identityData })
return
}
diff --git a/pages/onedrive-vercel-index-oauth/step-1.tsx b/pages/onedrive-vercel-index-oauth/step-1.tsx
new file mode 100644
index 0000000..7dc95b0
--- /dev/null
+++ b/pages/onedrive-vercel-index-oauth/step-1.tsx
@@ -0,0 +1,129 @@
+import Head from 'next/head'
+import router from 'next/router'
+
+import siteConfig from '../../config/site.json'
+import apiConfig from '../../config/api.json'
+import Navbar from '../../components/Navbar'
+import Footer from '../../components/Footer'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+
+export default function OAuthStep1() {
+ return (
+
+
+
{`OAuth Step 1 - ${siteConfig.title}`}
+
+
+
+
+
+
+
+
Welcome to your new onedrive-vercel-index 🎉
+
+
+ Authorisation is required as no valid{' '}
+ access_token or{' '}
+ refresh_token is
+ present on this deployed instance.
+
+
+
Step 1: Preparations
+
+ Check the following configurations (especially client_id and{' '}
+ client_secret (obfuscated)) and see if they match the official
+ ones specified in the documentation of{' '}
+
+ onedrive-vercel-index
+ {' '}
+ before proceeding with authorising this application with your own Microsoft account.
+
+
+
+
+
+
+
+ CLIENT_ID
+
+
+ {apiConfig.clientId}
+
+
+
+
+ CLIENT_SECRET*
+
+
+ {apiConfig.obfuscatedClientSecret}
+
+
+
+
+ REDIRECT_URI
+
+
+ {apiConfig.redirectUri}
+
+
+
+
+ Auth API URL
+
+
+ {apiConfig.authApi}
+
+
+
+
+ Drive API URL
+
+
+ {apiConfig.driveApi}
+
+
+
+
+ API Scope
+
+
+ {apiConfig.scope}
+
+
+
+
+
+
+
+ If you see anything
+ missing or incorrect, you need to reconfigure /config/api.json{' '}
+ and redeploy this instance.
+
+
+ ️ If you are previewing
+ this locally, you would need to reauthorise yourself when you eventually deploy the site on Vercel.
+
+
+
+ {
+ router.push('/onedrive-vercel-index-oauth/step-2')
+ }}
+ >
+ Proceed to OAuth
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/pages/onedrive-vercel-index-oauth/step-2.tsx b/pages/onedrive-vercel-index-oauth/step-2.tsx
new file mode 100644
index 0000000..578f28d
--- /dev/null
+++ b/pages/onedrive-vercel-index-oauth/step-2.tsx
@@ -0,0 +1,103 @@
+import Head from 'next/head'
+import router from 'next/router'
+import { useState } from 'react'
+
+import siteConfig from '../../config/site.json'
+import Navbar from '../../components/Navbar'
+import Footer from '../../components/Footer'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/accessTokenHandler'
+
+export default function OAuthStep2() {
+ const [oAuthRedirectedUrl, setOAuthRedirectedUrl] = useState('')
+ const [authCode, setAuthCode] = useState('')
+ const oAuthUrl = generateAuthorisationUrl()
+
+ return (
+
+
+
{`OAuth Step 2 - ${siteConfig.title}`}
+
+
+
+
+
+
+
+
Welcome to your new onedrive-vercel-index 🎉
+
+
+ Authorisation is required as no valid{' '}
+ access_token or{' '}
+ refresh_token is
+ present on this deployed instance.
+
+
+
Step 2: Get authorisation code
+
The OAuth URL has been generated for you:
+
{
+ window.open(oAuthUrl)
+ }}
+ >
+
+
+
+
+ {oAuthUrl}
+
+
+
+ Click on the link to get the{' '}
+ authorisation code . Your browser will
+ open a new tab to Microsoft's account login page. Authenticate with your Microsoft account and copy
+ the redirected URL down below.
+
+
+
{
+ setOAuthRedirectedUrl(e.target.value)
+ setAuthCode(extractAuthCodeFromRedirected(e.target.value))
+ }}
+ />
+
+
The authorisation code extracted is:
+
+ {authCode || Waiting for code... }
+
+
+
+ {authCode
+ ? '✅ You can now proceed onto the next step: requesting your access token and refresh token.'
+ : '❌ No valid code extracted.'}
+
+
+
+ {
+ router.push({ pathname: '/onedrive-vercel-index-oauth/step-3', query: { authCode } })
+ }}
+ >
+ Get tokens
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/pages/onedrive-vercel-index-oauth/step-3.tsx b/pages/onedrive-vercel-index-oauth/step-3.tsx
new file mode 100644
index 0000000..419645d
--- /dev/null
+++ b/pages/onedrive-vercel-index-oauth/step-3.tsx
@@ -0,0 +1,148 @@
+import Head from 'next/head'
+import router from 'next/router'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+
+import siteConfig from '../../config/site.json'
+import Navbar from '../../components/Navbar'
+import Footer from '../../components/Footer'
+import { requestTokenWithAuthCode } from '../../utils/accessTokenHandler'
+
+export default function OAuthStep3({ accessToken, refreshToken, error, description, errorUri }) {
+ return (
+
+
+
{`OAuth Step 3 - ${siteConfig.title}`}
+
+
+
+
+
+
+
+
Welcome to your new onedrive-vercel-index 🎉
+
+
+ Authorisation is required as no valid{' '}
+ access_token or{' '}
+ refresh_token is
+ present on this deployed instance.
+
+
+
Step 3: Get access and refresh tokens
+ {error ? (
+
+
+
+ Whoops, looks like we got a problem: {error}.
+
+
+ {description}
+
+ {errorUri && (
+
+ Check out{' '}
+
+ Microsoft's official explanation
+ {' '}
+ on the error message.
+
+ )}
+
+ {
+ router.push('/onedrive-vercel-index-oauth/step-1')
+ }}
+ >
+ Restart
+
+
+
+ ) : (
+
+
Success, the API returned what we needed.
+
+ {accessToken && (
+
+ {' '}
+
+ Acquired access_token:{' '}
+ {`${accessToken.substring(0, 30)}...`}.
+
+
+ )}
+ {refreshToken && (
+
+ {' '}
+
+ Acquired refresh_token:{' '}
+ {`${refreshToken.substring(0, 50)}...`}.
+
+
+ )}
+
+
+
Your onedrive-vercel-index should be up and running. Go back home to find out.
+
+
+ {
+ router.push('/')
+ }}
+ >
+ Back home
+
+
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+export async function getServerSideProps({ query }) {
+ const { authCode } = query
+
+ // Return if no auth code is present
+ if (!authCode) {
+ return {
+ props: {
+ error: 'No auth code present',
+ description: 'Where is the auth code??? Did you follow step 2 you silly donut?',
+ },
+ }
+ }
+
+ const response = await requestTokenWithAuthCode(authCode)
+
+ // If error response, return invalid
+ if ('error' in response) {
+ return {
+ props: {
+ error: response.error,
+ description: response.errorDescription,
+ errorUri: response.errorUri,
+ },
+ }
+ }
+
+ const { expiryTime, accessToken, refreshToken } = response
+ return {
+ props: {
+ error: null,
+ expiryTime,
+ accessToken,
+ refreshToken,
+ },
+ }
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 2123b73..c88d993 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -19,7 +19,9 @@ module.exports = {
indigo: colors.indigo,
purple: colors.purple,
pink: colors.pink,
- teal: colors.teal
+ teal: colors.teal,
+ cyan: colors.cyan,
+ orange: colors.orange,
},
extend: {
fontFamily: {
diff --git a/utils/accessTokenHandler.ts b/utils/accessTokenHandler.ts
new file mode 100644
index 0000000..5053f68
--- /dev/null
+++ b/utils/accessTokenHandler.ts
@@ -0,0 +1,85 @@
+import axios from 'axios'
+import CryptoJS from 'crypto-js'
+import Keyv from 'keyv'
+import KeyvFile from 'keyv-file'
+
+import apiConfig from '../config/api.json'
+
+const AES_SECRET_KEY = 'onedrive-vercel-index'
+
+export function obfuscateToken(token: string): string {
+ // Encrypt token with AES
+ const encrypted = CryptoJS.AES.encrypt(token, AES_SECRET_KEY)
+ return encrypted.toString()
+}
+
+export function revealObfuscatedToken(obfuscated: string): string {
+ // Decrypt SHA256 obfuscated token
+ const decrypted = CryptoJS.AES.decrypt(obfuscated, AES_SECRET_KEY)
+ return decrypted.toString(CryptoJS.enc.Utf8)
+}
+
+export function generateAuthorisationUrl(): string {
+ const { clientId, redirectUri, authApi } = apiConfig
+ const authUrl = authApi.replace('/token', '/authorize')
+
+ // Construct URL parameters for OAuth2
+ const params = new URLSearchParams()
+ params.append('client_id', clientId)
+ params.append('redirect_uri', redirectUri)
+ params.append('response_type', 'code')
+ params.append('scope', 'files.readwrite offline_access')
+ params.append('response_mode', 'query')
+
+ return `${authUrl}?${params.toString()}`
+}
+
+export function extractAuthCodeFromRedirected(url: string): string {
+ // Return empty string if the url is not the defined redirect uri
+ if (!url.startsWith(apiConfig.redirectUri)) {
+ return ''
+ }
+
+ // New URL search parameter
+ const params = new URLSearchParams(url.split('?')[1])
+ return params.get('code') || ''
+}
+
+export async function requestTokenWithAuthCode(
+ code: string
+): Promise<
+ | { expiryTime: string; accessToken: string; refreshToken: string }
+ | { error: string; errorDescription: string; errorUri: string }
+> {
+ const { clientId, redirectUri, authApi } = apiConfig
+ const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
+
+ // Construct URL parameters for OAuth2
+ const params = new URLSearchParams()
+ params.append('client_id', clientId)
+ params.append('redirect_uri', redirectUri)
+ params.append('client_secret', clientSecret)
+ params.append('code', code)
+ params.append('grant_type', 'authorization_code')
+
+ // Request access token
+ return axios
+ .post(authApi, params, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ })
+ .then(resp => {
+ const { expires_in, access_token, refresh_token } = resp.data
+ return { expiryTime: expires_in, accessToken: access_token, refreshToken: refresh_token }
+ })
+ .catch(err => {
+ const { error, error_description, error_uri } = err.response.data
+ return { error, errorDescription: error_description, errorUri: error_uri }
+ })
+}
+
+export function storeTokens(accessToken: string, refreshToken: string) {
+ // We can safely leverage Vercel's /tmp directory, and persist the tokens with file-system based KV storage
+ const kv = new Keyv({ store: new KeyvFile(), namespace: 'onedrive-vercel-index' })
+}