From d50bec8caa0c1e834e8ed1d906a530d14a86513b Mon Sep 17 00:00:00 2001 From: Henrique Dias <hacdias@gmail.com> Date: Tue, 1 Aug 2017 20:49:56 +0100 Subject: [PATCH] Internationalization (#183) * update dependencies to latest version * add mising dependencies * Syntax updates and such * Reorganize files and translate login to portuguese * Add i18n to buttons * Error translations and some bug fixes * Add i18n to files * i18n on prompts * update search * Prompts and Sidebar in * i18n to the header * Change to YAML * alphabetical order * # Add simplified Chinese language (#180) * Add Simplified Chinese and sort by alphabet * Add more text to translations * API Updates * Update zh_cn.yaml (#182) * Api Upgrades * Simplify api and clean zh_cn lang file * Improve error logging * Fix some route bugs and separate login styles * better organization * Fix bug on api * Build assets Tue, Aug 1, 2017 11:32:23 AM * Rename users path and fix bug scroll event * Start Portuguese translation and file org * Add more to the PT translation * Add show * Build assets Tue Aug 1 12:01:39 GMTST 2017 * Add locale to cofnig * Update portuguese translation * You can change the language :) * :D * Build assets Tue Aug 1 17:50:31 GMTST 2017 * Update requestContext variable names * Remove assets * Build assets Tue Aug 1 20:48:21 GMTST 2017 Former-commit-id: 08f373725c14990f61dbb00bea43118c496c5d32 [formerly 281e23007c79dac1e9b86424201891a99d20f73a] [formerly b1b73f42debbce06b4f36e4cf97e319789c85b9f [formerly d8bc73390c37409efa60804d94779a7629944caa]] Former-commit-id: 92e99405cbf9935d1cf77b0fe70b122fca552be6 [formerly 3cd365e862f2a54ada60e226a19ac607b8d0c43b] Former-commit-id: cf9815114ac686cdf75a6b1cba15adafe493d083 --- assets/build/webpack.base.conf.js | 4 + assets/index.html | 5 +- assets/src/components/Header.vue | 16 +- assets/src/components/Languages.vue | 19 ++ assets/src/components/Login.vue | 118 --------- assets/src/components/ProfileSettings.vue | 82 ------ assets/src/components/Search.vue | 16 +- assets/src/components/Sidebar.vue | 24 +- assets/src/components/buttons/Copy.vue | 4 +- assets/src/components/buttons/Delete.vue | 4 +- assets/src/components/buttons/Download.vue | 4 +- assets/src/components/buttons/Info.vue | 4 +- assets/src/components/buttons/Move.vue | 4 +- assets/src/components/buttons/Rename.vue | 4 +- assets/src/components/buttons/SwitchView.vue | 4 +- assets/src/components/buttons/Upload.vue | 4 +- assets/src/components/{ => files}/Editor.vue | 8 +- assets/src/components/{ => files}/Listing.vue | 22 +- .../components/{ => files}/ListingItem.vue | 0 assets/src/components/{ => files}/Preview.vue | 20 +- assets/src/components/prompts/Copy.vue | 11 +- assets/src/components/prompts/Delete.vue | 13 +- assets/src/components/prompts/Download.vue | 5 +- assets/src/components/prompts/Error.vue | 6 +- assets/src/components/prompts/FileList.vue | 2 +- assets/src/components/prompts/Help.vue | 27 +- assets/src/components/prompts/Info.vue | 24 +- assets/src/components/prompts/Move.vue | 11 +- assets/src/components/prompts/NewDir.vue | 11 +- assets/src/components/prompts/NewFile.vue | 11 +- assets/src/components/prompts/Prompts.vue | 5 +- assets/src/components/prompts/Rename.vue | 12 +- assets/src/components/prompts/Success.vue | 2 +- assets/src/css/dashboard.css | 33 ++- assets/src/css/login.css | 68 +++++ assets/src/css/styles.css | 1 + assets/src/i18n/en.yaml | 164 ++++++++++++ assets/src/i18n/index.js | 19 ++ assets/src/i18n/pt.yaml | 165 ++++++++++++ assets/src/i18n/zh-cn.yaml | 151 +++++++++++ assets/src/main.js | 2 + assets/src/router/index.js | 54 ++-- assets/src/store/mutations.js | 8 +- assets/src/utils/api.js | 243 +++++++----------- assets/src/utils/auth.js | 6 +- assets/src/{components => views}/Files.vue | 69 +++-- .../{components => views}/GlobalSettings.vue | 56 ++-- .../{components/Main.vue => views/Layout.vue} | 10 +- assets/src/views/Login.vue | 42 +++ assets/src/views/ProfileSettings.vue | 103 ++++++++ assets/src/{components => views}/User.vue | 99 +++---- assets/src/{components => views}/Users.vue | 8 +- .../src/{components => views}/errors/403.vue | 2 +- .../src/{components => views}/errors/404.vue | 2 +- .../src/{components => views}/errors/500.vue | 2 +- auth.go | 8 +- cmd/filemanager/main.go | 5 + download.go | 12 +- file.go | 2 +- filemanager.go | 36 ++- http.go | 34 ++- package.json | 34 +-- plugins/hugo.go | 6 +- resource.go | 12 +- rice-box.go.REMOVED.git-id | 2 +- settings.go | 159 ++++++------ users.go | 214 ++++++++------- 67 files changed, 1450 insertions(+), 887 deletions(-) create mode 100644 assets/src/components/Languages.vue delete mode 100644 assets/src/components/Login.vue delete mode 100644 assets/src/components/ProfileSettings.vue rename assets/src/components/{ => files}/Editor.vue (96%) rename assets/src/components/{ => files}/Listing.vue (93%) rename assets/src/components/{ => files}/ListingItem.vue (100%) rename assets/src/components/{ => files}/Preview.vue (79%) create mode 100644 assets/src/css/login.css create mode 100644 assets/src/i18n/en.yaml create mode 100644 assets/src/i18n/index.js create mode 100644 assets/src/i18n/pt.yaml create mode 100644 assets/src/i18n/zh-cn.yaml rename assets/src/{components => views}/Files.vue (83%) rename assets/src/{components => views}/GlobalSettings.vue (75%) rename assets/src/{components/Main.vue => views/Layout.vue} (84%) create mode 100644 assets/src/views/Login.vue create mode 100644 assets/src/views/ProfileSettings.vue rename assets/src/{components => views}/User.vue (64%) rename assets/src/{components => views}/Users.vue (74%) rename assets/src/{components => views}/errors/403.vue (78%) rename assets/src/{components => views}/errors/404.vue (77%) rename assets/src/{components => views}/errors/500.vue (79%) diff --git a/assets/build/webpack.base.conf.js b/assets/build/webpack.base.conf.js index 5016698b..f8188022 100644 --- a/assets/build/webpack.base.conf.js +++ b/assets/build/webpack.base.conf.js @@ -25,6 +25,10 @@ module.exports = { }, module: { rules: [ + { + test: /\.(yml|yaml)$/, + loader: 'yml-loader' + }, { test: /\.(js|vue)$/, loader: 'eslint-loader', diff --git a/assets/index.html b/assets/index.html index 31f4cab4..a0922521 100644 --- a/assets/index.html +++ b/assets/index.html @@ -21,11 +21,10 @@ <!-- Add to home screen for Windows --> <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileColor" content="#2979ff"> - - <% for (var chunk of webpack.chunks) { + <% for (var chunk of webpack.compilation.chunks) { for (var file of chunk.files) { if (file.match(/\.(js|css)$/)) { %> - <link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %> + <link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %> <!-- Plugins info --> <script>{{ .JavaScript }}</script> diff --git a/assets/src/components/Header.vue b/assets/src/components/Header.vue index 9f9b2a77..656d8361 100644 --- a/assets/src/components/Header.vue +++ b/assets/src/components/Header.vue @@ -1,19 +1,19 @@ <template> <header> <div> - <button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action"> + <button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action"> <i class="material-icons">menu</i> </button> <img src="../assets/logo.svg" alt="File Manager"> <search></search> </div> <div> - <button @click="openSearch" aria-label="Search" title="Search" class="search-button action"> + <button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action"> <i class="material-icons">search</i> </button> - <button v-show="showSaveButton" aria-label="Save" class="action" id="save-button"> - <i class="material-icons" title="Save">save</i> + <button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button"> + <i class="material-icons">save</i> </button> <div v-for="plugin in plugins" :key="plugin.name"> @@ -30,7 +30,7 @@ </button> </div> - <button @click="openMore" id="more" aria-label="More" title="More" class="action"> + <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action"> <i class="material-icons">more_vert</i> </button> @@ -71,9 +71,9 @@ <upload-button v-show="showUpload"></upload-button> <info-button v-show="showCommonButton"></info-button> - <button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action"> + <button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action"> <i class="material-icons">check_circle</i> - <span>Select</span> + <span>{{ $t('buttons.select') }}</span> </button> </div> <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> @@ -92,7 +92,7 @@ import SwitchButton from './buttons/SwitchView' import MoveButton from './buttons/Move' import CopyButton from './buttons/Copy' import {mapGetters, mapState} from 'vuex' -import api from '@/utils/api' +import * as api from '@/utils/api' import buttons from '@/utils/buttons' export default { diff --git a/assets/src/components/Languages.vue b/assets/src/components/Languages.vue new file mode 100644 index 00000000..a8374223 --- /dev/null +++ b/assets/src/components/Languages.vue @@ -0,0 +1,19 @@ +<template> + <select v-on:change="change" :value="selected"> + <option value="en">{{ $t('languages.en') }}</option> + <option value="pt">{{ $t('languages.pt') }}</option> + <option value="zh-cn">{{ $t('languages.zhCN') }}</option> + </select> +</template> + +<script> +export default { + name: 'languages', + props: [ 'selected' ], + methods: { + change (event) { + this.$emit('update:selected', event.target.value) + } + } +} +</script> diff --git a/assets/src/components/Login.vue b/assets/src/components/Login.vue deleted file mode 100644 index 62a642f8..00000000 --- a/assets/src/components/Login.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> - <div id="login"> - <form @submit="submit"> - <img src="../assets/logo.svg" alt="File Manager"> - <h1>File Manager</h1> - <div v-if="wrong" class="wrong">Wrong credentials</div> - <input type="text" v-model="username" placeholder="Username"> - <input type="password" v-model="password" placeholder="Password"> - <input type="submit" value="Login"> - </form> - </div> -</template> - -<script> -import auth from '@/utils/auth' - -export default { - name: 'login', - data: function () { - return { - wrong: false, - username: '', - password: '' - } - }, - methods: { - submit: function (event) { - event.preventDefault() - event.stopPropagation() - - let redirect = this.$route.query.redirect - if (redirect === '' || redirect === undefined || redirect === null) { - redirect = '/files/' - } - - auth.login(this.username, this.password) - .then(() => { - this.$router.push({ path: redirect }) - }) - .catch(() => { - this.wrong = true - }) - } - } -} -</script> - -<style> -#login { - background: #fff; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -#login img { - width: 4em; - height: 4em; - margin: 0 auto; - display: block; -} - -#login h1 { - text-align: center; - font-size: 2.5em; - margin: .4em 0 .67em; -} - -#login form { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - max-width: 16em; - width: 90%; -} - -#login input { - width: 100%; - width: 100%; - margin: .5em 0 0; -} - -#login .wrong { - background: #F44336; - color: #fff; - padding: .5em; - text-align: center; - animation: .2s opac forwards; -} - -@keyframes opac { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -#login input[type="text"], -#login input[type="password"] { - padding: .5em 1em; - border: 1px solid #e9e9e9; - transition: .2s ease border; - color: #333; -} - -#login input[type="text"]:focus, -#login input[type="password"]:focus, -#login input[type="text"]:hover, -#login input[type="password"]:hover { - border-color: #9f9f9f; -} -</style> - diff --git a/assets/src/components/ProfileSettings.vue b/assets/src/components/ProfileSettings.vue deleted file mode 100644 index ccb70947..00000000 --- a/assets/src/components/ProfileSettings.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> - <div class="dashboard"> - <h1>Profile Settings</h1> - - <ul v-if="user.admin"> - <li><router-link to="/settings/global">Go to Global Settings</router-link></li> - </ul> - - <form @submit="changePassword"> - <h2>Change Password</h2> - <p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p> - <p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p> - <p><input type="submit" value="Change Password"></p> - </form> - - <form @submit="updateCSS"> - <h2>Custom Stylesheet</h2> - <textarea v-model="css" name="css"></textarea> - <p><input type="submit" value="Update"></p> - </form> - </div> -</template> - -<script> -import { mapState, mapMutations } from 'vuex' -import api from '@/utils/api' - -export default { - name: 'settings', - data: function () { - return { - password: '', - passwordConf: '', - css: '' - } - }, - computed: { - ...mapState([ 'user' ]), - passwordClass () { - if (this.password === '' && this.passwordConf === '') { - return '' - } - - if (this.password === this.passwordConf) { - return 'green' - } - - return 'red' - } - }, - created () { - this.css = this.user.css - }, - methods: { - ...mapMutations([ 'showSuccess' ]), - changePassword (event) { - event.preventDefault() - - if (this.password !== this.passwordConf) { - return - } - - api.updatePassword(this.password).then(() => { - this.showSuccess('Password updated!') - }).catch(e => { - this.$store.commit('showError', e) - }) - }, - updateCSS (event) { - event.preventDefault() - - api.updateCSS(this.css).then(() => { - this.$store.commit('setUserCSS', this.css) - this.$emit('css-updated') - this.showSuccess('Styles updated!') - }).catch(e => { - this.$store.commit('showError', e) - }) - } - } -} -</script> diff --git a/assets/src/components/Search.vue b/assets/src/components/Search.vue index 369d2b59..6829597f 100644 --- a/assets/src/components/Search.vue +++ b/assets/src/components/Search.vue @@ -1,7 +1,7 @@ <template> <div id="search" @click="open" v-bind:class="{ active , ongoing }"> <div id="input"> - <button v-if="active" class="action" @click="close"> + <button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')"> <i class="material-icons">arrow_back</i> </button> <i v-else class="material-icons">search</i> @@ -11,7 +11,7 @@ ref="input" :autofocus="active" v-model.trim="value" - aria-label="Write here to search" + :aria-label="$t('search.writeToSearch')" :placeholder="placeholder"> </div> @@ -78,10 +78,10 @@ export default { // Placeholder value. placeholder: function () { if (this.user.allowCommands && this.user.commands.length > 0) { - return 'Search or execute a command...' + return this.$t('search.searchOrCommand') } - return 'Search...' + return this.$t('search.search') }, // The text that is shown on the results' box while // there is no search result or command output to show. @@ -92,16 +92,16 @@ export default { if (this.value.length === 0) { if (this.user.allowCommands && this.user.commands.length > 0) { - return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.` + return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.` } - return 'Type and press enter to search.' + this.$t('search.type') } if (!this.supported() || !this.user.allowCommands) { - return 'Press enter to search.' + return this.$t('search.pressToSearch') } else { - return 'Press enter to execute.' + return this.$t('search.pressToExecute') } } }, diff --git a/assets/src/components/Sidebar.vue b/assets/src/components/Sidebar.vue index cd462a94..87b9f40b 100644 --- a/assets/src/components/Sidebar.vue +++ b/assets/src/components/Sidebar.vue @@ -1,19 +1,19 @@ <template> <nav :class="{active}"> - <router-link class="action" to="/files/" aria-label="My Files" title="My Files"> + <router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')"> <i class="material-icons">folder</i> - <span>My Files</span> + <span>{{ $t('sidebar.myFiles') }}</span> </router-link> <div v-if="user.allowNew"> - <button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action"> + <button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"> <i class="material-icons">create_new_folder</i> - <span>New folder</span> + <span>{{ $t('sidebar.newFolder') }}</span> </button> - <button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action"> + <button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"> <i class="material-icons">note_add</i> - <span>New file</span> + <span>{{ $t('sidebar.newFile') }}</span> </button> </div> @@ -25,21 +25,21 @@ </div> <div> - <router-link class="action" to="/settings" aria-label="Settings" title="Settings"> + <router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')"> <i class="material-icons">settings_applications</i> - <span>Settings</span> + <span>{{ $t('sidebar.settings') }}</span> </router-link> - <button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout"> + <button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')"> <i class="material-icons">exit_to_app</i> - <span>Logout</span> + <span>{{ $t('sidebar.logout') }}</span> </button> </div> <p class="credits"> - <span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span> + <span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span> <span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span> - <span><a @click="help">Help</a></span> + <span><a @click="help">{{ $t('sidebar.help') }}</a></span> </p> </nav> </template> diff --git a/assets/src/components/buttons/Copy.vue b/assets/src/components/buttons/Copy.vue index 9ee554a4..164cf347 100644 --- a/assets/src/components/buttons/Copy.vue +++ b/assets/src/components/buttons/Copy.vue @@ -1,7 +1,7 @@ <template> - <button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button"> + <button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button"> <i class="material-icons">content_copy</i> - <span>Copy file</span> + <span>{{ $t('buttons.copyFile') }}</span> </button> </template> diff --git a/assets/src/components/buttons/Delete.vue b/assets/src/components/buttons/Delete.vue index 69c43c6e..bb3c0fdf 100644 --- a/assets/src/components/buttons/Delete.vue +++ b/assets/src/components/buttons/Delete.vue @@ -1,7 +1,7 @@ <template> - <button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button"> + <button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button"> <i class="material-icons">delete</i> - <span>Delete</span> + <span>{{ $t('buttons.delete') }}</span> </button> </template> diff --git a/assets/src/components/buttons/Download.vue b/assets/src/components/buttons/Download.vue index d2bf4a0c..788d746e 100644 --- a/assets/src/components/buttons/Download.vue +++ b/assets/src/components/buttons/Download.vue @@ -1,7 +1,7 @@ <template> - <button @click="download" aria-label="Download" title="Download" id="download-button" class="action"> + <button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action"> <i class="material-icons">file_download</i> - <span>Download</span> + <span>{{ $t('buttons.download') }}</span> <span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span> </button> </template> diff --git a/assets/src/components/buttons/Info.vue b/assets/src/components/buttons/Info.vue index 6e2ef91f..164e59c9 100644 --- a/assets/src/components/buttons/Info.vue +++ b/assets/src/components/buttons/Info.vue @@ -1,7 +1,7 @@ <template> - <button title="Info" aria-label="Info" class="action" @click="show"> + <button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show"> <i class="material-icons">info</i> - <span>Info</span> + <span>{{ $t('buttons.info') }}</span> </button> </template> diff --git a/assets/src/components/buttons/Move.vue b/assets/src/components/buttons/Move.vue index a7b5834d..5d9250f4 100644 --- a/assets/src/components/buttons/Move.vue +++ b/assets/src/components/buttons/Move.vue @@ -1,7 +1,7 @@ <template> - <button @click="show" aria-label="Move" title="Move" class="action" id="move-button"> + <button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button"> <i class="material-icons">forward</i> - <span>Move file</span> + <span>{{ $t('buttons.moveFile') }}</span> </button> </template> diff --git a/assets/src/components/buttons/Rename.vue b/assets/src/components/buttons/Rename.vue index 9b2922b0..a2fd2cbf 100644 --- a/assets/src/components/buttons/Rename.vue +++ b/assets/src/components/buttons/Rename.vue @@ -1,7 +1,7 @@ <template> - <button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button"> + <button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button"> <i class="material-icons">mode_edit</i> - <span>Rename</span> + <span>{{ $t('buttons.rename') }}</span> </button> </template> diff --git a/assets/src/components/buttons/SwitchView.vue b/assets/src/components/buttons/SwitchView.vue index d4e3fba2..48b1fb1c 100644 --- a/assets/src/components/buttons/SwitchView.vue +++ b/assets/src/components/buttons/SwitchView.vue @@ -1,7 +1,7 @@ <template> - <button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button"> + <button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button"> <i class="material-icons">{{ icon() }}</i> - <span>Switch view</span> + <span>{{ $t('buttons.switchView') }}</span> </button> </template> diff --git a/assets/src/components/buttons/Upload.vue b/assets/src/components/buttons/Upload.vue index a2922ea9..f5d738ad 100644 --- a/assets/src/components/buttons/Upload.vue +++ b/assets/src/components/buttons/Upload.vue @@ -1,7 +1,7 @@ <template> - <button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button"> + <button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button"> <i class="material-icons">file_upload</i> - <span>Upload</span> + <span>{{ $t('buttons.upload') }}</span> </button> </template> diff --git a/assets/src/components/Editor.vue b/assets/src/components/files/Editor.vue similarity index 96% rename from assets/src/components/Editor.vue rename to assets/src/components/files/Editor.vue index af789b1d..2d0f41bb 100644 --- a/assets/src/components/Editor.vue +++ b/assets/src/components/files/Editor.vue @@ -1,10 +1,10 @@ <template> <form id="editor" :class="req.language"> <div v-if="hasMetadata" id="metadata"> - <h2>Metadata</h2> + <h2>{{ $t('files.metadata') }}</h2> </div> - <h2 v-if="hasMetadata">Body</h2> + <h2 v-if="hasMetadata">{{ $t('files.body') }}</h2> </form> </template> @@ -123,7 +123,3 @@ export default { } } </script> - -<style> - -</style> diff --git a/assets/src/components/Listing.vue b/assets/src/components/files/Listing.vue similarity index 93% rename from assets/src/components/Listing.vue rename to assets/src/components/files/Listing.vue index 3abddcf6..8eb7497f 100644 --- a/assets/src/components/Listing.vue +++ b/assets/src/components/files/Listing.vue @@ -2,9 +2,9 @@ <div v-if="(req.numDirs + req.numFiles) == 0"> <h2 class="message"> <i class="material-icons">sentiment_dissatisfied</i> - <span>It feels lonely here...</span> + <span>{{ $t('files.lonely') }}</span> </h2> - <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple> + <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> </div> <div v-else id="listing" :class="req.display" @@ -16,23 +16,23 @@ <div></div> <div> <p :class="{ active: nameSorted }" class="name" @click="sort('name')"> - <span>Name</span> + <span>{{ $t('files.name') }}</span> <i class="material-icons">{{ nameIcon }}</i> </p> <p :class="{ active: sizeSorted }" class="size" @click="sort('size')"> - <span>Size</span> + <span>{{ $t('files.size') }}</span> <i class="material-icons">{{ sizeIcon }}</i> </p> <p :class="{ active: modifiedSorted }" class="modified" @click="sort('modified')"> - <span>Last modified</span> + <span>{{ $t('files.lastModified') }}</span> <i class="material-icons">{{ modifiedIcon }}</i> </p> </div> </div> </div> - <h2 v-if="req.numDirs > 0">Folders</h2> + <h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2> <div v-if="req.numDirs > 0"> <item v-for="(item, index) in req.items" v-if="item.isDir" @@ -47,7 +47,7 @@ </item> </div> - <h2 v-if="req.numFiles > 0">Files</h2> + <h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2> <div v-if="req.numFiles > 0"> <item v-for="(item, index) in req.items" v-if="!item.isDir" @@ -62,12 +62,12 @@ </item> </div> - <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple> + <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection"> - <p>Multiple selection enabled</p> - <div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action"> - <i class="material-icons" title="Clear">clear</i> + <p>{{ $t('files.multipleSelectionEnabled') }}</p> + <div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action"> + <i class="material-icons">clear</i> </div> </div> </div> diff --git a/assets/src/components/ListingItem.vue b/assets/src/components/files/ListingItem.vue similarity index 100% rename from assets/src/components/ListingItem.vue rename to assets/src/components/files/ListingItem.vue diff --git a/assets/src/components/Preview.vue b/assets/src/components/files/Preview.vue similarity index 79% rename from assets/src/components/Preview.vue rename to assets/src/components/files/Preview.vue index 142cf2ba..454eb981 100644 --- a/assets/src/components/Preview.vue +++ b/assets/src/components/files/Preview.vue @@ -1,7 +1,7 @@ <template> <div id="previewer"> <div class="bar"> - <button @click="back" class="action" aria-label="Close Preview" id="close"> + <button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close"> <i class="material-icons">close</i> </button> @@ -11,8 +11,12 @@ <info-button></info-button> </div> - <button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button> - <button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button> + <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')"> + <i class="material-icons">chevron_left</i> + </button> + <button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')"> + <i class="material-icons">chevron_right</i> + </button> <div class="preview"> <img v-if="req.type == 'image'" :src="raw()"> @@ -24,7 +28,7 @@ </video> <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object> <a v-else-if="req.type == 'blob'" :href="download()"> - <h2 class="message">Download <i class="material-icons">file_download</i></h2> + <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> </a> <pre v-else >{{ req.content }}</pre> </div> @@ -35,10 +39,10 @@ import { mapState } from 'vuex' import url from '@/utils/url' import api from '@/utils/api' -import InfoButton from './buttons/Info' -import DeleteButton from './buttons/Delete' -import RenameButton from './buttons/Rename' -import DownloadButton from './buttons/Download' +import InfoButton from '@/components/buttons/Info' +import DeleteButton from '@/components/buttons/Delete' +import RenameButton from '@/components/buttons/Rename' +import DownloadButton from '@/components/buttons/Download' export default { name: 'preview', diff --git a/assets/src/components/prompts/Copy.vue b/assets/src/components/prompts/Copy.vue index 184182f6..b4cb403c 100644 --- a/assets/src/components/prompts/Copy.vue +++ b/assets/src/components/prompts/Copy.vue @@ -1,13 +1,16 @@ <template> <div class="prompt"> - <h3>Copy</h3> - <p>Choose the place to copy your files:</p> + <h3>{{ $t('prompts.copy') }}</h3> + <p>{{ $t('prompts.copyMessage') }}</p> <file-list @update:selected="val => dest = val"></file-list> <div> - <button class="ok" @click="copy">Copy</button> - <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button> + <button class="ok" @click="copy">{{ $t('buttons.copy') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Delete.vue b/assets/src/components/prompts/Delete.vue index e0cba26e..3e14fbd0 100644 --- a/assets/src/components/prompts/Delete.vue +++ b/assets/src/components/prompts/Delete.vue @@ -1,11 +1,14 @@ <template> <div class="prompt"> - <h3>Delete files</h3> - <p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p> - <p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p> + <h3>{{ $t('prompts.deleteTitle') }}</h3> + <p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p> + <p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p> <div> - <button @click="submit" autofocus>Delete</button> - <button @click="closeHovers" class="cancel">Cancel</button> + <button @click="submit" autofocus>{{ $t('buttons.delete') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Download.vue b/assets/src/components/prompts/Download.vue index 18b401ba..8e89ea58 100644 --- a/assets/src/components/prompts/Download.vue +++ b/assets/src/components/prompts/Download.vue @@ -1,7 +1,8 @@ <template> <div class="prompt" id="download"> - <h3>Download files</h3> - <p>Choose the format you want to download.</p> + <h3>{{ $t('prompts.download') }}</h3> + <p>{{ $t('prompts.downloadMessage') }}</p> + <button @click="download('zip')" autofocus>zip</button> <button @click="download('tar')" autofocus>tar</button> <button @click="download('targz')" autofocus>tar.gz</button> diff --git a/assets/src/components/prompts/Error.vue b/assets/src/components/prompts/Error.vue index 2312eae8..c5e0ea49 100644 --- a/assets/src/components/prompts/Error.vue +++ b/assets/src/components/prompts/Error.vue @@ -1,11 +1,11 @@ <template> <div class="prompt error"> <i class="material-icons">error_outline</i> - <h3>Something went wrong</h3> + <h3>{{ $t('prompts.error') }}</h3> <pre>{{ $store.state.showMessage }}</pre> <div> - <button @click="close" autofocus>Close</button> - <button @click="reportIssue" class="cancel">Report Issue</button> + <button @click="close" autofocus>{{ $t('buttons.close') }}</button> + <button @click="reportIssue" class="cancel">{{ $t('buttons.reportIssue') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/FileList.vue b/assets/src/components/prompts/FileList.vue index 52864096..eaa29645 100644 --- a/assets/src/components/prompts/FileList.vue +++ b/assets/src/components/prompts/FileList.vue @@ -9,7 +9,7 @@ :data-url="item.url">{{ item.name }}</li> </ul> - <p>Currently navigating on: <code>{{ nav }}</code>.</p> + <p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p> </div> </template> diff --git a/assets/src/components/prompts/Help.vue b/assets/src/components/prompts/Help.vue index 454e76bf..fbf88282 100644 --- a/assets/src/components/prompts/Help.vue +++ b/assets/src/components/prompts/Help.vue @@ -1,26 +1,21 @@ <template> <div class="prompt help"> - <h3>Help</h3> + <h3>{{ $t('help.help') }}</h3> <ul> - <li><strong>F1</strong> - this information</li> - <li><strong>F2</strong> - rename file</li> - <li><strong>DEL</strong> - delete selected items</li> - <li><strong>ESC</strong> - clear selection and/or close the prompt</li> - <li><strong>CTRL + S</strong> - save a file or download the directory where you are</li> - <li><strong>CTRL + Click</strong> - select multiple files or directories</li> - <li><strong>Double click</strong> - open a file or directory</li> - <li><strong>Click</strong> - select file or directory</li> - </ul> - - <p>Not available yet</p> - - <ul> - <li><strong>Alt + Click</strong> - select a group of files</li> + <li><strong>F1</strong> - {{ $t('help.f1') }}</li> + <li><strong>F2</strong> - {{ $t('help.f2') }}</li> + <li><strong>DEL</strong> - {{ $t('help.del') }}</li> + <li><strong>ESC</strong> - {{ $t('help.esc') }}</li> + <li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li> + <li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li> + <li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li> + <li><strong>Click</strong> - {{ $t('help.click') }}</li> + <li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li> </ul> <div> - <button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button> + <button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Info.vue b/assets/src/components/prompts/Info.vue index 8f625e43..2752bb75 100644 --- a/assets/src/components/prompts/Info.vue +++ b/assets/src/components/prompts/Info.vue @@ -1,27 +1,27 @@ <template> <div class="prompt"> - <h3>File Information</h3> + <h3>{{ $t('prompts.fileInfo') }}</h3> - <p v-show="selected.length > 1">{{ selected.length }} files selected.</p> + <p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p> - <p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p> - <p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p> - <p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p> + <p v-show="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p> + <p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p> + <p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p> <section v-show="dir() && selected.length === 0"> - <p><strong>Number of files:</strong> {{ req.numFiles }}</p> - <p><strong>Number of directories:</strong> {{ req.numDirs }}</p> + <p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p> + <p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p> </section> <section v-show="!dir()"> - <p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p> - <p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p> - <p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p> - <p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p> + <p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p> + <p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p> + <p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p> + <p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p> </section> <div> - <button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button> + <button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Move.vue b/assets/src/components/prompts/Move.vue index ebfb7ad2..be6f42cd 100644 --- a/assets/src/components/prompts/Move.vue +++ b/assets/src/components/prompts/Move.vue @@ -1,13 +1,16 @@ <template> <div class="prompt"> - <h3>Move</h3> - <p>Choose new house for your file(s)/folder(s):</p> + <h3>{{ $t('prompts.move') }}</h3> + <p>{{ $t('prompts.moveMessage') }}</p> <file-list @update:selected="val => dest = val"></file-list> <div> - <button class="ok" @click="move">Move</button> - <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button> + <button class="ok" @click="move">{{ $t('buttons.move') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/NewDir.vue b/assets/src/components/prompts/NewDir.vue index bee602fd..9b33263e 100644 --- a/assets/src/components/prompts/NewDir.vue +++ b/assets/src/components/prompts/NewDir.vue @@ -1,11 +1,14 @@ <template> <div class="prompt"> - <h3>New directory</h3> - <p>Write the name of the new directory.</p> + <h3>{{ $t('prompts.newDir') }}</h3> + <p>{{ $t('prompts.newDirMessage') }}</p> <input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> <div> - <button class="ok" @click="submit">Create</button> - <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button> + <button class="ok" @click="submit">{{ $t('buttons.create') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/NewFile.vue b/assets/src/components/prompts/NewFile.vue index c40e2844..d9b6c333 100644 --- a/assets/src/components/prompts/NewFile.vue +++ b/assets/src/components/prompts/NewFile.vue @@ -1,11 +1,14 @@ <template> <div class="prompt"> - <h3>New file</h3> - <p>Write the name of the new file.</p> + <h3>{{ $t('prompts.newFile') }}</h3> + <p>{{ $t('prompts.newFileMessage') }}</p> <input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> <div> - <button class="ok" @click="submit">Create</button> - <button class="cancel" @click="$store.commit('closeHovers')">Cancel</button> + <button class="ok" @click="submit">{{ $t('buttons.create') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Prompts.vue b/assets/src/components/prompts/Prompts.vue index a78e3c78..0551bd7a 100644 --- a/assets/src/components/prompts/Prompts.vue +++ b/assets/src/components/prompts/Prompts.vue @@ -27,7 +27,10 @@ :placeholder="input.placeholder"> <div> <input type="submit" class="ok" :value="prompt.ok"> - <button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </form> </template> diff --git a/assets/src/components/prompts/Rename.vue b/assets/src/components/prompts/Rename.vue index 2e0bbd92..3951f7d5 100644 --- a/assets/src/components/prompts/Rename.vue +++ b/assets/src/components/prompts/Rename.vue @@ -1,11 +1,15 @@ <template> <div class="prompt"> - <h3>Rename</h3> - <p>Insert a new name for <code>{{ oldName() }}</code>:</p> + <h3>{{ $t('prompts.rename') }}</h3> + <p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p> + <input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> <div> - <button @click="submit" type="submit">Rename</button> - <button @click="cancel" class="cancel">Cancel</button> + <button @click="submit" type="submit">{{ $t('buttons.rename') }}</button> + <button class="cancel" + @click="$store.commit('closeHovers')" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> </div> </div> </template> diff --git a/assets/src/components/prompts/Success.vue b/assets/src/components/prompts/Success.vue index 263a846c..b26150af 100644 --- a/assets/src/components/prompts/Success.vue +++ b/assets/src/components/prompts/Success.vue @@ -3,7 +3,7 @@ <i class="material-icons">done</i> <h3>{{ $store.state.showMessage }}</h3> <div> - <button @click="close" autofocus>OK</button> + <button @click="close" autofocus>{{ $t('buttons.ok') }}</button> </div> </div> </template> diff --git a/assets/src/css/dashboard.css b/assets/src/css/dashboard.css index c873c6bc..003ced21 100644 --- a/assets/src/css/dashboard.css +++ b/assets/src/css/dashboard.css @@ -35,7 +35,7 @@ width: 1em } -.dashboard > *:first-child { +.dashboard > h1:first-of-type { margin-top: 0; } @@ -48,6 +48,7 @@ form.dashboard > p:last-child { margin-bottom: 0; } +.dashboard select, .dashboard textarea, .dashboard input[type="text"], .dashboard input[type="password"] { @@ -60,12 +61,18 @@ form.dashboard > p:last-child { width: 100%; } +.dashboard #locale, .dashboard #username, .dashboard #password, .dashboard #scope { max-width: 18em; } +.dashboard #locale { + border: 1px solid #dddddd; + margin-top: .5em; +} + .dashboard textarea:focus, .dashboard textarea:hover, .dashboard input[type="text"]:focus, @@ -118,3 +125,27 @@ p code { font-size: .8em; line-height: 1.5; } + +.dashboard #nav { + list-style: none; + display: flex; + color: rgb(84, 110, 122); + font-weight: 500; + padding: 0 0 1em; + margin: 0 0 1em; + font-size: .8em; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.dashboard #nav li { + width: 100%; +} + +.dashboard #nav li:last-child { + text-align: right +} + +.dashboard #nav i { + font-size: 1em; + vertical-align: middle; +} diff --git a/assets/src/css/login.css b/assets/src/css/login.css new file mode 100644 index 00000000..91180414 --- /dev/null +++ b/assets/src/css/login.css @@ -0,0 +1,68 @@ +#login { + background: #fff; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#login img { + width: 4em; + height: 4em; + margin: 0 auto; + display: block; +} + +#login h1 { + text-align: center; + font-size: 2.5em; + margin: .4em 0 .67em; +} + +#login form { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 16em; + width: 90%; +} + +#login input { + width: 100%; + width: 100%; + margin: .5em 0 0; +} + +#login .wrong { + background: #F44336; + color: #fff; + padding: .5em; + text-align: center; + animation: .2s opac forwards; +} + +@keyframes opac { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +#login input[type="text"], +#login input[type="password"] { + padding: .5em 1em; + border: 1px solid #e9e9e9; + transition: .2s ease border; + color: #333; +} + +#login input[type="text"]:focus, +#login input[type="password"]:focus, +#login input[type="text"]:hover, +#login input[type="password"]:hover { + border-color: #9f9f9f; +} diff --git a/assets/src/css/styles.css b/assets/src/css/styles.css index fbd1c131..f9c2f488 100644 --- a/assets/src/css/styles.css +++ b/assets/src/css/styles.css @@ -6,6 +6,7 @@ @import "./listing.css"; @import "./editor.css"; @import "./dashboard.css"; +@import "./login.css"; /* * * * * * * * * * * * * * * * * ACTION * diff --git a/assets/src/i18n/en.yaml b/assets/src/i18n/en.yaml new file mode 100644 index 00000000..dd707833 --- /dev/null +++ b/assets/src/i18n/en.yaml @@ -0,0 +1,164 @@ +buttons: + cancel: Cancel + close: Close + copy: Copy + copyFile: Copy file + create: Create + delete: Delete + download: Download + info: Info + more: More + move: Move + moveFile: Move file + new: New + next: Next + ok: OK + previous: Previous + rename: Rename + reportIssue: Report Issue + save: Save + search: Search + select: Select + selectMultiple: Select multiple + switchView: Swicth view + toggleSidebar: Toggle sidebar + update: Update + upload: Upload +errors: + forbidden: You're not welcome here. + internal: Something really went wrong. + notFound: This location can't be reached. +files: + folders: Folders + files: Files + body: Body + clear: Clear + closePreview: Close preview + home: Home + lastModified: Last modified + loading: Loading... + lonely: It feels lonely here... + metadata: Metadata + multipleSelectionEnabled: Multiple selection enabled + name: Name + size: Size +help: + click: select file or directory + ctrl: + click: select multiple files or directories + f: opens search + s: save a file or download the directory where you are + del: delete selected items + doubleClick: open a file or directory + esc: clear selection and/or close the prompt + f1: this information + f2: rename file + help: Help +login: + password: Password + submit: Login + username: Username + wrongCredentials: Wrong credentials +prompts: + copy: Copy + copyMessage: 'Choose the place to copy your files:' + currentlyNavigating: 'Currently navigating on:' + deleteMessageMultiple: Are you sure you want to delete {count} file(s)? + deleteMessageSingle: Are you sure you want to delete this file/folder? + deleteTitle: Delete files + displayName: 'Display Name:' + download: Download files + downloadMessage: Choose the format you want to download. + error: Something went wrong + fileInfo: File information + filesSelected: "{count} files selected." + lastModified: Last Modified + move: Move + moveMessage: 'Choose new house for your file(s)/folder(s):' + newDir: New directory + newDirMessage: Write the name of the new directory. + newFile: New file + newFileMessage: Write the name of the new file. + numberDirs: Number of directories + numberFiles: Number of files + rename: Rename + renameMessage: Insert a new name for + show: Show + size: Size +settings: + admin: Admin + administrator: Administrator + allowCommands: Execute commands + allowEdit: Edit, rename and delete files or directories. + allowNew: Create new files and directories + avoidChanges: "(leave blank to avoid changes)" + changePassword: Change Password + commands: Commands + commandsHelp: > + Here you can set commands that are executed in the named events. You + write one command per line. If the event is related to files, such as before and + after saving, the environment variable "file" will be available with the path + of the file. + commandsUpdated: Commands updated! + customStylesheet: Custom Stylesheet + examples: Examples + globalSettings: Global Settings + language: Language + newPassword: Your new password + newPasswordConfirm: Confirm your new password + newUser: New User + password: Password + passwordUpdated: Password updated! + permissions: Permissions + permissionsHelp: > + You can set the user to be an administrator or choose the permissions + individually. If you select "Administrator", all of the other options will be + automatically checked. The management of users remains a privilege of an administrator. + pluginsUpdated: Plugins settings updated! + profileSettings: Profile Settings + ruleExample1: > + 'prevents the access to any dot file (such as .git, .gitignore) in + every folder.' + ruleExample2: blocks the access to the file named Caddyfile on the root of the scope. + rules: Rules + rulesHelp1: > + 'Here you can define a set of allow and disallow rules for this specific + user. The blocked files won''t show up in the listings and they won''t be accessible + to the user. We support regex and paths relative to the user''s scope.' + rulesHelp2: > + Each rule goes in one different line and must start with the keyword + {0} or {1}. Then you should write {2} if you are using a regular expression and + then the expression or the path. + scope: Scope + settingsUpdated: Settings updated! + user: User + userCommands: Commands + userCommandsHelp: + 'A space separated list with the available commands for this user. + Example:' + userCreated: User created! + userDeleted: User deleted! + userManagement: User Management + username: Username + users: Users + userUpdated: User updated! +sidebar: + help: Help + logout: Logout + myFiles: My files + newFile: New file + newFolder: New folder + servedWith: Served with + settings: Settings +search: + writeToSearch: Write here to search + searchOrCommand: Search or execute a command... + searchOrSupportedCommand: 'Search or use one of your supported commands:' + search: Search... + type: Type and press enter to search. + pressToSearch: Press enter to search. + pressToExecute: Press enter to execute. +languages: + en: English + pt: Portuguese + zhCN: Chinese (Simplified) diff --git a/assets/src/i18n/index.js b/assets/src/i18n/index.js new file mode 100644 index 00000000..24ea77a1 --- /dev/null +++ b/assets/src/i18n/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' +import en from './en.yaml' +import pt from './pt.yaml' +import zhCN from './zh-cn.yaml' + +Vue.use(VueI18n) + +const i18n = new VueI18n({ + locale: 'en', + fallbackLocale: 'en', + messages: { + 'en': en, + 'pt': pt, + 'zh-cn': zhCN + } +}) + +export default i18n diff --git a/assets/src/i18n/pt.yaml b/assets/src/i18n/pt.yaml new file mode 100644 index 00000000..b0651136 --- /dev/null +++ b/assets/src/i18n/pt.yaml @@ -0,0 +1,165 @@ +buttons: + cancel: Cancelar + close: Fechar + copy: Copiar + copyFile: Copiar ficheiro + create: Criar + delete: Eliminar + download: Descarregar + info: Info + more: Mais + move: Mover + moveFile: Mover ficheiro + new: Novo + next: Próximo + ok: OK + previous: Anterior + rename: Renomear + reportIssue: Reportar Erro + save: Guardar + search: Pesquisar + select: Selecionar + selectMultiple: Selecionar múltiplos + switchView: Alterar modo de visão + toggleSidebar: Alternar barra lateral + update: Atualizar + upload: Enviar +errors: + forbidden: Tu não és bem-vindo aqui. + internal: Algo correu bastante mal. + notFound: Não conseguimos chegar a esta localização. +files: + folders: Pastas + files: Ficheiros + body: Corpo + clear: Limpar + closePreview: Fechar pré-visualização + home: Início + lastModified: Última modificação + loading: A carregar... + lonely: Sinto-me sozinho... + metadata: Metadados + multipleSelectionEnabled: Seleção múltipla ativada + name: Nome + size: Tamanho +help: + click: selecionar pasta ou ficheiro + ctrl: + click: selecionar várias pastas e ficheiros + f: pesquisar + s: guardar um ficheiro ou descarregar a pasta em que estás a navegar + del: eliminar os ficheiros selecionados + doubleClick: abrir pasta ou ficheiro + esc: limpar seleção e/ou fechar menu + f1: esta informação + f2: renomear ficheiro + help: Ajuda +login: + password: Palavra-passe + submit: Login + username: Nome de utilizador + wrongCredentials: Dados errados +prompts: + copy: Copiar + copyMessage: 'Escolhe um lugar para copiar os ficheiros:' + currentlyNavigating: 'A navegar em:' + deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)? + deleteMessageSingle: Deseja eliminar esta pasta/ficheiro? + deleteTitle: Eliminar ficheiros + displayName: 'Nome:' + download: Descarregar ficheiros + downloadMessage: Escolha o formato do ficheiro. + error: Algo correu mal + fileInfo: Informação do ficheiro + filesSelected: "{count} ficheiros selecionados." + lastModified: Última Modificação + move: Mover + moveMessage: 'Escolha uma nova casa para os seus ficheiros:' + newDir: Nova pasta + newDirMessage: Escreva o nome da nova pasta. + newFile: Novo ficheiro + newFileMessage: Escreva o nome do novo ficheiro. + numberDirs: Número de pastas + numberFiles: Número de ficheiros + rename: Renomear + renameMessage: Insira um novo nome para + show: Mostrar + size: Tamanho +settings: + admin: Admin + administrator: Administrador + allowCommands: Executar comandos + allowEdit: Editar, renomear e eliminar ficheiros ou pastas + allowNew: Criar novos ficheiros e pastas + avoidChanges: "(deixe em branco para manter)" + changePassword: Alterar Password + commands: Comandos + commandsHelp: > + Pode definir um conjunto de comandos a executar em determiandos eventos. Deve + escrever um comando por linha. Se o evento estiver relacionado com ficheiros, + como antes e depois de guardar, irá existir uma variável de ambiente denominada + "file" com o caminho do ficheiro. + commandsUpdated: Comandos atualizados! + customStylesheet: Estilos Personalizados + examples: Exemplos + globalSettings: Configurações Globais + language: Linguagem + newPassword: Nova palavra-passe + newPasswordConfirm: Confirme a nova palavra-passe + newUser: Novo Utilizador + password: Palavra-passe + passwordUpdated: Palavra-passe atualizada! + permissions: Permissões + permissionsHelp: > + Pode definir o utilizador como administrador ou escolher as permissões manualmente. + Se selecionar a opção "Administrador", todas as outras opções serão automaticamente + selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores. + pluginsUpdated: Plugins atualizados! + profileSettings: Configurações do Utilizador + ruleExample1: > + previne o acesso a qualquer "dotfile" (como .git, .gitignore) em qualquer pasta + ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile. + rules: Regras + rulesHelp1: > + Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso + do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não + irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos + dos ficheiros devem ser relativos à base do utilizador. + rulesHelp2: > + Cada regra deve ser colocada numa linha diferente e deve começar com as + palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2}, + caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta + ou a expressão regular. + scope: Base + settingsUpdated: Configurações atualizadas! + user: Utilizador + userCommands: Comandos + userCommandsHelp: + 'Uma lista, separada com espaços, de comandos disponíveis para este + utilizados. Exemplo:' + userCreated: Utilizador criado! + userDeleted: Utilizador eliminado! + userManagement: Gestão de Utilizadores + username: Nome de utilizador + users: Utilizadores + userUpdated: Utilizador atualizado! +sidebar: + help: Ajuda + logout: Sair + myFiles: Ficheiros + newFile: Novo ficheiro + newFolder: Nova pasta + servedWith: Servido com + settings: Configurações +search: + writeToSearch: Escreva aqui para pesquisar + searchOrCommand: Pesquise ou execute um comando... + searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:' + search: Pesquise... + type: Escreva e prima enter para pesquisar. + pressToSearch: Prima enter para pesquisar. + pressToExecute: Prima enter para executar. +languages: + en: Inglês + pt: Português + zhCN: Chinês (Simplificado) diff --git a/assets/src/i18n/zh-cn.yaml b/assets/src/i18n/zh-cn.yaml new file mode 100644 index 00000000..0385c30a --- /dev/null +++ b/assets/src/i18n/zh-cn.yaml @@ -0,0 +1,151 @@ +buttons: + cancel: 取消 + close: 关闭 + copy: 复制 + copyFile: 复制文件 + create: 创建 + delete: 删除 + download: 下载 + info: 信息 + more: 更多 + move: 移动 + moveFile: 移动文件 + new: 新 + next: 下一步 + ok: 确定 + previous: 以前 + rename: 重命名 + reportIssue: 报告问题 + save: 保存 + search: 搜索 + select: 选择 + selectMultiple: 选择多个 + switchView: 切换显示方式 + toggleSidebar: 切换侧边栏 + update: 更新 + upload: 上传 +errors: + forbidden: 你被禁止访问. + internal: 内部出现麻烦了. + notFound: 找不到文件. +files: + folders: 文件夹 + files: 文件 + body: Body + clear: 清理 + closePreview: 关闭预览 + home: 主页 + lastModified: 最后修改 + loading: 加载中... + lonely: 这里没有任何文件... + metadata: 元数据 + multipleSelectionEnabled: 启用多选模式(现在可以选择多个文件/文件夹) + name: 名称 + size: 大小 +help: + click: 选择文件或目录 + ctrl: + click: 选择多个文件或目录 + f: 打开搜索框 + s: 保存文件或下载文件夹 + del: 删除 所选文件/文件夹 + doubleClick: 打开文件或目录 + esc: 清除 当前所有选择 或 关闭提示信息 + f1: 显示 当前帮助信息 + f2: 重命名 文件/文件夹 + help: 帮助 +login: + password: 密码 + submit: 登录 + username: 用户名 + wrongCredentials: 账号或密码错误 +prompts: + copy: 复制 + copyMessage: '请选择欲复制至的目录:' + currentlyNavigating: '目前正在浏览:' + deleteMessageMultiple: 你确定要删除这 {count} 个文件吗? + deleteMessageSingle: 你确定要删除这个文件/文件夹吗? + deleteTitle: 删除文件 + displayName: '名称:' + download: 下载文件 + downloadMessage: 请选择要下载的压缩格式. + error: 出了一点问题... + fileInfo: 文件信息 + filesSelected: '选择 {count} 个文件.' + lastModified: 最后修改 + move: 移动 + moveMessage: '请选择欲移动至的目录:' + newDir: 新建目录 + newDirMessage: 请输入新建目录的名称. + newFile: 新建文件 + newFileMessage: 请输入新建文件的名称. + numberDirs: 目录数 + numberFiles: 文件数 + rename: 重命名 + renameMessage: '请输入新名称, 旧名称是:' + show: 揭示 + size: 大小 +settings: + admin: 管理员 + administrator: 管理员 + allowCommands: 执行命令(Linux 代码) + allowEdit: 编辑、重命名或删除文件/目录. + allowNew: 创建新文件和目录. + avoidChanges: '(留空以避免更改)' + changePassword: 更改密码 + commands: 命令(linux 代码) + commandsHelp: > + 'Here you can set commands that are executed in the named events. + 每行一条命令. If the event is related to files, such as before and after saving, + the environment variable "file" will be available with the path of the file.' + commandsUpdated: 命令更新! + customStylesheet: 自定义样式表 + examples: 例子 + globalSettings: 全局设置 + newPassword: 您的新密码 + newPasswordConfirm: 重输一遍新密码 + newUser: 新建用户 + password: 密码 + passwordUpdated: 密码更新! + permissions: 权限 + permissionsHelp: > + '您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" , + 将自动检查所有其他选项, 并且该用户可以管理其他用户.' + pluginsUpdated: 插件设置更新! + profileSettings: 配置文件设置 + ruleExample1: > + '阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).' + ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹. + rules: 规则 + rulesHelp1: > + '这里您可以为特定用户制定一组允许或不允许的规则, + 阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.' + rulesHelp2: > + 每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式, + 然后使用表达式或路径, 则需要在第二列单词加入 {2} . + scope: 目录范围 + user: 用户 + userCommands: 用户命令(Linux 代码) + userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:' + userCreated: 用户创建! + userDeleted: 用户删除! + userManagement: 用户管理 + username: 用户名 + users: 用户 + userUpdated: 用户更新! +sidebar: + help: 帮助 + logout: 注销 + myFiles: 我的文件 + newFile: 新建文件 + newFolder: 新建文件夹 + servedWith: 服务提供 + settings: 设置 +search: + writeToSearch: 请输入要搜索的内容 + searchOrCommand: 搜索或者执行命令(Linux 代码)... + searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):' + search: 搜索... + type: 键入并按 Enter 键(回车)进行搜索. + pressToSearch: 按 Enter 键(回车)进行搜索. + pressToExecute: 按 Enter 键(回车)执行. diff --git a/assets/src/main.js b/assets/src/main.js index 47008899..33c69173 100644 --- a/assets/src/main.js +++ b/assets/src/main.js @@ -2,6 +2,7 @@ import Vue from 'vue' import App from './App' import store from './store' import router from './router' +import i18n from './i18n' Vue.config.productionTip = true @@ -10,6 +11,7 @@ new Vue({ el: '#app', store, router, + i18n, template: '<App/>', components: { App } }) diff --git a/assets/src/router/index.js b/assets/src/router/index.js index 74e24481..e03ea50a 100644 --- a/assets/src/router/index.js +++ b/assets/src/router/index.js @@ -1,15 +1,15 @@ import Vue from 'vue' import Router from 'vue-router' -import Login from '@/components/Login' -import Main from '@/components/Main' -import Files from '@/components/Files' -import Users from '@/components/Users' -import User from '@/components/User' -import GlobalSettings from '@/components/GlobalSettings' -import ProfileSettings from '@/components/ProfileSettings' -import error403 from '@/components/errors/403' -import error404 from '@/components/errors/404' -import error500 from '@/components/errors/500' +import Login from '@/views/Login' +import Layout from '@/views/Layout' +import Files from '@/views/Files' +import Users from '@/views/Users' +import User from '@/views/User' +import GlobalSettings from '@/views/GlobalSettings' +import ProfileSettings from '@/views/ProfileSettings' +import Error403 from '@/views/errors/403' +import Error404 from '@/views/errors/404' +import Error500 from '@/views/errors/500' import auth from '@/utils/auth.js' import store from '@/store' @@ -25,24 +25,18 @@ const router = new Router({ component: Login, beforeEnter: function (to, from, next) { auth.loggedIn() - .then(() => { - next({ path: '/files' }) - }) - .catch(() => { - document.title = 'Login' - next() - }) - } - }, - { - path: '/', - redirect: { - path: '/files/' + .then(() => { + next({ path: '/files' }) + }) + .catch(() => { + document.title = 'Login' + next() + }) } }, { path: '/*', - component: Main, + component: Layout, meta: { requiresAuth: true }, @@ -75,17 +69,17 @@ const router = new Router({ { path: '/403', name: 'Forbidden', - component: error403 + component: Error403 }, { path: '/404', name: 'Not Found', - component: error404 + component: Error404 }, { path: '/500', name: 'Internal Server Error', - component: error500 + component: Error500 }, { path: '/users', @@ -95,12 +89,6 @@ const router = new Router({ requiresAdmin: true } }, - { - path: '/users/', - redirect: { - path: '/users' - } - }, { path: '/users/*', name: 'User', diff --git a/assets/src/store/mutations.js b/assets/src/store/mutations.js index 6797ef2c..481845f2 100644 --- a/assets/src/store/mutations.js +++ b/assets/src/store/mutations.js @@ -1,3 +1,5 @@ +import i18n from '@/i18n' + const mutations = { closeHovers: state => { state.show = null @@ -22,8 +24,10 @@ const mutations = { }, setLoading: (state, value) => { state.loading = value }, setReload: (state, value) => { state.reload = value }, - setUser: (state, value) => (state.user = value), - setUserCSS: (state, value) => (state.user.css = value), + setUser: (state, value) => { + i18n.locale = value.locale + state.user = value + }, setJWT: (state, value) => (state.jwt = value), multiple: (state, value) => (state.multiple = value), addSelected: (state, value) => (state.selected.push(value)), diff --git a/assets/src/utils/api.js b/assets/src/utils/api.js index e9b74f48..04fff2cd 100644 --- a/assets/src/utils/api.js +++ b/assets/src/utils/api.js @@ -2,7 +2,7 @@ import store from '@/store' const ssl = (window.location.protocol === 'https:') -function removePrefix (url) { +export function removePrefix (url) { if (url.startsWith('/files')) { return url.slice(6) } @@ -10,7 +10,7 @@ function removePrefix (url) { return url } -function fetch (url) { +export function fetch (url) { url = removePrefix(url) return new Promise((resolve, reject) => { @@ -24,10 +24,7 @@ function fetch (url) { resolve(JSON.parse(request.responseText)) break default: - reject({ - message: request.responseText, - status: request.status - }) + reject(new Error(request.status)) break } } @@ -36,7 +33,7 @@ function fetch (url) { }) } -function rm (url) { +export function rm (url) { url = removePrefix(url) return new Promise((resolve, reject) => { @@ -57,7 +54,7 @@ function rm (url) { }) } -function post (url, content = '') { +export function post (url, content = '') { url = removePrefix(url) return new Promise((resolve, reject) => { @@ -78,7 +75,7 @@ function post (url, content = '') { }) } -function put (url, content = '') { +export function put (url, content = '') { url = removePrefix(url) return new Promise((resolve, reject) => { @@ -132,15 +129,15 @@ function moveCopy (items, copy = false) { return Promise.all(promises) } -function move (items) { +export function move (items) { return moveCopy(items) } -function copy (items) { +export function copy (items) { return moveCopy(items, true) } -function checksum (url, algo) { +export function checksum (url, algo) { url = removePrefix(url) return new Promise((resolve, reject) => { @@ -160,7 +157,7 @@ function checksum (url, algo) { }) } -function command (url, command, onmessage, onclose) { +export function command (url, command, onmessage, onclose) { let protocol = (ssl ? 'wss:' : 'ws:') url = removePrefix(url) url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}` @@ -171,7 +168,7 @@ function command (url, command, onmessage, onclose) { conn.onclose = onclose } -function search (url, search, onmessage, onclose) { +export function search (url, search, onmessage, onclose) { let protocol = (ssl ? 'wss:' : 'ws:') url = removePrefix(url) url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}` @@ -182,7 +179,7 @@ function search (url, search, onmessage, onclose) { conn.onclose = onclose } -function download (format, ...files) { +export function download (format, ...files) { let url = `${store.state.baseURL}/api/download` if (files.length === 1) { @@ -206,7 +203,59 @@ function download (format, ...files) { window.open(url) } -function getUsers () { +export function getSettings () { + return new Promise((resolve, reject) => { + let request = new window.XMLHttpRequest() + request.open('GET', `${store.state.baseURL}/api/settings/`, true) + request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) + + request.onload = () => { + switch (request.status) { + case 200: + resolve(JSON.parse(request.responseText)) + break + default: + reject(request.responseText) + break + } + } + request.onerror = (error) => reject(error) + request.send() + }) +} + +export function updateSettings (param, which) { + return new Promise((resolve, reject) => { + let data = { + what: 'settings', + which: which, + data: {} + } + + data.data[which] = param + + let request = new window.XMLHttpRequest() + request.open('PUT', `${store.state.baseURL}/api/settings/`, true) + request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) + + request.onload = () => { + switch (request.status) { + case 200: + resolve() + break + default: + reject(request.responseText) + break + } + } + request.onerror = (error) => { reject(error) } + request.send(JSON.stringify(data)) + }) +} + +// USERS + +export function getUsers () { return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('GET', `${store.state.baseURL}/api/users/`, true) @@ -227,7 +276,7 @@ function getUsers () { }) } -function getUser (id) { +export function getUser (id) { return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('GET', `${store.state.baseURL}/api/users/${id}`, true) @@ -248,7 +297,7 @@ function getUser (id) { }) } -function newUser (user) { +export function newUser (user) { return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('POST', `${store.state.baseURL}/api/users/`, true) @@ -265,11 +314,15 @@ function newUser (user) { } } request.onerror = (error) => reject(error) - request.send(JSON.stringify(user)) + request.send(JSON.stringify({ + what: 'user', + which: 'new', + data: user + })) }) } -function updateUser (user) { +export function updateUser (user, which) { return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true) @@ -286,11 +339,15 @@ function updateUser (user) { } } request.onerror = (error) => reject(error) - request.send(JSON.stringify(user)) + request.send(JSON.stringify({ + what: 'user', + which: (typeof which === 'string') ? which : 'all', + data: user + })) }) } -function deleteUser (id) { +export function deleteUser (id) { return new Promise((resolve, reject) => { let request = new window.XMLHttpRequest() request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true) @@ -311,133 +368,8 @@ function deleteUser (id) { }) } -function updatePassword (password) { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve() - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send(JSON.stringify({ 'password': password })) - }) -} - -function updateCSS (css) { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve() - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send(JSON.stringify({ 'css': css })) - }) -} - -function getCommands () { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('GET', `${store.state.baseURL}/api/commands/`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve(JSON.parse(request.responseText)) - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send() - }) -} - -function updateCommands (commands) { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('PUT', `${store.state.baseURL}/api/commands/`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve() - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send(JSON.stringify(commands)) - }) -} - -function getPlugins () { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('GET', `${store.state.baseURL}/api/plugins/`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve(JSON.parse(request.responseText)) - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send() - }) -} - -function updatePlugins (data) { - return new Promise((resolve, reject) => { - let request = new window.XMLHttpRequest() - request.open('PUT', `${store.state.baseURL}/api/plugins/`, true) - request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) - - request.onload = () => { - switch (request.status) { - case 200: - resolve() - break - default: - reject(request.responseText) - break - } - } - request.onerror = (error) => reject(error) - request.send(JSON.stringify(data)) - }) -} - export default { + removePrefix, delete: rm, fetch, checksum, @@ -448,16 +380,13 @@ export default { command, search, download, - getUser, + // other things + getSettings, + updateSettings, + // User things newUser, - updateUser, + getUser, getUsers, - updatePassword, - updateCSS, - getCommands, - updateCommands, - removePrefix, - getPlugins, - updatePlugins, + updateUser, deleteUser } diff --git a/assets/src/utils/auth.js b/assets/src/utils/auth.js index bc25b343..cde1de92 100644 --- a/assets/src/utils/auth.js +++ b/assets/src/utils/auth.js @@ -23,10 +23,10 @@ function loggedIn () { parseToken(request.responseText) resolve() } else { - reject() + reject(new Error(request.responseText)) } } - request.onerror = () => reject() + request.onerror = () => reject(new Error('Could not finish the request')) request.send() }) } @@ -45,7 +45,7 @@ function login (user, password) { reject(request.responseText) } } - request.onerror = () => reject() + request.onerror = () => reject(new Error('Could not finish the request')) request.send(JSON.stringify(data)) }) } diff --git a/assets/src/components/Files.vue b/assets/src/views/Files.vue similarity index 83% rename from assets/src/components/Files.vue rename to assets/src/views/Files.vue index 38924647..5492823c 100644 --- a/assets/src/components/Files.vue +++ b/assets/src/views/Files.vue @@ -1,7 +1,7 @@ <template> <div> <div id="breadcrumbs"> - <router-link to="/files/"> + <router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')"> <i class="material-icons">home</i> </router-link> @@ -11,8 +11,8 @@ </span> </div> <div v-if="error"> - <not-found v-if="error === 404"></not-found> - <forbidden v-else-if="error === 403"></forbidden> + <not-found v-if="error.message === '404'"></not-found> + <forbidden v-else-if="error.message === '403'"></forbidden> <internal-error v-else></internal-error> </div> <editor v-else-if="isEditor"></editor> @@ -20,7 +20,7 @@ <preview v-else-if="isPreview"></preview> <div v-else> <h2 class="message"> - <span>Loading...</span> + <span>{{ $t('files.loading') }}</span> </h2> </div> </div> @@ -30,9 +30,9 @@ import Forbidden from './errors/403' import NotFound from './errors/404' import InternalError from './errors/500' -import Preview from './Preview' -import Listing from './Listing' -import Editor from './Editor' +import Preview from '@/components/files/Preview' +import Listing from '@/components/files/Listing' +import Editor from '@/components/files/Editor' import api from '@/utils/api' import { mapGetters, mapState, mapMutations } from 'vuex' @@ -116,20 +116,11 @@ export default { }, mounted () { window.addEventListener('keydown', this.keyEvent) - window.addEventListener('scroll', event => { - if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return - - let top = 112 - window.scrollY - - if (top < 64) { - top = 64 - } - - document.querySelector('#listing.list .item.header').style.top = top + 'px' - }) + window.addEventListener('scroll', this.scroll) }, beforeDestroy () { window.removeEventListener('keydown', this.keyEvent) + window.removeEventListener('scroll', this.scroll) }, destroyed () { this.$store.commit('updateRequest', {}) @@ -152,25 +143,19 @@ export default { if (url[0] !== '/') url = '/' + url api.fetch(url) - .then((req) => { - if (!url.endsWith('/') && req.url.endsWith('/')) { - window.history.replaceState(window.history.state, document.title, window.location.pathname + '/') - } + .then((req) => { + if (!url.endsWith('/') && req.url.endsWith('/')) { + window.history.replaceState(window.history.state, document.title, window.location.pathname + '/') + } - this.$store.commit('updateRequest', req) - document.title = req.name - this.setLoading(false) - }) - .catch(error => { - this.setLoading(false) - - if (typeof error === 'object') { - this.error = error.status - return - } - - this.error = error - }) + this.$store.commit('updateRequest', req) + document.title = req.name + this.setLoading(false) + }) + .catch(error => { + this.setLoading(false) + this.error = error + }) }, keyEvent (event) { // Esc! @@ -220,11 +205,21 @@ export default { if (this.req.kind !== 'editor') { document.getElementById('download-button').click() - return } } } }, + scroll (event) { + if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return + + let top = 112 - window.scrollY + + if (top < 64) { + top = 64 + } + + document.querySelector('#listing.list .item.header').style.top = top + 'px' + }, openSidebar () { this.$store.commit('showHover', 'sidebar') }, diff --git a/assets/src/components/GlobalSettings.vue b/assets/src/views/GlobalSettings.vue similarity index 75% rename from assets/src/components/GlobalSettings.vue rename to assets/src/views/GlobalSettings.vue index 0c852b94..0fd04ca0 100644 --- a/assets/src/components/GlobalSettings.vue +++ b/assets/src/views/GlobalSettings.vue @@ -1,12 +1,20 @@ <template> <div class="dashboard"> - <h1>Global Settings</h1> - - <ul> - <li><router-link to="/settings/profile">Go to Profile Settings</router-link></li> - <li><router-link to="/users">Go to User Management</router-link></li> + <ul id="nav"> + <li> + <router-link to="/settings/profile"> + <i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }} + </router-link> + </li> + <li> + <router-link to="/users"> + {{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i> + </router-link> + </li> </ul> + <h1>{{ $t('settings.globalSettings') }}</h1> + <form @submit="savePlugin" v-if="plugins.length > 0"> <template v-for="plugin in plugins"> <h2>{{ capitalize(plugin.name) }}</h2> @@ -23,11 +31,9 @@ </form> <form @submit="saveCommands"> - <h2>Commands</h2> + <h2>{{ $t('settings.commands') }}</h2> - <p class="small">Here you can set commands that are executed in the named events. You write one command - per line. If the event is related to files, such as before and after saving, the environment variable - <code>file</code> will be available with the path of the file.</p> + <p class="small">{{ $t('settings.commandsHelp') }}</p> <template v-for="command in commands"> <h3>{{ capitalize(command.name) }}</h3> @@ -42,7 +48,7 @@ <script> import { mapState, mapMutations } from 'vuex' -import api from '@/utils/api' +import { getSettings, updateSettings } from '@/utils/api' export default { name: 'settings', @@ -56,24 +62,20 @@ export default { ...mapState([ 'user' ]) }, created () { - api.getCommands() - .then(commands => { - for (let key in commands) { + getSettings() + .then(settings => { + for (let key in settings.plugins) { + this.plugins.push(this.parsePlugin(key, settings.plugins[key])) + } + + for (let key in settings.commands) { this.commands.push({ name: key, - value: commands[key].join('\n') + value: settings.commands[key].join('\n') }) } }) .catch(error => { this.showError(error) }) - - api.getPlugins() - .then(plugins => { - for (let key in plugins) { - this.plugins.push(this.parsePlugin(key, plugins[key])) - } - }) - .catch(error => { this.showError(error) }) }, methods: { ...mapMutations([ 'showSuccess', 'showError' ]), @@ -102,8 +104,8 @@ export default { commands[command.name] = value } - api.updateCommands(commands) - .then(() => { this.showSuccess('Commands updated!') }) + updateSettings(commands, 'commands') + .then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) }) .catch(error => { this.showError(error) }) }, savePlugin (event) { @@ -129,10 +131,8 @@ export default { plugins[plugin.name] = p } - console.log(plugins) - - api.updatePlugins(plugins) - .then(() => { this.showSuccess('Plugins settings updated!') }) + updateSettings(plugins, 'plugins') + .then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) }) .catch(error => { this.showError(error) }) }, parsePlugin (name, plugin) { diff --git a/assets/src/components/Main.vue b/assets/src/views/Layout.vue similarity index 84% rename from assets/src/components/Main.vue rename to assets/src/views/Layout.vue index b09a5598..dc0af7a6 100644 --- a/assets/src/components/Main.vue +++ b/assets/src/views/Layout.vue @@ -10,13 +10,13 @@ </template> <script> -import Search from './Search' -import Sidebar from './Sidebar' -import Prompts from './prompts/Prompts' -import SiteHeader from './Header' +import Search from '@/components/Search' +import Sidebar from '@/components/Sidebar' +import Prompts from '@/components/prompts/Prompts' +import SiteHeader from '@/components/Header' export default { - name: 'main', + name: 'layout', components: { Search, Sidebar, diff --git a/assets/src/views/Login.vue b/assets/src/views/Login.vue new file mode 100644 index 00000000..8e64f349 --- /dev/null +++ b/assets/src/views/Login.vue @@ -0,0 +1,42 @@ +<template> + <div id="login"> + <form @submit="submit"> + <img src="../assets/logo.svg" alt="File Manager"> + <h1>File Manager</h1> + <div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div> + <input type="text" v-model="username" :placeholder="$t('login.username')"> + <input type="password" v-model="password" :placeholder="$t('login.password')"> + <input type="submit" :value="$t('login.submit')"> + </form> + </div> +</template> + +<script> +import auth from '@/utils/auth' + +export default { + name: 'login', + data: function () { + return { + wrong: false, + username: '', + password: '' + } + }, + methods: { + submit: function (event) { + event.preventDefault() + event.stopPropagation() + + let redirect = this.$route.query.redirect + if (redirect === '' || redirect === undefined || redirect === null) { + redirect = '/files/' + } + + auth.login(this.username, this.password) + .then(() => { this.$router.push({ path: redirect }) }) + .catch(() => { this.wrong = true }) + } + } +} +</script> diff --git a/assets/src/views/ProfileSettings.vue b/assets/src/views/ProfileSettings.vue new file mode 100644 index 00000000..87c34774 --- /dev/null +++ b/assets/src/views/ProfileSettings.vue @@ -0,0 +1,103 @@ +<template> + <div class="dashboard"> + <ul id="nav" v-if="user.admin"> + <li> + <router-link to="/settings/global"> + {{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i> + </router-link> + </li> + </ul> + + <h1>{{ $t('settings.profileSettings') }}</h1> + + <form @submit="updateSettings"> + <h3>{{ $t('settings.language') }}</h3> + <p><languages id="locale" :selected.sync="locale"></languages></p> + <h3>{{ $t('settings.customStylesheet') }}</h3> + <textarea v-model="css" name="css"></textarea> + <p><input type="submit" :value="$t('buttons.update')"></p> + </form> + + <form @submit="updatePassword"> + <h3>{{ $t('settings.changePassword') }}</h3> + <p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p> + <p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p> + <p><input type="submit" :value="$t('buttons.update')"></p> + </form> + </div> +</template> + +<script> +import { mapState, mapMutations } from 'vuex' +import { updateUser } from '@/utils/api' +import Languages from '@/components/Languages' + +export default { + name: 'settings', + components: { + Languages + }, + data: function () { + return { + password: '', + passwordConf: '', + css: '', + locale: '' + } + }, + computed: { + ...mapState([ 'user' ]), + passwordClass () { + if (this.password === '' && this.passwordConf === '') { + return '' + } + + if (this.password === this.passwordConf) { + return 'green' + } + + return 'red' + } + }, + created () { + this.css = this.user.css + this.locale = this.user.locale + }, + methods: { + ...mapMutations([ 'showSuccess' ]), + updatePassword (event) { + event.preventDefault() + + if (this.password !== this.passwordConf) { + return + } + + let user = { + ID: this.$store.state.user.ID, + password: this.password + } + + updateUser(user, 'password').then(location => { + this.showSuccess(this.$t('settings.passwordUpdated')) + }).catch(e => { + this.$store.commit('showError', e) + }) + }, + updateSettings (event) { + event.preventDefault() + + let user = {...this.$store.state.user} + user.css = this.css + user.locale = this.locale + + updateUser(user, 'partial').then(location => { + this.$store.commit('setUser', user) + this.$emit('css-updated') + this.showSuccess(this.$t('settings.settingsUpdated')) + }).catch(e => { + this.$store.commit('showError', e) + }) + } + } +} +</script> diff --git a/assets/src/components/User.vue b/assets/src/views/User.vue similarity index 64% rename from assets/src/components/User.vue rename to assets/src/views/User.vue index a2f0546d..99b32761 100644 --- a/assets/src/components/User.vue +++ b/assets/src/views/User.vue @@ -1,58 +1,56 @@ <template> <div> <form @submit="save" class="dashboard"> - <h1 v-if="id === 0">New User</h1> - <h1 v-else>User {{ username }}</h1> + <h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1> + <h1 v-else>{{ $t('settings.user') }} {{ username }}</h1> - <p><label for="username">Username</label><input type="text" v-model="username" id="username"></p> - <p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p> - <p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p> + <p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p> + <p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p> + <p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p> + <p> + <label for="locale">{{ $t('settings.language') }}</label> + <languages id="locale" :selected.sync="locale"></languages> + </p> - <h2>Permissions</h2> + <h2>{{ $t('settings.permissions') }}</h2> + <p class="small">{{ $t('settings.permissionsHelp') }}</p> - <p class="small">You can set the user to be an administrator or choose the permissions individually. - If you select "Administrator", all of the other options will be automatically checked. - The management of users remains a privilege of an administrator.</p> - - <p><input type="checkbox" v-model="admin"> Administrator</p> - <p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p> - <p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p> - <p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p> + <p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p> + <p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p> + <p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p> + <p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p> <p v-for="(value, key) in permissions" :key="key"> <input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }} </p> - <h3>Commands</h3> - - <p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p> - + <h3>{{ $t('settings.userCommands') }}</h3> + <p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p> <input type="text" v-model.trim="commands"> - <h2>Rules</h2> + <h2>{{ $t('settings.rules') }}</h2> - <p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't - show up in the listings and they won't be accessible to the user. We support regex and paths relative to - the user's scope.</p> + <p class="small">{{ $t('settings.rulesHelp1') }}</p> - <p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>. - Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p> + <i18n path="settings.rulesHelp2" tag="p" class="small"> + <code>allow</code><code>disallow</code><code>regex</code> + </i18n> - <p class="small"><strong>Examples</strong></p> + <p class="small"><strong>{{ $t('settings.examples') }}</strong></p> <ul class="small"> - <li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li> - <li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li> + <li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li> + <li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li> </ul> <textarea v-model.trim="rules"></textarea> - <h2>Custom Stylesheet</h2> + <h2>{{ $t('settings.customStylesheet') }}</h2> <textarea name="css"></textarea> <p> - <button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button> - <input type="submit" value="Save"> + <button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button> + <input type="submit" :value="$t('buttons.save')"> </p> </form> @@ -60,8 +58,13 @@ <h3>Delete User</h3> <p>Are you sure you want to delete this user?</p> <div> - <button @click="deleteUser" autofocus>Delete</button> - <button @click="closeHovers" class="cancel">Cancel</button> + <button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button> + <button class="cancel" + @click="closeHovers" + :aria-label="$t('buttons.cancel')" + :title="$t('buttons.cancel')"> + {{ $t('buttons.cancel') }} + </button> </div> </div> </div> @@ -69,10 +72,12 @@ <script> import { mapMutations } from 'vuex' -import api from '@/utils/api' +import { getUser, newUser, updateUser, deleteUser } from '@/utils/api' +import Languages from '@/components/Languages' export default { name: 'user', + components: { Languages }, data: () => { return { id: 0, @@ -85,6 +90,7 @@ export default { username: '', filesystem: '', rules: '', + locale: '', css: '', commands: '' } @@ -92,7 +98,7 @@ export default { computed: { passwordPlaceholder () { if (this.$route.path === '/users/new') return '' - return '(leave blank to avoid changes)' + return this.$t('settings.avoidChanges') } }, created () { @@ -119,7 +125,7 @@ export default { user = 'base' } - api.getUser(user).then(user => { + getUser(user).then(user => { this.id = user.ID this.admin = user.admin this.allowCommands = user.allowCommands @@ -130,6 +136,7 @@ export default { this.commands = user.commands.join(' ') this.css = user.css this.permissions = user.permissions + this.locale = user.locale for (let rule of user.rules) { if (rule.allow) { @@ -173,6 +180,7 @@ export default { this.username = '' this.filesystem = '' this.rules = '' + this.locale = '' this.css = '' this.commands = '' }, @@ -182,9 +190,9 @@ export default { deleteUser (event) { event.preventDefault() - api.deleteUser(this.id).then(location => { + deleteUser(this.id).then(location => { this.$router.push({ path: '/users' }) - this.$store.commit('showSuccess', 'User deleted!') + this.$store.commit('showSuccess', this.$t('settings.userDeleted')) }).catch(e => { this.$store.commit('showError', e) }) @@ -194,9 +202,9 @@ export default { let user = this.parseForm() if (this.$route.path === '/users/new') { - api.newUser(user).then(location => { + newUser(user).then(location => { this.$router.push({ path: location }) - this.$store.commit('showSuccess', 'User created!') + this.$store.commit('showSuccess', this.$t('settings.userCreated')) }).catch(e => { this.$store.commit('showError', e) }) @@ -204,8 +212,12 @@ export default { return } - api.updateUser(user).then(location => { - this.$store.commit('showSuccess', 'User updated!') + updateUser(user).then(location => { + if (user.ID === this.$store.state.user.ID) { + this.$store.commit('setUser', user) + } + + this.$store.commit('showSuccess', this.$t('settings.userUpdated')) }).catch(e => { this.$store.commit('showError', e) }) @@ -222,6 +234,7 @@ export default { allowEdit: this.allowEdit, permissions: this.permissions, css: this.css, + locale: this.locale, commands: this.commands.split(' '), rules: [] } @@ -269,7 +282,3 @@ export default { } } </script> - -<style> - -</style> diff --git a/assets/src/components/Users.vue b/assets/src/views/Users.vue similarity index 74% rename from assets/src/components/Users.vue rename to assets/src/views/Users.vue index 71ce0c53..81081a2d 100644 --- a/assets/src/components/Users.vue +++ b/assets/src/views/Users.vue @@ -1,12 +1,12 @@ <template> <div class="dashboard"> - <h1>Users <router-link to="/users/new"><button>New</button></router-link></h1> + <h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1> <table> <tr> - <th>Username</th> - <th>Admin</th> - <th>Scope</th> + <th>{{ $t('settings.username') }}</th> + <th>{{ $t('settings.admin') }}</th> + <th>{{ $t('settings.scope') }}</th> <th></th> </tr> diff --git a/assets/src/components/errors/403.vue b/assets/src/views/errors/403.vue similarity index 78% rename from assets/src/components/errors/403.vue rename to assets/src/views/errors/403.vue index 5c19daf5..47c6c897 100644 --- a/assets/src/components/errors/403.vue +++ b/assets/src/views/errors/403.vue @@ -2,7 +2,7 @@ <div> <h2 class="message"> <i class="material-icons">error</i> - <span>You're not welcome here.</span> + <span>{{ $t('errors.forbidden') }}</span> </h2> </div> </template> diff --git a/assets/src/components/errors/404.vue b/assets/src/views/errors/404.vue similarity index 77% rename from assets/src/components/errors/404.vue rename to assets/src/views/errors/404.vue index d73682e3..61dbe144 100644 --- a/assets/src/components/errors/404.vue +++ b/assets/src/views/errors/404.vue @@ -2,7 +2,7 @@ <div> <h2 class="message"> <i class="material-icons">gps_off</i> - <span>This location can't be reached.</span> + <span>{{ $t('errors.notFound') }}</span> </h2> </div> </template> diff --git a/assets/src/components/errors/500.vue b/assets/src/views/errors/500.vue similarity index 79% rename from assets/src/components/errors/500.vue rename to assets/src/views/errors/500.vue index 681f78a9..0bd86786 100644 --- a/assets/src/components/errors/500.vue +++ b/assets/src/views/errors/500.vue @@ -2,7 +2,7 @@ <div> <h2 class="message"> <i class="material-icons">error_outline</i> - <span>Something really went wrong.</span> + <span>{{ $t('errors.internal') }}</span> </h2> </div> </template> diff --git a/auth.go b/auth.go index 81736ea9..d48f7e92 100644 --- a/auth.go +++ b/auth.go @@ -27,7 +27,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int } // Checks if the user exists. - u, ok := c.FM.Users[cred.Username] + u, ok := c.Users[cred.Username] if !ok { return http.StatusForbidden, nil } @@ -78,7 +78,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) { // Creates the token and signs it. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - string, err := token.SignedString(c.FM.key) + string, err := token.SignedString(c.key) if err != nil { return http.StatusInternalServerError, err @@ -114,7 +114,7 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) { // User if it is valid. func validateAuth(c *RequestContext, r *http.Request) (bool, *User) { keyFunc := func(token *jwt.Token) (interface{}, error) { - return c.FM.key, nil + return c.key, nil } var claims claims token, err := request.ParseFromRequestWithClaims(r, @@ -127,7 +127,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) { return false, nil } - u, ok := c.FM.Users[claims.User.Username] + u, ok := c.Users[claims.User.Username] if !ok { return false, nil } diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index 75937beb..c7e8899e 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -28,6 +28,7 @@ var ( commands string logfile string plugin string + locale string port int allowCommands bool allowEdit bool @@ -47,6 +48,7 @@ func init() { flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") + flag.StringVar(&locale, "locale", "en", "Default locale for new users") flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable") flag.BoolVarP(&showVer, "version", "v", false, "Show version") } @@ -62,6 +64,7 @@ func setupViper() { viper.SetDefault("AllowEdit", true) viper.SetDefault("AllowNew", true) viper.SetDefault("Plugin", "") + viper.SetDefault("Locale", "en") viper.BindPFlag("Port", flag.Lookup("port")) viper.BindPFlag("Address", flag.Lookup("address")) @@ -72,6 +75,7 @@ func setupViper() { viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) + viper.BindPFlag("Locale", flag.Lookup("locale")) viper.BindPFlag("Plugin", flag.Lookup("plugin")) viper.SetConfigName("filemanager") @@ -133,6 +137,7 @@ func main() { AllowNew: viper.GetBool("AllowNew"), Commands: viper.GetStringSlice("Commands"), Rules: []*filemanager.Rule{}, + Locale: viper.GetString("Locale"), CSS: "", FileSystem: fileutils.Dir(viper.GetString("Scope")), }) diff --git a/download.go b/download.go index 5b6a5130..ce52f7b4 100644 --- a/download.go +++ b/download.go @@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) // If the file isn't a directory, serve it using http.ServeFile. We display it // inline if it is requested. - if !c.FI.IsDir { + if !c.File.IsDir { if r.URL.Query().Get("inline") == "true" { w.Header().Set("Content-Disposition", "inline") } else { - w.Header().Set("Content-Disposition", "attachment; filename="+c.FI.Name) + w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name) } - http.ServeFile(w, r, c.FI.Path) + http.ServeFile(w, r, c.File.Path) return 0, nil } @@ -46,10 +46,10 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) // Clean the slashes. name = fileutils.SlashClean(name) - files = append(files, filepath.Join(c.FI.Path, name)) + files = append(files, filepath.Join(c.File.Path, name)) } } else { - files = append(files, c.FI.Path) + files = append(files, c.File.Path) } // If the format is true, just set it to "zip". @@ -93,7 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) } // Defines the file name. - name := c.FI.Name + name := c.File.Name if name == "." || name == "" { name = "download" } diff --git a/file.go b/file.go index 675744f2..db9965ec 100644 --- a/file.go +++ b/file.go @@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { func (i *file) getListing(c *RequestContext, r *http.Request) error { // Gets the directory information using the Virtual File System of // the user configuration. - f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0) + f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0) if err != nil { return err } diff --git a/filemanager.go b/filemanager.go index 92c15d3c..b3788c94 100644 --- a/filemanager.go +++ b/filemanager.go @@ -70,11 +70,15 @@ import ( ) var ( - errUserExist = errors.New("user already exists") - errUserNotExist = errors.New("user does not exist") - errEmptyRequest = errors.New("request body is empty") - errEmptyPassword = errors.New("password is empty") - plugins = map[string]Plugin{} + errUserExist = errors.New("user already exists") + errUserNotExist = errors.New("user does not exist") + errEmptyRequest = errors.New("request body is empty") + errEmptyPassword = errors.New("password is empty") + errEmptyUsername = errors.New("username is empty") + errEmptyScope = errors.New("scope is empty") + errWrongDataType = errors.New("wrong data type") + errInvalidUpdateField = errors.New("invalid field to update") + plugins = map[string]Plugin{} ) // FileManager is a file manager instance. It should be creating using the @@ -139,6 +143,9 @@ type User struct { // Custom styles for this user. CSS string `json:"css"` + // Locale is the language of the user. + Locale string `json:"locale"` + // These indicate if the user can perform certain actions. AllowNew bool `json:"allowNew"` // Create files and folders AllowEdit bool `json:"allowEdit"` // Edit/rename files @@ -208,6 +215,7 @@ var DefaultUser = User{ Rules: []*Rule{}, CSS: "", Admin: true, + Locale: "en", FileSystem: fileutils.Dir("."), } @@ -428,23 +436,25 @@ func (m *FileManager) registerPermission(name string, value bool) error { // Compatible with http.Handler. func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) { code, err := serveHTTP(&RequestContext{ - FM: m, - User: nil, - FI: nil, + FileManager: m, + User: nil, + File: nil, }, w, r) - if code != 0 { + if code >= 400 { w.WriteHeader(code) - if err != nil { - log.Print(err) - w.Write([]byte(err.Error())) - } else { + if err == nil { txt := http.StatusText(code) log.Printf("%v: %v %v\n", r.URL.Path, code, txt) w.Write([]byte(txt)) } } + + if err != nil { + log.Print(err) + w.Write([]byte(err.Error())) + } } // Allowed checks if the user has permission to access a directory/file. diff --git a/http.go b/http.go index 55810311..d33c4a8f 100644 --- a/http.go +++ b/http.go @@ -10,9 +10,9 @@ import ( // RequestContext contains the needed information to make handlers work. type RequestContext struct { + *FileManager User *User - FM *FileManager - FI *file + File *file // On API handlers, Router is the APi handler we want. Router string } @@ -21,9 +21,9 @@ type RequestContext struct { func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // Checks if the URL contains the baseURL and strips it. Otherwise, it just // returns a 404 error because we're not supposed to be here! - p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL) + p := strings.TrimPrefix(r.URL.Path, c.BaseURL) - if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" { + if len(p) >= len(r.URL.Path) && c.BaseURL != "" { return http.StatusNotFound, nil } @@ -34,7 +34,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, if r.URL.Path == "/sw.js" { return renderFile( w, - c.FM.assets.MustString("sw.js"), + c.assets.MustString("sw.js"), "application/javascript", c, ) @@ -65,7 +65,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, return renderFile( w, - c.FM.assets.MustString("index.html"), + c.assets.MustString("index.html"), "text/html", c, ) @@ -74,13 +74,13 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, // staticHandler handles the static assets path. func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { if r.URL.Path != "/static/manifest.json" { - http.FileServer(c.FM.assets.HTTPBox()).ServeHTTP(w, r) + http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r) return 0, nil } return renderFile( w, - c.FM.assets.MustString("static/manifest.json"), + c.assets.MustString("static/manifest.json"), "application/json", c, ) @@ -107,7 +107,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, return http.StatusForbidden, nil } - for p := range c.FM.Plugins { + for p := range c.Plugins { code, err := plugins[p].Handler.Before(c, w, r) if code != 0 || err != nil { return code, err @@ -116,7 +116,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, if c.Router == "checksum" || c.Router == "download" { var err error - c.FI, err = getInfo(r.URL, c.FM, c.User) + c.File, err = getInfo(r.URL, c.FileManager, c.User) if err != nil { return errorToHTTP(err, false), err } @@ -138,10 +138,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, code, err = resourceHandler(c, w, r) case "users": code, err = usersHandler(c, w, r) - case "commands": - code, err = commandsHandler(c, w, r) - case "plugins": - code, err = pluginsHandler(c, w, r) + case "settings": + code, err = settingsHandler(c, w, r) default: code = http.StatusNotFound } @@ -150,7 +148,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, return code, err } - for p := range c.FM.Plugins { + for p := range c.Plugins { code, err := plugins[p].Handler.After(c, w, r) if code != 0 || err != nil { return code, err @@ -164,7 +162,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { query := r.URL.Query().Get("algo") - val, err := c.FI.Checksum(query) + val, err := c.File.Checksum(query) if err == errInvalidOption { return http.StatusBadRequest, err } else if err != nil { @@ -198,12 +196,12 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque w.Header().Set("Content-Type", contentType+"; charset=utf-8") var javascript = "" - for name := range c.FM.Plugins { + for name := range c.Plugins { javascript += plugins[name].JavaScript + "\n" } err := tpl.Execute(w, map[string]interface{}{ - "BaseURL": c.FM.RootURL(), + "BaseURL": c.RootURL(), "JavaScript": template.JS(javascript), }) if err != nil { diff --git a/package.json b/package.json index 3ed7d997..4b2dd4df 100644 --- a/package.json +++ b/package.json @@ -14,53 +14,57 @@ "moment": "^2.18.1", "normalize.css": "^7.0.0", "vue": "^2.3.3", + "vue-i18n": "^7.1.0", "vue-router": "^2.7.0", "vuex": "^2.3.1" }, "devDependencies": { - "autoprefixer": "^6.7.2", + "autoprefixer": "^7.1.2", "babel-core": "^6.22.1", "babel-eslint": "^7.1.1", - "babel-loader": "^6.2.10", + "babel-loader": "^7.1.1", "babel-plugin-transform-runtime": "^6.22.0", "babel-preset-env": "^1.3.2", "babel-preset-stage-2": "^6.22.0", "babel-register": "^6.22.0", - "chalk": "^1.1.3", + "chalk": "^2.0.1", "connect-history-api-fallback": "^1.3.0", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", - "eslint": "^3.19.0", - "eslint-config-standard": "^6.2.1", - "eslint-friendly-formatter": "^2.0.7", + "eslint": "^4.3.0", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", "eslint-loader": "^1.7.1", - "eslint-plugin-html": "^2.0.0", + "eslint-plugin-html": "^3.1.1", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-node": "^5.1.1", "eslint-plugin-promise": "^3.4.0", - "eslint-plugin-standard": "^2.0.1", + "eslint-plugin-standard": "^3.0.1", "eventsource-polyfill": "^0.9.6", "express": "^4.14.1", - "extract-text-webpack-plugin": "^2.0.0", + "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.1", "friendly-errors-webpack-plugin": "^1.1.3", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", - "opn": "^4.0.2", - "optimize-css-assets-webpack-plugin": "^1.3.0", + "opn": "^5.1.0", + "optimize-css-assets-webpack-plugin": "^3.0.0", "ora": "^1.2.0", "rimraf": "^2.6.0", "semver": "^5.3.0", "shelljs": "^0.7.6", - "sw-precache-webpack-plugin": "^0.9.1", + "sw-precache-webpack-plugin": "^0.11.4", "uglify-js": "^3.0.23", "url-loader": "^0.5.8", - "vue-loader": "^12.1.0", + "vue-loader": "^13.0.2", "vue-style-loader": "^3.0.1", "vue-template-compiler": "^2.3.3", - "webpack": "^2.6.1", + "webpack": "^3.4.1", "webpack-bundle-analyzer": "^2.2.1", "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.18.0", - "webpack-merge": "^4.1.0" + "webpack-merge": "^4.1.0", + "yml-loader": "^2.1.0" }, "engines": { "node": ">= 4.0.0", diff --git a/plugins/hugo.go b/plugins/hugo.go index 2a70a051..a19c4e41 100644 --- a/plugins/hugo.go +++ b/plugins/hugo.go @@ -117,7 +117,7 @@ func (h Hugo) undraft(file string) error { type hugo struct{} func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - o := c.FM.Plugins["hugo"].(*Hugo) + o := c.Plugins["hugo"].(*Hugo) // If we are using the 'magic url' for the settings, we should redirect the // request for the acutual path. @@ -189,7 +189,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) // Before save command handler. - if err := c.FM.Runner("before_publish", filename); err != nil { + if err := c.Runner("before_publish", filename); err != nil { return http.StatusInternalServerError, err } @@ -205,7 +205,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht o.run(false) // Executed the before publish command. - if err := c.FM.Runner("before_publish", filename); err != nil { + if err := c.Runner("before_publish", filename); err != nil { return http.StatusInternalServerError, err } diff --git a/resource.go b/resource.go index 1e1766d3..6eff1db6 100644 --- a/resource.go +++ b/resource.go @@ -34,7 +34,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) case http.MethodPut: // Before save command handler. path := filepath.Join(string(c.User.FileSystem), r.URL.Path) - if err := c.FM.Runner("before_save", path); err != nil { + if err := c.Runner("before_save", path); err != nil { return http.StatusInternalServerError, err } @@ -44,7 +44,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) } // After save command handler. - if err := c.FM.Runner("after_save", path); err != nil { + if err := c.Runner("after_save", path); err != nil { return http.StatusInternalServerError, err } @@ -60,7 +60,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // Gets the information of the directory/file. - f, err := getInfo(r.URL, c.FM, c.User) + f, err := getInfo(r.URL, c.FileManager, c.User) if err != nil { return errorToHTTP(err, false), err } @@ -73,7 +73,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques // If it is a dir, go and serve the listing. if f.IsDir { - c.FI = f + c.File = f return listingHandler(c, w, r) } @@ -101,7 +101,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques } func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - f := c.FI + f := c.File f.Kind = "listing" // Tries to get the listing data. @@ -112,7 +112,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) ( listing := f.listing // Defines the cookie scope. - cookieScope := c.FM.RootURL() + cookieScope := c.RootURL() if cookieScope == "" { cookieScope = "/" } diff --git a/rice-box.go.REMOVED.git-id b/rice-box.go.REMOVED.git-id index 18bc6310..70ad3bed 100644 --- a/rice-box.go.REMOVED.git-id +++ b/rice-box.go.REMOVED.git-id @@ -1 +1 @@ -49dd472ced00d5e02963554cd0b84393f8c08d75 \ No newline at end of file +b5a8f3badeeb5ea5e285f23298ddef20ce247376 \ No newline at end of file diff --git a/settings.go b/settings.go index e5e5c317..f0b771f5 100644 --- a/settings.go +++ b/settings.go @@ -2,66 +2,18 @@ package filemanager import ( "encoding/json" - "errors" "net/http" "reflect" "github.com/mitchellh/mapstructure" ) -func commandsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - switch r.Method { - case http.MethodGet: - return commandsGetHandler(c, w, r) - case http.MethodPut: - return commandsPutHandler(c, w, r) - } - - return http.StatusMethodNotAllowed, nil -} - -func commandsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.Admin { - return http.StatusForbidden, nil - } - - return renderJSON(w, c.FM.Commands) -} - -func commandsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.Admin { - return http.StatusForbidden, nil - } - - if r.Body == nil { - return http.StatusBadGateway, errors.New("Empty request body") - } - - var commands map[string][]string - - // Parses the user and checks for error. - err := json.NewDecoder(r.Body).Decode(&commands) - if err != nil { - return http.StatusBadRequest, errors.New("Invalid JSON") - } - - if err := c.FM.db.Set("config", "commands", commands); err != nil { - return http.StatusInternalServerError, err - } - - c.FM.Commands = commands - return http.StatusOK, nil -} - -func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - switch r.Method { - case http.MethodGet: - return pluginsGetHandler(c, w, r) - case http.MethodPut: - return pluginsPutHandler(c, w, r) - } - - return http.StatusMethodNotAllowed, nil +type modifySettingsRequest struct { + *modifyRequest + Data struct { + Commands map[string][]string `json:"commands"` + Plugins map[string]map[string]interface{} `json:"plugins"` + } `json:"data"` } type pluginOption struct { @@ -70,19 +22,63 @@ type pluginOption struct { Value interface{} `json:"value"` } -func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { +func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) { + // Checks if the request body is empty. + if r.Body == nil { + return nil, errEmptyRequest + } + + // Parses the request body and checks if it's well formed. + mod := &modifySettingsRequest{} + err := json.NewDecoder(r.Body).Decode(mod) + if err != nil { + return nil, err + } + + // Checks if the request type is right. + if mod.What != "settings" { + return nil, errWrongDataType + } + + return mod, nil +} + +func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path != "" && r.URL.Path != "/" { + return http.StatusNotFound, nil + } + + switch r.Method { + case http.MethodGet: + return settingsGetHandler(c, w, r) + case http.MethodPut: + return settingsPutHandler(c, w, r) + } + + return http.StatusMethodNotAllowed, nil +} + +type settingsGetRequest struct { + Commands map[string][]string `json:"commands"` + Plugins map[string][]pluginOption `json:"plugins"` +} + +func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { if !c.User.Admin { return http.StatusForbidden, nil } - plugins := map[string][]pluginOption{} + result := &settingsGetRequest{ + Commands: c.Commands, + Plugins: map[string][]pluginOption{}, + } - for name, p := range c.FM.Plugins { - plugins[name] = []pluginOption{} + for name, p := range c.Plugins { + result.Plugins[name] = []pluginOption{} t := reflect.TypeOf(p).Elem() for i := 0; i < t.NumField(); i++ { - plugins[name] = append(plugins[name], pluginOption{ + result.Plugins[name] = append(result.Plugins[name], pluginOption{ Variable: t.Field(i).Name, Name: t.Field(i).Tag.Get("name"), Value: reflect.ValueOf(p).Elem().FieldByName(t.Field(i).Name).Interface(), @@ -90,37 +86,44 @@ func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request } } - return renderJSON(w, plugins) + return renderJSON(w, result) } -func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { +func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { if !c.User.Admin { return http.StatusForbidden, nil } - if r.Body == nil { - return http.StatusBadGateway, errors.New("Empty request body") - } - - var raw map[string]map[string]interface{} - - // Parses the user and checks for error. - err := json.NewDecoder(r.Body).Decode(&raw) + mod, err := parsePutSettingsRequest(r) if err != nil { return http.StatusBadRequest, err } - - for name, plugin := range raw { - err = mapstructure.Decode(plugin, c.FM.Plugins[name]) - if err != nil { + // Update the commands. + if mod.Which == "commands" { + if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil { return http.StatusInternalServerError, err } - err = c.FM.db.Set("plugins", name, c.FM.Plugins[name]) - if err != nil { - return http.StatusInternalServerError, err - } + c.Commands = mod.Data.Commands + return http.StatusOK, nil } - return http.StatusOK, nil + // Update the plugins. + if mod.Which == "plugins" { + for name, plugin := range mod.Data.Plugins { + err = mapstructure.Decode(plugin, c.Plugins[name]) + if err != nil { + return http.StatusInternalServerError, err + } + + err = c.db.Set("plugins", name, c.Plugins[name]) + if err != nil { + return http.StatusInternalServerError, err + } + } + + return http.StatusOK, nil + } + + return http.StatusMethodNotAllowed, nil } diff --git a/users.go b/users.go index 6eab4e03..1171d749 100644 --- a/users.go +++ b/users.go @@ -11,20 +11,22 @@ import ( "github.com/asdine/storm" ) +type modifyRequest struct { + What string `json:"what"` // Answer to: what data type? + Which string `json:"which"` // Answer to: which field? +} + +type modifyUserRequest struct { + *modifyRequest + Data *User `json:"data"` +} + // usersHandler is the entry point of the users API. It's just a router // to send the request to its func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path == "/change-password" { - return usersUpdatePassword(c, w, r) - } - - if r.URL.Path == "/change-css" { - return usersUpdateCSS(c, w, r) - } - - // If the user is admin and the HTTP Method is not - // PUT, then we return forbidden. - if !c.User.Admin { + // If the user isn't admin and isn't making a PUT + // request, then return forbidden. + if !c.User.Admin && r.Method != http.MethodPut { return http.StatusForbidden, nil } @@ -61,32 +63,38 @@ func getUserID(r *http.Request) (int, error) { // getUser returns the user which is present in the request // body. If the body is empty or the JSON is invalid, it // returns an error. -func getUser(r *http.Request) (*User, error) { +func getUser(r *http.Request) (*User, string, error) { + // Checks if the request body is empty. if r.Body == nil { - return nil, errEmptyRequest + return nil, "", errEmptyRequest } - u := &User{} - - err := json.NewDecoder(r.Body).Decode(u) + // Parses the request body and checks if it's well formed. + mod := &modifyUserRequest{} + err := json.NewDecoder(r.Body).Decode(mod) if err != nil { - return nil, err + return nil, "", err } - return u, nil + // Checks if the request type is right. + if mod.What != "user" { + return nil, "", errWrongDataType + } + + return mod.Data, mod.Which, nil } func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // Request for the default user data. if r.URL.Path == "/base" { - return renderJSON(w, c.FM.DefaultUser) + return renderJSON(w, c.DefaultUser) } // Request for the listing of users. if r.URL.Path == "/" { users := []User{} - for _, user := range c.FM.Users { + for _, user := range c.Users { // Copies the user info and removes its // password so it won't be sent to the // front-end. @@ -108,7 +116,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) } // Searches for the user and prints the one who matches. - for _, user := range c.FM.Users { + for _, user := range c.Users { if user.ID != id { continue } @@ -127,11 +135,26 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) return http.StatusMethodNotAllowed, nil } - u, err := getUser(r) + u, _, err := getUser(r) if err != nil { return http.StatusBadRequest, err } + // Checks if username isn't empty. + if u.Username == "" { + return http.StatusBadRequest, errEmptyUsername + } + + // Checks if filesystem isn't empty. + if u.FileSystem == "" { + return http.StatusBadRequest, errEmptyScope + } + + // Checks if password isn't empty. + if u.Password == "" { + return http.StatusBadRequest, errEmptyPassword + } + // The username, password and scope cannot be empty. if u.Username == "" || u.Password == "" || u.FileSystem == "" { return http.StatusBadRequest, errors.New("username, password or scope is empty") @@ -161,7 +184,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) u.Password = pw // Saves the user to the database. - err = c.FM.db.Save(u) + err = c.db.Save(u) if err == storm.ErrAlreadyExists { return http.StatusConflict, errUserExist } @@ -171,7 +194,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) } // Saves the user to the memory. - c.FM.Users[u.Username] = u + c.Users[u.Username] = u // Set the Location header and return. w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID)) @@ -190,7 +213,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques } // Deletes the user from the database. - err = c.FM.db.DeleteStruct(&User{ID: id}) + err = c.db.DeleteStruct(&User{ID: id}) if err == storm.ErrNotFound { return http.StatusNotFound, errUserNotExist } @@ -200,9 +223,9 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques } // Delete the user from the in-memory users map. - for _, user := range c.FM.Users { + for _, user := range c.Users { if user.ID == id { - delete(c.FM.Users, user.Username) + delete(c.Users, user.Username) break } } @@ -210,72 +233,79 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques return http.StatusOK, nil } -func usersUpdatePassword(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if r.Method != http.MethodPut { - return http.StatusMethodNotAllowed, nil - } - - u, err := getUser(r) - if err != nil { - return http.StatusBadRequest, err - } - - if u.Password == "" { - return http.StatusBadRequest, errEmptyPassword - } - - pw, err := hashPassword(u.Password) - if err != nil { - return http.StatusInternalServerError, err - } - - c.User.Password = pw - err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "Password", pw) - if err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - -func usersUpdateCSS(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if r.Method != http.MethodPut { - return http.StatusMethodNotAllowed, nil - } - - u, err := getUser(r) - if err != nil { - return http.StatusBadRequest, err - } - - c.User.CSS = u.CSS - err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS) - if err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // New users should be created on /api/users. if r.URL.Path == "/" { return http.StatusMethodNotAllowed, nil } + // Gets the user ID from the URL and checks if it's valid. id, err := getUserID(r) if err != nil { return http.StatusInternalServerError, err } - u, err := getUser(r) + // Checks if the user has permission to access this page. + if !c.User.Admin && id != c.User.ID { + return http.StatusForbidden, nil + } + + // Gets the user from the request body. + u, which, err := getUser(r) if err != nil { return http.StatusBadRequest, err } - // The username and the filesystem cannot be empty. - if u.Username == "" || u.FileSystem == "" { - return http.StatusBadRequest, errors.New("Username, password or scope are empty") + // Updates the CSS and locale. + if which == "partial" { + c.User.CSS = u.CSS + c.User.Locale = u.Locale + err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS) + if err != nil { + return http.StatusInternalServerError, err + } + + err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale) + if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + } + + // Updates the Password. + if which == "password" { + if u.Password == "" { + return http.StatusBadRequest, errEmptyPassword + } + + pw, err := hashPassword(u.Password) + if err != nil { + return http.StatusInternalServerError, err + } + + c.User.Password = pw + err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw) + if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + } + + // If can only be all. + if which != "all" { + return http.StatusBadRequest, errInvalidUpdateField + } + + // Checks if username isn't empty. + if u.Username == "" { + return http.StatusBadRequest, errEmptyUsername + } + + // Checks if filesystem isn't empty. + if u.FileSystem == "" { + return http.StatusBadRequest, errEmptyScope } // Initialize rules if they're not initialized. @@ -288,48 +318,50 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) u.Commands = []string{} } - var ouser *User - for _, user := range c.FM.Users { + // Gets the current saved user from the in-memory map. + var suser *User + for _, user := range c.Users { if user.ID == id { - ouser = user + suser = user break } } - - if ouser == nil { + if suser == nil { return http.StatusNotFound, nil } u.ID = id - if u.Password == "" { - u.Password = ouser.Password - } else { + // Changes the password if the request wants it. + if u.Password != "" { pw, err := hashPassword(u.Password) if err != nil { return http.StatusInternalServerError, err } u.Password = pw + } else { + u.Password = suser.Password } + // Default permissions if current are nil. if u.Permissions == nil { - u.Permissions = c.FM.DefaultUser.Permissions + u.Permissions = c.DefaultUser.Permissions } // Updates the whole User struct because we always are supposed // to send a new entire object. - err = c.FM.db.Save(u) + err = c.db.Save(u) if err != nil { return http.StatusInternalServerError, err } // If the user changed the username, delete the old user // from the in-memory user map. - if ouser.Username != u.Username { - delete(c.FM.Users, ouser.Username) + if suser.Username != u.Username { + delete(c.Users, suser.Username) } - c.FM.Users[u.Username] = u + c.Users[u.Username] = u return http.StatusOK, nil }