aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/database/migrations/20190221225812_initialMigration.js24
-rw-r--r--src/api/database/migrations/20190221225813_addTags.js21
-rw-r--r--src/api/routes/admin/banIP.js25
-rw-r--r--src/api/routes/admin/unBanIP.js27
-rw-r--r--src/api/routes/admin/userGET.js32
-rw-r--r--src/api/routes/files/fileGET.js29
-rw-r--r--src/api/structures/Route.js5
-rw-r--r--src/site/assets/images/blank.pngbin466 -> 1892 bytes
-rw-r--r--src/site/components/grid/Grid.vue290
-rw-r--r--src/site/components/sidebar/Sidebar.vue4
-rw-r--r--src/site/pages/a/_identifier.vue3
-rw-r--r--src/site/pages/dashboard/admin/file/_id.vue170
-rw-r--r--src/site/pages/dashboard/admin/settings.vue (renamed from src/site/pages/dashboard/settings.vue)2
-rw-r--r--src/site/pages/dashboard/admin/user/_id.vue102
-rw-r--r--src/site/pages/dashboard/admin/users.vue (renamed from src/site/pages/dashboard/users.vue)4
15 files changed, 642 insertions, 96 deletions
diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js
index a9ce2c7..84bda7e 100644
--- a/src/api/database/migrations/20190221225812_initialMigration.js
+++ b/src/api/database/migrations/20190221225812_initialMigration.js
@@ -62,6 +62,27 @@ exports.up = async knex => {
table.integer('albumId');
table.integer('linkId');
});
+
+ await knex.schema.createTable('tags', table => {
+ table.increments();
+ table.string('uuid');
+ table.integer('userId');
+ table.string('name');
+ table.timestamp('createdAt');
+ table.timestamp('editedAt');
+ });
+
+ await knex.schema.createTable('fileTags', table => {
+ table.increments();
+ table.integer('fileId');
+ table.integer('tagId');
+ });
+
+ await knex.schema.createTable('bans', table => {
+ table.increments();
+ table.string('ip');
+ table.timestamp('createdAt');
+ });
};
exports.down = async knex => {
await knex.schema.dropTableIfExists('users');
@@ -70,4 +91,7 @@ exports.down = async knex => {
await knex.schema.dropTableIfExists('links');
await knex.schema.dropTableIfExists('albumsFiles');
await knex.schema.dropTableIfExists('albumsLinks');
+ await knex.schema.dropTableIfExists('tags');
+ await knex.schema.dropTableIfExists('fileTags');
+ await knex.schema.dropTableIfExists('bans');
};
diff --git a/src/api/database/migrations/20190221225813_addTags.js b/src/api/database/migrations/20190221225813_addTags.js
deleted file mode 100644
index ef71877..0000000
--- a/src/api/database/migrations/20190221225813_addTags.js
+++ /dev/null
@@ -1,21 +0,0 @@
-exports.up = async knex => {
- await knex.schema.createTable('tags', table => {
- table.increments();
- table.string('uuid');
- table.integer('userId');
- table.string('name');
- table.timestamp('createdAt');
- table.timestamp('editedAt');
- });
-
- await knex.schema.createTable('fileTags', table => {
- table.increments();
- table.integer('fileId');
- table.integer('tagId');
- });
-};
-
-exports.down = async knex => {
- await knex.schema.dropTableIfExists('tags');
- await knex.schema.dropTableIfExists('fileTags');
-};
diff --git a/src/api/routes/admin/banIP.js b/src/api/routes/admin/banIP.js
new file mode 100644
index 0000000..692880d
--- /dev/null
+++ b/src/api/routes/admin/banIP.js
@@ -0,0 +1,25 @@
+const Route = require('../../structures/Route');
+
+class banIP extends Route {
+ constructor() {
+ super('/admin/ban/ip', 'post', { adminOnly: true });
+ }
+
+ async run(req, res, db) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { ip } = req.body;
+ if (!ip) return res.status(400).json({ message: 'No ip provided' });
+
+ try {
+ await db.table('bans').insert({ ip });
+ } catch (error) {
+ return super.error(res, error);
+ }
+
+ return res.json({
+ message: 'Successfully banned the ip'
+ });
+ }
+}
+
+module.exports = banIP;
diff --git a/src/api/routes/admin/unBanIP.js b/src/api/routes/admin/unBanIP.js
new file mode 100644
index 0000000..493834b
--- /dev/null
+++ b/src/api/routes/admin/unBanIP.js
@@ -0,0 +1,27 @@
+const Route = require('../../structures/Route');
+
+class unBanIP extends Route {
+ constructor() {
+ super('/admin/unban/ip', 'post', { adminOnly: true });
+ }
+
+ async run(req, res, db) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { ip } = req.body;
+ if (!ip) return res.status(400).json({ message: 'No ip provided' });
+
+ try {
+ await db.table('bans')
+ .where({ ip })
+ .delete();
+ } catch (error) {
+ return super.error(res, error);
+ }
+
+ return res.json({
+ message: 'Successfully unbanned the ip'
+ });
+ }
+}
+
+module.exports = unBanIP;
diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js
new file mode 100644
index 0000000..895a565
--- /dev/null
+++ b/src/api/routes/admin/userGET.js
@@ -0,0 +1,32 @@
+const Route = require('../../structures/Route');
+const Util = require('../../utils/Util');
+
+class usersGET extends Route {
+ constructor() {
+ super('/admin/users/:id', 'get', { adminOnly: true });
+ }
+
+ async run(req, res, db) {
+ const { id } = req.params;
+ if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' });
+
+ try {
+ const user = await db.table('users').where({ id }).first();
+ const files = await db.table('files').where({ userId: user.id });
+
+ for (let file of files) {
+ file = Util.constructFilePublicLink(file);
+ }
+
+ return res.json({
+ message: 'Successfully retrieved user',
+ user,
+ files
+ });
+ } catch (error) {
+ return super.error(res, error);
+ }
+ }
+}
+
+module.exports = usersGET;
diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js
new file mode 100644
index 0000000..3bb8da4
--- /dev/null
+++ b/src/api/routes/files/fileGET.js
@@ -0,0 +1,29 @@
+const Route = require('../../structures/Route');
+const Util = require('../../utils/Util');
+
+class filesGET extends Route {
+ constructor() {
+ super('/file/:id', 'get', { adminOnly: true });
+ }
+
+ async run(req, res, db) {
+ const { id } = req.params;
+ if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
+
+ let file = await db.table('files').where({ id }).first();
+ const user = await db.table('users').where({ id: file.userId }).first();
+ file = Util.constructFilePublicLink(file);
+
+ // Additional relevant data
+ const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
+ user.fileCount = filesFromUser.length;
+
+ return res.json({
+ message: 'Successfully retrieved file',
+ file,
+ user
+ });
+ }
+}
+
+module.exports = filesGET;
diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js
index c04c585..2db9bc6 100644
--- a/src/api/structures/Route.js
+++ b/src/api/structures/Route.js
@@ -52,7 +52,10 @@ class Route {
this.options = options || {};
}
- authorize(req, res) {
+ async authorize(req, res) {
+ const banned = await db.table('bans').where({ ip: req.ip }).first();
+ if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' });
+
if (this.options.bypassAuth) return this.run(req, res, db);
if (req.headers.apiKey) return this.authorizeApiKey(req, res, req.headers.apiKey);
if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' });
diff --git a/src/site/assets/images/blank.png b/src/site/assets/images/blank.png
index 8615039..224a81c 100644
--- a/src/site/assets/images/blank.png
+++ b/src/site/assets/images/blank.png
Binary files differ
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue
index 19acde9..2e2f911 100644
--- a/src/site/components/grid/Grid.vue
+++ b/src/site/components/grid/Grid.vue
@@ -4,6 +4,30 @@
transition: all .25s cubic-bezier(.55,0,.1,1);
-webkit-transition: all .25s cubic-bezier(.55,0,.1,1);
}
+
+ div.toolbar {
+ padding: 1rem;
+
+ .block {
+ text-align: right;
+ }
+ }
+
+ span.extension {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ pointer-events: none;
+ opacity: .75;
+ }
+
div.actions {
opacity: 0;
-webkit-transition: opacity 0.1s linear;
@@ -23,6 +47,11 @@
span {
padding: 3px;
+ &.more {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
&:nth-child(1), &:nth-child(2) {
align-items: flex-end;
@@ -38,7 +67,7 @@
justify-content: center;
align-items: center;
display: flex;
- &:before {
+ &.btn:before {
content: '';
width: 30px;
height: 30px;
@@ -76,64 +105,174 @@
</style>
<template>
- <Waterfall
- :gutterWidth="10"
- :gutterHeight="4">
- <!-- Gotta implement search and pagination here -->
- <input v-if="enableSearch"
- v-model="searchTerm"
- type="text"
- placeholder="Search..."
- @input="search()"
- @keyup.enter="search()">
-
- <WaterfallItem v-for="(item, index) in files"
- v-if="showWaterfall && item.thumb"
- :key="index"
- :width="width"
- move-class="item-move">
- <template v-if="isPublic">
- <a :href="`${item.url}`"
- target="_blank">
- <img :src="`${item.thumb}`">
- </a>
- </template>
- <template v-else>
- <img :src="`${item.thumb}`">
- <div v-if="!isPublic"
- :class="{ fixed }"
- class="actions">
- <b-tooltip label="Link"
- position="is-top">
- <a :href="`${item.url}`"
- target="_blank">
- <i class="icon-web-code" />
- </a>
- </b-tooltip>
- <b-tooltip label="Albums"
- position="is-top">
- <a @click="$parent.openAlbumModal(item)">
- <i class="icon-interface-window" />
- </a>
- </b-tooltip>
- <!--
- <b-tooltip label="Tags"
- position="is-top">
- <a @click="manageTags(item)">
- <i class="icon-ecommerce-tag-c" />
- </a>
- </b-tooltip>
- -->
- <b-tooltip label="Delete"
- position="is-top">
- <a @click="deleteFile(item, index)">
- <i class="icon-editorial-trash-a-l" />
- </a>
- </b-tooltip>
- </div>
- </template>
- </WaterfallItem>
- </Waterfall>
+ <div>
+ <div v-if="enableToolbar"
+ class="toolbar">
+ <div class="block">
+ <b-radio v-model="showList"
+ name="name"
+ :native-value="true">
+ List
+ </b-radio>
+ <b-radio v-model="showList"
+ name="name"
+ :native-value="false">
+ Grid
+ </b-radio>
+ </div>
+ </div>
+
+ <Waterfall v-if="!showList"
+ :gutterWidth="10"
+ :gutterHeight="4">
+ <!--
+ TODO: Implement search based on originalName, albumName and tags
+ <input v-if="enableSearch"
+ v-model="searchTerm"
+ type="text"
+ placeholder="Search..."
+ @input="search()"
+ @keyup.enter="search()">
+ -->
+
+ <!-- TODO: Implement pagination -->
+
+ <WaterfallItem v-for="(item, index) in files"
+ v-if="showWaterfall"
+ :key="index"
+ :width="width"
+ move-class="item-move">
+ <template v-if="isPublic">
+ <a :href="`${item.url}`"
+ target="_blank">
+ <img :src="item.thumb ? item.thumb : blank">
+ </a>
+ </template>
+ <template v-else>
+ <img :src="item.thumb ? item.thumb : blank">
+ <span v-if="!item.thumb && item.name"
+ class="extension">{{ item.name.split('.').pop() }}</span>
+ <div v-if="!isPublic"
+ :class="{ fixed }"
+ class="actions">
+ <b-tooltip label="Link"
+ position="is-top">
+ <a :href="`${item.url}`"
+ target="_blank"
+ class="btn">
+ <i class="icon-web-code" />
+ </a>
+ </b-tooltip>
+ <b-tooltip label="Albums"
+ position="is-top">
+ <a class="btn"
+ @click="$parent.openAlbumModal(item)">
+ <i class="icon-interface-window" />
+ </a>
+ </b-tooltip>
+ <!--
+ <b-tooltip label="Tags"
+ position="is-top">
+ <a @click="manageTags(item)">
+ <i class="icon-ecommerce-tag-c" />
+ </a>
+ </b-tooltip>
+ -->
+ <b-tooltip label="Delete"
+ position="is-top">
+ <a class="btn"
+ @click="deleteFile(item, index)">
+ <i class="icon-editorial-trash-a-l" />
+ </a>
+ </b-tooltip>
+ <b-tooltip v-if="user && user.isAdmin"
+ label="More info"
+ position="is-top"
+ class="more">
+ <nuxt-link :to="`/dashboard/admin/file/${item.id}`">
+ <i class="icon-interface-more" />
+ </nuxt-link>
+ </b-tooltip>
+ </div>
+ </template>
+ </WaterfallItem>
+ </Waterfall>
+ <div v-else>
+ <b-table
+ :data="files || []"
+ :mobile-cards="true">
+ <template slot-scope="props">
+ <template v-if="!props.row.hideFromList">
+ <b-table-column field="url"
+ label="URL">
+ <a :href="props.row.url"
+ target="_blank">{{ props.row.url }}</a>
+ </b-table-column>
+
+ <b-table-column field="albums"
+ label="Albums"
+ centered>
+ <template v-for="(album, index) in props.row.albums">
+ <nuxt-link :key="index"
+ :to="`/dashboard/albums/${album.id}`">
+ {{ album.name }}
+ </nuxt-link>
+ <template v-if="index < props.row.albums.length - 1">, </template>
+ </template>
+
+ {{ props.row.username }}
+ </b-table-column>
+
+ <b-table-column field="uploaded"
+ label="Uploaded"
+ centered>
+ <span><timeago :since="props.row.createdAt" /></span>
+ </b-table-column>
+
+ <b-table-column field="purge"
+ centered>
+ <b-tooltip label="Albums"
+ position="is-top">
+ <a class="btn"
+ @click="$parent.openAlbumModal(props.row)">
+ <i class="icon-interface-window" />
+ </a>
+ </b-tooltip>
+ <b-tooltip label="Delete"
+ position="is-top"
+ class="is-danger">
+ <a class="is-danger"
+ @click="deleteFile(props.row)">
+ <i class="icon-editorial-trash-a-l" />
+ </a>
+ </b-tooltip>
+ <b-tooltip v-if="user && user.isAdmin"
+ label="More info"
+ position="is-top"
+ class="more">
+ <nuxt-link :to="`/dashboard/admin/file/${props.row.id}`">
+ <i class="icon-interface-more" />
+ </nuxt-link>
+ </b-tooltip>
+ </b-table-column>
+ </template>
+ </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">
+ {{ files.length }} files
+ </div>
+ </template>
+ </b-table>
+ </div>
+ </div>
</template>
<script>
import Waterfall from './waterfall/Waterfall.vue';
@@ -164,16 +303,26 @@ export default {
enableSearch: {
type: Boolean,
default: true
+ },
+ enableToolbar: {
+ type: Boolean,
+ default: true
}
},
data() {
return {
showWaterfall: true,
- searchTerm: null
+ searchTerm: null,
+ showList: false
};
},
- mounted() {
- this.$search.items(this.files);
+ computed: {
+ user() {
+ return this.$store.state.user;
+ },
+ blank() {
+ return require('@/assets/images/blank.png');
+ }
},
methods: {
async search() {
@@ -194,12 +343,17 @@ export default {
hasIcon: true,
onConfirm: async () => {
const response = await this.$axios.$delete(`file/${file.id}`);
- this.showWaterfall = false;
- this.files.splice(index, 1);
- this.$nextTick(() => {
- this.showWaterfall = true;
- });
- return this.buefy.$toast.open(response.message);
+ if (this.showList) {
+ file.hideFromList = true;
+ this.$forceUpdate();
+ } else {
+ this.showWaterfall = false;
+ this.files.splice(index, 1);
+ this.$nextTick(() => {
+ this.showWaterfall = true;
+ });
+ }
+ return this.$buefy.toast.open(response.message);
}
});
}
diff --git a/src/site/components/sidebar/Sidebar.vue b/src/site/components/sidebar/Sidebar.vue
index 101a70b..9fe1be5 100644
--- a/src/site/components/sidebar/Sidebar.vue
+++ b/src/site/components/sidebar/Sidebar.vue
@@ -46,12 +46,12 @@
</router-link>
<template v-if="user && user.isAdmin">
<hr>
- <router-link to="/dashboard/users">
+ <router-link to="/dashboard/admin/users">
<i class="icon-setting-gear-a" />Users
</router-link>
<!--
TODO: Dont wanna deal with this now
- <router-link to="/dashboard/settings">
+ <router-link to="/dashboard/admin/settings">
<i class="icon-setting-gear-a" />Settings
</router-link>
-->
diff --git a/src/site/pages/a/_identifier.vue b/src/site/pages/a/_identifier.vue
index 923e8cc..62035b3 100644
--- a/src/site/pages/a/_identifier.vue
+++ b/src/site/pages/a/_identifier.vue
@@ -35,7 +35,8 @@
:files="files"
:isPublic="true"
:width="200"
- :enableSearch="false" />
+ :enableSearch="false"
+ :enableToolbar="false" />
</div>
</div>
</template>
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/settings.vue b/src/site/pages/dashboard/admin/settings.vue
index 35a23e8..052a641 100644
--- a/src/site/pages/dashboard/settings.vue
+++ b/src/site/pages/dashboard/admin/settings.vue
@@ -103,7 +103,7 @@ export default {
components: {
Sidebar
},
- middleware: 'admin',
+ middleware: ['auth', 'admin'],
data() {
return {
options: {}
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/users.vue b/src/site/pages/dashboard/admin/users.vue
index 66ccebe..1fefa1e 100644
--- a/src/site/pages/dashboard/users.vue
+++ b/src/site/pages/dashboard/admin/users.vue
@@ -146,7 +146,7 @@
<b-table-column field="username"
label="Username"
centered>
- {{ props.row.username }}
+ <nuxt-link :to="`/dashboard/admin/user/${props.row.id}`">{{ props.row.username }}</nuxt-link>
</b-table-column>
<b-table-column field="enabled"
@@ -192,7 +192,7 @@
</template>
<script>
-import Sidebar from '../../components/sidebar/Sidebar.vue';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {