Switch to Nuxt.js

This commit is contained in:
Pitu 2018-09-19 04:45:50 -03:00
parent 8e1711ed6c
commit 430af8306b
42 changed files with 2608 additions and 1398 deletions

2
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules/
_dist/
.ream/
.nuxt/
# Log files
logs/
@ -15,3 +16,4 @@ logs/
config.js
database.db
uploads/
src/oldsite

38
nuxt.config.js Normal file
View File

@ -0,0 +1,38 @@
import autoprefixer from 'autoprefixer';
import serveStatic from 'serve-static';
import path from 'path';
import config from './config';
export default {
server: {
port: config.server.ports.frontend
},
srcDir: 'src/site/',
head: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' }
]
},
plugins: [
'~/plugins/vue-axios',
'~/plugins/buefy',
'~/plugins/v-clipboard',
'~/plugins/vue-analytics',
'~/plugins/vue-isyourpasswordsafe',
'~/plugins/vue-timeago'
],
serverMiddleware: [
{ path: '/', handler: serveStatic(path.join(__dirname, 'uploads')) }
],
css: [],
build: {
extractCSS: true,
postcss: [
autoprefixer
]
}
};

View File

@ -12,8 +12,9 @@
"scripts": {
"api": "nodemon src/start api",
"site": "node src/start site",
"build": "ream build",
"start": "cross-env NODE_ENV=production node src/start"
"build": "nuxt build",
"start": "cross-env NODE_ENV=production node src/start && nuxt start",
"nuxt": "nuxt --port 5002"
},
"repository": {
"type": "git",
@ -48,67 +49,43 @@
"moment": "^2.22.1",
"multer": "^1.3.0",
"nuxt-dropzone": "^0.2.7",
"nuxt-edge": "^2.0.0-25621471.65432e6",
"one-liner": "^1.3.0",
"path": "^0.12.7",
"pg": "^7.4.3",
"randomstring": "^1.1.5",
"serve-static": "^1.13.2",
"sharp": "^0.20.3",
"v-clipboard": "^1.0.4",
"vue-analytics": "^5.9.1",
"vue-axios": "^2.0.2",
"vue-isyourpasswordsafe": "^1.0.1",
"vue-lazyload": "^1.2.2",
"vue-plyr": "^2.1.1",
"vue-timeago": "^3.4.4",
"vuex": "^3.0.1"
},
"devDependencies": {
"babel-eslint": "^8.2.2",
"autoprefixer": "^9.1.5",
"babel-eslint": "^9.0.0",
"cross-env": "^5.1.4",
"eslint": "^4.19.1",
"eslint-config-aqua": "^3.0.0",
"eslint-plugin-vue": "^4.4.0",
"node-sass": "^4.7.2",
"eslint": "^5.6.0",
"eslint-config-aqua": "^4.4.1",
"eslint-plugin-vue": "^5.0.0-beta.3",
"node-sass": "^4.9.3",
"nodemon": "^1.17.5",
"postcss-nested": "^3.0.0",
"ream": "^3.2.7",
"sass-loader": "^6.0.7",
"sass-loader": "^7.1.0",
"vue-eslint-parser": "^2.0.3"
},
"eslintConfig": {
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "babel-eslint"
},
"extends": [
"plugin:vue/recommended",
"aqua"
"aqua/vue",
"aqua/node"
],
"env": {
"browser": true,
"node": true
},
"rules": {
"func-names": 0,
"capitalized-comments": 0,
"max-len": 0,
"id-length": 0,
"no-warning-comments": 0,
"vue/html-indent": [
"error",
"tab"
],
"vue/max-attributes-per-line": [
2,
{
"singleline": 1,
"multiline": {
"max": 1,
"allowFirstLine": true
}
}
],
"vue/attribute-hyphenation": 0
"vue/attribute-hyphenation": 0,
"quote-props": 0
}
},
"keywords": [

View File

@ -0,0 +1,380 @@
const Route = require('../../structures/Route');
const config = require('../../../../config');
const path = require('path');
const multer = require('multer');
const Util = require('../../utils/Util');
const db = require('knex')(config.server.database);
const moment = require('moment');
const log = require('../../utils/Log');
const jetpack = require('fs-jetpack');
const Busboy = require('busboy');
const fs = require('fs');
// WE SHOULD ALSO STRIP EXIF UNLESS THE USER SPECIFIED THEY WANT IT.
// https://github.com/WeebDev/lolisafe/issues/110
class uploadPOST extends Route {
constructor() {
super('/upload', 'post', { bypassAuth: true });
}
async run(req, res) {
const user = Util.isAuthorized(req);
if (!user && !config.uploads.allowAnonymousUploads) return res.status(401).json({ message: 'Not authorized to use this resource' });
/*
const albumId = req.body.albumId || req.headers.albumId;
if (this.albumId && !this.user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
if (this.albumId && this.user) {
const album = await db.table('albums').where({ id: this.albumId, userId: this.user.id }).first();
if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
}
*/
return this.uploadFile(req, res, user);
}
async processFile(req, res, user, file) {
/*
Check if the user is trying to upload to an album
*/
const albumId = req.body.albumId || req.headers.albumId;
if (albumId && !user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
if (albumId && user) {
const album = await db.table('albums').where({ id: albumId, userId: user.id }).first();
if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
}
let upload = file.data;
/*
If it's a chunked upload but this is not the last part of the chunk, just green light.
Otherwise, put the file together and process it
*/
if (file.body.uuid) {
if (file.body.chunkindex < file.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if
/*
We got a chunk that is not the last part, send smoke signal that we received it.
*/
return res.json({ message: 'Successfully uploaded chunk' });
} else {
/*
Seems we finally got the last part of a chunk upload
*/
const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder);
const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', file.body.uuid);
const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' });
const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.'));
const tempFile = {
filename: Util.getUniqueFilename(originalname),
originalname,
size: file.body.totalfilesize
};
for (const chunkFile of chunkFiles) {
try {
const data = await jetpack.readAsync(chunkFile, 'buffer'); // eslint-disable-line no-await-in-loop
await jetpack.appendAsync(path.join(uploadsDir, tempFile.filename), data); // eslint-disable-line no-await-in-loop
} catch (error) {
console.error(error);
}
}
upload = tempFile;
}
}
console.log(upload);
const hash = await Util.getFileHash(upload.filename); // eslint-disable-line no-await-in-loop
const exists = await db.table('files') // eslint-disable-line no-await-in-loop
.where(function() {
if (!user) this.whereNull('userId'); // eslint-disable-line no-invalid-this
else this.where('userId', user.id); // eslint-disable-line no-invalid-this
})
.where({
hash,
size: upload.size
})
.first();
if (exists) {
res.json({
message: 'Successfully uploaded file',
name: exists.name,
size: exists.size,
url: `${config.filesServeLocation}/${exists.name}`
});
return Util.deleteFile(upload.filename);
}
const now = moment.utc().toDate();
try {
await db.table('files').insert({
userId: user ? user.id : null,
name: upload.filename,
original: upload.originalname,
type: upload.mimetype || '',
size: upload.size,
hash,
ip: req.ip,
albumId: albumId ? albumId : null,
createdAt: now,
editedAt: now
});
} catch (error) {
log.error('There was an error saving the file to the database');
console.log(error);
return res.status(500).json({ message: 'There was an error uploading the file.' });
}
res.json({
message: 'Successfully uploaded file',
name: upload.filename,
size: upload.size,
url: `${config.filesServeLocation}/${upload.filename}`
});
if (albumId) {
try {
db.table('albums').where('id', albumId).update('editedAt', now);
} catch (error) {
log.error('There was an error updating editedAt on an album');
console.error(error);
}
}
// return Util.generateThumbnail(file.filename);
}
uploadFile(req, res, user) {
const busboy = new Busboy({
headers: req.headers,
limits: {
fileSize: config.uploads.uploadMaxSize * (1000 * 1000),
files: 1
}
});
const fileToUpload = {
data: {},
body: {}
};
/*
Note: For this to work on every case, whoever is uploading a chunk
should really send the body first and the file last. Otherwise lolisafe
may not catch the field on time and the chunk may end up being saved
as a standalone file, completely broken.
*/
busboy.on('field', (fieldname, val) => {
if (/^dz/.test(fieldname)) {
fileToUpload.body[fieldname.substring(2)] = val;
} else {
fileToUpload.body[fieldname] = val;
}
});
/*
Hey ther's a file! Let's upload it.
*/
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
let name, saveTo;
/*
Let check whether the file is part of a chunk upload or if it's a standalone one.
If the former, we should store them separately and join all the pieces after we
receive the last one.
*/
if (!fileToUpload.body.uuid) {
name = Util.getUniqueFilename(filename);
if (!name) return res.status(500).json({ message: 'There was a problem allocating a filename for your upload' });
saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, name);
} else {
name = `${filename}.${fileToUpload.body.chunkindex}`;
const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid);
jetpack.dir(chunkDir);
saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid, name);
}
/*
Let's save some metadata for the db.
*/
fileToUpload.data = { filename: name, originalname: filename, encoding, mimetype };
const stream = fs.createWriteStream(saveTo);
file.on('data', data => {
fileToUpload.data.size = data.length;
});
/*
The file that is being uploaded is bigger than the limit specified on the config file
and thus we should close the stream and delete the file.
*/
file.on('limit', () => {
file.unpipe(stream);
stream.end();
jetpack.removeAsync(saveTo);
res.status(400).json({ message: 'The file is too big.' });
});
file.pipe(stream);
});
busboy.on('error', err => {
log.error('There was an error uploading a file');
console.error(err);
return res.status(500).json({ message: 'There was an error uploading the file.' });
});
busboy.on('finish', () => this.processFile(req, res, user, fileToUpload));
req.pipe(busboy);
// return req.pipe(busboy);
/*
return upload(this.req, this.res, async err => {
if (err) {
log.error('There was an error uploading a file');
console.error(err);
return this.res.status(500).json({ message: 'There was an error uploading the file.' });
}
log.info('---');
console.log(this.req.file);
log.info('---');
let file = this.req.file;
if (this.req.body.uuid) {
// If it's a chunked upload but this is not the last part of the chunk, just green light.
// Otherwise, put the file together and process it
if (this.req.body.chunkindex < this.req.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if
log.info('Hey this is a chunk, sweet.');
return this.res.json({ message: 'Successfully uploaded chunk' });
} else {
log.info('Hey this is the last part of a chunk, sweet.');
const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder);
const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', this.req.body.uuid);
const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' });
const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.'));
const tempFile = {
filename: Util.getUniqueFilename(originalname),
originalname,
size: this.req.body.totalfilesize
};
for (const chunkFile of chunkFiles) {
try {
const data = await jetpack.readAsync(chunkFile, 'buffer'); // eslint-disable-line no-await-in-loop
await jetpack.appendAsync(path.join(uploadsDir, tempFile.filename), data); // eslint-disable-line no-await-in-loop
} catch (error) {
console.error(error);
}
}
file = tempFile;
}
}
const { user } = this;
// console.log(file);
if (!file.filename) return log.error('This file doesnt have a filename!');
// console.log(file);
const hash = await Util.getFileHash(file.filename); // eslint-disable-line no-await-in-loop
const exists = await db.table('files') // eslint-disable-line no-await-in-loop
.where(function() {
if (!user) this.whereNull('userId'); // eslint-disable-line no-invalid-this
else this.where('userId', user.id); // eslint-disable-line no-invalid-this
})
.where({
hash,
size: file.size
})
.first();
if (exists) {
this.res.json({
message: 'Successfully uploaded file',
name: exists.name,
size: exists.size,
url: `${config.filesServeLocation}/${exists.name}`
});
return Util.deleteFile(file.filename);
}
const now = moment.utc().toDate();
try {
await db.table('files').insert({
userId: this.user ? this.user.id : null,
name: file.filename,
original: file.originalname,
type: file.mimetype || '',
size: file.size,
hash,
ip: this.req.ip,
albumId: this.albumId ? this.albumId : null,
createdAt: now,
editedAt: now
});
} catch (error) {
log.error('There was an error saving the file to the database');
console.log(error);
return this.res.status(500).json({ message: 'There was an error uploading the file.' });
}
this.res.json({
message: 'Successfully uploaded file',
name: file.filename,
size: file.size,
url: `${config.filesServeLocation}/${file.filename}`
});
if (this.albumId) {
try {
db.table('albums').where('id', this.albumId).update('editedAt', now);
} catch (error) {
log.error('There was an error updating editedAt on an album');
console.error(error);
}
}
// return Util.generateThumbnail(file.filename);
});
*/
}
}
/*
const upload = multer({
limits: config.uploads.uploadMaxSize,
fileFilter(req, file, cb) {
const ext = path.extname(file.originalname).toLowerCase();
if (Util.isExtensionBlocked(ext)) return cb('This file extension is not allowed');
// Remove those pesky dz prefixes. Thanks to BobbyWibowo.
for (const key in req.body) {
if (!/^dz/.test(key)) continue;
req.body[key.replace(/^dz/, '')] = req.body[key];
delete req.body[key];
}
return cb(null, true);
},
storage: multer.diskStorage({
destination(req, file, cb) {
if (!req.body.uuid) return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder));
// Hey, we have chunks
const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', req.body.uuid);
jetpack.dir(chunkDir);
return cb(null, chunkDir);
return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder));
},
filename(req, file, cb) {
// if (req.body.uuid) return cb(null, `${file.originalname}.${req.body.chunkindex}`);
const filename = Util.getUniqueFilename(file.originalname);
// if (!filename) return cb('Could not allocate a unique file name');
return cb(null, filename);
}
})
}).single('file');
*/
module.exports = uploadPOST;

View File

@ -1,208 +0,0 @@
<template>
<div id="app"
@dragover="isDrag = true"
@dragend="isDrag = false"
@dragleave="isDrag = false"
@drop="isDrag = false">
<router-view :key="$route.fullPath"/>
<!--
<div v-if="!ready"
id="loading">
<div class="background"/>
<Loading class="square"/>
</div>
-->
<div v-if="false"
id="drag-overlay">
<div class="background"/>
<div class="drop">
Drop your files here
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import Fuse from 'fuse.js';
import Logo from './components/logo/Logo.vue';
import Loading from './components/loading/CubeShadow.vue';
const protectedRoutes = [
'/dashboard',
'/dashboard/albums',
'/dashboard/settings'
];
export default {
components: {
Loading,
Logo
},
data() {
return {
pageTitle: '',
ready: false,
isDrag: false
};
},
computed: {
user() {
return this.$store.state.user;
},
loggedIn() {
return this.$store.state.loggedIn;
},
config() {
return this.$store.state.config;
}
},
mounted() {
console.log(`%c Running lolisafe %c v${this.config.version} %c`, 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 'background:#ff015b; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 'background:transparent');
this.$store.commit('config', Vue.prototype.$config);
this.ready = true;
},
metaInfo() { // eslint-disable-line complexity
return {
title: this.pageTitle || 'A small safe worth protecting.',
titleTemplate: '%s | lolisafe',
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700', body: true },
// { rel: 'stylesheet', href: 'https://cdn.materialdesignicons.com/2.1.99/css/materialdesignicons.min.css', body: true },
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/public/images/icons/apple-touch-icon.png' },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/public/images/icons/favicon-32x32.png' },
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/public/images/icons/favicon-16x16.png' },
{ rel: 'manifest', href: '/public/images/icons/manifest.json' },
{ rel: 'mask-icon', color: '#FF015B', href: '/public/images/icons/safari-pinned-tab.svg' },
{ rel: 'shortcut icon', href: '/public/images/icons/favicon.ico' },
{ rel: 'chrome-webstore-item', href: 'https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj' },
{ type: 'application/json+oembed', href: '/public/oembed.json' }
],
meta: [
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
{ vmid: 'description', name: '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: 'keywords', name: 'keywords', content: 'lolisafe, file, upload, uploader, vue, node, open source, free' },
{ vmid: 'apple-mobile-web-app-title', name: 'apple-mobile-web-app-title', content: 'lolisafe' },
{ vmid: 'application-name', name: 'application-name', content: 'lolisafe' },
{ vmid: 'msapplication-config', name: 'msapplication-config', content: '/public/images/icons/browserconfig.xml' },
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary_large_image' },
{ vmid: 'twitter:site', name: 'twitter:site', content: '@its_pitu' },
{ vmid: 'twitter:creator', name: 'twitter:creator', content: '@its_pitu' },
{ vmid: 'twitter:title', name: 'twitter:title', content: `lolisafe` },
{ 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: 'twitter:image', name: 'twitter:image', content: '/public/images/share.jpg' },
{ vmid: 'og:url', property: 'og:url', content: 'https://lolisafe.moe' },
{ vmid: 'og:type', property: 'og:type', content: 'website' },
{ 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:image', property: 'og:image', content: '/public/images/share.jpg' },
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: '/public/images/share.jpg' },
{ vmid: 'og:site_name', property: 'og:site_name', content: 'lolisafe' }
]
};
},
created() {
/*
Register our global handles
*/
const App = this; // eslint-disable-line consistent-this
this.$store.commit('config', Vue.prototype.$config);
Vue.prototype.$search = function(term, list, options) {
return new Promise(resolve => {
const run = new Fuse(list, options);
const results = run.search(term);
return resolve(results);
});
};
Vue.prototype.$onPromiseError = function(error, logout = false) {
App.processCatch(error, logout);
};
Vue.prototype.$showToast = function(text, error, duration) {
App.showToast(text, error, duration);
};
Vue.prototype.$logOut = function() {
App.$store.commit('user', null);
App.$store.commit('loggedIn', false);
App.$store.commit('token', null);
};
this.$router.beforeEach((to, from, next) => {
if (this.$store.state.loggedIn) return next();
if (process.browser) {
if (localStorage && localStorage.getItem('ls-token')) return this.tryToLogin(next, `/login?redirect=${to.path}`);
}
for (const match of to.matched) {
if (protectedRoutes.includes(match.path)) {
if (this.$store.state.loggedIn === false) return next(`/login?redirect=${to.path}`);
}
}
return next();
});
if (process.browser) this.tryToLogin();
},
methods: {
showToast(text, error, duration) {
this.$toast.open({
duration: duration || 2500,
message: text,
position: 'is-bottom',
type: error ? 'is-danger' : 'is-success'
});
},
processCatch(error, logout) {
if (error.response && error.response.data && error.response.data.message) {
this.showToast(error.response.data.message, true, 5000);
if (error.response.status === 429) return;
if (error.response.status === 502) return;
if (logout) {
this.$logOut();
setTimeout(() => this.$router.push('/'), 3000);
}
} else {
console.error(error);
this.showToast('Something went wrong, please check the console :(', true, 5000);
}
},
tryToLogin(next, destination) {
if (process.browser) this.$store.commit('token', localStorage.getItem('ls-token'));
this.axios.get(`${this.$config.baseURL}/verify`).then(res => {
this.$store.commit('user', res.data.user);
this.$store.commit('loggedIn', true);
if (next) return next();
return null;
}).catch(error => {
if (error.response && error.response.status === 520) return;
if (error.response && error.response.status === 429) {
setTimeout(() => {
this.tryToLogin(next, destination);
}, 1000);
return next(false);
} else {
this.$store.commit('user', null);
this.$store.commit('loggedIn', false);
this.$store.commit('token', null);
if (next && destination) return next(destination);
if (next) return next('/');
return null;
}
});
}
}
};
</script>
<style lang="scss">
@import "./styles/style.scss";
@import "./styles/icons.min.css";
</style>

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/_colors.scss';
@import '~/assets/styles/_colors.scss';
.item-move {
transition: all .25s cubic-bezier(.55,0,.1,1);
-webkit-transition: all .25s cubic-bezier(.55,0,.1,1);
@ -99,25 +99,25 @@
position="is-top">
<a :href="`${item.url}`"
target="_blank">
<i class="icon-web-code"/>
<i class="icon-web-code" />
</a>
</b-tooltip>
<b-tooltip label="Albums"
position="is-top">
<a @click="manageAlbums(item)">
<i class="icon-interface-window"/>
<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"/>
<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"/>
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
</div>
@ -155,6 +155,11 @@ export default {
data() {
return { showWaterfall: true };
},
computed: {
config() {
return this.$store.state.config;
}
},
methods: {
deleteFile(file, index) {
this.$dialog.confirm({
@ -165,7 +170,7 @@ export default {
hasIcon: true,
onConfirm: async () => {
try {
const response = await this.axios.delete(`${this.$config.baseURL}/file/${file.id}`);
const response = await this.axios.delete(`${this.config.baseURL}/file/${file.id}`);
this.showWaterfall = false;
this.files.splice(index, 1);
this.$nextTick(() => {

View File

@ -5,20 +5,19 @@
</style>
<template>
<div class="waterfall">
<slot/>
<slot />
</div>
</template>
<script>
// import {quickSort, getMinIndex, _, sum} from './util'
const quickSort = (arr, type) => {
let left = [];
let right = [];
let povis;
const left = [];
const right = [];
if (arr.length <= 1) {
return arr;
}
povis = arr[0];
const povis = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i][type] < povis[type]) {
left.push(arr[i]);

View File

@ -5,7 +5,7 @@
</style>
<template>
<div class="waterfall-item">
<slot/>
<slot />
</div>
</template>
<script>

View File

@ -1,76 +0,0 @@
<template>
<div class="vue-waterfall-slot" v-show="isShow">
<slot></slot>
</div>
</template>
<style>
.vue-waterfall-slot {
position: absolute;
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
<script>
export default {
data: () => ({
isShow: false
}),
props: {
width: {
required: true,
validator: (val) => val >= 0
},
height: {
required: true,
validator: (val) => val >= 0
},
order: {
default: 0
},
moveClass: {
default: ''
}
},
methods: {
notify () {
this.$parent.$emit('reflow', this)
},
getMeta () {
return {
vm: this,
node: this.$el,
order: this.order,
width: this.width,
height: this.height,
moveClass: this.moveClass
}
}
},
created () {
this.rect = {
top: 0,
left: 0,
width: 0,
height: 0
}
this.$watch(() => (
this.width,
this.height
), this.notify)
},
mounted () {
this.$parent.$once('reflowed', () => {
this.isShow = true
})
this.notify()
},
destroyed () {
this.notify()
}
}
</script>

View File

@ -1,442 +0,0 @@
<template>
<div class="vue-waterfall" :style="style">
<slot></slot>
</div>
</template>
<style>
.vue-waterfall {
position: relative;
/*overflow: hidden; cause clientWidth = 0 in IE if height not bigger than 0 */
}
</style>
<script>
const MOVE_CLASS_PROP = '_wfMoveClass'
export default {
props: {
autoResize: {
default: true
},
interval: {
default: 200,
validator: (val) => val >= 0
},
align: {
default: 'left',
validator: (val) => ~['left', 'right', 'center'].indexOf(val)
},
line: {
default: 'v',
validator: (val) => ~['v', 'h'].indexOf(val)
},
lineGap: {
required: true,
validator: (val) => val >= 0
},
minLineGap: {
validator: (val) => val >= 0
},
maxLineGap: {
validator: (val) => val >= 0
},
singleMaxWidth: {
validator: (val) => val >= 0
},
fixedHeight: {
default: false
},
grow: {
validator: (val) => val instanceof Array
},
watch: {
default: () => ({})
}
},
data: () => ({
style: {
height: '',
overflow: ''
},
token: null
}),
methods: {
reflowHandler,
autoResizeHandler,
reflow
},
created () {
this.virtualRects = []
this.$on('reflow', () => {
this.reflowHandler()
})
this.$watch(() => (
this.align,
this.line,
this.lineGap,
this.minLineGap,
this.maxLineGap,
this.singleMaxWidth,
this.fixedHeight,
this.watch
), this.reflowHandler)
this.$watch('grow', this.reflowHandler)
},
mounted () {
this.$watch('autoResize', this.autoResizeHandler)
on(this.$el, getTransitionEndEvent(), tidyUpAnimations, true)
this.autoResizeHandler(this.autoResize)
},
beforeDestroy () {
this.autoResizeHandler(false)
off(this.$el, getTransitionEndEvent(), tidyUpAnimations, true)
}
}
function autoResizeHandler (autoResize) {
if (autoResize === false || !this.autoResize) {
off(window, 'resize', this.reflowHandler, false)
} else {
on(window, 'resize', this.reflowHandler, false)
}
}
function tidyUpAnimations (event) {
let node = event.target
let moveClass = node[MOVE_CLASS_PROP]
if (moveClass) {
removeClass(node, moveClass)
}
}
function reflowHandler () {
clearTimeout(this.token)
this.token = setTimeout(this.reflow, this.interval)
}
function reflow () {
if (!this.$el) { return }
let width = this.$el.clientWidth
let metas = this.$children.map((slot) => slot.getMeta())
metas.sort((a, b) => a.order - b.order)
this.virtualRects = metas.map(() => ({}))
calculate(this, metas, this.virtualRects)
setTimeout(() => {
if (isScrollBarVisibilityChange(this.$el, width)) {
calculate(this, metas, this.virtualRects)
}
this.style.overflow = 'hidden'
render(this.virtualRects, metas)
this.$emit('reflowed', this)
}, 0)
}
function isScrollBarVisibilityChange (el, lastClientWidth) {
return lastClientWidth !== el.clientWidth
}
function calculate (vm, metas, styles) {
let options = getOptions(vm)
let processor = vm.line === 'h' ? horizontalLineProcessor : verticalLineProcessor
processor.calculate(vm, options, metas, styles)
}
function getOptions (vm) {
const maxLineGap = vm.maxLineGap ? +vm.maxLineGap : vm.lineGap
return {
align: ~['left', 'right', 'center'].indexOf(vm.align) ? vm.align : 'left',
line: ~['v', 'h'].indexOf(vm.line) ? vm.line : 'v',
lineGap: +vm.lineGap,
minLineGap: vm.minLineGap ? +vm.minLineGap : vm.lineGap,
maxLineGap: maxLineGap,
singleMaxWidth: Math.max(vm.singleMaxWidth || 0, maxLineGap),
fixedHeight: !!vm.fixedHeight,
grow: vm.grow && vm.grow.map(val => +val)
}
}
var verticalLineProcessor = (() => {
function calculate (vm, options, metas, rects) {
let width = vm.$el.clientWidth
let grow = options.grow
let strategy = grow
? getRowStrategyWithGrow(width, grow)
: getRowStrategy(width, options)
let tops = getArrayFillWith(0, strategy.count)
metas.forEach((meta, index) => {
let offset = tops.reduce((last, top, i) => top < tops[last] ? i : last, 0)
let width = strategy.width[offset % strategy.count]
let rect = rects[index]
rect.top = tops[offset]
rect.left = strategy.left + (offset ? sum(strategy.width.slice(0, offset)) : 0)
rect.width = width
rect.height = meta.height * (options.fixedHeight ? 1 : width / meta.width)
tops[offset] = tops[offset] + rect.height
})
vm.style.height = Math.max.apply(Math, tops) + 'px'
}
function getRowStrategy (width, options) {
let count = width / options.lineGap
let slotWidth
if (options.singleMaxWidth >= width) {
count = 1
slotWidth = Math.max(width, options.minLineGap)
} else {
let maxContentWidth = options.maxLineGap * ~~count
let minGreedyContentWidth = options.minLineGap * ~~(count + 1)
let canFit = maxContentWidth >= width
let canFitGreedy = minGreedyContentWidth <= width
if (canFit && canFitGreedy) {
count = Math.round(count)
slotWidth = width / count
} else if (canFit) {
count = ~~count
slotWidth = width / count
} else if (canFitGreedy) {
count = ~~(count + 1)
slotWidth = width / count
} else {
count = ~~count
slotWidth = options.maxLineGap
}
if (count === 1) {
slotWidth = Math.min(width, options.singleMaxWidth)
slotWidth = Math.max(slotWidth, options.minLineGap)
}
}
return {
width: getArrayFillWith(slotWidth, count),
count: count,
left: getLeft(width, slotWidth * count, options.align)
}
}
function getRowStrategyWithGrow (width, grow) {
let total = sum(grow)
return {
width: grow.map(val => width * val / total),
count: grow.length,
left: 0
}
}
return {
calculate
}
})()
var horizontalLineProcessor = (() => {
function calculate (vm, options, metas, rects) {
let width = vm.$el.clientWidth
let total = metas.length
let top = 0
let offset = 0
while (offset < total) {
let strategy = getRowStrategy(width, options, metas, offset)
for (let i = 0, left = 0, meta, rect; i < strategy.count; i++) {
meta = metas[offset + i]
rect = rects[offset + i]
rect.top = top
rect.left = strategy.left + left
rect.width = meta.width * strategy.height / meta.height
rect.height = strategy.height
left += rect.width
}
offset += strategy.count
top += strategy.height
}
vm.style.height = top + 'px'
}
function getRowStrategy (width, options, metas, offset) {
let greedyCount = getGreedyCount(width, options.lineGap, metas, offset)
let lazyCount = Math.max(greedyCount - 1, 1)
let greedySize = getContentSize(width, options, metas, offset, greedyCount)
let lazySize = getContentSize(width, options, metas, offset, lazyCount)
let finalSize = chooseFinalSize(lazySize, greedySize, width)
let height = finalSize.height
let fitContentWidth = finalSize.width
if (finalSize.count === 1) {
fitContentWidth = Math.min(options.singleMaxWidth, width)
height = metas[offset].height * fitContentWidth / metas[offset].width
}
return {
left: getLeft(width, fitContentWidth, options.align),
count: finalSize.count,
height: height
}
}
function getGreedyCount (rowWidth, rowHeight, metas, offset) {
let count = 0
for (let i = offset, width = 0; i < metas.length && width <= rowWidth; i++) {
width += metas[i].width * rowHeight / metas[i].height
count++
}
return count
}
function getContentSize (rowWidth, options, metas, offset, count) {
let originWidth = 0
for (let i = count - 1; i >= 0; i--) {
let meta = metas[offset + i]
originWidth += meta.width * options.lineGap / meta.height
}
let fitHeight = options.lineGap * rowWidth / originWidth
let canFit = (fitHeight <= options.maxLineGap && fitHeight >= options.minLineGap)
if (canFit) {
return {
cost: Math.abs(options.lineGap - fitHeight),
count: count,
width: rowWidth,
height: fitHeight
}
} else {
let height = originWidth > rowWidth ? options.minLineGap : options.maxLineGap
return {
cost: Infinity,
count: count,
width: originWidth * height / options.lineGap,
height: height
}
}
}
function chooseFinalSize (lazySize, greedySize, rowWidth) {
if (lazySize.cost === Infinity && greedySize.cost === Infinity) {
return greedySize.width < rowWidth ? greedySize : lazySize
} else {
return greedySize.cost >= lazySize.cost ? lazySize : greedySize
}
}
return {
calculate
}
})()
function getLeft (width, contentWidth, align) {
switch (align) {
case 'right':
return width - contentWidth
case 'center':
return (width - contentWidth) / 2
default:
return 0
}
}
function sum (arr) {
return arr.reduce((sum, val) => sum + val)
}
function render (rects, metas) {
let metasNeedToMoveByTransform = metas.filter((meta) => meta.moveClass)
let firstRects = getRects(metasNeedToMoveByTransform)
applyRects(rects, metas)
let lastRects = getRects(metasNeedToMoveByTransform)
metasNeedToMoveByTransform.forEach((meta, i) => {
meta.node[MOVE_CLASS_PROP] = meta.moveClass
setTransform(meta.node, firstRects[i], lastRects[i])
})
document.body.clientWidth // forced reflow
metasNeedToMoveByTransform.forEach((meta) => {
addClass(meta.node, meta.moveClass)
clearTransform(meta.node)
})
}
function getRects (metas) {
return metas.map((meta) => meta.vm.rect)
}
function applyRects (rects, metas) {
rects.forEach((rect, i) => {
let style = metas[i].node.style
metas[i].vm.rect = rect
for (let prop in rect) {
style[prop] = rect[prop] + 'px'
}
})
}
function setTransform (node, firstRect, lastRect) {
let dx = firstRect.left - lastRect.left
let dy = firstRect.top - lastRect.top
let sw = firstRect.width / lastRect.width
let sh = firstRect.height / lastRect.height
node.style.transform =
node.style.WebkitTransform = `translate(${dx}px,${dy}px) scale(${sw},${sh})`
node.style.transitionDuration = '0s'
}
function clearTransform (node) {
node.style.transform = node.style.WebkitTransform = ''
node.style.transitionDuration = ''
}
function getTransitionEndEvent () {
let isWebkitTrans =
window.ontransitionend === undefined &&
window.onwebkittransitionend !== undefined
let transitionEndEvent = isWebkitTrans
? 'webkitTransitionEnd'
: 'transitionend'
return transitionEndEvent
}
/**
* util
*/
function getArrayFillWith (item, count) {
let getter = (typeof item === 'function') ? () => item() : () => item
let arr = []
for (let i = 0; i < count; i++) {
arr[i] = getter()
}
return arr
}
function addClass (elem, name) {
if (!hasClass(elem, name)) {
let cur = attr(elem, 'class').trim()
let res = (cur + ' ' + name).trim()
attr(elem, 'class', res)
}
}
function removeClass (elem, name) {
let reg = new RegExp('\\s*\\b' + name + '\\b\\s*', 'g')
let res = attr(elem, 'class').replace(reg, ' ').trim()
attr(elem, 'class', res)
}
function hasClass (elem, name) {
return (new RegExp('\\b' + name + '\\b')).test(attr(elem, 'class'))
}
function attr (elem, name, value) {
if (typeof value !== 'undefined') {
elem.setAttribute(name, value)
} else {
return elem.getAttribute(name) || ''
}
}
function on (elem, type, listener, useCapture = false) {
elem.addEventListener(type, listener, useCapture)
}
function off (elem, type, listener, useCapture = false) {
elem.removeEventListener(type, listener, useCapture)
}
</script>

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../../styles/_colors.scss';
@import '~/assets/styles/_colors.scss';
.links {
margin-bottom: 3em;
align-items: stretch;
@ -96,5 +96,5 @@
</div>
</template>
<script>
export default {}
export default {};
</script>

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/_colors.scss';
@import '~/assets/styles/_colors.scss';
#logo {
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
@ -50,7 +50,7 @@
<template>
<p id="logo">
<img src="../../public/images/logo.png">
<img src="~/assets/images/logo.png">
</p>
</template>

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
nav.navbar {
background: transparent;
box-shadow: none;
@ -47,7 +47,7 @@
<div class="navbar-brand">
<router-link to="/"
class="navbar-item no-active">
<i class="icon-ecommerce-safebox"/> {{ config.serviceName }}
<i class="icon-ecommerce-safebox" /> {{ config.serviceName }}
</router-link>
<!--
@ -78,12 +78,12 @@
<router-link v-if="!loggedIn"
class="navbar-item"
to="/login"><i class="hidden"/>Login</router-link>
to="/login"><i class="hidden" />Login</router-link>
<router-link v-else
to="/dashboard"
class="navbar-item no-active"
exact><i class="hidden"/>Dashboard</router-link>
exact><i class="hidden" />Dashboard</router-link>
</div>
</nav>
</template>

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
.dashboard-menu {
a {
display: block;
@ -25,19 +25,17 @@
</style>
<template>
<div class="dashboard-menu">
<router-link to="/"><i class="icon-ecommerce-safebox"/>lolisafe</router-link>
<router-link to="/"><i class="icon-ecommerce-safebox" />lolisafe</router-link>
<hr>
<a><i class="icon-interface-cloud-upload"/>Upload files</a>
<a><i class="icon-interface-cloud-upload" />Upload files</a>
<hr>
<router-link to="/dashboard"><i class="icon-com-pictures"/>Files</router-link>
<router-link to="/dashboard/albums"><i class="icon-interface-window"/>Albums</router-link>
<router-link to="/dashboard/tags"><i class="icon-ecommerce-tag-c"/>Tags</router-link>
<router-link to="/dashboard"><i class="icon-com-pictures" />Files</router-link>
<router-link to="/dashboard/albums"><i class="icon-interface-window" />Albums</router-link>
<router-link to="/dashboard/tags"><i class="icon-ecommerce-tag-c" />Tags</router-link>
<hr>
<router-link to="/dashboard/settings"><i class="icon-setting-gear-a"/>Settings</router-link>
<router-link to="/dashboard/settings"><i class="icon-setting-gear-a" />Settings</router-link>
</div>
</template>
<script>
export default {
}
export default {};
</script>

View File

@ -8,8 +8,8 @@
expanded>
<option
v-for="album in albums"
:value="album.id"
:key="album.id">
:key="album.id"
:value="album.id">
{{ album.name }}
</option>
</b-select>
@ -29,20 +29,20 @@
ref="template">
<div class="dz-preview dz-file-preview">
<div class="dz-details">
<div class="dz-filename"><span data-dz-name/></div>
<div class="dz-size"><span data-dz-size/></div>
<div class="dz-filename"><span data-dz-name /></div>
<div class="dz-size"><span data-dz-size /></div>
</div>
<div class="result">
<div class="copyLink">
<b-tooltip label="Copy link">
<i class="icon-web-code"/>
<i class="icon-web-code" />
</b-tooltip>
</div>
<div class="openLink">
<b-tooltip label="Open file">
<a class="link"
target="_blank">
<i class="icon-web-url"/>
<i class="icon-web-url" />
</a>
</b-tooltip>
</div>
@ -51,14 +51,14 @@
<div>
<span>
<span class="error-message"
data-dz-errormessage/>
<i class="icon-web-warning"/>
data-dz-errormessage />
<i class="icon-web-warning" />
</span>
</div>
</div>
<div class="dz-progress">
<span class="dz-upload"
data-dz-uploadprogress/>
data-dz-uploadprogress />
</div>
<!--
<div class="dz-error-message"><span data-dz-errormessage/></div>
@ -72,7 +72,7 @@
<script>
import Dropzone from 'nuxt-dropzone';
import '../../styles/dropzone.scss';
import '~/assets/styles/dropzone.scss';
export default {
components: { Dropzone },
@ -107,7 +107,7 @@ export default {
},
mounted() {
this.dropzoneOptions = {
url: `${this.$config.baseURL}/upload`,
url: `${this.config.baseURL}/upload`,
autoProcessQueue: true,
addRemoveLinks: false,
parallelUploads: 5,
@ -135,7 +135,7 @@ export default {
*/
async getAlbums() {
try {
const response = await this.axios.get(`${this.$config.baseURL}/albums/dropdown`);
const response = await this.axios.get(`${this.config.baseURL}/albums/dropdown`);
this.albums = response.data.albums;
this.updateDropzoneConfig();
} catch (error) {
@ -218,7 +218,7 @@ export default {
}
</style>
<style lang="scss">
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
.filepond--panel-root {
background: transparent;
border: 2px solid #2c3340;

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui" />
<!--ream-head-placeholder-->
<!--ream-styles-placeholder-->
</head>
<body>
<!--ream-app-placeholder-->
<!--ream-scripts-placeholder-->
</body>
</html>

View File

@ -1,49 +0,0 @@
import Vue from 'vue';
import VueMeta from 'vue-meta';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Buefy from 'buefy';
import VueTimeago from 'vue-timeago';
import VueLazyload from 'vue-lazyload';
import VueAnalytics from 'vue-analytics';
import Clipboard from 'v-clipboard';
import VueIsYourPasswordSafe from 'vue-isyourpasswordsafe';
import router from './router';
import store from './store';
const isProduction = process.env.NODE_ENV === 'production';
Vue.use(VueMeta);
Vue.use(VueLazyload);
Vue.use(VueAnalytics, {
id: 'UA-000000000-0',
debug: {
enabled: !isProduction,
sendHitTask: isProduction
}
});
Vue.use(VueIsYourPasswordSafe, {
minLength: 6,
maxLength: 64
});
Vue.use(VueAxios, axios);
Vue.use(Buefy);
Vue.use(VueTimeago, {
name: 'timeago',
locale: 'en-US',
locales: { 'en-US': require('vue-timeago/locales/en-US.json') }
});
Vue.use(Clipboard);
Vue.axios.defaults.headers.common.Accept = 'application/vnd.lolisafe.json';
Vue.prototype.$config = require('./config');
export default () => {
return {
root: () => import('./App.vue'),
router,
store
};
};

View File

@ -0,0 +1,114 @@
<template>
<nuxt />
</template>
<script>
import Vue from 'vue';
import Fuse from 'fuse.js';
const protectedRoutes = [
'/dashboard',
'/dashboard/albums',
'/dashboard/settings'
];
export default {
computed: {
config() {
return this.$store.state.config;
}
},
mounted() {
console.log(`%c lolisafe %c v${this.config.version} %c`, 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 'background:#ff015b; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 'background:transparent');
},
created() {
Vue.prototype.$search = (term, list, options) => {
return new Promise(resolve => {
const run = new Fuse(list, options);
const results = run.search(term);
return resolve(results);
});
};
Vue.prototype.$onPromiseError = (error, logout = false) => {
this.processCatch(error, logout);
};
Vue.prototype.$showToast = (text, error, duration) => {
this.showToast(text, error, duration);
};
Vue.prototype.$logOut = () => {
this.$store.commit('user', null);
this.$store.commit('loggedIn', false);
this.$store.commit('token', null);
};
this.$router.beforeEach((to, from, next) => {
if (this.$store.state.loggedIn) return next();
if (process.browser) {
if (localStorage && localStorage.getItem('lolisafe-token')) return this.tryToLogin(next, `/login?redirect=${to.path}`);
}
for (const match of to.matched) {
if (protectedRoutes.includes(match.path)) {
if (this.$store.state.loggedIn === false) return next(`/login?redirect=${to.path}`);
}
}
return next();
});
if (process.browser) this.tryToLogin();
},
methods: {
showToast(text, error, duration) {
this.$toast.open({
duration: duration || 2500,
message: text,
position: 'is-bottom',
type: error ? 'is-danger' : 'is-success'
});
},
processCatch(error, logout) {
if (error.response && error.response.data && error.response.data.message) {
this.showToast(error.response.data.message, true, 5000);
if (error.response.status === 429) return;
if (error.response.status === 502) return;
if (logout) {
this.$logOut();
setTimeout(() => this.$router.push('/'), 3000);
}
} else {
console.error(error);
this.showToast('Something went wrong, please check the console :(', true, 5000);
}
},
tryToLogin(next, destination) {
if (process.browser) this.$store.commit('token', localStorage.getItem('lolisafe-token'));
this.axios.get(`${this.config.baseURL}/verify`).then(res => {
this.$store.commit('user', res.data.user);
this.$store.commit('loggedIn', true);
if (next) return next();
return null;
}).catch(error => {
if (error.response && error.response.status === 520) return;
if (error.response && error.response.status === 429) {
setTimeout(() => {
this.tryToLogin(next, destination);
}, 1000);
return next(false);
}
this.$store.commit('user', null);
this.$store.commit('loggedIn', false);
this.$store.commit('token', null);
if (next && destination) return next(destination);
if (next) return next('/');
return null;
});
}
}
};
</script>
<style lang="scss">
@import "~/assets/styles/style.scss";
@import "~assets/styles/icons.min.css";
</style>

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import "../styles/_colors.scss";
@import "~/assets/styles/_colors.scss";
h2 {
font-weight: 100;
color: $textColor;
@ -10,7 +10,7 @@
<template>
<section class="hero is-fullheight">
<Navbar :isWhite="true"/>
<Navbar :isWhite="true" />
<div class="hero-body">
<div class="container">
<h2>404エラ</h2>
@ -20,7 +20,7 @@
</template>
<script>
import Navbar from '../components/navbar/Navbar.vue';
import Navbar from '~/components/navbar/Navbar.vue';
export default {
components: { Navbar },

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
section { background-color: $backgroundLight1 !important; }
section.hero div.hero-body.align-top {
@ -14,7 +14,7 @@
}
</style>
<style lang="scss">
@import '../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
</style>
<template>
@ -49,24 +49,25 @@
</template>
<script>
import Grid from '../components/grid/Grid.vue';
import Loading from '../components/loading/CubeShadow.vue';
import Grid from '~/components/grid/Grid.vue';
import Loading from '~/components/loading/CubeShadow.vue';
import axios from 'axios';
import config from '../config.js';
import config from '~/config.js';
export default {
components: { Grid, Loading },
async getInitialData({ route, store }) {
async asyncData({ params, error }) {
try {
const res = await axios.get(`${config.baseURL}/album/${route.params.identifier}`);
const downloadLink = res.data.downloadEnabled ? `${config.baseURL}/album/${route.params.identifier}/zip` : null;
const res = await axios.get(`${config.baseURL}/album/${params.identifier}`);
const downloadLink = res.data.downloadEnabled ? `${config.baseURL}/album/${params.identifier}/zip` : null;
return {
name: res.data.name,
downloadEnabled: res.data.downloadEnabled,
files: res.data.files,
downloadLink
};
} catch (error) {
} catch (err) {
/*
return {
name: null,
downloadEnabled: false,
@ -74,13 +75,20 @@ export default {
downloadLink: null,
error: error.response.status
};
*/
error({ statusCode: 404, message: 'Post not found' });
}
},
data() {
return {};
},
computed: {
config() {
return this.$store.state.config;
}
},
metaInfo() {
if (!this.files) {
if (this.files) {
return {
title: `${this.name ? this.name : ''}`,
meta: [
@ -98,31 +106,31 @@ export default {
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${this.files.length > 0 ? this.files[0].thumbSquare : '/public/images/share.jpg'}` }
]
};
} else {
return {
title: `${this.name ? this.name : ''}`,
meta: [
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary' },
{ vmid: 'twitter:title', name: 'twitter:title', content: 'lolisafe' },
{ 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: `${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.' }
]
};
}
return {
title: `${this.name ? this.name : ''}`,
meta: [
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary' },
{ vmid: 'twitter:title', name: 'twitter:title', content: 'lolisafe' },
{ 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: `${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.' }
]
};
},
mounted() {
/*
if (this.error) {
if (this.error === 404) {
this.$toast.open('Album not found', true, 3000);
setTimeout(() => this.$router.push('/404'), 3000);
return;
} else {
this.$toast.open(`Error code ${this.error}`, true, 3000);
}
this.$toast.open(`Error code ${this.error}`, true, 3000);
}
*/
this.$ga.page({
page: `/a/${this.$route.params.identifier}`,
title: `Album | ${this.name}`,

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
section { background-color: $backgroundLight1 !important; }
section.hero div.hero-body {
align-items: baseline;
@ -118,7 +118,7 @@
div.column > h2.subtitle { padding-top: 1px; }
</style>
<style lang="scss">
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
.b-table {
.table-wrapper {
@ -147,7 +147,7 @@
<b-input v-model="newAlbumName"
placeholder="Album name..."
type="text"
@keyup.enter.native="createAlbum"/>
@keyup.enter.native="createAlbum" />
<p class="control">
<button class="button is-primary"
@click="createAlbum">Create album</button>
@ -227,14 +227,14 @@
label="Allow download"
centered>
<b-switch v-model="props.row.enableDownload"
@input="linkOptionsChanged(props.row)"/>
@input="linkOptionsChanged(props.row)" />
</b-table-column>
<b-table-column field="enabled"
label="Enabled"
centered>
<b-switch v-model="props.row.enabled"
@input="linkOptionsChanged(props.row)"/>
@input="linkOptionsChanged(props.row)" />
</b-table-column>
<!--
@ -252,7 +252,7 @@
</template>
<template slot="empty">
<div class="has-text-centered">
<i class="icon-misc-mood-sad"/>
<i class="icon-misc-mood-sad" />
</div>
<div class="has-text-centered">
Nothing here
@ -281,12 +281,10 @@
<script>
import Sidebar from '../../components/sidebar/Sidebar.vue';
import Grid from '../../components/grid/Grid.vue';
export default {
components: {
Sidebar,
Grid
Sidebar
},
data() {
return {
@ -313,7 +311,7 @@ export default {
methods: {
async linkOptionsChanged(link) {
try {
const response = await this.axios.post(`${this.$config.baseURL}/album/link/edit`,
const response = await this.axios.post(`${this.config.baseURL}/album/link/edit`,
{
identifier: link.identifier,
enableDownload: link.enableDownload,
@ -327,7 +325,7 @@ export default {
async createLink(album) {
album.isCreatingLink = true;
try {
const response = await this.axios.post(`${this.$config.baseURL}/album/link/new`,
const response = await this.axios.post(`${this.config.baseURL}/album/link/new`,
{ albumId: album.id });
this.$toast.open(response.data.message);
album.links.push({
@ -346,7 +344,7 @@ export default {
async createAlbum() {
if (!this.newAlbumName || this.newAlbumName === '') return;
try {
const response = await this.axios.post(`${this.$config.baseURL}/album/new`,
const response = await this.axios.post(`${this.config.baseURL}/album/new`,
{ name: this.newAlbumName });
this.newAlbumName = null;
this.$toast.open(response.data.message);
@ -358,7 +356,7 @@ export default {
},
async getAlbums() {
try {
const response = await this.axios.get(`${this.$config.baseURL}/albums/mini`);
const response = await this.axios.get(`${this.config.baseURL}/albums/mini`);
for (const album of response.data.albums) {
album.isDetailsOpen = false;
}

View File

@ -1,12 +1,12 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
section { background-color: $backgroundLight1 !important; }
section.hero div.hero-body {
align-items: baseline;
}
</style>
<style lang="scss">
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
</style>
@ -25,7 +25,7 @@
<hr>
-->
<Grid v-if="files.length"
:files="files"/>
:files="files" />
</div>
</div>
</div>
@ -34,8 +34,8 @@
</template>
<script>
import Sidebar from '../../components/sidebar/Sidebar.vue';
import Grid from '../../components/grid/Grid.vue';
import Sidebar from '~/components/sidebar/Sidebar.vue';
import Grid from '~/components/grid/Grid.vue';
export default {
components: {
@ -45,6 +45,11 @@ export default {
data() {
return { files: [] };
},
computed: {
config() {
return this.$store.state.config;
}
},
metaInfo() {
return { title: 'Uploads' };
},
@ -59,7 +64,7 @@ export default {
methods: {
async getFiles() {
try {
const response = await this.axios.get(`${this.$config.baseURL}/files`);
const response = await this.axios.get(`${this.config.baseURL}/files`);
this.files = response.data.files;
console.log(this.files);
} catch (error) {

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
section { background-color: $backgroundLight1 !important; }
section.hero div.hero-body {
align-items: baseline;
@ -10,7 +10,7 @@
}
</style>
<style lang="scss">
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
</style>
@ -20,7 +20,7 @@
<div class="container">
<div class="columns">
<div class="column is-narrow">
<Sidebar/>
<Sidebar />
</div>
<div class="column">
<!--
@ -45,18 +45,11 @@
</template>
<script>
import Sidebar from '../../components/sidebar/Sidebar.vue';
import Grid from '../../components/grid/Grid.vue';
// import Waterfall from '../../components/waterfall/Waterfall.vue';
// import WaterfallItem from '../../components/waterfall/WaterfallItem.vue';
import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
Sidebar,
Grid
// Waterfall,
// WaterfallSlot
// WaterfallItem
Sidebar
},
data() {
return {

View File

@ -1,30 +1,29 @@
<style lang="scss" scoped>
@import "../styles/_colors.scss";
@import "~/assets/styles/_colors.scss";
div.home {
color: $textColor;
// background-color: #1e2430;
}
.columns {
.column {
&.centered {
display: flex;
align-items: center;
.columns {
.column {
&.centered {
display: flex;
align-items: center;
}
}
}
}
h4 {
color: $textColorHighlight;
margin-bottom: 1em;
}
p {
font-size: 1.25em;
font-weight: 600;
line-height: 1.5;
strong {
h4 {
color: $textColorHighlight;
margin-bottom: 1em;
}
p {
font-size: 1.25em;
font-weight: 600;
line-height: 1.5;
strong {
color: $textColorHighlight;
}
}
}
</style>
@ -32,7 +31,7 @@
<template>
<div class="home">
<section class="hero is-fullheight has-text-centered">
<Navbar :isWhite="true"/>
<Navbar :isWhite="true" />
<div class="hero-body">
<div class="container">
<div class="columns">
@ -64,10 +63,10 @@
</template>
<script>
import Navbar from '../components/navbar/Navbar.vue';
import Logo from '../components/logo/Logo.vue';
import Uploader from '../components/uploader/Uploader.vue';
import Links from '../components/home/links/Links.vue';
import Navbar from '~/components/navbar/Navbar.vue';
import Logo from '~/components/logo/Logo.vue';
import Uploader from '~/components/uploader/Uploader.vue';
import Links from '~/components/home/links/Links.vue';
export default {
name: 'Home',

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
</style>
<template>
@ -20,14 +20,14 @@
<b-input v-model="username"
type="text"
placeholder="Username"
@keyup.enter.native="login"/>
@keyup.enter.native="login" />
</b-field>
<b-field>
<b-input v-model="password"
type="password"
placeholder="Password"
password-reveal
@keyup.enter.native="login"/>
@keyup.enter.native="login" />
</b-field>
<p class="control has-addons is-pulled-right">
@ -70,7 +70,7 @@
</template>
<script>
import Navbar from '../../components/navbar/Navbar.vue';
import Navbar from '~/components/navbar/Navbar.vue';
export default {
name: 'Login',
@ -107,7 +107,7 @@ export default {
return;
}
this.isLoading = true;
this.axios.post(`${this.$config.baseURL}/auth/login`, {
this.axios.post(`${this.config.baseURL}/auth/login`, {
username: this.username,
password: this.password
}).then(res => {

View File

@ -1,5 +1,5 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
@import '~/assets/styles/_colors.scss';
</style>
<template>
@ -19,20 +19,20 @@
<b-field>
<b-input v-model="username"
type="text"
placeholder="Username"/>
placeholder="Username" />
</b-field>
<b-field>
<b-input v-model="password"
type="password"
placeholder="Password"
password-reveal/>
password-reveal />
</b-field>
<b-field>
<b-input v-model="rePassword"
type="password"
placeholder="Re-type Password"
password-reveal
@keyup.enter.native="register"/>
@keyup.enter.native="register" />
</b-field>
<p class="control has-addons is-pulled-right">
@ -50,7 +50,7 @@
</template>
<script>
import Navbar from '../../components/navbar/Navbar.vue';
import Navbar from '~/components/navbar/Navbar.vue';
export default {
name: 'Register',
@ -63,6 +63,11 @@ export default {
isLoading: false
};
},
computed: {
config() {
return this.$store.state.config;
}
},
metaInfo() {
return { title: 'Register' };
},
@ -81,7 +86,7 @@ export default {
return;
}
this.isLoading = true;
this.axios.post(`${this.$config.baseURL}/auth/register`, {
this.axios.post(`${this.config.baseURL}/auth/register`, {
username: this.username,
password: this.password
}).then(response => {

View File

@ -0,0 +1,4 @@
import Vue from 'vue';
import Buefy from 'buefy';
Vue.use(Buefy);

View File

@ -0,0 +1,4 @@
import Vue from 'vue';
import Clipboard from 'v-clipboard';
Vue.use(Clipboard);

View File

@ -0,0 +1,12 @@
import Vue from 'vue';
import VueAnalytics from 'vue-analytics';
const isProduction = process.env.NODE_ENV === 'production';
Vue.use(VueAnalytics, {
id: 'UA-000000000-0',
debug: {
enabled: !isProduction,
sendHitTask: isProduction
}
});

View File

@ -0,0 +1,6 @@
import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
Vue.use(VueAxios, axios);
Vue.axios.defaults.headers.common.Accept = 'application/vnd.lolisafe.json';

View File

@ -0,0 +1,7 @@
import Vue from 'vue';
import VueIsYourPasswordSafe from 'vue-isyourpasswordsafe';
Vue.use(VueIsYourPasswordSafe, {
minLength: 6,
maxLength: 64
});

View File

@ -0,0 +1,8 @@
import Vue from 'vue';
import VueTimeago from 'vue-timeago';
Vue.use(VueTimeago, {
name: 'timeago',
locale: 'en-US',
locales: { 'en-US': require('vue-timeago/locales/en-US.json') }
});

View File

@ -1,21 +0,0 @@
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{ path: '/', component: () => import('../views/Home.vue') },
{ path: '/login', component: () => import('../views/Auth/Login.vue') },
{ path: '/register', component: () => import('../views/Auth/Register.vue') },
{ path: '/dashboard', component: () => import('../views/Dashboard/Uploads.vue') },
{ path: '/dashboard/albums', component: () => import('../views/Dashboard/Albums.vue') },
{ path: '/dashboard/settings', component: () => import('../views/Dashboard/Settings.vue') },
{ path: '/a/:identifier', component: () => import('../views/PublicAlbum.vue'), props: true },
{ path: '/404', component: () => import('../views/NotFound.vue') },
{ path: '*', component: () => import('../views/NotFound.vue') }
]
});
export default router;

View File

@ -1,8 +1,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
loggedIn: false,
user: {},
@ -18,19 +16,19 @@ const mutations = {
user(state, payload) {
if (!payload) {
state.user = {};
localStorage.removeItem('ls-user');
localStorage.removeItem('lolisafe-user');
return;
}
localStorage.setItem('ls-user', JSON.stringify(payload));
localStorage.setItem('lolisafe-user', JSON.stringify(payload));
state.user = payload;
},
token(state, payload) {
if (!payload) {
localStorage.removeItem('ls-token');
localStorage.removeItem('lolisafe-token');
state.token = null;
return;
}
localStorage.setItem('ls-token', payload);
localStorage.setItem('lolisafe-token', payload);
setAuthorizationHeader(payload);
state.token = payload;
},
@ -39,13 +37,22 @@ const mutations = {
}
};
const actions = {
nuxtServerInit({ commit }, { req }) {
const config = require('~/config.js');
commit('config', config);
}
};
const setAuthorizationHeader = payload => {
console.log('hihi');
Vue.axios.defaults.headers.common.Authorization = payload ? `Bearer ${payload}` : '';
};
const store = new Vuex.Store({
const store = () => new Vuex.Store({
state,
mutations
mutations,
actions
});
export default store;

View File

@ -1,172 +0,0 @@
<style lang="scss" scoped>
@import '../../styles/colors.scss';
section { background-color: $backgroundLight1 !important; }
section.hero div.hero-body {
align-items: baseline;
}
div.view-container {
padding: 2rem;
}
div.album {
display: flex;
margin-bottom: 10px;
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;
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; }
}
}
}
}
</style>
<style lang="scss">
@import '../../styles/colors.scss';
</style>
<template>
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column is-narrow">
<Sidebar/>
</div>
<div class="column">
<h1 class="title">{{ albumName }}</h1>
<hr>
<div class="view-container">
<div v-for="album in albums"
:key="album.id"
class="album">
<div class="thumb">
<figure class="image is-64x64 thumb">
<img src="../../assets/images/blank.png">
</figure>
</div>
<div class="info">
<h4>
<router-link :to="`/dashboard/albums/${album.id}`">{{ album.name }}</router-link>
</h4>
<span>Updated <timeago :since="album.editedAt" /></span>
<span>{{ album.fileCount || 0 }} files</span>
</div>
<div class="latest">
<div v-for="file of album.files"
:key="file.id"
class="thumb">
<figure class="image is-64x64">
<a :href="file.url"
target="_blank">
<img :src="file.thumbSquare">
</a>
</figure>
</div>
<div v-if="album.fileCount > 5"
class="thumb more no-background">
<router-link :to="`/dashboard/albums/${album.id}`">{{ album.fileCount - 5 }}+ more</router-link>
</div>
</div>
</div>
</div>
</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
},
data() {
return {
albums: [],
newAlbumName: null
};
},
metaInfo() {
return { title: 'Uploads' };
},
mounted() {
this.getAlbums();
this.$ga.page({
page: '/dashboard/albums',
title: 'Albums',
location: window.location.href
});
},
methods: {
async createAlbum() {
if (!this.newAlbumName || this.newAlbumName === '') return;
try {
const response = await this.axios.post(`${this.$config.baseURL}/album/new`,
{ name: this.newAlbumName });
this.newAlbumName = null;
this.$toast.open(response.data.message);
this.getAlbums();
return;
} catch (error) {
this.$onPromiseError(error);
}
},
async getAlbums() {
try {
const response = await this.axios.get(`${this.$config.baseURL}/albums/mini`);
this.albums = response.data.albums;
console.log(this.albums);
} catch (error) {
console.error(error);
}
}
}
};
</script>

View File

@ -1,23 +1,103 @@
const Backend = require('./api/structures/Server');
const express = require('express');
const compression = require('compression');
const ream = require('ream');
// const ream = require('ream');
const config = require('../config');
const path = require('path');
const log = require('./api/utils/Log');
const dev = process.env.NODE_ENV !== 'production';
const oneliner = require('one-liner');
const jetpack = require('fs-jetpack');
// const { Nuxt, Builder } = require('nuxt-edge');
// const nuxtConfig = require('./nuxt/nuxt.config.js');
function startProduction() {
startAPI();
startSite();
// startSite();
// startNuxt();
}
function startAPI() {
writeFrontendConfig();
new Backend().start();
}
async function startNuxt() {
/*
Make sure the frontend has enough data to prepare the service
*/
writeFrontendConfig();
/*
Starting Nuxt's custom server powered by express
*/
const app = express();
/*
Instantiate Nuxt.js
*/
nuxtConfig.dev = true;
const nuxt = new Nuxt(nuxtConfig);
/*
Start the server or build it if we're on dev mode
*/
if (nuxtConfig.dev) {
try {
await new Builder(nuxt).build();
} catch (error) {
log.error(error);
process.exit(1);
}
}
/*
Render every route with Nuxt.js
*/
app.use(nuxt.render);
/*
Start the server and listen to the configured port
*/
app.listen(config.server.ports.frontend, '127.0.0.1');
log.info(`> Frontend ready and listening on port ${config.server.ports.frontend}`);
/*
Starting Nuxt's custom server powered by express
*/
/*
const app = express();
app.set('port', config.server.ports.frontend);
// Configure dev enviroment
nuxtConfig.dev = dev;
// Init Nuxt.js
const nuxt = new Nuxt(nuxtConfig);
// Build only in dev mode
if (nuxtConfig.dev) {
const builder = new Builder(nuxt);
await builder.build();
}
// Give nuxt middleware to express
app.use(nuxt.render);
if (config.serveFilesWithNode) {
app.use('/', express.static(`./${config.uploads.uploadFolder}`));
}
// Listen the server
app.listen(config.server.ports.frontend, '127.0.0.1');
app.on('renderer-ready', () => log.info(`> Frontend ready and listening on port ${config.server.ports.frontend}`));
// log.success(`> Frontend ready and listening on port ${config.server.ports.frontend}`);
// console.log(`Server listening on http://${host}:${port}`); // eslint-disable-line no-console
*/
}
function startSite() {
/*
Make sure the frontend has enough data to prepare the service
@ -74,5 +154,5 @@ function writeFrontendConfig() {
const args = process.argv[2];
if (!args) startProduction();
else if (args === 'api') startAPI();
else if (args === 'site') startSite();
else if (args === 'site') startNuxt();
else process.exit(0);

1999
yarn.lock

File diff suppressed because it is too large Load Diff