diff --git a/README.md b/README.md
index 7fdbe1f..2211f0c 100644
--- a/README.md
+++ b/README.md
@@ -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`:
diff --git a/components/Auth.tsx b/components/Auth.tsx
index ba4b4d4..ce1b18f 100644
--- a/components/Auth.tsx
+++ b/components/Auth.tsx
@@ -17,7 +17,7 @@ const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
return (
-
+
Enter Password
diff --git a/components/FileListing.tsx b/components/FileListing.tsx
index 74a25d6..889a282 100644
--- a/components/FileListing.tsx
+++ b/components/FileListing.tsx
@@ -179,6 +179,14 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
if (error) {
+ console.log(error)
+
+ // If error includes 403 which means the user has not completed initial setup, redirect to OAuth page
+ if (error.message.includes('403')) {
+ router.push('/onedrive-vercel-index-oauth/step-1')
+ return
+ }
+
return (
+ Authorisation is required as no valid{' '}
+ access_token or{' '}
+ refresh_token is
+ present on this deployed instance. Check the following configurations before proceeding with authorising
+ onedrive-vercel-index with your own Microsoft account.
+
+
+
+
+
+
+
+ CLIENT_ID
+
+
+ {apiConfig.clientId}
+
+
+
+
+ CLIENT_SECRET*
+
+
+ {apiConfig.obfuscatedClientSecret}
+
+
+
+
+ REDIRECT_URI
+
+
+ {apiConfig.redirectUri}
+
+
+
+
+ Auth API URL
+
+
+ {apiConfig.authApi}
+
+
+
+
+ Drive API URL
+
+
+ {apiConfig.driveApi}
+
+
+
+
+ API Scope
+
+
+ {apiConfig.scope}
+
+
+
+
+
+
+
+ If you see anything
+ missing or incorrect, you need to reconfigure /config/api.json{' '}
+ and redeploy this instance.
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/pages/onedrive-vercel-index-oauth/step-2.tsx b/pages/onedrive-vercel-index-oauth/step-2.tsx
new file mode 100644
index 0000000..752f2dc
--- /dev/null
+++ b/pages/onedrive-vercel-index-oauth/step-2.tsx
@@ -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 (
+
+ If you are not the owner of this website,
+ stop now, as continuing with this process may expose your personal files in OneDrive.
+
+
+
{
+ window.open(oAuthUrl)
+ }}
+ >
+
+
+
+
+ {oAuthUrl}
+
+
+
+
+ The OAuth link for getting the authorisation code has been created. Click on the link above to get the{' '}
+ authorisation code. 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{' '}
+ the entire redirected URL down below.
+
+ 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.
+
+
+ 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.
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+export async function getServerSideProps({ query }) {
+ const { authCode } = query
+
+ // Return if no auth code is present
+ if (!authCode) {
+ return {
+ props: {
+ error: 'No auth code present',
+ description: 'Where is the auth code? Did you follow step 2 you silly donut?',
+ },
+ }
+ }
+
+ const response = await requestTokenWithAuthCode(authCode)
+
+ // If error response, return invalid
+ if ('error' in response) {
+ return {
+ props: {
+ error: response.error,
+ description: response.errorDescription,
+ errorUri: response.errorUri,
+ },
+ }
+ }
+
+ const { expiryTime, accessToken, refreshToken } = response
+
+ return {
+ props: {
+ error: null,
+ expiryTime,
+ accessToken,
+ refreshToken,
+ },
+ }
+}
diff --git a/public/images/empty.png b/public/images/empty.png
deleted file mode 100644
index 04accb9..0000000
Binary files a/public/images/empty.png and /dev/null differ
diff --git a/public/images/fabulous-celebration.png b/public/images/fabulous-celebration.png
new file mode 100644
index 0000000..a3e0f39
Binary files /dev/null and b/public/images/fabulous-celebration.png differ
diff --git a/public/images/fabulous-come-back-later.png b/public/images/fabulous-come-back-later.png
new file mode 100644
index 0000000..726bb9d
Binary files /dev/null and b/public/images/fabulous-come-back-later.png differ
diff --git a/public/images/fabulous-fireworks.png b/public/images/fabulous-fireworks.png
new file mode 100644
index 0000000..8ab46e4
Binary files /dev/null and b/public/images/fabulous-fireworks.png differ
diff --git a/public/images/fabulous-page-not-found.png b/public/images/fabulous-page-not-found.png
new file mode 100644
index 0000000..6266a49
Binary files /dev/null and b/public/images/fabulous-page-not-found.png differ
diff --git a/public/images/fabulous-rip-2.png b/public/images/fabulous-rip-2.png
new file mode 100644
index 0000000..655fd06
Binary files /dev/null and b/public/images/fabulous-rip-2.png differ
diff --git a/public/images/fabulous-wapmire-weekdays.png b/public/images/fabulous-wapmire-weekdays.png
new file mode 100644
index 0000000..b0d8c35
Binary files /dev/null and b/public/images/fabulous-wapmire-weekdays.png differ
diff --git a/public/images/no-looking.png b/public/images/no-looking.png
deleted file mode 100644
index 740051d..0000000
Binary files a/public/images/no-looking.png and /dev/null differ
diff --git a/public/images/step-2-screenshot.png b/public/images/step-2-screenshot.png
new file mode 100644
index 0000000..7037417
Binary files /dev/null and b/public/images/step-2-screenshot.png differ
diff --git a/tailwind.config.js b/tailwind.config.js
index 2123b73..c88d993 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -19,7 +19,9 @@ module.exports = {
indigo: colors.indigo,
purple: colors.purple,
pink: colors.pink,
- teal: colors.teal
+ teal: colors.teal,
+ cyan: colors.cyan,
+ orange: colors.orange,
},
extend: {
fontFamily: {
diff --git a/utils/oAuthHandler.ts b/utils/oAuthHandler.ts
new file mode 100644
index 0000000..8b8202b
--- /dev/null
+++ b/utils/oAuthHandler.ts
@@ -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 }
+ })
+}
diff --git a/utils/odAuthTokenStore.ts b/utils/odAuthTokenStore.ts
new file mode 100644
index 0000000..fc98391
--- /dev/null
+++ b/utils/odAuthTokenStore.ts
@@ -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 {
+ await kv.set('access_token', accessToken, 'ex', accessTokenExpiry)
+ await kv.set('refresh_token', refreshToken)
+}