diff --git a/components/Auth.tsx b/components/Auth.tsx
new file mode 100644
index 0000000..e3df505
--- /dev/null
+++ b/components/Auth.tsx
@@ -0,0 +1,39 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+
+import { FunctionComponent, useState } from 'react'
+import Image from 'next/image'
+import { useRouter } from 'next/router'
+
+import { matchProtectedRoute } from '../utils/tools'
+import useLocalStorage from '../utils/useLocalStorage'
+
+const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
+ const authTokenPath = matchProtectedRoute(redirect)
+
+ const router = useRouter()
+ const [token, setToken] = useLocalStorage(authTokenPath, '')
+
+ return (
+
+
Enter Password
+
{
+ setToken(e.target.value)
+ }}
+ />
+
+
+ )
+}
+
+export default Auth
diff --git a/components/FileListing.tsx b/components/FileListing.tsx
index bf413a5..8c0c2f3 100644
--- a/components/FileListing.tsx
+++ b/components/FileListing.tsx
@@ -17,6 +17,7 @@ import { VideoPreview } from './previews/VideoPreview'
import { AudioPreview } from './previews/AudioPreview'
import Loading from './Loading'
import FourOhFour from './FourOhFour'
+import Auth from './Auth'
import TextPreview from './previews/TextPreview'
import MarkdownPreview from './previews/MarkdownPreview'
import CodePreview from './previews/CodePreview'
@@ -105,12 +106,12 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
const path = queryToPath(query)
- const { data, error } = useStaleSWR(`/api?path=${path}`)
+ const { data, error } = useStaleSWR(`/api?path=${path}`, path)
if (error) {
return (
-
+ {error.message.includes('401') ?
:
}
)
}
diff --git a/config/site.json b/config/site.json
index a6b64a9..3d06b69 100644
--- a/config/site.json
+++ b/config/site.json
@@ -1,4 +1,8 @@
{
"title": "Spencer's OneDrive Index",
- "footer": "Powered by onedrive-vercel-index. Made with ❤ by SpencerWoo."
+ "footer": "Powered by onedrive-vercel-index. Made with ❤ by SpencerWoo.",
+ "protectedRoutes": [
+ "/🌞 Private folder/u-need-a-password",
+ "/🌞 Private folder/this-is-public"
+ ]
}
diff --git a/pages/api/index.ts b/pages/api/index.ts
index 9c96111..56f4634 100644
--- a/pages/api/index.ts
+++ b/pages/api/index.ts
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import { posix as pathPosix } from 'path'
import apiConfig from '../../config/api.json'
+import siteConfig from '../../config/site.json'
const basePath = pathPosix.resolve('/', apiConfig.base)
const encodePath = (path: string) => {
@@ -49,6 +50,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
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'])
+ if (req.headers['od-protected-token'] !== odProtectedToken.data) {
+ res.status(401).json({ error: 'Password required for this folder.' })
+ return
+ }
+ } catch (error) {
+ // 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
+ }
+ }
+
+ // Handle response from OneDrive API
const requestUrl = `${apiConfig.driveApi}/root${encodePath(path)}`
const { data } = await axios.get(requestUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
diff --git a/utils/tools.ts b/utils/tools.ts
index 47fb184..53118ae 100644
--- a/utils/tools.ts
+++ b/utils/tools.ts
@@ -1,5 +1,7 @@
-import useSWR, { cache, Key } from 'swr'
import axios from 'axios'
+import useSWR, { cache, Key } from 'swr'
+
+import siteConfig from '../config/site.json'
/**
* Extract the current web page's base url
@@ -13,19 +15,46 @@ export const getBaseUrl = () => {
}
// Common axios fetch function
-const fetcher = (url: string) => axios.get(url).then(res => res.data)
+const fetcher = (url: string, token: string) =>
+ axios
+ .get(url, {
+ headers: { 'od-protected-token': token },
+ })
+ .then(res => res.data)
/**
* Use stale SWR instead of revalidating on each request. Not ideal for this scenario but have to do
* if fetching serverside props from component instead of pages.
- * @param dataKey request url
+ * @param url request url
* @returns useSWR instance
*/
-export const useStaleSWR = (dataKey: Key) => {
+export const useStaleSWR = (url: Key, path: string) => {
const revalidationOptions = {
- revalidateOnMount: !cache.has(dataKey),
+ revalidateOnMount: !cache.has(url),
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
- return useSWR(dataKey, fetcher, revalidationOptions)
+ const token =
+ typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : ''
+
+ return useSWR([url, token], fetcher, revalidationOptions)
+}
+
+export const matchProtectedRoute = (route: string) => {
+ const protectedRoutes = siteConfig.protectedRoutes
+ let authTokenPath = ''
+ for (const r of protectedRoutes) {
+ if (
+ route.startsWith(
+ r
+ .split('/')
+ .map(p => encodeURIComponent(p))
+ .join('/')
+ )
+ ) {
+ authTokenPath = r
+ break
+ }
+ }
+ return authTokenPath
}
diff --git a/utils/useLocalStorage.ts b/utils/useLocalStorage.ts
new file mode 100644
index 0000000..b137b2f
--- /dev/null
+++ b/utils/useLocalStorage.ts
@@ -0,0 +1,78 @@
+import { Dispatch, SetStateAction, useEffect, useState } from 'react'
+
+type SetValue = Dispatch>
+
+function useLocalStorage(key: string, initialValue: T): [T, SetValue] {
+ // Get from local storage then
+ // parse stored json or return initialValue
+ const readValue = (): T => {
+ // Prevent build error "window is undefined" but keep keep working
+ if (typeof window === 'undefined') {
+ return initialValue
+ }
+
+ try {
+ const item = window.localStorage.getItem(key)
+ return item ? (JSON.parse(item) as T) : initialValue
+ } catch (error) {
+ console.warn(`Error reading localStorage key “${key}”:`, error)
+ return initialValue
+ }
+ }
+
+ // State to store our value
+ // Pass initial state function to useState so logic is only executed once
+ const [storedValue, setStoredValue] = useState(readValue)
+
+ // Return a wrapped version of useState's setter function that ...
+ // ... persists the new value to localStorage.
+ const setValue: SetValue = value => {
+ // Prevent build error "window is undefined" but keeps working
+ if (typeof window == 'undefined') {
+ console.warn(`Tried setting localStorage key “${key}” even though environment is not a client`)
+ }
+
+ try {
+ // Allow value to be a function so we have the same API as useState
+ const newValue = value instanceof Function ? value(storedValue) : value
+
+ // Save to local storage
+ window.localStorage.setItem(key, JSON.stringify(newValue))
+
+ // Save state
+ setStoredValue(newValue)
+
+ // We dispatch a custom event so every useLocalStorage hook are notified
+ window.dispatchEvent(new Event('local-storage'))
+ } catch (error) {
+ console.warn(`Error setting localStorage key “${key}”:`, error)
+ }
+ }
+
+ useEffect(() => {
+ setStoredValue(readValue())
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ useEffect(() => {
+ const handleStorageChange = () => {
+ setStoredValue(readValue())
+ }
+
+ // this only works for other documents, not the current one
+ window.addEventListener('storage', handleStorageChange)
+
+ // this is a custom event, triggered in writeValueToLocalStorage
+ window.addEventListener('local-storage', handleStorageChange)
+
+ return () => {
+ window.removeEventListener('storage', handleStorageChange)
+ window.removeEventListener('local-storage', handleStorageChange)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return [storedValue, setValue]
+}
+
+export default useLocalStorage