feat: try fixing THE SHITTY WATERFALL

This commit is contained in:
Zephyrrus 2020-07-07 02:02:59 +03:00
parent 15f296a780
commit fb0bc57542
16 changed files with 404 additions and 303 deletions

View File

@ -11,14 +11,14 @@ const clientConfig = {
maxFileSize: parseInt(process.env.MAX_SIZE, 10),
chunkSize: parseInt(process.env.CHUNK_SIZE, 10),
maxLinksPerAlbum: parseInt(process.env.MAX_LINKS_PER_ALBUM, 10),
publicMode: process.env.PUBLIC_MODE === 'true' ? true : false,
userAccounts: process.env.USER_ACCOUNTS === 'true' ? true : false
publicMode: process.env.PUBLIC_MODE === 'true',
userAccounts: process.env.USER_ACCOUNTS === 'true',
};
export default {
mode: 'spa',
server: {
port: process.env.WEBSITE_PORT
port: process.env.WEBSITE_PORT,
},
srcDir: 'src/site/',
head: {
@ -34,7 +34,7 @@ export default {
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: `${process.env.SERVICE_NAME}`
content: `${process.env.SERVICE_NAME}`,
},
{ hid: 'application-name', name: 'application-name', content: `${process.env.SERVICE_NAME}` },
// { hid: 'msapplication-config', name: 'msapplication-config', content: `${process.env.DOMAIN}/browserconfig.xml` },
@ -50,14 +50,14 @@ export default {
{ hid: 'og:description', property: 'og:description', content: `${process.env.META_DESCRIPTION}` },
{ hid: 'og:image', property: 'og:image', content: `${process.env.DOMAIN}/share.jpg` },
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${process.env.DOMAIN}/share.jpg` },
{ hid: 'og:site_name', property: 'og:site_name', content: `${process.env.SERVICE_NAME}` }
{ hid: 'og:site_name', property: 'og:site_name', content: `${process.env.SERVICE_NAME}` },
],
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
// This one is a pain in the ass to make it customizable, so you should edit it manually
{ type: 'application/json+oembed', href: `${process.env.DOMAIN}/oembed.json` }
]
{ type: 'application/json+oembed', href: `${process.env.DOMAIN}/oembed.json` },
],
},
plugins: [
'~/plugins/axios',
@ -67,19 +67,20 @@ export default {
'~/plugins/vue-timeago',
'~/plugins/flexsearch',
'~/plugins/vuebar',
'~/plugins/nuxt-client-init'
'~/plugins/nuxt-client-init',
'~/plugins/notifier',
],
css: [],
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt'],
axios: {
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`,
},
build: {
extractCSS: true,
postcss: {
preset: {
autoprefixer
}
autoprefixer,
},
},
extend(config, { isClient, isDev }) {
// Extend only webpack config for client-bundle
@ -89,6 +90,6 @@ export default {
if (isDev) {
config.devtool = isClient ? 'source-map' : 'inline-source-map';
}
}
}
},
},
};

View File

@ -41,6 +41,7 @@ class albumGET extends Route {
count = files.length;
}
// eslint-disable-next-line no-restricted-syntax
for (let file of files) {
file = Util.constructFilePublicLink(file);
}
@ -49,7 +50,7 @@ class albumGET extends Route {
message: 'Successfully retrieved album',
name: album.name,
files,
count
count,
});
}
}

View File

@ -25,6 +25,7 @@ class albumGET extends Route {
.orderBy('files.id', 'desc');
// Create the links for each file
// eslint-disable-next-line no-restricted-syntax
for (let file of files) {
file = Util.constructFilePublicLink(file);
}

View File

@ -1,9 +1,11 @@
/* eslint-disable no-console */
const randomstring = require('randomstring');
const jetpack = require('fs-jetpack');
const qoa = require('qoa');
qoa.config({
prefix: '>',
underlineQuery: false
underlineQuery: false,
});
async function start() {
@ -15,82 +17,82 @@ async function start() {
{
type: 'input',
query: 'Port to run the API in:',
handle: 'SERVER_PORT'
handle: 'SERVER_PORT',
},
{
type: 'input',
query: 'Port to run the Website in when in dev mode:',
handle: 'WEBSITE_PORT'
handle: 'WEBSITE_PORT',
},
{
type: 'input',
query: 'Full domain this instance is gonna be running on (Ex: https://lolisafe.moe):',
handle: 'DOMAIN'
handle: 'DOMAIN',
},
{
type: 'input',
query: 'Name of the service? (Ex: lolisafe):',
handle: 'SERVICE_NAME'
handle: 'SERVICE_NAME',
},
{
type: 'input',
query: 'Maximum allowed upload file size in MB (Ex: 100):',
handle: 'MAX_SIZE'
handle: 'MAX_SIZE',
},
{
type: 'confirm',
query: 'Generate thumbnails for images/videos? (Requires ffmpeg installed and in your PATH)',
handle: 'GENERATE_THUMBNAILS',
accept: 'y',
deny: 'n'
deny: 'n',
},
{
type: 'confirm',
query: 'Allow users to download entire albums in ZIP format?',
handle: 'GENERATE_ZIPS',
accept: 'y',
deny: 'n'
deny: 'n',
},
{
type: 'confirm',
query: 'Serve files with node?',
handle: 'SERVE_WITH_NODE',
accept: 'y',
deny: 'n'
deny: 'n',
},
{
type: 'input',
query: 'Base number of characters for generated file URLs (12 should be good enough):',
handle: 'GENERATED_FILENAME_LENGTH'
handle: 'GENERATED_FILENAME_LENGTH',
},
{
type: 'input',
query: 'Base number of characters for generated album URLs (6 should be enough):',
handle: 'GENERATED_ALBUM_LENGTH'
handle: 'GENERATED_ALBUM_LENGTH',
},
{
type: 'confirm',
query: 'Run lolisafe in public mode? (People will be able to upload without an account)',
handle: 'PUBLIC_MODE',
accept: 'y',
deny: 'n'
deny: 'n',
},
{
type: 'confirm',
query: 'Enable user signup for new accounts?',
handle: 'USER_ACCOUNTS',
accept: 'y',
deny: 'n'
deny: 'n',
},
{
type: 'input',
query: 'Name of the admin account?',
handle: 'ADMIN_ACCOUNT'
handle: 'ADMIN_ACCOUNT',
},
{
type: 'secure',
query: 'Type a secure password for the admin account:',
handle: 'ADMIN_PASSWORD'
handle: 'ADMIN_PASSWORD',
},
{
type: 'interactive',
@ -100,29 +102,29 @@ async function start() {
menu: [
'sqlite3',
'pg',
'mysql'
]
'mysql',
],
},
{
type: 'input',
query: 'Database host (Ignore if you selected sqlite3):',
handle: 'DB_HOST'
handle: 'DB_HOST',
},
{
type: 'input',
query: 'Database user (Ignore if you selected sqlite3):',
handle: 'DB_USER'
handle: 'DB_USER',
},
{
type: 'input',
query: 'Database password (Ignore if you selected sqlite3):',
handle: 'DB_PASSWORD'
handle: 'DB_PASSWORD',
},
{
type: 'input',
query: 'Database name (Ignore if you selected sqlite3):',
handle: 'DB_DATABASE'
}
handle: 'DB_DATABASE',
},
];
const response = await qoa.prompt(wizard);
@ -140,12 +142,13 @@ async function start() {
META_THEME_COLOR: '#20222b',
META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀',
META_KEYWORDS: 'lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
META_TWITTER_HANDLE: '@its_pitu'
META_TWITTER_HANDLE: '@its_pitu',
};
const allSettings = Object.assign(defaultSettings, response);
const keys = Object.keys(allSettings);
// eslint-disable-next-line no-restricted-syntax
for (const item of keys) {
envfile += `${item}=${allSettings[item]}\n`;
}

View File

@ -24,12 +24,10 @@
<Waterfall
v-if="showWaterfall"
:gutterWidth="10"
:gutterHeight="4">
<WaterfallItem
v-for="(item, index) in gridFiles"
:key="item.id"
:width="width"
move-class="item-move">
:gutterHeight="4"
:itemWidth="width"
:items="gridFiles">
<template v-slot="{item}">
<template v-if="isPublic">
<a
:href="`${item.url}`"
@ -81,7 +79,7 @@
</a>
</b-tooltip>
<b-tooltip label="Delete" position="is-top">
<a class="btn" @click="deleteFile(item, index)">
<a class="btn" @click="deleteFile(item)">
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
@ -92,7 +90,7 @@
</b-tooltip>
</div>
</template>
</WaterfallItem>
</template>
</Waterfall>
</template>
<div v-else>
@ -185,12 +183,10 @@
import { mapState } from 'vuex';
import Waterfall from './waterfall/Waterfall.vue';
import WaterfallItem from './waterfall/WaterfallItem.vue';
export default {
components: {
Waterfall,
WaterfallItem,
},
props: {
files: {
@ -253,13 +249,13 @@ export default {
const data = await this.$search.do(this.searchTerm, ['name', 'original', 'type', 'albums:name']);
console.log('> Search result data', data);
},
deleteFile(file, index) {
this.$buefy.dialog.confirm({
deleteFile(file) {
this.$emit('delete', file);
/* this.$buefy.dialog.confirm({
title: 'Deleting file',
message: 'Are you sure you want to <b>delete</b> this file?',
confirmText: 'Delete File',
type: 'is-danger',
hasIcon: true,
onConfirm: async () => {
const response = await this.$axios.$delete(`file/${file.id}`);
if (this.showList) {
@ -274,7 +270,7 @@ export default {
}
return this.$buefy.toast.open(response.message);
},
});
}); */
},
isAlbumSelected(id) {
if (!this.showingModalForFile) return false;
@ -311,10 +307,6 @@ export default {
const foundIndex = this.hoveredItems.indexOf(id);
if (foundIndex > -1) return;
this.hoveredItems.push(id);
/// XXX: THIS IS NOT OK!
this.$nextTick(() => {
this.$refs.video.forEach((e) => e.play().catch(() => {}));
});
},
mouseOut(id) {
console.log('out', id);

View File

@ -1,14 +1,14 @@
<style>
.waterfall {
position: relative;
}
</style>
<template>
<div class="waterfall">
<slot />
<WaterfallItem v-for="(item, index) in items" :key="item.id" :ref="`item-${item.id}`" :width="itemWidth">
<slot :item="item" />
</WaterfallItem>
</div>
</template>
<script>
import WaterfallItem from './WaterfallItem.vue';
const quickSort = (arr, type) => {
const left = [];
const right = [];
@ -49,6 +49,9 @@ const sum = (arr) => arr.reduce((acc, val) => acc + val, 0);
export default {
name: 'Waterfall',
components: {
WaterfallItem,
},
props: {
gutterWidth: {
type: Number,
@ -82,6 +85,14 @@ export default {
type: Array,
default: null,
},
itemWidth: {
type: Number,
default: 150,
},
items: {
type: Array,
default: () => [],
},
},
data() {
return {
@ -89,15 +100,21 @@ export default {
colNum: 0,
lastWidth: 0,
percentWidthArr: [],
readyChildCount: 0,
};
},
watch: {
items() {
this.$nextTick(() => this.render('watch'));
},
},
created() {
this.$on('itemRender', () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.render();
this.render('created');
}, 0);
});
},
@ -105,6 +122,10 @@ export default {
this.resizeHandle();
this.$watch('resizable', this.resizeHandle);
},
beforeDestroy() {
this.$off('itemRender');
_.off(window, 'resize', this.render);
},
methods: {
calulate(arr) {
const pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
@ -135,10 +156,12 @@ export default {
_.off(window, 'resize', this.render, false);
}
},
render() {
render(context) {
console.log(context);
if (!this.items) return;
//
let childArr = [];
childArr = this.$children.map((child) => child.getMeta());
childArr = this.items.map(({ id }) => this.$refs[`item-${id}`][0].getMeta());
childArr = quickSort(childArr, 'order');
//
this.calulate(childArr[0]);
@ -180,3 +203,9 @@ export default {
},
};
</script>
<style>
.waterfall {
position: relative;
}
</style>

View File

@ -1,9 +1,3 @@
<style scoped>
.waterfall-item {
position: absolute;
}
</style>
<template>
<div class="waterfall-item">
<slot />
@ -80,3 +74,9 @@ export default {
},
};
</script>
<style scoped>
.waterfall-item {
position: absolute;
}
</style>

View File

@ -20,15 +20,21 @@
<template v-if="files && files.length">
<div class="align-top">
<div class="container">
<h1 class="title">{{ name }}</h1>
<h2 class="subtitle">Serving {{ files ? files.length : 0 }} files</h2>
<a v-if="downloadLink"
<h1 class="title">
{{ name }}
</h1>
<h2 class="subtitle">
Serving {{ files ? files.length : 0 }} files
</h2>
<a
v-if="downloadLink"
:href="downloadLink">Download Album</a>
<hr>
</div>
</div>
<div class="container">
<Grid v-if="files && files.length"
<Grid
v-if="files && files.length"
:files="files"
:isPublic="true"
:width="200"
@ -38,16 +44,20 @@
</template>
<template v-else>
<div class="container">
<h1 class="title">:(</h1>
<h2 class="subtitle">This album seems to be empty</h2>
<h1 class="title">
:(
</h1>
<h2 class="subtitle">
This album seems to be empty
</h2>
</div>
</template>
</section>
</template>
<script>
import Grid from '~/components/grid/Grid.vue';
import axios from 'axios';
import Grid from '~/components/grid/Grid.vue';
export default {
components: { Grid },
@ -57,7 +67,7 @@ export default {
computed: {
config() {
return this.$store.state.config;
}
},
},
async asyncData({ app, params, error }) {
try {
@ -67,7 +77,7 @@ export default {
name: data.name,
downloadEnabled: data.downloadEnabled,
files: data.files,
downloadLink
downloadLink,
};
} catch (err) {
console.log('Error when retrieving album', err);
@ -90,8 +100,8 @@ export default {
{ vmid: 'og:title', property: 'og:title', content: `Album: ${this.name} | Files: ${this.files.length}` },
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
{ vmid: 'og:image', property: 'og:image', content: `${this.files.length > 0 ? this.files[0].thumbSquare : '/public/images/share.jpg'}` },
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${this.files.length > 0 ? this.files[0].thumbSquare : '/public/images/share.jpg'}` }
]
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${this.files.length > 0 ? this.files[0].thumbSquare : '/public/images/share.jpg'}` },
],
};
}
return {
@ -103,9 +113,9 @@ export default {
{ vmid: 'twitter:description', name: 'twitter:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
{ vmid: 'og:url', property: 'og:url', content: `${this.config.URL}/a/${this.$route.params.identifier}` },
{ vmid: 'og:title', property: 'og:title', content: 'lolisafe' },
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' }
]
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
],
};
}
},
};
</script>

View File

@ -6,62 +6,82 @@
<Sidebar />
</div>
<div class="column">
<h2 class="subtitle">Account settings</h2>
<h2 class="subtitle">
Account settings
</h2>
<hr>
<b-field label="Username"
<b-field
label="Username"
message="Nothing to do here"
horizontal>
<b-input :value="user.username"
<b-input
:value="user.username"
expanded
disabled />
</b-field>
<b-field label="Current password"
<b-field
label="Current password"
message="If you want to change your password input the current one here"
horizontal>
<b-input v-model="password"
<b-input
v-model="password"
type="password"
expanded />
</b-field>
<b-field label="New password"
<b-field
label="New password"
message="Your new password"
horizontal>
<b-input v-model="newPassword"
<b-input
v-model="newPassword"
type="password"
expanded />
</b-field>
<b-field label="New password again"
<b-field
label="New password again"
message="Your new password once again"
horizontal>
<b-input v-model="reNewPassword"
<b-input
v-model="reNewPassword"
type="password"
expanded />
</b-field>
<div class="mb2 mt2 text-center">
<button class="button is-primary"
@click="changePassword">Change password</button>
<button
class="button is-primary"
@click="changePassword">
Change password
</button>
</div>
<b-field label="API key"
<b-field
label="API key"
message="This API key lets you use the service from other apps"
horizontal>
<b-field expanded>
<b-input :value="apiKey"
<b-input
:value="apiKey"
expanded
disabled />
<p class="control">
<button class="button is-primary">Copy</button>
<button class="button is-primary">
Copy
</button>
</p>
</b-field>
</b-field>
<div class="mb2 mt2 text-center">
<button class="button is-primary"
@click="promptNewAPIKey">Request new API key</button>
<button
class="button is-primary"
@click="promptNewAPIKey">
Request new API key
</button>
</div>
</div>
</div>
@ -75,7 +95,7 @@ import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
Sidebar
Sidebar,
},
middleware: ['auth', ({ store }) => {
store.dispatch('auth/fetchCurrentUser');
@ -84,21 +104,21 @@ export default {
return {
password: '',
newPassword: '',
reNewPassword: ''
reNewPassword: '',
};
},
},
computed: {
...mapGetters({ 'apiKey': 'auth/getApiKey' }),
...mapState({
user: state => state.auth.user
})
user: (state) => state.auth.user,
}),
},
metaInfo() {
return { title: 'Account' };
},
methods: {
...mapActions({
getUserSetttings: 'auth/fetchCurrentUser'
getUserSetttings: 'auth/fetchCurrentUser',
}),
async changePassword() {
const { password, newPassword, reNewPassword } = this;
@ -106,21 +126,21 @@ export default {
if (!password || !newPassword || !reNewPassword) {
this.$store.dispatch('alert/set', {
text: 'One or more fields are missing',
error: true
error: true,
});
return;
}
if (newPassword !== reNewPassword) {
this.$store.dispatch('alert/set', {
text: 'Passwords don\'t match',
error: true
error: true,
});
return;
}
const response = await this.$store.dispatch('auth/changePassword', {
password,
newPassword
newPassword,
});
if (response) {
@ -131,13 +151,13 @@ export default {
this.$buefy.dialog.confirm({
type: 'is-danger',
message: 'Are you sure you want to regenerate your API key? Previously generated API keys will stop working. Make sure to write the new key down as this is the only time it will be displayed to you.',
onConfirm: () => this.requestNewAPIKey()
onConfirm: () => this.requestNewAPIKey(),
});
},
async requestNewAPIKey() {
const response = await this.$store.dispatch('auth/requestAPIKey');
const response = await this.$store.dispatch('auth/requestAPIKey');
this.$buefy.toast.open(response.message);
}
}
},
},
};
</script>

View File

@ -9,40 +9,51 @@
<Sidebar />
</div>
<div class="column">
<h2 class="subtitle">User details</h2>
<h2 class="subtitle">
User details
</h2>
<hr>
<b-field label="User Id"
<b-field
label="User Id"
horizontal>
<span>{{ user.id }}</span>
</b-field>
<b-field label="Username"
<b-field
label="Username"
horizontal>
<span>{{ user.username }}</span>
</b-field>
<b-field label="Enabled"
<b-field
label="Enabled"
horizontal>
<span>{{ user.enabled }}</span>
</b-field>
<b-field label="Registered"
<b-field
label="Registered"
horizontal>
<span><timeago :since="user.createdAt" /></span>
</b-field>
<b-field label="Files"
<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>
<button
class="button is-danger"
@click="promptDisableUser">
Disable user
</button>
</div>
<Grid v-if="files.length"
<Grid
v-if="files.length"
:files="files" />
</div>
</div>
@ -57,14 +68,14 @@ import Grid from '~/components/grid/Grid.vue';
export default {
components: {
Sidebar,
Grid
Grid,
},
middleware: ['auth', 'admin'],
data() {
return {
options: {},
files: null,
user: null
user: null,
};
},
async asyncData({ $axios, route }) {
@ -72,13 +83,13 @@ export default {
const response = await $axios.$get(`/admin/users/${route.params.id}`);
return {
files: response.files ? response.files : null,
user: response.user ? response.user : null
user: response.user ? response.user : null,
};
} catch (error) {
console.error(error);
return {
files: null,
user: null
user: null,
};
}
},
@ -87,15 +98,15 @@ export default {
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()
onConfirm: () => this.disableUser(),
});
},
async disableUser() {
const response = await this.$axios.$post('admin/users/disable', {
id: this.user.id
id: this.user.id,
});
this.$buefy.toast.open(response.message);
}
}
},
},
};
</script>

View File

@ -1,3 +1,143 @@
<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">
<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>
</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>
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
div.view-container {
@ -107,9 +247,6 @@
}
div.column > h2.subtitle { padding-top: 1px; }
</style>
<style lang="scss">
@import '~/assets/styles/_colors.scss';
.b-table {
.table-wrapper {
@ -118,130 +255,3 @@
}
}
</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">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>
</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>

View File

@ -14,7 +14,7 @@
<div class="level-left">
<div class="level-item">
<h1 class="title is-3">
{{ name }}
{{ images.name }}
</h1>
</div>
<div class="level-item">
@ -45,7 +45,7 @@
<Grid
v-if="totalFiles"
:files="album.files"
:files="images.files"
:total="totalFiles">
<template v-slot:pagination>
<b-pagination
@ -83,7 +83,7 @@ export default {
Grid,
},
middleware: ['auth', ({ route, store }) => {
store.dispatch('album/fetchById', { id: route.params.id });
store.dispatch('images/fetchByAlbumId', { id: route.params.id });
}],
data() {
return {
@ -92,12 +92,11 @@ export default {
},
computed: {
...mapGetters({
totalFiles: 'album/getTotalFiles',
shouldPaginate: 'album/shouldPaginate',
limit: 'album/getLimit',
name: 'album/getName',
totalFiles: 'images/getTotalFiles',
shouldPaginate: 'images/shouldPaginate',
limit: 'images/getLimit',
}),
...mapState(['album']),
...mapState(['images']),
id() {
return this.$route.params.id;
},
@ -110,7 +109,7 @@ export default {
},
methods: {
...mapActions({
fetch: 'album/fetchById',
fetch: 'images/fetchByAlbumId',
}),
fetchPaginate() {
this.fetch({ id: this.id, page: this.current });

View File

@ -9,7 +9,9 @@
<nav class="level">
<div class="level-left">
<div class="level-item">
<h2 class="subtitle">Your uploaded files</h2>
<h2 class="subtitle">
Your uploaded files
</h2>
</div>
</div>
<div class="level-right">
@ -17,7 +19,7 @@
<b-field>
<b-input
placeholder="Search"
type="search"/>
type="search" />
<p class="control">
<button
outlined
@ -28,15 +30,17 @@
</b-field>
</div>
</div>
</nav>
</nav>
<hr>
<b-loading :active="images.isLoading" />
<Grid v-if="totalFiles"
<Grid
v-if="totalFiles && !isLoading"
:files="images.files"
:enableSearch="false"
class="grid">
class="grid"
@delete="handleFileDelete">
<template v-slot:pagination>
<b-pagination
v-if="shouldPaginate"
@ -70,38 +74,44 @@ import Grid from '~/components/grid/Grid.vue';
export default {
components: {
Sidebar,
Grid
Grid,
},
middleware: ['auth', ({ store }) => {
store.dispatch('images/fetch');
}],
data() {
return {
current: 1
current: 1,
isLoading: false,
};
},
computed: {
...mapGetters({
...mapGetters({
totalFiles: 'images/getTotalFiles',
shouldPaginate: 'images/shouldPaginate',
limit: 'images/getLimit'
limit: 'images/getLimit',
}),
...mapState(['images'])
...mapState(['images']),
},
metaInfo() {
return { title: 'Uploads' };
},
watch: {
current: 'fetchPaginate'
current: 'fetchPaginate',
},
methods: {
...mapActions({
fetch: 'images/fetch'
fetch: 'images/fetch',
}),
fetchPaginate() {
this.fetch(this.current);
}
}
async fetchPaginate() {
this.isLoading = true;
await this.fetch(this.current);
this.isLoading = false;
},
handleFileDelete(file) {
console.log('yep!', file.id);
},
},
};
</script>

View File

@ -10,13 +10,15 @@
<div class="columns">
<div class="column is-4 is-offset-4">
<b-field>
<b-input v-model="username"
<b-input
v-model="username"
type="text"
placeholder="Username"
@keyup.enter.native="login" />
</b-field>
<b-field>
<b-input v-model="password"
<b-input
v-model="password"
type="password"
placeholder="Password"
password-reveal
@ -24,12 +26,18 @@
</b-field>
<p class="control has-addons is-pulled-right">
<router-link v-if="config.userAccounts"
<router-link
v-if="config.userAccounts"
to="/register"
class="is-text">Don't have an account?</router-link>
class="is-text">
Don't have an account?
</router-link>
<span v-else>Registration is closed at the moment</span>
<button class="button is-primary big ml1"
@click="login">login</button>
<button
class="button is-primary big ml1"
@click="login">
login
</button>
</p>
</div>
</div>
@ -73,7 +81,7 @@ export default {
password: null,
mfaCode: null,
isMfaModalActive: false,
isLoading: false
isLoading: false,
};
},
computed: mapState(['config', 'auth']),
@ -87,21 +95,27 @@ export default {
},
methods: {
async login() {
if (this.auth.isLoading) return;
if (this.isLoading) return;
const { username, password } = this;
if (!username || !password) {
this.$store.dispatch('alert/set', {
text: 'Please fill both fields before attempting to log in.',
error: true
error: true,
});
return;
}
await this.$store.dispatch('auth/login', { username, password });
if (this.auth.loggedIn) {
this.redirect();
try {
this.isLoading = true;
await this.$store.dispatch('auth/login', { username, password });
if (this.auth.loggedIn) {
this.redirect();
}
} catch (e) {
this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
} finally {
this.isLoading = false;
}
},
/*
@ -118,14 +132,14 @@ export default {
this.isLoading = false;
this.$onPromiseError(err);
});
},*/
}, */
redirect() {
if (typeof this.$route.query.redirect !== 'undefined') {
this.$router.push(this.$route.query.redirect);
return;
}
this.$router.push('/dashboard');
}
}
},
},
};
</script>

View File

@ -1,7 +1,7 @@
export default ({ store }, inject) => {
inject('notifier', {
showMessage({ content = '', type = '' }) {
store.commit('alert/set', { content, color });
}
store.commit('alert/set', { content, type });
},
});
};