aboutsummaryrefslogtreecommitdiff
path: root/src/site/pages/dashboard/admin
diff options
context:
space:
mode:
authorPitu <[email protected]>2021-01-04 01:04:20 +0900
committerPitu <[email protected]>2021-01-04 01:04:20 +0900
commitfcd39dc550dec8dbcb8325e07e938c5024cbc33d (patch)
treef41acb4e0d5fd3c3b1236fe4324b3fef9ec6eafe /src/site/pages/dashboard/admin
parentCreate FUNDING.yml (diff)
parentchore: update todo (diff)
downloadhost.fuwn.me-fcd39dc550dec8dbcb8325e07e938c5024cbc33d.tar.xz
host.fuwn.me-fcd39dc550dec8dbcb8325e07e938c5024cbc33d.zip
Merge branch 'dev'
Diffstat (limited to 'src/site/pages/dashboard/admin')
-rw-r--r--src/site/pages/dashboard/admin/file/_id.vue176
-rw-r--r--src/site/pages/dashboard/admin/settings.vue162
-rw-r--r--src/site/pages/dashboard/admin/user/_id.vue120
-rw-r--r--src/site/pages/dashboard/admin/users.vue247
4 files changed, 705 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..135d066
--- /dev/null
+++ b/src/site/pages/dashboard/admin/file/_id.vue
@@ -0,0 +1,176 @@
+<style lang="scss" scoped>
+ .underline { text-decoration: underline; }
+</style>
+<template>
+ <section class="section is-fullheight dashboard">
+ <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>{{ admin.file.id }}</span>
+ </b-field>
+
+ <b-field
+ label="Name"
+ horizontal>
+ <span>{{ admin.file.name }}</span>
+ </b-field>
+
+ <b-field
+ label="Original Name"
+ horizontal>
+ <span>{{ admin.file.original }}</span>
+ </b-field>
+
+ <b-field
+ label="IP"
+ horizontal>
+ <span class="underline">{{ admin.file.ip }}</span>
+ </b-field>
+
+ <b-field
+ label="Link"
+ horizontal>
+ <a
+ :href="admin.file.url"
+ target="_blank">{{ admin.file.url }}</a>
+ </b-field>
+
+ <b-field
+ label="Size"
+ horizontal>
+ <span>{{ formatBytes(admin.file.size) }}</span>
+ </b-field>
+
+ <b-field
+ label="Hash"
+ horizontal>
+ <span>{{ admin.file.hash }}</span>
+ </b-field>
+
+ <b-field
+ label="Uploaded"
+ horizontal>
+ <span><timeago :since="admin.file.createdAt" /></span>
+ </b-field>
+ </div>
+ <div class="column is-6">
+ <b-field
+ label="User Id"
+ horizontal>
+ <span>{{ admin.user.id }}</span>
+ </b-field>
+
+ <b-field
+ label="Username"
+ horizontal>
+ <span>{{ admin.user.username }}</span>
+ </b-field>
+
+ <b-field
+ label="Enabled"
+ horizontal>
+ <span>{{ admin.user.enabled }}</span>
+ </b-field>
+
+ <b-field
+ label="Registered"
+ horizontal>
+ <span><timeago :since="admin.user.createdAt" /></span>
+ </b-field>
+
+ <b-field
+ label="Files"
+ horizontal>
+ <span>
+ <nuxt-link :to="`/dashboard/admin/user/${admin.user.id}`">{{ admin.user.fileCount }}</nuxt-link>
+ </span>
+ </b-field>
+ </div>
+ </div>
+
+ <div class="mb2 mt2 text-center">
+ <b-button
+ v-if="admin.user.id !== auth.user.id"
+ type="is-danger"
+ @click="promptBanIP">
+ Ban IP
+ </b-button>
+ <b-button
+ v-if="admin.user.id !== auth.user.id"
+ type="is-danger"
+ @click="promptDisableUser">
+ Disable user
+ </b-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
+
+export default {
+ components: {
+ Sidebar
+ },
+ middleware: ['auth', 'admin', ({ route, store }) => {
+ try {
+ store.dispatch('admin/fetchFile', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
+ computed: mapState(['admin', 'auth']),
+ methods: {
+ promptDisableUser() {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to disable the account of the user that uploaded this file?',
+ onConfirm: () => this.disableUser()
+ });
+ },
+ disableUser() {
+ this.$handler.executeAction('admin/disableUser', this.user.id);
+ },
+ promptBanIP() {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to ban the IP this file was uploaded from?',
+ onConfirm: () => this.banIP()
+ });
+ },
+ banIP() {
+ this.$handler.executeAction('admin/banIP', this.file.ip);
+ },
+ 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));
+
+ // eslint-disable-next-line no-mixed-operators
+ return `${parseFloat((bytes / 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..71df2a6
--- /dev/null
+++ b/src/site/pages/dashboard/admin/settings.vue
@@ -0,0 +1,162 @@
+<template>
+ <section class="section is-fullheight dashboard">
+ <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="settings.serviceName"
+ class="chibisafe-input"
+ expanded />
+ </b-field>
+
+ <b-field
+ label="Upload folder"
+ message="Where to store the files relative to the working directory"
+ horizontal>
+ <b-input
+ v-model="settings.uploadFolder"
+ class="chibisafe-input"
+ expanded />
+ </b-field>
+
+ <b-field
+ label="Links per album"
+ message="Maximum links allowed per album"
+ horizontal>
+ <b-input
+ v-model="settings.linksPerAlbum"
+ class="chibisafe-input"
+ type="number"
+ expanded />
+ </b-field>
+
+ <b-field
+ label="Max upload size"
+ message="Maximum allowed file size in MB"
+ horizontal>
+ <b-input
+ v-model="settings.maxUploadSize"
+ class="chibisafe-input"
+ expanded />
+ </b-field>
+
+ <b-field
+ label="Filename length"
+ message="How many characters long should the generated filenames be"
+ horizontal>
+ <b-input
+ v-model="settings.filenameLength"
+ class="chibisafe-input"
+ 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="settings.albumLinkLength"
+ class="chibisafe-input"
+ expanded />
+ </b-field>
+
+ <b-field
+ label="Generate thumbnails"
+ message="Generate thumbnails when uploading a file if possible"
+ horizontal>
+ <b-switch
+ v-model="settings.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="settings.generateZips"
+ :true-value="true"
+ :false-value="false" />
+ </b-field>
+
+ <b-field
+ label="Public mode"
+ message="Enable anonymous uploades"
+ horizontal>
+ <b-switch
+ v-model="settings.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="settings.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>
+ </section>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
+
+export default {
+ components: {
+ Sidebar
+ },
+ middleware: ['auth', 'admin', ({ store }) => {
+ try {
+ store.dispatch('admin/fetchSettings');
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
+ metaInfo() {
+ return { title: 'Settings' };
+ },
+ computed: mapState({
+ settings: state => state.admin.settings
+ }),
+ methods: {
+ 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()
+ });
+ },
+ restartService() {
+ this.$handler.executeAction('admin/restartService');
+ }
+ }
+};
+</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..0ed3e86
--- /dev/null
+++ b/src/site/pages/dashboard/admin/user/_id.vue
@@ -0,0 +1,120 @@
+<style lang="scss" scoped>
+ .underline { text-decoration: underline; }
+</style>
+<template>
+ <section class="section is-fullheight dashboard">
+ <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>{{ user.files.length }}</span>
+ </b-field>
+
+ <div class="mb2 mt2 text-center">
+ <b-button
+ v-if="user.enabled"
+ type="is-danger"
+ @click="promptDisableUser">
+ Disable user
+ </b-button>
+ <b-button
+ v-if="!user.enabled"
+ type="is-success"
+ @click="promptEnableUser">
+ Enable user
+ </b-button>
+ </div>
+
+ <Grid
+ v-if="user.files.length"
+ :files="user.files" />
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
+import Grid from '~/components/grid/Grid.vue';
+
+export default {
+ components: {
+ Sidebar,
+ Grid
+ },
+ middleware: ['auth', 'admin', ({ route, store }) => {
+ try {
+ store.dispatch('admin/fetchUser', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
+ data() {
+ return {
+ options: {}
+ };
+ },
+ computed: mapState({
+ user: state => state.admin.user
+ }),
+ methods: {
+ promptDisableUser() {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to disable the account of this user?',
+ onConfirm: () => this.disableUser()
+ });
+ },
+ promptEnableUser() {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to enable the account of this user?',
+ onConfirm: () => this.enableUser()
+ });
+ },
+ disableUser() {
+ this.$handler.executeAction('admin/disableUser', this.user.id);
+ },
+ enableUser() {
+ this.$handler.executeAction('admin/enableUser', this.user.id);
+ }
+ }
+};
+</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..d86bffd
--- /dev/null
+++ b/src/site/pages/dashboard/admin/users.vue
@@ -0,0 +1,247 @@
+<template>
+ <section class="section is-fullheight dashboard">
+ <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">
+ <b-table-column
+ v-slot="props"
+ field="id"
+ label="Id"
+ centered>
+ {{ props.row.id }}
+ </b-table-column>
+
+ <b-table-column
+ v-slot="props"
+ 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
+ v-slot="props"
+ field="enabled"
+ label="Enabled"
+ centered>
+ <b-switch
+ :value="props.row.enabled"
+ @input="changeEnabledStatus(props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ v-slot="props"
+ field="isAdmin"
+ label="Admin"
+ centered>
+ <b-switch
+ :value="props.row.isAdmin"
+ @input="changeIsAdmin(props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ v-slot="props"
+ field="purge"
+ centered>
+ <b-button
+ type="is-danger"
+ @click="promptPurgeFiles(props.row)">
+ Purge files
+ </b-button>
+ </b-table-column>
+
+ <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>
+ </section>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
+
+export default {
+ components: {
+ Sidebar
+ },
+ middleware: ['auth', 'admin', ({ route, store }) => {
+ try {
+ store.dispatch('admin/fetchUsers', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
+ computed: mapState({
+ users: state => state.admin.users,
+ config: state => state.config
+ }),
+ metaInfo() {
+ return { title: 'Uploads' };
+ },
+ methods: {
+ async changeEnabledStatus(row) {
+ if (row.enabled) {
+ this.$handler.executeAction('admin/disableUser', row.id);
+ } else {
+ this.$handler.executeAction('admin/enableUser', row.id);
+ }
+ },
+ async changeIsAdmin(row) {
+ if (row.isAdmin) {
+ this.$handler.executeAction('admin/demoteUser', row.id);
+ } else {
+ this.$handler.executeAction('admin/promoteUser', row.id);
+ }
+ },
+ 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) {
+ this.$handler.executeAction('admin/purgeUserFiles', row.id);
+ }
+ }
+};
+</script>
+
+<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;
+ transition: transform 0.1s linear;
+
+ &.active {
+ transform: rotate(-45deg);
+ }
+ }
+ }
+ div.thumb {
+ width: 64px;
+ height: 64px;
+ 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 {
+ box-shadow: $boxShadowLight;
+ }
+ }
+ }
+ }
+
+ div.column > h2.subtitle { padding-top: 1px; }
+
+ .b-table {
+ .table-wrapper {
+ box-shadow: $boxShadowLight;
+ }
+ }
+</style>