Merge pull request #210 from spencerwooo/public-client-id-secret

OAuth inside main project with public client ID and secret
This commit is contained in:
Spencer Woo
2021-12-31 22:28:51 +08:00
committed by GitHub
26 changed files with 1038 additions and 161 deletions
+31 -8
View File
@@ -85,6 +85,7 @@ Live demo at [Spencer's OneDrive](https://drive.spencerwoo.com).
... and more:
- Streamlined deployment, without having to get your tokens manually anymore!
- Direct raw-file serving, proxied file serving ...
- Permalink copy, proxied download link copy ...
- Full dark mode support, style and website customisations ...
@@ -93,14 +94,36 @@ Live demo at [Spencer's OneDrive](https://drive.spencerwoo.com).
## Deployment
> No time to write deployment documentation! Here are some quick hints, play around with caution! (I promise detailed docs are on the way.)
> Starting now, you don't have to manually acquire your tokens, and you don't need to register a client at Microsoft anymore (PR #210).
- Fork the project to your own account, as you will be maintaining your custom version of this project with your own configurations.
- Change configuration file [`config/api.json`](config/api.json) and [`config/site.json`](config/site.json) according to your configs (see [Configurations](#configurations) ↓).
- Define environment variables inside Vercel: `REFRESH_TOKEN`, `ACCESS_TOKEN`, `CLIENT_SECRET`.
- Deploy inside Vercel, profit.
### New users
The authentication tokens and variables are the same as what you configured in the [`onedrive-cf-index`](https://github.com/spencerwooo/onedrive-cf-index) project. Detailed documentations can also be found there (for now). This project is at its early stages, for discussions *please, please, please* post to the [discussion forum](https://github.com/spencerwooo/onedrive-vercel-index/discussions).
- Fork the project to your own GitHub account, as you will be maintaining your custom version of this project with your own configurations.
- Modify [`config/site.json`](config/site.json) according to your configs (see [Configurations](#configurations) ↓).
- Only change [`config/api.json`](config/api.json) if you must (if you are not a OneDrive international user).
- SharePoint users need to define their own API endpoints.
- Import your forked `onedrive-vercel-index` GitHub project to Vercel. Vercel will automatically build the Next.js project, so please wait for deployment to finish.
- Create a Redis database, and set the URL of the Redis instance to environment variable `REDIS_URL` inside the Vercel project.
- You can use Upstash for this, completely free, full integration with Vercel, documentation here: [Vercel Integration
](https://docs.upstash.com/redis/howto/vercelintegration).
- Finally, trigger a redeployment on Vercel to use the new environment variable, navigate to the newly deployed page, and perform authorisation as guided by `onedrive-vercel-index`.
### Migrating from an old project
- You need to first create a Redis instance, and define the URL of the instance in the environment variable `REDIS_URL` in Vercel.
- Likewise, you can still use Upstash for this, same as above.
- You need to change your custom `config/api.json` exactly like the new project, where specifically:
```json
{
"clientId": "d87bcc39-1750-4ca0-ad54-f8d0efbb2735",
"obfuscatedClientSecret": "U2FsdGVkX1830zo3/pFDqaBCVBb37iLw3WnBDWGF9GIB2f4apzv0roemp8Y+iIxI3Ih5ecyukqELQEGzZlYiWg==",
}
```
- You can safely delete old environment variables such as your own `CLIENT_SECRET`, `ACCESS_TOKEN`, and `REFRESH_TOKEN`.
Deployment issues *please, please, please* post to the [discussion forum](https://github.com/spencerwooo/onedrive-vercel-index/discussions) with tags `FAQ`.
## Configurations
@@ -108,8 +131,8 @@ The authentication tokens and variables are the same as what you configured in t
Two configuration files are used for customisations - `config/api.json` and `config/site.json`.
- `config/api.json` - is used to define your API endpoints and tokens, and the path for your shared OneDrive folder.
- `config/site.json` - is used for customising the website, such as the title, used Google fonts, site icons, contact info, etc.
- `config/api.json` - is used to define your API endpoints and tokens. **OneDrive international users don't have to change anything.**
- `config/site.json` - is used for customising the website, such as the folder to share, the title, used Google fonts, site icons, contact info, etc.
A few things to keep in mind for `config/site.json`:
+1 -1
View File
@@ -17,7 +17,7 @@ const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
return (
<div className="md:my-10 flex flex-col max-w-sm mx-auto space-y-4">
<div className="md:w-5/6 w-3/4 mx-auto">
<Image src={'/images/no-looking.png'} alt="authenticate" width={912} height={912} />
<Image src={'/images/fabulous-wapmire-weekdays.png'} alt="authenticate" width={912} height={912} />
</div>
<div className="dark:text-gray-100 text-lg font-bold text-gray-900">Enter Password</div>
+8
View File
@@ -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 <div></div>
}
return (
<div className="dark:bg-gray-900 p-3 bg-white rounded">
{error.message.includes('401') ? <Auth redirect={path} /> : <FourOhFour errorMsg={error.message} />}
+2 -2
View File
@@ -4,8 +4,8 @@ import { FunctionComponent } from 'react'
const FourOhFour: FunctionComponent<{ errorMsg: string }> = ({ errorMsg }) => {
return (
<div className="my-12">
<div className="md:w-1/4 w-1/3 mx-auto">
<Image src={'/images/empty.png'} alt="404" width={912} height={912} />
<div className="w-1/3 mx-auto">
<Image src='/images/fabulous-rip-2.png' alt="404" width={912} height={912} />
</div>
<div className="mt-6 text-gray-500 max-w-xl mx-auto">
<div className="text-xl font-bold mb-8">
+3 -2
View File
@@ -1,7 +1,8 @@
{
"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"
}
+1
View File
@@ -1,6 +1,7 @@
{
"icon": "/icons/128.png",
"title": "Spencer's OneDrive",
"baseDirectory": "/Public",
"maxItems": 100,
"googleFontSans": "Inter",
"googleFontMono": "Fira Mono",
+2
View File
@@ -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/,
+201 -17
View File
@@ -17,14 +17,15 @@
"axios": "^0.21.1",
"crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2",
"ioredis": "^4.28.2",
"jszip": "^3.7.1",
"next": "^12.0.7",
"nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2",
"prismjs": "^1.23.0",
"react": "17.0.2",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.0.0",
"react-markdown": "^6.0.2",
"react-player": "^2.9.0",
@@ -39,6 +40,7 @@
},
"devDependencies": {
"@types/crypto-js": "^4.0.2",
"@types/ioredis": "^4.28.5",
"@types/prismjs": "^1.16.5",
"@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0",
@@ -49,7 +51,7 @@
"eslint": "7.29.0",
"eslint-config-next": "11.0.0",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.5",
"tailwindcss": "^3.0.8",
"typescript": "4.3.4"
}
},
@@ -1121,6 +1123,15 @@
"@types/unist": "*"
}
},
"node_modules/@types/ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-bp5mdpzscWZMEE/jLvvzze5TZFYGhynB1am69l/a0XPqZRXWpbswY6lb5buEht57jOnw5pPG5zL9pFUWw1nggw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -1993,6 +2004,14 @@
}
]
},
"node_modules/cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2326,6 +2345,14 @@
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
"dev": true
},
"node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -4051,6 +4078,31 @@
"node": ">= 0.4"
}
},
"node_modules/ioredis": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.2.tgz",
"integrity": "sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg==",
"dependencies": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
@@ -4647,6 +4699,21 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5659,6 +5726,14 @@
"node": ">=4"
}
},
"node_modules/p-map": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
"engines": {
"node": ">=6"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -5932,9 +6007,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
"integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz",
"integrity": "sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
@@ -6345,6 +6420,30 @@
"node": ">=8.10.0"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
@@ -6883,6 +6982,11 @@
"node": ">=8"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -7189,9 +7293,9 @@
"dev": true
},
"node_modules/tailwindcss": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.5.tgz",
"integrity": "sha512-59pNgzx2o+wkAk7IZGIH7H9eNS53gzZGrO3+NPyOEWHDbquHgiLL/c993T5t1vPSAeBxox4X5OgZwNuRvXVf+g==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.8.tgz",
"integrity": "sha512-Yww1eRYO1AxITJmW/KduZPxNvYdHuedeKwPju9Oakp7MdiixRi5xkpLhirsc81QCxHL0eoce6qKmxXwYGt4Cjw==",
"dev": true,
"dependencies": {
"arg": "^5.0.1",
@@ -7210,7 +7314,7 @@
"postcss-js": "^3.0.3",
"postcss-load-config": "^3.1.0",
"postcss-nested": "5.0.6",
"postcss-selector-parser": "^6.0.6",
"postcss-selector-parser": "^6.0.7",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
"resolve": "^1.20.0",
@@ -8632,6 +8736,15 @@
"@types/unist": "*"
}
},
"@types/ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-bp5mdpzscWZMEE/jLvvzze5TZFYGhynB1am69l/a0XPqZRXWpbswY6lb5buEht57jOnw5pPG5zL9pFUWw1nggw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -9283,6 +9396,11 @@
"resolved": "https://registry.npmjs.org/clipboard-copy/-/clipboard-copy-3.2.0.tgz",
"integrity": "sha512-vooFaGFL6ulEP1liiaWFBmmfuPm3cY3y7T9eB83ZTnYc/oFeAKsq3NcDrOkBC8XaauEE8zHQwI7k0+JSYiVQSQ=="
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -9553,6 +9671,11 @@
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
"dev": true
},
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -10868,6 +10991,24 @@
"side-channel": "^1.0.4"
}
},
"ioredis": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.2.tgz",
"integrity": "sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
},
"is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
@@ -11299,6 +11440,21 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -12047,6 +12203,11 @@
}
}
},
"p-map": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -12238,9 +12399,9 @@
}
},
"postcss-selector-parser": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
"integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz",
"integrity": "sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
@@ -12556,6 +12717,24 @@
"picomatch": "^2.2.1"
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
@@ -12932,6 +13111,11 @@
}
}
},
"standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -13181,9 +13365,9 @@
}
},
"tailwindcss": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.5.tgz",
"integrity": "sha512-59pNgzx2o+wkAk7IZGIH7H9eNS53gzZGrO3+NPyOEWHDbquHgiLL/c993T5t1vPSAeBxox4X5OgZwNuRvXVf+g==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.8.tgz",
"integrity": "sha512-Yww1eRYO1AxITJmW/KduZPxNvYdHuedeKwPju9Oakp7MdiixRi5xkpLhirsc81QCxHL0eoce6qKmxXwYGt4Cjw==",
"dev": true,
"requires": {
"arg": "^5.0.1",
@@ -13202,7 +13386,7 @@
"postcss-js": "^3.0.3",
"postcss-load-config": "^3.1.0",
"postcss-nested": "5.0.6",
"postcss-selector-parser": "^6.0.6",
"postcss-selector-parser": "^6.0.7",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
"resolve": "^1.20.0",
+5 -3
View File
@@ -18,14 +18,15 @@
"axios": "^0.21.1",
"crypto-js": "^4.1.1",
"emoji-regex": "^9.2.2",
"ioredis": "^4.28.2",
"jszip": "^3.7.1",
"next": "^12.0.7",
"nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2",
"prismjs": "^1.23.0",
"react": "17.0.2",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.0.0",
"react-markdown": "^6.0.2",
"react-player": "^2.9.0",
@@ -40,6 +41,7 @@
},
"devDependencies": {
"@types/crypto-js": "^4.0.2",
"@types/ioredis": "^4.28.5",
"@types/prismjs": "^1.16.5",
"@types/react": "17.0.11",
"@types/react-copy-to-clipboard": "^5.0.0",
@@ -50,7 +52,7 @@
"eslint": "7.29.0",
"eslint-config-next": "11.0.0",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.5",
"tailwindcss": "^3.0.8",
"typescript": "4.3.4"
}
}
+11 -1
View File
@@ -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) {
+174 -126
View File
@@ -1,12 +1,17 @@
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 apiConfig from '../../config/api.json'
import siteConfig from '../../config/site.json'
import { revealObfuscatedToken } from '../../utils/oAuthHandler'
import { compareHashedToken } from '../../utils/protectedRouteHandler'
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
const basePath = pathPosix.resolve('/', apiConfig.base)
const encodePath = (path: string) => {
let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path))
if (encodedPath === '/' || encodedPath === '') {
@@ -16,19 +21,27 @@ const encodePath = (path: string) => {
return `:${encodeURIComponent(encodedPath)}`
}
// Store access token in memory, cuz Vercel doesn't provide key-value storage natively
let _access_token = ''
const getAccessToken = async () => {
if (_access_token) {
console.log('Fetch token from memory.')
return _access_token
async function getAccessToken(): Promise<any> {
const { accessToken, refreshToken } = await getOdAuthTokens()
// Return in storage access token if it is still valid
if (typeof accessToken === 'string') {
console.log('Fetch access token from storage.')
return accessToken
}
// Return empty string if no refresh token is stored, which requires the application to be re-authenticated
if (typeof refreshToken !== 'string') {
console.log('No refresh token, return empty access token.')
return ''
}
// Fetch new access token with in storage refresh token
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', refreshToken)
body.append('grant_type', 'refresh_token')
const resp = await axios.post(apiConfig.authApi, body, {
@@ -37,132 +50,167 @@ const getAccessToken = async () => {
},
})
if (resp.data.access_token) {
_access_token = resp.data.access_token
return _access_token
if ('access_token' in resp.data && 'refresh_token' in resp.data) {
const { expires_in, access_token, refresh_token } = resp.data
await storeOdAuthTokens({
accessToken: access_token,
accessTokenExpiry: parseInt(expires_in),
refreshToken: refresh_token,
})
console.log('Fetch new access token with stored refresh token.')
return access_token
}
return ''
}
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 (req.method === 'POST') {
const { obfuscatedAccessToken, accessTokenExpiry, obfuscatedRefreshToken } = req.body
const accessToken = revealObfuscatedToken(obfuscatedAccessToken)
const refreshToken = revealObfuscatedToken(obfuscatedRefreshToken)
if (typeof accessToken !== 'string' || typeof refreshToken !== 'string') {
res.status(400).send('Invalid request body')
return
}
await storeOdAuthTokens({
accessToken,
accessTokenExpiry,
refreshToken,
})
res.status(200).send('OK')
return
}
// If method is GET, then the API is a normal request to the OneDrive API for files or folders
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
}
@@ -0,0 +1,136 @@
import Head from 'next/head'
import Image from 'next/image'
import { useRouter } 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() {
const router = useRouter()
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">
<div className="mx-auto w-52">
<Image src="/images/fabulous-fireworks.png" width={912} height={912} alt="fabulous fireworks" />
</div>
<h3 className="font-medium text-xl mb-4 text-center">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="font-medium text-lg mt-4 mb-2">Step 1/3: Preparations</h3>
<p className="py-1 text-yellow-400 font-medium text-sm">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1" /> If you have not specified a REDIS_URL
inside your Vercel env variable, go initialise one at{' '}
<a href="https://upstash.com/" target="_blank" rel="noopener noreferrer" className="underline">
Upstash
</a>
. Docs:{' '}
<a
href="https://docs.upstash.com/redis/howto/vercelintegration"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Vercel Integration - Upstash
</a>
.
</p>
<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. Check the following configurations before proceeding with authorising
onedrive-vercel-index 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 font-medium text-sm">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1 text-yellow-400" /> If you see anything
missing or incorrect, you need to reconfigure <code className="text-xs font-mono">/config/api.json</code>{' '}
and redeploy this instance.
</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,130 @@
import Head from 'next/head'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import siteConfig from '../../config/site.json'
import Navbar from '../../components/Navbar'
import Footer from '../../components/Footer'
import { LoadingIcon } from '../../components/Loading'
import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/oAuthHandler'
export default function OAuthStep2() {
const router = useRouter()
const [oAuthRedirectedUrl, setOAuthRedirectedUrl] = useState('')
const [authCode, setAuthCode] = useState('')
const [buttonLoading, setButtonLoading] = useState(false)
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">
<div className="mx-auto w-52">
<Image
src="/images/fabulous-come-back-later.png"
width={912}
height={912}
alt="fabulous come back later"
/>
</div>
<h3 className="font-medium text-xl mb-4 text-center">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="font-medium text-lg mt-4 mb-2">Step 2/3: Get authorisation code</h3>
<p className="py-1 text-red-400 font-medium text-sm">
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" /> If you are not the owner of this website,
stop now, as continuing with this process may expose your personal files in OneDrive.
</p>
<div
className="relative my-2 font-mono border border-gray-500/50 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">
The OAuth link for getting the authorisation code has been created. Click on the link above 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. After logging in and authenticating with your
Microsoft account, you will be redirected to a blank page on localhost. Paste{' '}
<b className="underline decoration-wavy decoration-teal-500">the entire redirected URL</b> down below.
</p>
<div className="my-4 rounded overflow-hidden w-2/3 mx-auto">
<Image src="/images/step-2-screenshot.png" width={1466} height={607} alt="step 2 screenshot" />
</div>
<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={() => {
setButtonLoading(true)
router.push({ pathname: '/onedrive-vercel-index-oauth/step-3', query: { authCode } })
}}
>
{buttonLoading ? (
<>
<span>Requesting tokens</span> <LoadingIcon className="animate-spin w-4 h-4 ml-1 inline" />
</>
) : (
<>
<span>Get tokens</span> <FontAwesomeIcon icon="arrow-right" />
</>
)}
</button>
</div>
</div>
</div>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,218 @@
import axios from 'axios'
import Head from 'next/head'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import siteConfig from '../../config/site.json'
import Navbar from '../../components/Navbar'
import Footer from '../../components/Footer'
import { obfuscateToken, requestTokenWithAuthCode } from '../../utils/oAuthHandler'
import { LoadingIcon } from '../../components/Loading'
export default function OAuthStep3({ accessToken, expiryTime, refreshToken, error, description, errorUri }) {
const router = useRouter()
const [expiryTimeLeft, setExpiryTimeLeft] = useState(expiryTime)
useEffect(() => {
if (!expiryTimeLeft) return
const intervalId = setInterval(() => {
setExpiryTimeLeft(expiryTimeLeft - 1)
}, 1000)
return () => clearInterval(intervalId)
}, [expiryTimeLeft])
const [buttonContent, setButtonContent] = useState(
<>
<span>Store tokens</span> <FontAwesomeIcon icon="key" />
</>
)
const sendAuthTokensToServer = async () => {
setButtonContent(
<>
<span>Storing tokens</span> <LoadingIcon className="animate-spin w-4 h-4 ml-1 inline" />
</>
)
await axios
.post(
'/api',
{
obfuscatedAccessToken: obfuscateToken(accessToken),
accessTokenExpiry: parseInt(expiryTime),
obfuscatedRefreshToken: obfuscateToken(refreshToken),
},
{
headers: {
'Content-Type': 'application/json',
},
}
)
.then(_ => {
setButtonContent(
<>
<span>Stored! Going home...</span> <FontAwesomeIcon icon="check" />
</>
)
setTimeout(() => {
router.push('/')
}, 2000)
})
.catch(_ => {
setButtonContent(
<>
<span>Error storing the token</span> <FontAwesomeIcon icon="exclamation-circle" />
</>
)
})
}
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">
<div className="mx-auto w-52">
<Image src="/images/fabulous-celebration.png" width={912} height={912} alt="fabulous celebration" />
</div>
<h3 className="font-medium text-xl mb-4 text-center">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="font-medium text-lg mt-4 mb-2">Step 3/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, 60)}...`}</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, 60)}...`}</code>
</span>
</li>
)}
</ol>
<p className="py-1 font-medium text-sm text-teal-500">
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" /> These tokens may take a few seconds to
populate after you click the button below. If you go back home and still see the welcome page telling
you to re-authenticate, revisit home and do a hard refresh.
</p>
<p className="py-1">
Final step, click the button below to store these tokens persistently before they expire after{' '}
{Math.floor(expiryTimeLeft / 60)} minutes {expiryTimeLeft - Math.floor(expiryTimeLeft / 60) * 60}{' '}
seconds. Don&apos;t worry, after storing them, onedrive-vercel-index will take care of token refreshes
and updates after your site goes live.
</p>
<div className="text-right mb-2 mt-6">
<button
className="text-white bg-gradient-to-br from-green-500 to-teal-300 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={sendAuthTokensToServer}
>
{buttonContent}
</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,
},
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

+3 -1
View File
@@ -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: {
+84
View File
@@ -0,0 +1,84 @@
import axios from 'axios'
import CryptoJS from 'crypto-js'
import apiConfig from '../config/api.json'
// Just a disguise to obfuscate required tokens (including but not limited to client secret,
// access tokens, and refresh tokens), used along with the following two functions
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)
}
// Generate the Microsoft OAuth 2.0 authorization URL, used for requesting the authorisation code
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()}`
}
// The code returned from the Microsoft OAuth 2.0 authorization URL is a request URL with hostname
// http://localhost and URL parameter code. This function extracts the code from the request URL
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') || ''
}
// After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL
// will be used to request an access token. This function requests the access token with the authorisation code
// and returns the access token and refresh token on success.
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 }
})
}
+28
View File
@@ -0,0 +1,28 @@
import Redis from 'ioredis'
// Persistent key-value store is provided by Redis, hosted on Upstash
// https://vercel.com/integrations/upstash
const kv = new Redis(process.env.REDIS_URL)
export async function getOdAuthTokens(): Promise<{ accessToken: unknown; refreshToken: unknown }> {
const accessToken = await kv.get('access_token')
const refreshToken = await kv.get('refresh_token')
return {
accessToken,
refreshToken,
}
}
export async function storeOdAuthTokens({
accessToken,
accessTokenExpiry,
refreshToken,
}: {
accessToken: string
accessTokenExpiry: number
refreshToken: string
}): Promise<void> {
await kv.set('access_token', accessToken, 'ex', accessTokenExpiry)
await kv.set('refresh_token', refreshToken)
}