Merge pull request #210 from spencerwooo/public-client-id-secret
OAuth inside main project with public client ID and secret
@@ -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`:
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
{
|
||||
"icon": "/icons/128.png",
|
||||
"title": "Spencer's OneDrive",
|
||||
"baseDirectory": "/Public",
|
||||
"maxItems": 100,
|
||||
"googleFontSans": "Inter",
|
||||
"googleFontMono": "Fira Mono",
|
||||
|
||||
@@ -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/,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'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'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'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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 647 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 327 KiB |
@@ -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: {
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||