diff options
| author | Pitu <[email protected]> | 2019-10-13 02:53:45 +0900 |
|---|---|---|
| committer | Pitu <[email protected]> | 2019-10-13 02:53:45 +0900 |
| commit | cba7bf8586f59a049f79aba586db201ac6f3530b (patch) | |
| tree | 46aeabe2b5463456ef3eb241a38407a5699e3728 /src/site/pages/dashboard/admin | |
| parent | don't log out on API error (diff) | |
| download | host.fuwn.me-cba7bf8586f59a049f79aba586db201ac6f3530b.tar.xz host.fuwn.me-cba7bf8586f59a049f79aba586db201ac6f3530b.zip | |
This commit adds a bunch of features for admins:
* banning IP
* see files from other users if you are admin
* be able to see details of an uploaded file and it's user
* improved display of thumbnails for non-image files
Diffstat (limited to 'src/site/pages/dashboard/admin')
| -rw-r--r-- | src/site/pages/dashboard/admin/file/_id.vue | 170 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/settings.vue | 135 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/user/_id.vue | 102 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/users.vue | 249 |
4 files changed, 656 insertions, 0 deletions
diff --git a/src/site/pages/dashboard/admin/file/_id.vue b/src/site/pages/dashboard/admin/file/_id.vue new file mode 100644 index 0000000..6718b32 --- /dev/null +++ b/src/site/pages/dashboard/admin/file/_id.vue @@ -0,0 +1,170 @@ +<style lang="scss" scoped> + .underline { text-decoration: underline; } +</style> +<template> + <section class="hero is-fullheight dashboard"> + <div class="hero-body"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle">File details</h2> + <hr> + + <div class="columns"> + <div class="column is-6"> + <b-field label="ID" + horizontal> + <span>{{ file.id }}</span> + </b-field> + + <b-field label="Name" + horizontal> + <span>{{ file.name }}</span> + </b-field> + + <b-field label="Original Name" + horizontal> + <span>{{ file.original }}</span> + </b-field> + + <b-field label="IP" + horizontal> + <span class="underline">{{ file.ip }}</span> + </b-field> + + <b-field label="Link" + horizontal> + <a :href="file.url" + target="_blank">{{ file.url }}</a> + </b-field> + + <b-field label="Size" + horizontal> + <span>{{ formatBytes(file.size) }}</span> + </b-field> + + <b-field label="Hash" + horizontal> + <span>{{ file.hash }}</span> + </b-field> + + <b-field label="Uploaded" + horizontal> + <span><timeago :since="file.createdAt" /></span> + </b-field> + </div> + <div class="column is-6"> + <b-field label="User Id" + horizontal> + <span>{{ user.id }}</span> + </b-field> + + <b-field label="Username" + horizontal> + <span>{{ user.username }}</span> + </b-field> + + <b-field label="Enabled" + horizontal> + <span>{{ user.enabled }}</span> + </b-field> + + <b-field label="Registered" + horizontal> + <span><timeago :since="user.createdAt" /></span> + </b-field> + + <b-field label="Files" + horizontal> + <span> + <nuxt-link :to="`/dashboard/admin/user/${user.id}`">{{ user.fileCount }}</nuxt-link> + </span> + </b-field> + </div> + </div> + + <div class="mb2 mt2 text-center"> + <button class="button is-danger" + @click="promptBanIP">Ban IP</button> + <button class="button is-danger" + @click="promptDisableUser">Disable user</button> + </div> + </div> + </div> + </div> + </div> + </section> +</template> + +<script> +import Sidebar from '~/components/sidebar/Sidebar.vue'; + +export default { + components: { + Sidebar + }, + middleware: ['auth', 'admin'], + data() { + return { + options: {}, + file: null, + user: null + }; + }, + async asyncData({ $axios, route }) { + try { + const response = await $axios.$get(`file/${route.params.id}`); + return { + file: response.file ? response.file : null, + user: response.user ? response.user : null + }; + } catch (error) { + console.error(error); + return { + file: null, + user: null + }; + } + }, + methods: { + promptDisableUser() { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to disable the account of the user that uploaded this file?', + onConfirm: () => this.disableUser() + }); + }, + async disableUser() { + const response = await this.$axios.$post('admin/users/disable', { + id: this.user.id + }); + this.$buefy.toast.open(response.message); + }, + promptBanIP() { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to ban the IP this file was uploaded from?', + onConfirm: () => this.banIP() + }); + }, + async banIP() { + const response = await this.$axios.$post('admin/ban/ip', { + ip: this.file.ip + }); + this.$buefy.toast.open(response.message); + }, + formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } + } +}; +</script> diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue new file mode 100644 index 0000000..052a641 --- /dev/null +++ b/src/site/pages/dashboard/admin/settings.vue @@ -0,0 +1,135 @@ +<template> + <section class="hero is-fullheight dashboard"> + <div class="hero-body"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle">Service settings</h2> + <hr> + + <b-field label="Service name" + message="Please enter the name which this service is gonna be identified as" + horizontal> + <b-input v-model="options.serviceName" + expanded /> + </b-field> + + <b-field label="Upload folder" + message="Where to store the files relative to the working directory" + horizontal> + <b-input v-model="options.uploadFolder" + expanded /> + </b-field> + + <b-field label="Links per album" + message="Maximum links allowed per album" + horizontal> + <b-input v-model="options.linksPerAlbum" + type="number" + expanded /> + </b-field> + + <b-field label="Max upload size" + message="Maximum allowed file size in MB" + horizontal> + <b-input v-model="options.maxUploadSize" + expanded /> + </b-field> + + <b-field label="Filename length" + message="How many characters long should the generated filenames be" + horizontal> + <b-input v-model="options.filenameLength" + expanded /> + </b-field> + + <b-field label="Album link length" + message="How many characters a link for an album should have" + horizontal> + <b-input v-model="options.albumLinkLength" + expanded /> + </b-field> + + <b-field label="Generate thumbnails" + message="Generate thumbnails when uploading a file if possible" + horizontal> + <b-switch v-model="options.generateThumbnails" + :true-value="true" + :false-value="false" /> + </b-field> + + <b-field label="Generate zips" + message="Allow generating zips to download entire albums" + horizontal> + <b-switch v-model="options.generateZips" + :true-value="true" + :false-value="false" /> + </b-field> + + <b-field label="Public mode" + message="Enable anonymous uploades" + horizontal> + <b-switch v-model="options.publicMode" + :true-value="true" + :false-value="false" /> + </b-field> + + <b-field label="Enable creating account" + message="Enable creating new accounts in the platform" + horizontal> + <b-switch v-model="options.enableAccounts" + :true-value="true" + :false-value="false" /> + </b-field> + + <div class="mb2 mt2 text-center"> + <button class="button is-primary" + @click="promptRestartService">Save and restart service</button> + </div> + </div> + </div> + </div> + </div> + </section> +</template> + +<script> +import Sidebar from '~/components/sidebar/Sidebar.vue'; + +export default { + components: { + Sidebar + }, + middleware: ['auth', 'admin'], + data() { + return { + options: {} + }; + }, + metaInfo() { + return { title: 'Settings' }; + }, + mounted() { + this.getSettings(); + }, + methods: { + async getSettings() { + const response = await this.$axios.$get(`service/config`); + this.options = response.config; + }, + promptRestartService() { + this.$buefy.dialog.confirm({ + message: 'Keep in mind that restarting only works if you have PM2 or something similar set up. Continue?', + onConfirm: () => this.restartService() + }); + }, + async restartService() { + const response = await this.$axios.$post(`service/restart`); + this.$buefy.toast.open(response.message); + } + } +}; +</script> diff --git a/src/site/pages/dashboard/admin/user/_id.vue b/src/site/pages/dashboard/admin/user/_id.vue new file mode 100644 index 0000000..7703b1c --- /dev/null +++ b/src/site/pages/dashboard/admin/user/_id.vue @@ -0,0 +1,102 @@ +<style lang="scss" scoped> + .underline { text-decoration: underline; } +</style> +<template> + <section class="hero is-fullheight dashboard"> + <div class="hero-body"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle">User details</h2> + <hr> + + <b-field label="User Id" + horizontal> + <span>{{ user.id }}</span> + </b-field> + + <b-field label="Username" + horizontal> + <span>{{ user.username }}</span> + </b-field> + + <b-field label="Enabled" + horizontal> + <span>{{ user.enabled }}</span> + </b-field> + + <b-field label="Registered" + horizontal> + <span><timeago :since="user.createdAt" /></span> + </b-field> + + <b-field label="Files" + horizontal> + <span>{{ files.length }}</span> + </b-field> + + <div class="mb2 mt2 text-center"> + <button class="button is-danger" + @click="promptDisableUser">Disable user</button> + </div> + + <Grid v-if="files.length" + :files="files" /> + </div> + </div> + </div> + </div> + </section> +</template> + +<script> +import Sidebar from '~/components/sidebar/Sidebar.vue'; +import Grid from '~/components/grid/Grid.vue'; + +export default { + components: { + Sidebar, + Grid + }, + middleware: ['auth', 'admin'], + data() { + return { + options: {}, + files: null, + user: null + }; + }, + async asyncData({ $axios, route }) { + try { + const response = await $axios.$get(`/admin/users/${route.params.id}`); + return { + files: response.files ? response.files : null, + user: response.user ? response.user : null + }; + } catch (error) { + console.error(error); + return { + files: null, + user: null + }; + } + }, + methods: { + promptDisableUser() { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to disable the account of the user that uploaded this file?', + onConfirm: () => this.disableUser() + }); + }, + async disableUser() { + const response = await this.$axios.$post('admin/users/disable', { + id: this.user.id + }); + this.$buefy.toast.open(response.message); + } + } +}; +</script> diff --git a/src/site/pages/dashboard/admin/users.vue b/src/site/pages/dashboard/admin/users.vue new file mode 100644 index 0000000..1fefa1e --- /dev/null +++ b/src/site/pages/dashboard/admin/users.vue @@ -0,0 +1,249 @@ +<style lang="scss" scoped> + @import '~/assets/styles/_colors.scss'; + div.view-container { + padding: 2rem; + } + div.album { + display: flex; + flex-wrap: wrap; + margin-bottom: 10px; + + div.arrow-container { + width: 2em; + height: 64px; + position: relative; + cursor: pointer; + + i { + border: 2px solid $defaultTextColor; + border-right: 0; + border-top: 0; + display: block; + height: 1em; + position: absolute; + transform: rotate(-135deg); + transform-origin: center; + width: 1em; + z-index: 4; + top: 22px; + + -webkit-transition: transform 0.1s linear; + -moz-transition: transform 0.1s linear; + -ms-transition: transform 0.1s linear; + -o-transition: transform 0.1s linear; + transition: transform 0.1s linear; + + &.active { + transform: rotate(-45deg); + } + } + } + div.thumb { + width: 64px; + height: 64px; + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + + div.info { + margin-left: 15px; + h4 { + font-size: 1.5rem; + a { + color: $defaultTextColor; + font-weight: 400; + &:hover { text-decoration: underline; } + } + } + span { display: block; } + span:nth-child(3) { + font-size: 0.9rem; + } + } + + div.latest { + flex-grow: 1; + justify-content: flex-end; + display: flex; + margin-left: 15px; + + span.no-files { + font-size: 1.5em; + color: #b1b1b1; + padding-top: 17px; + } + + div.more { + width: 64px; + height: 64px; + background: white; + display: flex; + align-items: center; + padding: 10px; + text-align: center; + a { + line-height: 1rem; + color: $defaultTextColor; + &:hover { text-decoration: underline; } + } + } + } + + div.details { + flex: 0 1 100%; + padding-left: 2em; + padding-top: 1em; + min-height: 50px; + + .b-table { + padding: 2em 0em; + + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } + } + } + + div.column > h2.subtitle { padding-top: 1px; } +</style> +<style lang="scss"> + @import '~/assets/styles/_colors.scss'; + + .b-table { + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } +</style> + + +<template> + <section class="hero is-fullheight dashboard"> + <div class="hero-body"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle">Manage your users</h2> + <hr> + + <div class="view-container"> + <b-table + :data="users || []" + :mobile-cards="true"> + <template slot-scope="props"> + <b-table-column field="id" + label="Id" + centered> + {{ props.row.id }} + </b-table-column> + + <b-table-column field="username" + label="Username" + centered> + <nuxt-link :to="`/dashboard/admin/user/${props.row.id}`">{{ props.row.username }}</nuxt-link> + </b-table-column> + + <b-table-column field="enabled" + label="Enabled" + centered> + <b-switch v-model="props.row.enabled" + @input="changeEnabledStatus(props.row)" /> + </b-table-column> + + <b-table-column field="isAdmin" + label="Admin" + centered> + <b-switch v-model="props.row.isAdmin" + @input="changeIsAdmin(props.row)" /> + </b-table-column> + + <b-table-column field="purge" + centered> + <button class="button is-primary" + @click="promptPurgeFiles(props.row)">Purge files</button> + </b-table-column> + </template> + <template slot="empty"> + <div class="has-text-centered"> + <i class="icon-misc-mood-sad" /> + </div> + <div class="has-text-centered"> + Nothing here + </div> + </template> + <template slot="footer"> + <div class="has-text-right"> + {{ users.length }} users + </div> + </template> + </b-table> + </div> + </div> + </div> + </div> + </div> + </section> +</template> + +<script> +import Sidebar from '~/components/sidebar/Sidebar.vue'; + +export default { + components: { + Sidebar + }, + middleware: ['auth', 'admin'], + data() { + return { + users: [] + }; + }, + computed: { + config() { + return this.$store.state.config; + } + }, + metaInfo() { + return { title: 'Uploads' }; + }, + mounted() { + this.getUsers(); + }, + methods: { + async getUsers() { + const response = await this.$axios.$get(`admin/users`); + this.users = response.users; + }, + async changeEnabledStatus(row) { + const response = await this.$axios.$post(`admin/users/${row.enabled ? 'enable' : 'disable'}`, { + id: row.id + }); + this.$buefy.toast.open(response.message); + }, + async changeIsAdmin(row) { + const response = await this.$axios.$post(`admin/users/${row.isAdmin ? 'promote' : 'demote'}`, { + id: row.id + }); + this.$buefy.toast.open(response.message); + }, + promptPurgeFiles(row) { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to delete this user\'s files?', + onConfirm: () => this.purgeFiles(row) + }); + }, + async purgeFiles(row) { + const response = await this.$axios.$post(`admin/users/purge`, { + id: row.id + }); + this.$buefy.toast.open(response.message); + } + } +}; +</script> |