oauth inside main project, with public client id and secret

This commit is contained in:
spencerwooo
2021-12-31 03:44:03 +08:00
parent 8658b480ed
commit 38d24146fa
12 changed files with 749 additions and 124 deletions
+8
View File
@@ -179,6 +179,14 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
const { data, error, size, setSize } = useProtectedSWRInfinite(path) const { data, error, size, setSize } = useProtectedSWRInfinite(path)
if (error) { 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 <div></div>
}
return ( return (
<div className="dark:bg-gray-900 p-3 bg-white rounded"> <div className="dark:bg-gray-900 p-3 bg-white rounded">
{error.message.includes('401') ? <Auth redirect={path} /> : <FourOhFour errorMsg={error.message} />} {error.message.includes('401') ? <Auth redirect={path} /> : <FourOhFour errorMsg={error.message} />}
+3 -1
View File
@@ -1,7 +1,9 @@
{ {
"clientId": "d87bcc39-1750-4ca0-ad54-f8d0efbb2735", "clientId": "d87bcc39-1750-4ca0-ad54-f8d0efbb2735",
"obfuscatedClientSecret": "U2FsdGVkX1830zo3/pFDqaBCVBb37iLw3WnBDWGF9GIB2f4apzv0roemp8Y+iIxI3Ih5ecyukqELQEGzZlYiWg==",
"redirectUri": "http://localhost", "redirectUri": "http://localhost",
"base": "/Public", "base": "/Public",
"authApi": "https://login.microsoftonline.com/common/oauth2/v2.0/token", "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"
} }
+2
View File
@@ -1,5 +1,7 @@
module.exports = { module.exports = {
webpack: (config) => { webpack: (config) => {
config.resolve.fallback = { fs: false, path: false, stream: false, constants: false };
// load worker files as a urls with `file-loader` // load worker files as a urls with `file-loader`
config.module.rules.unshift({ config.module.rules.unshift({
test: /pdf\.worker\.(min\.)?js/, test: /pdf\.worker\.(min\.)?js/,
+118 -4
View File
@@ -18,6 +18,8 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2", "emoji-regex": "^9.2.2",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"keyv": "^4.0.4",
"keyv-file": "^0.2.0",
"next": "^12.0.7", "next": "^12.0.7",
"nextjs-progressbar": "^0.0.13", "nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2", "preview-office-docs": "^1.0.2",
@@ -39,6 +41,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.0.2", "@types/crypto-js": "^4.0.2",
"@types/keyv": "^3.1.3",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/react": "17.0.11", "@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0", "@types/react-copy-to-clipboard": "^5.0.0",
@@ -1132,6 +1135,15 @@
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==" "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": { "node_modules/@types/localforage": {
"version": "0.0.34", "version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz", "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
@@ -3273,6 +3285,16 @@
"url": "https://www.patreon.com/infusion" "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": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4435,6 +4457,11 @@
"node": ">=4" "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": { "node_modules/json-parse-better-errors": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -4474,6 +4501,14 @@
"node": ">=6" "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": { "node_modules/jsx-ast-utils": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
@@ -4536,6 +4571,24 @@
"katex": "cli.js" "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": { "node_modules/language-subtag-registry": {
"version": "0.3.21", "version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -7390,8 +7443,7 @@
"node_modules/tslib": { "node_modules/tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"dev": true
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",
@@ -7600,6 +7652,14 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==" "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": { "@types/localforage": {
"version": "0.0.34", "version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz", "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
@@ -10308,6 +10377,16 @@
"integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==",
"dev": true "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": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -11107,6 +11186,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"peer": true "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": { "json-parse-better-errors": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "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" "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": { "jsx-ast-utils": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
@@ -11198,6 +11290,24 @@
"commander": "^6.0.0" "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": { "language-subtag-registry": {
"version": "0.3.21", "version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -13341,8 +13451,7 @@
"tslib": { "tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"dev": true
}, },
"tsutils": { "tsutils": {
"version": "3.21.0", "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": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+3
View File
@@ -19,6 +19,8 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2", "emoji-regex": "^9.2.2",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"keyv": "^4.0.4",
"keyv-file": "^0.2.0",
"next": "^12.0.7", "next": "^12.0.7",
"nextjs-progressbar": "^0.0.13", "nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2", "preview-office-docs": "^1.0.2",
@@ -40,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.0.2", "@types/crypto-js": "^4.0.2",
"@types/keyv": "^3.1.3",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/react": "17.0.11", "@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0", "@types/react-copy-to-clipboard": "^5.0.0",
+11 -1
View File
@@ -20,6 +20,7 @@ import {
faArrowAltCircleDown, faArrowAltCircleDown,
faTrashAlt, faTrashAlt,
faEnvelope, faEnvelope,
faCheckCircle,
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
import { import {
faPlus, faPlus,
@@ -36,6 +37,10 @@ import {
faSignOutAlt, faSignOutAlt,
faCloud, faCloud,
faChevronCircleDown, faChevronCircleDown,
faExternalLinkAlt,
faExclamationCircle,
faExclamationTriangle,
faHome,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import * as Icons from '@fortawesome/free-brands-svg-icons' import * as Icons from '@fortawesome/free-brands-svg-icons'
@@ -78,7 +83,12 @@ library.add(
faEnvelope, faEnvelope,
faCloud, faCloud,
faChevronCircleDown, faChevronCircleDown,
...iconList, faExternalLinkAlt,
faExclamationCircle,
faExclamationTriangle,
faHome,
faCheckCircle,
...iconList
) )
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
+32 -13
View File
@@ -1,9 +1,13 @@
import axios from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import { posix as pathPosix } from 'path' 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 apiConfig from '../../config/api.json'
import siteConfig from '../../config/site.json' import siteConfig from '../../config/site.json'
import { revealObfuscatedToken } from '../../utils/accessTokenHandler'
import { compareHashedToken } from '../../utils/protectedRouteHandler' import { compareHashedToken } from '../../utils/protectedRouteHandler'
const basePath = pathPosix.resolve('/', apiConfig.base) const basePath = pathPosix.resolve('/', apiConfig.base)
@@ -16,19 +20,28 @@ const encodePath = (path: string) => {
return `:${encodeURIComponent(encodedPath)}` return `:${encodeURIComponent(encodedPath)}`
} }
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
// Store access token in memory, cuz Vercel doesn't provide key-value storage natively // Store access token in memory, cuz Vercel doesn't provide key-value storage natively
let _access_token = '' let _access_token = ''
let _refresh_token = ''
const getAccessToken = async () => { const getAccessToken = async () => {
if (_access_token) { if (_access_token) {
console.log('Fetch token from memory.') console.log('Fetch access token from memory.')
return _access_token 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() const body = new URLSearchParams()
body.append('client_id', apiConfig.clientId) body.append('client_id', apiConfig.clientId)
body.append('redirect_uri', apiConfig.redirectUri) body.append('redirect_uri', apiConfig.redirectUri)
body.append('client_secret', process.env.CLIENT_SECRET ? process.env.CLIENT_SECRET : '') body.append('client_secret', clientSecret)
body.append('refresh_token', process.env.REFRESH_TOKEN ? process.env.REFRESH_TOKEN : '') body.append('refresh_token', _refresh_token)
body.append('grant_type', 'refresh_token') body.append('grant_type', 'refresh_token')
const resp = await axios.post(apiConfig.authApi, body, { const resp = await axios.post(apiConfig.authApi, body, {
@@ -45,14 +58,26 @@ const getAccessToken = async () => {
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { path = '/', raw = false, next = '' } = req.query const { path = '/', raw = false, next = '' } = req.query
// Sometimes the path parameter is defaulted to '[...path]' which we need to handle
if (path === '[...path]') { if (path === '[...path]') {
res.status(400).json({ error: 'No path specified.' }) res.status(400).json({ error: 'No path specified.' })
return return
} }
// If the path is not a valid path, return 400
if (typeof path !== 'string') {
res.status(400).json({ error: 'Path query invalid.' })
return
}
if (typeof path === 'string') {
const accessToken = await getAccessToken() 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 // Handle authentication through .password
const protectedRoutes = siteConfig.protectedRoutes const protectedRoutes = siteConfig.protectedRoutes
let authTokenPath = '' let authTokenPath = ''
@@ -147,9 +172,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
// Extract next page token from full @odata.nextLink // Extract next page token from full @odata.nextLink
const nextPage = folderData['@odata.nextLink'] const nextPage = folderData['@odata.nextLink'] ? folderData['@odata.nextLink'].match(/&\$skiptoken=(.+)/i)[1] : null
? folderData['@odata.nextLink'].match(/&\$skiptoken=(.+)/i)[1]
: null
// Return paging token if specified // Return paging token if specified
if (nextPage) { if (nextPage) {
@@ -161,8 +184,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
res.status(200).json({ file: identityData }) res.status(200).json({ file: identityData })
return return
}
res.status(404).json({ error: 'Path query invalid.' })
return
} }
@@ -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 (
<div className="dark:bg-gray-900 flex flex-col items-center justify-center min-h-screen bg-white">
<Head>
<title>{`OAuth Step 1 - ${siteConfig.title}`}</title>
</Head>
<main className="bg-gray-50 dark:bg-gray-800 flex flex-col flex-1 w-full">
<Navbar />
<div className="w-full max-w-5xl p-4 mx-auto">
<div className="dark:bg-gray-900 dark:text-gray-100 bg-white rounded p-3">
<h3 className="font-medium text-xl mb-4">Welcome to your new onedrive-vercel-index 🎉</h3>
<p className="py-1">
Authorisation is required as no valid{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-pink-600">access_token</code> or{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-green-600">refresh_token</code> is
present on this deployed instance.
</p>
<h3 className="font-medium text-lg mt-4 mb-2">Step 1: Preparations</h3>
<p className="py-1">
Check the following configurations (especially <code className="text-sm font-mono">client_id</code> and{' '}
<code className="text-sm font-mono">client_secret</code> (obfuscated)) and see if they match the official
ones specified in the documentation of{' '}
<a
href="https://github.com/spencerwooo/onedrive-vercel-index"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-blue-600 dark:text-blue-500"
>
onedrive-vercel-index
</a>{' '}
before proceeding with authorising this application with your own Microsoft account.
</p>
<div className="overflow-hidden my-4">
<table className="table-auto min-w-full">
<tbody>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
CLIENT_ID
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.clientId}</code>
</td>
</tr>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
CLIENT_SECRET*
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.obfuscatedClientSecret}</code>
</td>
</tr>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
REDIRECT_URI
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.redirectUri}</code>
</td>
</tr>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
Auth API URL
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.authApi}</code>
</td>
</tr>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
Drive API URL
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.driveApi}</code>
</td>
</tr>
<tr className="bg-white border-y dark:bg-gray-900 dark:border-gray-700">
<td className="bg-gray-50 dark:bg-gray-800 py-1 px-3 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400">
API Scope
</td>
<td className="py-1 px-3 text-gray-500 whitespace-nowrap dark:text-gray-400">
<code className="text-sm font-mono">{apiConfig.scope}</code>
</td>
</tr>
</tbody>
</table>
</div>
<p className="py-1">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1 text-yellow-400" /> If you see anything
missing or incorrect, you need to reconfigure <code className="text-sm font-mono">/config/api.json</code>{' '}
and redeploy this instance.
</p>
<p className="py-1">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1 text-yellow-400" /> If you are previewing
this locally, you would need to reauthorise yourself when you eventually deploy the site on Vercel.
</p>
<div className="text-right mb-2 mt-6">
<button
className="text-white bg-gradient-to-r from-cyan-500 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center"
onClick={() => {
router.push('/onedrive-vercel-index-oauth/step-2')
}}
>
<span>Proceed to OAuth</span> <FontAwesomeIcon icon="arrow-right" />
</button>
</div>
</div>
</div>
</main>
<Footer />
</div>
)
}
@@ -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 (
<div className="dark:bg-gray-900 flex flex-col items-center justify-center min-h-screen bg-white">
<Head>
<title>{`OAuth Step 2 - ${siteConfig.title}`}</title>
</Head>
<main className="bg-gray-50 dark:bg-gray-800 flex flex-col flex-1 w-full">
<Navbar />
<div className="w-full max-w-5xl p-4 mx-auto">
<div className="dark:bg-gray-900 dark:text-gray-100 bg-white rounded p-3">
<h3 className="font-medium text-xl mb-4">Welcome to your new onedrive-vercel-index 🎉</h3>
<p className="py-1">
Authorisation is required as no valid{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-pink-600">access_token</code> or{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-green-600">refresh_token</code> is
present on this deployed instance.
</p>
<h3 className="font-medium text-lg mt-4 mb-2">Step 2: Get authorisation code</h3>
<p className="py-1">The OAuth URL has been generated for you:</p>
<div
className="relative my-2 font-mono border border-gray-400/20 rounded text-sm bg-gray-50 dark:bg-gray-800 cursor-pointer hover:opacity-80"
onClick={() => {
window.open(oAuthUrl)
}}
>
<div className="absolute top-0 right-0 p-1 opacity-60">
<FontAwesomeIcon icon="external-link-alt" />
</div>
<pre className="p-2 whitespace-pre-wrap overflow-x-auto">
<code>{oAuthUrl}</code>
</pre>
</div>
<p className="py-1">
Click on the link to get the{' '}
<b className="underline decoration-wavy decoration-yellow-400">authorisation code</b>. Your browser will
open a new tab to Microsoft&apos;s account login page. Authenticate with your Microsoft account and copy
the redirected URL down below.
</p>
<input
className={`w-full flex-1 border bg-gray-50 dark:bg-gray-800 dark:text-white focus:ring focus:outline-none p-2 font-mono rounded my-2 font-medium text-sm ${
authCode
? 'border-green-500/50 focus:ring-green-500/30 dark:focus:ring-green-500/40'
: 'border-red-500/50 focus:ring-red-500/30 dark:focus:ring-red-500/40'
}`}
autoFocus
type="text"
placeholder="http://localhost/?code=M.R3_BAY.c0..."
value={oAuthRedirectedUrl}
onChange={e => {
setOAuthRedirectedUrl(e.target.value)
setAuthCode(extractAuthCodeFromRedirected(e.target.value))
}}
/>
<p className="py-1">The authorisation code extracted is:</p>
<p className="my-2 font-mono border border-gray-400/20 rounded text-sm bg-gray-50 dark:bg-gray-800 p-2 opacity-80">
{authCode || <span className="animate-pulse">Waiting for code...</span>}
</p>
<p>
{authCode
? '✅ You can now proceed onto the next step: requesting your access token and refresh token.'
: '❌ No valid code extracted.'}
</p>
<div className="text-right mb-2 mt-6">
<button
className="text-white bg-gradient-to-br from-green-500 to-cyan-400 hover:bg-gradient-to-bl focus:ring-4 focus:ring-green-200 dark:focus:ring-green-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center disabled:cursor-not-allowed disabled:grayscale"
disabled={authCode === ''}
onClick={() => {
router.push({ pathname: '/onedrive-vercel-index-oauth/step-3', query: { authCode } })
}}
>
<span>Get tokens</span> <FontAwesomeIcon icon="arrow-right" />
</button>
</div>
</div>
</div>
</main>
<Footer />
</div>
)
}
@@ -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 (
<div className="dark:bg-gray-900 flex flex-col items-center justify-center min-h-screen bg-white">
<Head>
<title>{`OAuth Step 3 - ${siteConfig.title}`}</title>
</Head>
<main className="bg-gray-50 dark:bg-gray-800 flex flex-col flex-1 w-full">
<Navbar />
<div className="w-full max-w-5xl p-4 mx-auto">
<div className="dark:bg-gray-900 dark:text-gray-100 bg-white rounded p-3">
<h3 className="font-medium text-xl mb-4">Welcome to your new onedrive-vercel-index 🎉</h3>
<p className="py-1">
Authorisation is required as no valid{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-pink-600">access_token</code> or{' '}
<code className="text-sm font-mono underline decoration-wavy decoration-green-600">refresh_token</code> is
present on this deployed instance.
</p>
<h3 className="font-medium text-lg mt-4 mb-2">Step 3: Get access and refresh tokens</h3>
{error ? (
<div>
<p className="text-red-500 py-1 font-medium">
<FontAwesomeIcon icon="exclamation-circle" className="mr-2" />
<span>Whoops, looks like we got a problem: {error}.</span>
</p>
<p className="my-2 font-mono border border-gray-400/20 rounded text-sm bg-gray-50 dark:bg-gray-800 p-2 opacity-80 whitespace-pre-line">
{description}
</p>
{errorUri && (
<p>
Check out{' '}
<a
href={errorUri}
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-blue-600 dark:text-blue-500"
>
Microsoft&apos;s official explanation
</a>{' '}
on the error message.
</p>
)}
<div className="text-right mb-2 mt-6">
<button
className="text-white bg-gradient-to-br from-red-500 to-orange-400 hover:bg-gradient-to-bl focus:ring-4 focus:ring-red-200 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center disabled:cursor-not-allowed disabled:grayscale"
onClick={() => {
router.push('/onedrive-vercel-index-oauth/step-1')
}}
>
<FontAwesomeIcon icon="arrow-left" /> <span>Restart</span>
</button>
</div>
</div>
) : (
<div>
<p className="py-1 font-medium">Success, the API returned what we needed.</p>
<ol className="py-1">
{accessToken && (
<li>
<FontAwesomeIcon icon={['far', 'check-circle']} className="text-green-500" />{' '}
<span>
Acquired access_token:{' '}
<code className="text-sm font-mono opacity-80">{`${accessToken.substring(0, 30)}...`}</code>.
</span>
</li>
)}
{refreshToken && (
<li>
<FontAwesomeIcon icon={['far', 'check-circle']} className="text-green-500" />{' '}
<span>
Acquired refresh_token:{' '}
<code className="text-sm font-mono opacity-80">{`${refreshToken.substring(0, 50)}...`}</code>.
</span>
</li>
)}
</ol>
<p className="py-1">Your onedrive-vercel-index should be up and running. Go back home to find out.</p>
<div className="text-right mb-2 mt-6">
<button
className="text-white bg-gradient-to-br from-green-400 via-green-500 to-green-600 hover:bg-gradient-to-bl focus:ring-4 focus:ring-green-200 dark:focus:ring-green-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center disabled:cursor-not-allowed disabled:grayscale"
onClick={() => {
router.push('/')
}}
>
<FontAwesomeIcon icon="home" /> <span>Back home</span>
</button>
</div>
</div>
)}
</div>
</div>
</main>
<Footer />
</div>
)
}
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,
},
}
}
+3 -1
View File
@@ -19,7 +19,9 @@ module.exports = {
indigo: colors.indigo, indigo: colors.indigo,
purple: colors.purple, purple: colors.purple,
pink: colors.pink, pink: colors.pink,
teal: colors.teal teal: colors.teal,
cyan: colors.cyan,
orange: colors.orange,
}, },
extend: { extend: {
fontFamily: { fontFamily: {
+85
View File
@@ -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' })
}