mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
store access token and refresh token in serverless mem
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
"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",
|
||||
"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",
|
||||
|
||||
+29
-7
@@ -5,11 +5,14 @@ import axios from 'axios'
|
||||
|
||||
import apiConfig from '../../config/api.json'
|
||||
import siteConfig from '../../config/site.json'
|
||||
import { revealObfuscatedToken } from '../../utils/accessTokenHandler'
|
||||
import { revealObfuscatedToken } from '../../utils/oAuthHandler'
|
||||
import { compareHashedToken } from '../../utils/protectedRouteHandler'
|
||||
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
|
||||
import TokenStore from '../../utils/odAuthTokenStore'
|
||||
|
||||
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
||||
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
||||
const tokenStore = new TokenStore()
|
||||
|
||||
const basePath = pathPosix.resolve('/', apiConfig.base)
|
||||
const encodePath = (path: string) => {
|
||||
let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path))
|
||||
if (encodedPath === '/' || encodedPath === '') {
|
||||
@@ -19,10 +22,8 @@ const encodePath = (path: string) => {
|
||||
return `:${encodeURIComponent(encodedPath)}`
|
||||
}
|
||||
|
||||
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
||||
|
||||
async function getAccessToken(): Promise<any> {
|
||||
const { accessToken, refreshToken } = await getOdAuthTokens()
|
||||
const { accessToken, refreshToken } = await tokenStore.getOdAuthTokens()
|
||||
|
||||
// Return in storage access token if it is still valid
|
||||
if (typeof accessToken === 'string') {
|
||||
@@ -52,7 +53,7 @@ async function getAccessToken(): Promise<any> {
|
||||
|
||||
if ('access_token' in resp.data && 'refresh_token' in resp.data) {
|
||||
const { expires_in, access_token, refresh_token } = resp.data
|
||||
await storeOdAuthTokens({
|
||||
await tokenStore.storeOdAuthTokens({
|
||||
accessToken: access_token,
|
||||
accessTokenExpiry: parseInt(expires_in),
|
||||
refreshToken: refresh_token,
|
||||
@@ -65,6 +66,27 @@ async function getAccessToken(): Promise<any> {
|
||||
}
|
||||
|
||||
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 tokenStore.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
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function OAuthStep1() {
|
||||
present on this deployed instance.
|
||||
</p>
|
||||
|
||||
<h3 className="font-medium text-lg mt-4 mb-2">Step 1: Preparations</h3>
|
||||
<h3 className="font-medium text-lg mt-4 mb-2">Step 1/3: Preparations</h3>
|
||||
<p className="py-1">
|
||||
Check the following configurations (especially <code className="text-sm font-mono">client_id</code> and{' '}
|
||||
<code className="text-sm font-mono">client_secret</code> (obfuscated)) and see if they match the official
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import Head from 'next/head'
|
||||
import router 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/accessTokenHandler'
|
||||
import { LoadingIcon } from '../../components/Loading'
|
||||
import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/oAuthHandler'
|
||||
|
||||
export default function OAuthStep2() {
|
||||
const [oAuthRedirectedUrl, setOAuthRedirectedUrl] = useState('')
|
||||
const [authCode, setAuthCode] = useState('')
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
|
||||
const oAuthUrl = generateAuthorisationUrl()
|
||||
|
||||
return (
|
||||
@@ -33,10 +36,10 @@ export default function OAuthStep2() {
|
||||
present on this deployed instance.
|
||||
</p>
|
||||
|
||||
<h3 className="font-medium text-lg mt-4 mb-2">Step 2: Get authorisation code</h3>
|
||||
<h3 className="font-medium text-lg mt-4 mb-2">Step 2/3: Get authorisation code</h3>
|
||||
<p className="py-1">The OAuth URL has been generated for you:</p>
|
||||
<div
|
||||
className="relative my-2 font-mono border border-gray-400/20 rounded text-sm bg-gray-50 dark:bg-gray-800 cursor-pointer hover:opacity-80"
|
||||
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)
|
||||
}}
|
||||
@@ -87,10 +90,19 @@ export default function OAuthStep2() {
|
||||
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>
|
||||
|
||||
@@ -6,8 +6,8 @@ import siteConfig from '../../config/site.json'
|
||||
import Navbar from '../../components/Navbar'
|
||||
import Footer from '../../components/Footer'
|
||||
|
||||
import { requestTokenWithAuthCode } from '../../utils/accessTokenHandler'
|
||||
import { storeOdAuthTokens } from '../../utils/odAuthTokenStore'
|
||||
import { obfuscateToken, requestTokenWithAuthCode } from '../../utils/oAuthHandler'
|
||||
import axios from 'axios'
|
||||
|
||||
export default function OAuthStep3({ accessToken, refreshToken, error, description, errorUri }) {
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ export default function OAuthStep3({ accessToken, refreshToken, error, descripti
|
||||
present on this deployed instance.
|
||||
</p>
|
||||
|
||||
<h3 className="font-medium text-lg mt-4 mb-2">Step 3: Get access and refresh tokens</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">
|
||||
@@ -140,8 +140,15 @@ export async function getServerSideProps({ query }) {
|
||||
|
||||
const { expiryTime, accessToken, refreshToken } = response
|
||||
|
||||
// We can safely leverage Vercel's /tmp directory, and persist the tokens with file-system based KV storage
|
||||
await storeOdAuthTokens({ accessToken, accessTokenExpiry: parseInt(expiryTime), refreshToken })
|
||||
// await tokenStore.storeOdAuthTokens({ accessToken, accessTokenExpiry: parseInt(expiryTime), refreshToken })
|
||||
|
||||
// We perform a POST request to the default API route to store the tokens inside the main route memory
|
||||
// This is a bit of a hack, but it's the only way to get the tokens to the main route
|
||||
await axios.post('/api', {
|
||||
obfuscatedAccessToken: obfuscateToken(accessToken),
|
||||
accessTokenExpiry: parseInt(expiryTime),
|
||||
obfuscatedRefreshToken: obfuscateToken(refreshToken),
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -3,9 +3,10 @@ import CryptoJS from 'crypto-js'
|
||||
|
||||
import apiConfig from '../config/api.json'
|
||||
|
||||
// Just a disguise to obfuscate the client secret, used along with the following two functions
|
||||
// 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'
|
||||
function obfuscateToken(token: string): string {
|
||||
export function obfuscateToken(token: string): string {
|
||||
// Encrypt token with AES
|
||||
const encrypted = CryptoJS.AES.encrypt(token, AES_SECRET_KEY)
|
||||
return encrypted.toString()
|
||||
+24
-16
@@ -1,20 +1,37 @@
|
||||
// This should be only used on the server side, where the tokens are stored with KV store using
|
||||
// a file system based storage. The tokens are stored in the file system as JSON at /tmp path.
|
||||
import os from 'os'
|
||||
// import os from 'os'
|
||||
|
||||
import Keyv from 'keyv'
|
||||
// import { KeyvFile } from 'keyv-file'
|
||||
|
||||
console.log(`${os.tmpdir()}/od-auth-token.json`)
|
||||
// console.log(`${os.tmpdir()}/od-auth-token.json`)
|
||||
|
||||
// const kv = new Keyv({
|
||||
export default class TokenStore {
|
||||
kv: Keyv
|
||||
|
||||
constructor() {
|
||||
this.kv = new Keyv()
|
||||
|
||||
// this.kv = new Keyv({
|
||||
// store: new KeyvFile({
|
||||
// filename: `${os.tmpdir()}/od-auth-token.json`,
|
||||
// }),
|
||||
// })
|
||||
const kv = new Keyv({})
|
||||
console.log('TokenStore constructor')
|
||||
}
|
||||
|
||||
export async function storeOdAuthTokens({
|
||||
async getOdAuthTokens(): Promise<{ accessToken: unknown; refreshToken: unknown }> {
|
||||
const accessToken = await this.kv.get('access_token')
|
||||
const refreshToken = await this.kv.get('refresh_token')
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
}
|
||||
}
|
||||
|
||||
async storeOdAuthTokens({
|
||||
accessToken,
|
||||
accessTokenExpiry,
|
||||
refreshToken,
|
||||
@@ -23,16 +40,7 @@ export async function storeOdAuthTokens({
|
||||
accessTokenExpiry: number
|
||||
refreshToken: string
|
||||
}): Promise<void> {
|
||||
await kv.set('access_token', accessToken, accessTokenExpiry)
|
||||
await kv.set('refresh_token', refreshToken)
|
||||
}
|
||||
|
||||
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,
|
||||
await this.kv.set('access_token', accessToken, accessTokenExpiry)
|
||||
await this.kv.set('refresh_token', refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user