diff --git a/README.md b/README.md index 8331792..a998665 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Read this in other languages: [Español](READMEes.md), [Português](READMEpt.md) ## Features +- PWA support (install this app on your phone) - [Email Explorer](https://r2explorer.dev/guides/setup-email-explorer/) (using Cloudflare Email Routing) - [Basic Auth](https://r2explorer.dev/getting-started/security/#basic-auth) - [Cloudflare Access Authentication](https://r2explorer.dev/getting-started/security/) diff --git a/packages/dashboard/src/components/BucketExplorerWrapper.vue b/packages/dashboard/src/components/BucketExplorerWrapper.vue index d78d950..719b747 100644 --- a/packages/dashboard/src/components/BucketExplorerWrapper.vue +++ b/packages/dashboard/src/components/BucketExplorerWrapper.vue @@ -29,27 +29,29 @@ diff --git a/packages/dashboard/src/components/storage/Folders.vue b/packages/dashboard/src/components/storage/Folders.vue index 317e148..cd0fbba 100644 --- a/packages/dashboard/src/components/storage/Folders.vue +++ b/packages/dashboard/src/components/storage/Folders.vue @@ -50,7 +50,7 @@ export default { this.$store.dispatch('navigate', folder.Prefix) } }, - created () { + mounted () { this.$watch( () => this.$route.params.folder, (newFolder, oldFolder) => { diff --git a/packages/dashboard/src/registerServiceWorker.js b/packages/dashboard/src/registerServiceWorker.js index 76cede0..93ec9d7 100644 --- a/packages/dashboard/src/registerServiceWorker.js +++ b/packages/dashboard/src/registerServiceWorker.js @@ -20,7 +20,8 @@ if (process.env.NODE_ENV === 'production') { console.log('New content is downloading.') }, updated () { - console.log('New content is available; please refresh.') + console.log('New content is available; refreshing...') + window.location.reload(true) }, offline () { console.log('No internet connection found. App is running in offline mode.') diff --git a/packages/dashboard/src/store/index.js b/packages/dashboard/src/store/index.js index a603d3b..d849c1a 100644 --- a/packages/dashboard/src/store/index.js +++ b/packages/dashboard/src/store/index.js @@ -1,16 +1,16 @@ -import { createStore } from "vuex"; -import axios from "axios"; -import repo from "@/api"; -import router from "@/router"; +import { createStore } from 'vuex' +import axios from 'axios' +import repo from '@/api' +import router from '@/router' export default createStore({ state: { user: { - username: "test@example.com" + username: 'test@example.com' }, config: {}, activeBucket: null, - currentFolder: "", + currentFolder: '', files: [], folders: [], buckets: [], @@ -20,123 +20,129 @@ export default createStore({ mobileSidebar: false, serverVersion: null, serverVersionInt: 0, - activeTab: "storage", + activeTab: 'storage', serverUrl: null, - loginMethod: null, + loginMethod: null }, getters: {}, mutations: { - setServerUrl(state, serverUrl) { - state.serverUrl = serverUrl; + setServerUrl (state, serverUrl) { + state.serverUrl = serverUrl }, - loadObjects(state, payload) { - state.files = payload.files; - state.folders = payload.folders; + loadObjects (state, payload) { + state.files = payload.files + state.folders = payload.folders }, - toggleMobileSidebar(state, payload) { + toggleMobileSidebar (state, payload) { if (payload !== true && payload !== false) { - state.mobileSidebar = !state.mobileSidebar; + state.mobileSidebar = !state.mobileSidebar } else { - state.mobileSidebar = payload; + state.mobileSidebar = payload } }, - changeBucket(state, payload) { - state.activeBucket = payload; - if (this.state.activeTab === "email") { - state.currentFolder = "inbox"; + changeBucket (state, payload) { + state.activeBucket = payload + if (this.state.activeTab === 'email') { + state.currentFolder = 'inbox' } else { - state.currentFolder = ""; + state.currentFolder = '' } }, - changeTab(state, payload) { + changeTab (state, payload) { if (payload === state.activeTab) { - return; + return } - state.activeTab = payload; - state.currentFolder = ""; + state.activeTab = payload + state.currentFolder = '' // state.files = [] // state.folders = [] }, - goTo(state, folder) { - state.currentFolder = folder; + goTo (state, folder) { + state.currentFolder = folder }, - loadUserDisks(state, data) { - state.buckets = data.buckets; + loadUserDisks (state, data) { + state.buckets = data.buckets if ((state.activeBucket === null && data.buckets.length > 0) || router.currentRoute.value.href.startsWith('/auth')) { const targetView = (location.pathname.startsWith('/email')) ? 'email-home' : 'storage-home' - router.push({ name: targetView, params: { bucket: data.buckets[0].name } }); + const lastOpenTab = localStorage.getItem('lastOpenTab') + if (lastOpenTab && location.pathname === '/') { + router.push(JSON.parse(lastOpenTab)) + return + } + + router.push({ name: targetView, params: { bucket: data.buckets[0].name } }) } }, - loadServerConfigs(state, data) { - state.user = data.user; - state.config = data.config; - state.serverVersion = data.version; - state.serverVersionInt = parseInt(data.version.replace("v", "").replaceAll(".", "")); + loadServerConfigs (state, data) { + state.user = data.user + state.config = data.config + state.serverVersion = data.version + state.serverVersionInt = parseInt(data.version.replace('v', '').replaceAll('.', '')) }, - changeToastMessage(state, { message, spin }) { - state.toastMessage = message; - state.toastSpin = spin || false; + changeToastMessage (state, { message, spin }) { + state.toastMessage = message + state.toastSpin = spin || false }, - addUploadingFiles(state, filenames) { + addUploadingFiles (state, filenames) { for (const filename of filenames) { - state.uploadingFiles[filename] = {}; + state.uploadingFiles[filename] = {} } }, - clearUploadingFiles(state) { - state.uploadingFiles = {}; + clearUploadingFiles (state) { + state.uploadingFiles = {} }, - setUploadProgress(state, { filename, progress }) { + setUploadProgress (state, { filename, progress }) { if (state.uploadingFiles[filename] === undefined) { - return; + return } - state.uploadingFiles[filename].progress = Math.ceil(progress); + state.uploadingFiles[filename].progress = Math.ceil(progress) } }, actions: { - makeToast(context, { message, timeout, spin }) { - context.commit("changeToastMessage", { + makeToast (context, { message, timeout, spin }) { + context.commit('changeToastMessage', { message, spin - }); + }) if (timeout !== null && timeout !== undefined) { setTimeout(() => { - context.commit("changeToastMessage", { + context.commit('changeToastMessage', { message: null, spin: false - }); - }, timeout); + }) + }, timeout) } }, - async navigate(context, folder) { - if (folder === "/") { - folder = ""; + async navigate (context, folder) { + if (folder === '/') { + folder = '' } - context.commit("goTo", folder); - await context.dispatch("refreshObjects"); + context.commit('goTo', folder) + await context.dispatch('refreshObjects') }, - navigateToHash(context, folder) { - folder = decodeURIComponent(escape(atob(folder))); - context.dispatch("navigate", folder); + navigateToHash (context, folder) { + folder = decodeURIComponent(escape(atob(folder))) + context.dispatch('navigate', folder) }, - addUploadingFiles(context, filenames) { - context.commit("addUploadingFiles", filenames); + addUploadingFiles (context, filenames) { + context.commit('addUploadingFiles', filenames) }, - clearUploadingFiles(context, filenames) { - context.commit("clearUploadingFiles"); + clearUploadingFiles (context, filenames) { + context.commit('clearUploadingFiles') }, - setUploadProgress(context, { filename, progress }) { - context.commit("setUploadProgress", { filename, progress }); + setUploadProgress (context, { filename, progress }) { + context.commit('setUploadProgress', { filename, progress }) }, - loadUserDisks({ commit }) { - axios.get("/api/buckets").then((response) => { - commit("loadUserDisks", response.data); - }); + loadUserDisks ({ commit }) { + axios.get('/api/buckets').then((response) => { + commit('loadUserDisks', response.data) + }) }, - async tryLogin(context, data) { + async tryLogin (context, data) { const token = 'Basic ' + btoa(data.username + ':' + data.password) let result @@ -144,40 +150,51 @@ export default createStore({ result = await axios.get('/api/server/config', { headers: { Authorization: token + }, + validateStatus: function (status) { + return status >= 200 && status < 300 } }) - } catch (e) {} + } catch (e) { + console.log(e) + if (e.response.status === 302) { + const nextUrl = e.response.headers.Location + if (nextUrl) { + window.location.replace(nextUrl) + } + } + } if (result?.status === 200) { axios.defaults.headers.common.Authorization = token localStorage.setItem('basicAuth', token) - context.state.loginMethod = "basic" - context.commit("loadServerConfigs", result.data); + context.state.loginMethod = 'basic' + context.commit('loadServerConfigs', result.data) context.dispatch('loadUserDisks') } else { return 'Invalid username or password' } }, - async checkBasicAuthStorage(context) { + async checkBasicAuthStorage (context) { const token = localStorage.getItem('basicAuth') if (token) { axios.defaults.headers.common.Authorization = token - context.state.loginMethod = "basic" + context.state.loginMethod = 'basic' } }, - loadServerConfigs({ commit }) { - axios.get("/api/server/config").then((response) => { - commit("loadServerConfigs", response.data); - }).catch(function(error) { + loadServerConfigs ({ commit }) { + axios.get('/api/server/config').then((response) => { + commit('loadServerConfigs', response.data) + }).catch(function (error) { if (error.response?.status === 401) { - router.push({ name: "login"}); + router.push({ name: 'login' }) } - }); + }) }, - async refreshObjects({ commit }) { - await commit("loadObjects", await repo.listObjects()); + async refreshObjects ({ commit }) { + await commit('loadObjects', await repo.listObjects()) } }, modules: {} -}); +}) diff --git a/packages/dashboard/vue.config.js b/packages/dashboard/vue.config.js index b4a33fa..8637ad6 100644 --- a/packages/dashboard/vue.config.js +++ b/packages/dashboard/vue.config.js @@ -20,7 +20,10 @@ module.exports = { url: '/storage', icons: [{ src: '/img/icons/android-chrome-192x192.png', sizes: '192x192' }] } - ] + ], + workboxOptions: { + skipWaiting: true + } // configure the workbox plugin // workboxPluginMode: "InjectManifest", diff --git a/worker/dev/index.ts b/worker/dev/index.ts index d832981..ada4add 100644 --- a/worker/dev/index.ts +++ b/worker/dev/index.ts @@ -3,6 +3,7 @@ import { R2Explorer } from "../src"; export default R2Explorer({ readonly: false, cors: true, + showHiddenFiles: true, dashboardUrl: "https://dev.r2-explorer-dashboard.pages.dev/", basicAuth: [{ username: 'teste', diff --git a/worker/dev/wrangler.toml b/worker/dev/wrangler.toml index 456783e..9dedf2c 100644 --- a/worker/dev/wrangler.toml +++ b/worker/dev/wrangler.toml @@ -13,6 +13,11 @@ binding = 'r2-explorer-example-2' bucket_name = 'r2-explorer-example-2' preview_bucket_name = 'r2-explorer-example-2' +[[r2_buckets]] +binding = 'r2-explorer-example-1' +bucket_name = 'r2-explorer-example-1' +preview_bucket_name = 'r2-explorer-example-1' + [[r2_buckets]] binding = 'drive' bucket_name = 'drive' diff --git a/worker/src/emails/receiveEmail.ts b/worker/src/emails/receiveEmail.ts index 94ed9e9..5bbc3de 100644 --- a/worker/src/emails/receiveEmail.ts +++ b/worker/src/emails/receiveEmail.ts @@ -51,6 +51,7 @@ export async function receiveEmail(event, env, ctx: Context) { to_name: (parsedEmail.to.length > 0) ? parsedEmail.to[0].name : null, has_attachments: parsedEmail.attachments.length > 0, read: false, + timestamp: Date.now() } }) diff --git a/worker/src/interfaces.ts b/worker/src/interfaces.ts index 3951f1c..1720c71 100644 --- a/worker/src/interfaces.ts +++ b/worker/src/interfaces.ts @@ -11,7 +11,7 @@ export interface R2ExplorerConfig { emailRouting?: { targetBucket: string }, - showHiddenFiles?: string + showHiddenFiles?: boolean cacheAssets?: boolean basicAuth?: BasicAuth | BasicAuth[] }