aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorZephyrrus <[email protected]>2020-07-10 01:17:00 +0300
committerGitHub <[email protected]>2020-07-10 01:17:00 +0300
commita721681944e9eb06742e5b3f71c71aed9c1c117d (patch)
tree93ff9fd13a0434d91fb1ae7ca0da48d6929c4d00 /src
parentfeat: backend pagination for albums (diff)
parentrefactor: finish refactoring all the components to use vuex (diff)
downloadhost.fuwn.me-a721681944e9eb06742e5b3f71c71aed9c1c117d.tar.xz
host.fuwn.me-a721681944e9eb06742e5b3f71c71aed9c1c117d.zip
Merge pull request #1 from Zephyrrus/feature/store_refactor
Feature/store refactor
Diffstat (limited to 'src')
-rw-r--r--src/api/database/migrations/20190221225812_initialMigration.js40
-rw-r--r--src/api/database/seeds/initial.js5
-rw-r--r--src/api/databaseMigration.js35
-rw-r--r--src/api/generateThumbs.js57
-rw-r--r--src/api/routes/admin/banIP.js2
-rw-r--r--src/api/routes/admin/fileGET.js7
-rw-r--r--src/api/routes/admin/unBanIP.js2
-rw-r--r--src/api/routes/admin/userDemote.js2
-rw-r--r--src/api/routes/admin/userDisable.js2
-rw-r--r--src/api/routes/admin/userEnable.js2
-rw-r--r--src/api/routes/admin/userGET.js7
-rw-r--r--src/api/routes/admin/userPromote.js2
-rw-r--r--src/api/routes/admin/userPurge.js2
-rw-r--r--src/api/routes/admin/usersGET.js2
-rw-r--r--src/api/routes/albums/albumDELETE.js1
-rw-r--r--src/api/routes/albums/albumFullGET.js16
-rw-r--r--src/api/routes/albums/albumGET.js5
-rw-r--r--src/api/routes/albums/albumPOST.js21
-rw-r--r--src/api/routes/albums/albumZipGET.js10
-rw-r--r--src/api/routes/albums/albumsGET.js32
-rw-r--r--src/api/routes/albums/link/linkDELETE.js3
-rw-r--r--src/api/routes/albums/link/linkEditPOST.js22
-rw-r--r--src/api/routes/albums/link/linkPOST.js22
-rw-r--r--src/api/routes/albums/link/linksGET.js2
-rw-r--r--src/api/routes/auth/loginPOST.js8
-rw-r--r--src/api/routes/auth/registerPOST.js8
-rw-r--r--src/api/routes/files/albumAddPOST.js3
-rw-r--r--src/api/routes/files/albumDelPOST.js3
-rw-r--r--src/api/routes/files/filesAlbumsGET.js4
-rw-r--r--src/api/routes/files/filesGET.js2
-rw-r--r--src/api/routes/files/tagAddPOST.js5
-rw-r--r--src/api/routes/service/configGET.js10
-rw-r--r--src/api/routes/tags/tagPOST.js4
-rw-r--r--src/api/routes/tags/tagsGET.js3
-rw-r--r--src/api/routes/uploads/chunksPOST.js14
-rw-r--r--src/api/routes/uploads/uploadPOST.js54
-rw-r--r--src/api/routes/user/apiKey.js6
-rw-r--r--src/api/routes/user/changePasswordPOST.js6
-rw-r--r--src/api/routes/user/userGET.js5
-rw-r--r--src/api/routes/verifyGET.js4
-rw-r--r--src/api/structures/Route.js46
-rw-r--r--src/api/structures/Server.js41
-rw-r--r--src/api/utils/Log.js7
-rw-r--r--src/api/utils/ThumbUtil.js108
-rw-r--r--src/api/utils/Util.js126
-rw-r--r--src/api/utils/videoPreview/FragmentPreview.js88
-rw-r--r--src/api/utils/videoPreview/FrameIntervalPreview.js73
-rw-r--r--src/setup.js49
-rw-r--r--src/site/assets/styles/_bulma_colors_extender.scss16
-rw-r--r--src/site/assets/styles/_colors.scss41
-rw-r--r--src/site/assets/styles/style.scss150
-rw-r--r--src/site/components/album/AlbumDetails.vue211
-rw-r--r--src/site/components/album/AlbumEntry.vue187
-rw-r--r--src/site/components/footer/Footer.vue70
-rw-r--r--src/site/components/grid/Grid.vue550
-rw-r--r--src/site/components/grid/waterfall/Waterfall.vue261
-rw-r--r--src/site/components/grid/waterfall/WaterfallItem.vue52
-rw-r--r--src/site/components/imageInfo/ImageInfo.vue0
-rw-r--r--src/site/components/loading/BulmaLoading.vue33
-rw-r--r--src/site/components/loading/CubeShadow.vue17
-rw-r--r--src/site/components/loading/Origami.vue18
-rw-r--r--src/site/components/loading/PingPong.vue26
-rw-r--r--src/site/components/loading/RotateSquare.vue13
-rw-r--r--src/site/components/navbar/Navbar.vue26
-rw-r--r--src/site/components/sidebar/Sidebar.vue116
-rw-r--r--src/site/components/uploader/Uploader.vue75
-rw-r--r--src/site/constants/alertTypes.js10
-rw-r--r--src/site/layouts/default.vue105
-rw-r--r--src/site/layouts/error.vue2
-rw-r--r--src/site/middleware/admin.js7
-rw-r--r--src/site/middleware/auth.js5
-rw-r--r--src/site/pages/a/_identifier.vue38
-rw-r--r--src/site/pages/dashboard/account.vue130
-rw-r--r--src/site/pages/dashboard/admin/file/_id.vue145
-rw-r--r--src/site/pages/dashboard/admin/settings.vue109
-rw-r--r--src/site/pages/dashboard/admin/user/_id.vue96
-rw-r--r--src/site/pages/dashboard/admin/users.vue264
-rw-r--r--src/site/pages/dashboard/albums/_id.vue138
-rw-r--r--src/site/pages/dashboard/albums/index.vue357
-rw-r--r--src/site/pages/dashboard/index.vue133
-rw-r--r--src/site/pages/dashboard/tags/index.vue45
-rw-r--r--src/site/pages/faq.vue23
-rw-r--r--src/site/pages/index.vue18
-rw-r--r--src/site/pages/login.vue90
-rw-r--r--src/site/pages/logout.vue8
-rw-r--r--src/site/pages/register.vue76
-rw-r--r--src/site/plugins/axios.js36
-rw-r--r--src/site/plugins/flexsearch.js5
-rw-r--r--src/site/plugins/handler.js25
-rw-r--r--src/site/plugins/notifier.js25
-rw-r--r--src/site/plugins/nuxt-client-init.js2
-rw-r--r--src/site/plugins/vue-isyourpasswordsafe.js2
-rw-r--r--src/site/plugins/vue-timeago.js2
-rw-r--r--src/site/store/.eslintrc.json5
-rw-r--r--src/site/store/admin.js122
-rw-r--r--src/site/store/albums.js129
-rw-r--r--src/site/store/alert.js33
-rw-r--r--src/site/store/auth.js106
-rw-r--r--src/site/store/config.js18
-rw-r--r--src/site/store/images.js127
-rw-r--r--src/site/store/index.js61
101 files changed, 3342 insertions, 1995 deletions
diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js
index a27a08a..b755a33 100644
--- a/src/api/database/migrations/20190221225812_initialMigration.js
+++ b/src/api/database/migrations/20190221225812_initialMigration.js
@@ -1,40 +1,44 @@
-exports.up = async knex => {
- await knex.schema.createTable('users', table => {
+exports.up = async (knex) => {
+ await knex.schema.createTable('users', (table) => {
table.increments();
- table.string('username');
+ table.string('username').unique();
table.text('password');
table.boolean('enabled');
table.boolean('isAdmin');
- table.string('apiKey');
+ table.string('apiKey').unique();
table.timestamp('passwordEditedAt');
table.timestamp('apiKeyEditedAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
- await knex.schema.createTable('albums', table => {
+ await knex.schema.createTable('albums', (table) => {
table.increments();
table.integer('userId');
table.string('name');
+ table.boolean('nsfw').defaultTo(false);
table.timestamp('zippedAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
+
+ table.unique(['userId', 'name']);
});
- await knex.schema.createTable('files', table => {
+ await knex.schema.createTable('files', (table) => {
table.increments();
table.integer('userId');
table.string('name');
table.string('original');
table.string('type');
table.integer('size');
+ table.boolean('nsfw').defaultTo(false);
table.string('hash');
table.string('ip');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
- await knex.schema.createTable('links', table => {
+ await knex.schema.createTable('links', (table) => {
table.increments();
table.integer('userId');
table.integer('albumId');
@@ -45,42 +49,50 @@ exports.up = async knex => {
table.timestamp('expiresAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
+
+ table.unique(['userId', 'albumId', 'identifier']);
});
- await knex.schema.createTable('albumsFiles', table => {
+ await knex.schema.createTable('albumsFiles', (table) => {
table.increments();
table.integer('albumId');
table.integer('fileId');
+
+ table.unique(['albumId', 'fileId']);
});
- await knex.schema.createTable('albumsLinks', table => {
+ await knex.schema.createTable('albumsLinks', (table) => {
table.increments();
table.integer('albumId');
- table.integer('linkId');
+ table.integer('linkId').unique();
});
- await knex.schema.createTable('tags', table => {
+ await knex.schema.createTable('tags', (table) => {
table.increments();
table.string('uuid');
table.integer('userId');
table.string('name');
table.timestamp('createdAt');
table.timestamp('editedAt');
+
+ table.unique(['userId', 'name']);
});
- await knex.schema.createTable('fileTags', table => {
+ await knex.schema.createTable('fileTags', (table) => {
table.increments();
table.integer('fileId');
table.integer('tagId');
+
+ table.unique(['fileId', 'tagId']);
});
- await knex.schema.createTable('bans', table => {
+ await knex.schema.createTable('bans', (table) => {
table.increments();
table.string('ip');
table.timestamp('createdAt');
});
};
-exports.down = async knex => {
+exports.down = async (knex) => {
await knex.schema.dropTableIfExists('users');
await knex.schema.dropTableIfExists('albums');
await knex.schema.dropTableIfExists('files');
diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js
index 280fd74..cdbfa80 100644
--- a/src/api/database/seeds/initial.js
+++ b/src/api/database/seeds/initial.js
@@ -1,7 +1,8 @@
+/* eslint-disable no-console */
const bcrypt = require('bcrypt');
const moment = require('moment');
-exports.seed = async db => {
+exports.seed = async (db) => {
const now = moment.utc().toDate();
const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first();
if (user) return;
@@ -14,7 +15,7 @@ exports.seed = async db => {
createdAt: now,
editedAt: now,
enabled: true,
- isAdmin: true
+ isAdmin: true,
});
console.log();
console.log('=========================================================');
diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js
index 5cf4b39..d95605d 100644
--- a/src/api/databaseMigration.js
+++ b/src/api/databaseMigration.js
@@ -1,28 +1,31 @@
+/* eslint-disable eqeqeq */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-console */
const nodePath = require('path');
const moment = require('moment');
const oldDb = require('knex')({
client: 'sqlite3',
connection: {
- filename: nodePath.join(__dirname, '..', '..', 'db')
+ filename: nodePath.join(__dirname, '..', '..', 'db'),
},
- useNullAsDefault: true
+ useNullAsDefault: true,
});
const newDb = require('knex')({
client: 'sqlite3',
connection: {
- filename: nodePath.join(__dirname, '..', '..', 'database.sqlite')
+ filename: nodePath.join(__dirname, '..', '..', 'database.sqlite'),
},
- postProcessResponse: result => {
+ postProcessResponse: (result) => {
const booleanFields = [
'enabled',
'enableDownload',
- 'isAdmin'
+ 'isAdmin',
];
- const processResponse = row => {
- Object.keys(row).forEach(key => {
+ const processResponse = (row) => {
+ Object.keys(row).forEach((key) => {
if (booleanFields.includes(key)) {
if (row[key] === 0) row[key] = false;
else if (row[key] === 1) row[key] = true;
@@ -31,11 +34,11 @@ const newDb = require('knex')({
return row;
};
- if (Array.isArray(result)) return result.map(row => processResponse(row));
+ if (Array.isArray(result)) return result.map((row) => processResponse(row));
if (typeof result === 'object') return processResponse(result);
return result;
},
- useNullAsDefault: true
+ useNullAsDefault: true,
});
const start = async () => {
@@ -49,13 +52,13 @@ const start = async () => {
id: user.id,
username: user.username,
password: user.password,
- enabled: user.enabled == 1 ? true : false,
+ enabled: user.enabled == 1,
isAdmin: false,
apiKey: user.token,
passwordEditedAt: now,
apiKeyEditedAt: now,
createdAt: now,
- editedAt: now
+ editedAt: now,
};
await newDb.table('users').insert(userToInsert);
}
@@ -71,7 +74,7 @@ const start = async () => {
name: album.name,
zippedAt: album.zipGeneratedAt ? moment.unix(album.zipGeneratedAt).toDate() : null,
createdAt: moment.unix(album.timestamp).toDate(),
- editedAt: moment.unix(album.editedAt).toDate()
+ editedAt: moment.unix(album.editedAt).toDate(),
};
const linkToInsert = {
userId: album.userid,
@@ -81,13 +84,13 @@ const start = async () => {
enabled: true,
enableDownload: true,
createdAt: now,
- editedAt: now
+ editedAt: now,
};
await newDb.table('albums').insert(albumToInsert);
const insertedId = await newDb.table('links').insert(linkToInsert);
await newDb.table('albumsLinks').insert({
albumId: album.id,
- linkId: insertedId[0]
+ linkId: insertedId[0],
});
}
console.log('Finished migrating albums...');
@@ -106,12 +109,12 @@ const start = async () => {
hash: file.hash,
ip: file.ip,
createdAt: moment.unix(file.timestamp).toDate(),
- editedAt: moment.unix(file.timestamp).toDate()
+ editedAt: moment.unix(file.timestamp).toDate(),
};
filesToInsert.push(fileToInsert);
albumsFilesToInsert.push({
albumId: file.albumid,
- fileId: file.id
+ fileId: file.id,
});
}
await newDb.batchInsert('files', filesToInsert, 20);
diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js
index 8517608..0377fe7 100644
--- a/src/api/generateThumbs.js
+++ b/src/api/generateThumbs.js
@@ -1,62 +1,17 @@
require('dotenv').config();
-const jetpack = require('fs-jetpack');
-const path = require('path');
const fs = require('fs');
-const log = require('./utils/Log');
-const sharp = require('sharp');
-const ffmpeg = require('fluent-ffmpeg');
-const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp'];
-const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
-
-class ThumbGenerator {
- static generateThumbnails(filename) {
- const ext = path.extname(filename).toLowerCase();
- const output = `${filename.slice(0, -ext.length)}.png`;
- if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output);
- if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename);
- return null;
- }
-
- static async generateThumbnailForImage(filename, output) {
- const file = await jetpack.readAsync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer');
- await sharp(file)
- .resize(64, 64)
- .toFormat('png')
- .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output));
- await sharp(file)
- .resize(225, null)
- .toFormat('png')
- .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output));
- }
-
- static generateThumbnailForVideo(filename) {
- ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'),
- size: '64x64'
- })
- .on('error', error => log.error(error.message));
- ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'),
- size: '150x?'
- })
- .on('error', error => log.error(error.message));
- }
-}
+const path = require('path');
+const ThumbUtil = require('./utils/ThumbUtil');
const start = async () => {
const files = fs.readdirSync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER));
for (const fileName of files) {
console.log(`Generating thumb for '${fileName}`);
- await ThumbGenerator.generateThumbnails(fileName);
+ // eslint-disable-next-line no-await-in-loop
+ await ThumbUtil.generateThumbnails(fileName);
}
-}
+};
-start(); \ No newline at end of file
+start();
diff --git a/src/api/routes/admin/banIP.js b/src/api/routes/admin/banIP.js
index 692880d..4dfe03c 100644
--- a/src/api/routes/admin/banIP.js
+++ b/src/api/routes/admin/banIP.js
@@ -17,7 +17,7 @@ class banIP extends Route {
}
return res.json({
- message: 'Successfully banned the ip'
+ message: 'Successfully banned the ip',
});
}
}
diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js
index 3bb8da4..239b128 100644
--- a/src/api/routes/admin/fileGET.js
+++ b/src/api/routes/admin/fileGET.js
@@ -11,7 +11,10 @@ class filesGET extends Route {
if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
let file = await db.table('files').where({ id }).first();
- const user = await db.table('users').where({ id: file.userId }).first();
+ const user = await db.table('users')
+ .select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
+ .where({ id: file.userId })
+ .first();
file = Util.constructFilePublicLink(file);
// Additional relevant data
@@ -21,7 +24,7 @@ class filesGET extends Route {
return res.json({
message: 'Successfully retrieved file',
file,
- user
+ user,
});
}
}
diff --git a/src/api/routes/admin/unBanIP.js b/src/api/routes/admin/unBanIP.js
index 493834b..725468c 100644
--- a/src/api/routes/admin/unBanIP.js
+++ b/src/api/routes/admin/unBanIP.js
@@ -19,7 +19,7 @@ class unBanIP extends Route {
}
return res.json({
- message: 'Successfully unbanned the ip'
+ message: 'Successfully unbanned the ip',
});
}
}
diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js
index b430a48..3f6623d 100644
--- a/src/api/routes/admin/userDemote.js
+++ b/src/api/routes/admin/userDemote.js
@@ -20,7 +20,7 @@ class userDemote extends Route {
}
return res.json({
- message: 'Successfully demoted user'
+ message: 'Successfully demoted user',
});
}
}
diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js
index e39c811..029e4af 100644
--- a/src/api/routes/admin/userDisable.js
+++ b/src/api/routes/admin/userDisable.js
@@ -20,7 +20,7 @@ class userDisable extends Route {
}
return res.json({
- message: 'Successfully disabled user'
+ message: 'Successfully disabled user',
});
}
}
diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js
index cff622f..aca7a0b 100644
--- a/src/api/routes/admin/userEnable.js
+++ b/src/api/routes/admin/userEnable.js
@@ -20,7 +20,7 @@ class userEnable extends Route {
}
return res.json({
- message: 'Successfully enabled user'
+ message: 'Successfully enabled user',
});
}
}
diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js
index 14a6c92..f5f2508 100644
--- a/src/api/routes/admin/userGET.js
+++ b/src/api/routes/admin/userGET.js
@@ -11,7 +11,10 @@ class usersGET extends Route {
if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' });
try {
- const user = await db.table('users').where({ id }).first();
+ const user = await db.table('users')
+ .select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
+ .where({ id })
+ .first();
const files = await db.table('files')
.where({ userId: user.id })
.orderBy('id', 'desc');
@@ -23,7 +26,7 @@ class usersGET extends Route {
return res.json({
message: 'Successfully retrieved user',
user,
- files
+ files,
});
} catch (error) {
return super.error(res, error);
diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js
index 4a5ed88..3e14cb7 100644
--- a/src/api/routes/admin/userPromote.js
+++ b/src/api/routes/admin/userPromote.js
@@ -20,7 +20,7 @@ class userPromote extends Route {
}
return res.json({
- message: 'Successfully promoted user'
+ message: 'Successfully promoted user',
});
}
}
diff --git a/src/api/routes/admin/userPurge.js b/src/api/routes/admin/userPurge.js
index 90f6ec9..8f61ff9 100644
--- a/src/api/routes/admin/userPurge.js
+++ b/src/api/routes/admin/userPurge.js
@@ -18,7 +18,7 @@ class userDemote extends Route {
}
return res.json({
- message: 'Successfully deleted the user\'s files'
+ message: 'Successfully deleted the user\'s files',
});
}
}
diff --git a/src/api/routes/admin/usersGET.js b/src/api/routes/admin/usersGET.js
index 52a707f..4e9b954 100644
--- a/src/api/routes/admin/usersGET.js
+++ b/src/api/routes/admin/usersGET.js
@@ -12,7 +12,7 @@ class usersGET extends Route {
return res.json({
message: 'Successfully retrieved users',
- users
+ users,
});
} catch (error) {
return super.error(res, error);
diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js
index 4e6640e..f9c22e6 100644
--- a/src/api/routes/albums/albumDELETE.js
+++ b/src/api/routes/albums/albumDELETE.js
@@ -1,5 +1,4 @@
const Route = require('../../structures/Route');
-const Util = require('../../utils/Util');
class albumDELETE extends Route {
constructor() {
diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js
index cf434e4..2c3a790 100644
--- a/src/api/routes/albums/albumFullGET.js
+++ b/src/api/routes/albums/albumFullGET.js
@@ -10,22 +10,27 @@ class albumGET extends Route {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid id supplied' });
- const album = await db.table('albums').where({ id, userId: user.id }).first();
+ const album = await db
+ .table('albums')
+ .where({ id, userId: user.id })
+ .first();
if (!album) return res.status(404).json({ message: 'Album not found' });
let count = 0;
- let files = db.table('albumsFiles')
+ let files = db
+ .table('albumsFiles')
.where({ albumId: id })
.join('files', 'albumsFiles.fileId', 'files.id')
- .select('files.id', 'files.name')
+ .select('files.id', 'files.name', 'files.createdAt')
.orderBy('files.id', 'desc');
const { page, limit = 100 } = req.query;
if (page && page >= 0) {
files = await files.offset((page - 1) * limit).limit(limit);
- const dbRes = await db.table('albumsFiles')
+ const dbRes = await db
+ .table('albumsFiles')
.count('* as count')
.where({ albumId: id })
.first();
@@ -36,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);
}
@@ -44,7 +50,7 @@ class albumGET extends Route {
message: 'Successfully retrieved album',
name: album.name,
files,
- count
+ count,
});
}
}
diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js
index 1bf3630..81edc95 100644
--- a/src/api/routes/albums/albumGET.js
+++ b/src/api/routes/albums/albumGET.js
@@ -21,10 +21,11 @@ class albumGET extends Route {
const files = await db.table('albumsFiles')
.where({ albumId: link.albumId })
.join('files', 'albumsFiles.fileId', 'files.id')
- .select('files.name')
+ .select('files.name', 'files.id')
.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);
}
@@ -36,7 +37,7 @@ class albumGET extends Route {
message: 'Successfully retrieved files',
name: album.name,
downloadEnabled: link.enableDownload,
- files
+ files,
});
}
}
diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js
index 0d3a44c..94ee8a7 100644
--- a/src/api/routes/albums/albumPOST.js
+++ b/src/api/routes/albums/albumPOST.js
@@ -1,5 +1,5 @@
-const Route = require('../../structures/Route');
const moment = require('moment');
+const Route = require('../../structures/Route');
class albumPOST extends Route {
constructor() {
@@ -14,18 +14,25 @@ class albumPOST extends Route {
/*
Check that an album with that name doesn't exist yet
*/
- const album = await db.table('albums').where({ name, userId: user.id }).first();
- if (album) return res.status(401).json({ message: 'There\'s already an album with that name' });
+ const album = await db
+ .table('albums')
+ .where({ name, userId: user.id })
+ .first();
+ if (album) return res.status(401).json({ message: "There's already an album with that name" });
const now = moment.utc().toDate();
- await db.table('albums').insert({
+ const insertObj = {
name,
userId: user.id,
createdAt: now,
- editedAt: now
- });
+ editedAt: now,
+ };
+
+ const dbRes = await db.table('albums').insert(insertObj);
+
+ insertObj.id = dbRes.pop();
- return res.json({ message: 'The album was created successfully' });
+ return res.json({ message: 'The album was created successfully', data: insertObj });
}
}
diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js
index a6ef6fd..bd74ef3 100644
--- a/src/api/routes/albums/albumZipGET.js
+++ b/src/api/routes/albums/albumZipGET.js
@@ -1,8 +1,8 @@
+const path = require('path');
+const jetpack = require('fs-jetpack');
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
const log = require('../../utils/Log');
-const path = require('path');
-const jetpack = require('fs-jetpack');
class albumGET extends Route {
constructor() {
@@ -21,7 +21,7 @@ class albumGET extends Route {
.where({
identifier,
enabled: true,
- enableDownload: true
+ enableDownload: true,
})
.first();
if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' });
@@ -64,11 +64,11 @@ class albumGET extends Route {
/*
Get the actual files
*/
- const fileIds = fileList.map(el => el.fileId);
+ const fileIds = fileList.map((el) => el.fileId);
const files = await db.table('files')
.whereIn('id', fileIds)
.select('name');
- const filesToZip = files.map(el => el.name);
+ const filesToZip = files.map((el) => el.name);
try {
Util.createZip(filesToZip, album);
diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js
index 1a7db87..c9ab025 100644
--- a/src/api/routes/albums/albumsGET.js
+++ b/src/api/routes/albums/albumsGET.js
@@ -1,3 +1,4 @@
+/* eslint-disable max-classes-per-file */
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
@@ -12,30 +13,28 @@ class albumsGET extends Route {
of the album files for displaying on the dashboard. It's probably useless
for anyone consuming the API outside of the lolisafe frontend.
*/
- const albums = await db.table('albums')
+ const albums = await db
+ .table('albums')
.where('albums.userId', user.id)
- .select('id', 'name', 'editedAt');
+ .select('id', 'name', 'createdAt', 'editedAt')
+ .orderBy('createdAt', 'desc');
for (const album of albums) {
- // TODO: Optimize the shit out of this. Ideally a JOIN that grabs all the needed stuff in 1 query instead of 3
-
// Fetch the total amount of files each album has.
- const fileCount = await db.table('albumsFiles') // eslint-disable-line no-await-in-loop
+ const fileCount = await db // eslint-disable-line no-await-in-loop
+ .table('albumsFiles')
.where('albumId', album.id)
.count({ count: 'id' });
// Fetch the file list from each album but limit it to 5 per album
- const filesToFetch = await db.table('albumsFiles') // eslint-disable-line no-await-in-loop
+ const files = await db // eslint-disable-line no-await-in-loop
+ .table('albumsFiles')
+ .join('files', { 'files.id': 'albumsFiles.fileId' })
.where('albumId', album.id)
- .select('fileId')
- .orderBy('id', 'desc')
+ .select('files.id', 'files.name')
+ .orderBy('albumsFiles.id', 'desc')
.limit(5);
- // Fetch the actual files
- const files = await db.table('files') // eslint-disable-line no-await-in-loop
- .whereIn('id', filesToFetch.map(el => el.fileId))
- .select('id', 'name');
-
// Fetch thumbnails and stuff
for (let file of files) {
file = Util.constructFilePublicLink(file);
@@ -47,7 +46,7 @@ class albumsGET extends Route {
return res.json({
message: 'Successfully retrieved albums',
- albums
+ albums,
});
}
}
@@ -58,12 +57,13 @@ class albumsDropdownGET extends Route {
}
async run(req, res, db, user) {
- const albums = await db.table('albums')
+ const albums = await db
+ .table('albums')
.where('userId', user.id)
.select('id', 'name');
return res.json({
message: 'Successfully retrieved albums',
- albums
+ albums,
});
}
}
diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js
index b02d0b4..0381b50 100644
--- a/src/api/routes/albums/link/linkDELETE.js
+++ b/src/api/routes/albums/link/linkDELETE.js
@@ -1,5 +1,4 @@
const Route = require('../../../structures/Route');
-const { dump } = require('dumper.js');
class linkDELETE extends Route {
constructor() {
@@ -28,7 +27,7 @@ class linkDELETE extends Route {
}
return res.json({
- message: 'Successfully deleted link'
+ message: 'Successfully deleted link',
});
}
}
diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js
index 6776b73..4e0e0e1 100644
--- a/src/api/routes/albums/link/linkEditPOST.js
+++ b/src/api/routes/albums/link/linkEditPOST.js
@@ -1,5 +1,4 @@
const Route = require('../../../structures/Route');
-const log = require('../../../utils/Log');
class linkEditPOST extends Route {
constructor() {
@@ -14,17 +13,22 @@ class linkEditPOST extends Route {
/*
Make sure the link exists
*/
- const link = await db.table('links').where({ identifier, userId: user.id }).first();
- if (!link) return res.status(400).json({ message: 'The link doesn\'t exist or doesn\'t belong to the user' });
+ const link = await db
+ .table('links')
+ .where({ identifier, userId: user.id })
+ .first();
+ if (!link) return res.status(400).json({ message: "The link doesn't exist or doesn't belong to the user" });
try {
- await db.table('links')
+ const updateObj = {
+ enableDownload: enableDownload || false,
+ expiresAt, // This one should be null if not supplied
+ };
+ await db
+ .table('links')
.where({ identifier })
- .update({
- enableDownload: enableDownload || false,
- expiresAt // This one should be null if not supplied
- });
- return res.json({ message: 'Editing the link was successful' });
+ .update(updateObj);
+ return res.json({ message: 'Editing the link was successful', data: updateObj });
} catch (error) {
return super.error(res, error);
}
diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js
index 6009922..d58598a 100644
--- a/src/api/routes/albums/link/linkPOST.js
+++ b/src/api/routes/albums/link/linkPOST.js
@@ -14,14 +14,21 @@ class linkPOST extends Route {
/*
Make sure the album exists
*/
- const exists = await db.table('albums').where({ id: albumId, userId: user.id }).first();
+ const exists = await db
+ .table('albums')
+ .where({ id: albumId, userId: user.id })
+ .first();
if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
/*
Count the amount of links created for that album already and error out if max was reached
*/
- const count = await db.table('links').where('albumId', albumId).count({ count: 'id' });
- if (count[0].count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' });
+ const count = await db
+ .table('links')
+ .where('albumId', albumId)
+ .count({ count: 'id' })
+ .first();
+ if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' });
/*
Try to allocate a new identifier on the db
@@ -30,19 +37,20 @@ class linkPOST extends Route {
if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' });
try {
- await db.table('links').insert({
+ const insertObj = {
identifier,
userId: user.id,
albumId,
enabled: true,
enableDownload: true,
expiresAt: null,
- views: 0
- });
+ views: 0,
+ };
+ await db.table('links').insert(insertObj);
return res.json({
message: 'The link was created successfully',
- identifier
+ data: insertObj,
});
} catch (error) {
return super.error(res, error);
diff --git a/src/api/routes/albums/link/linksGET.js b/src/api/routes/albums/link/linksGET.js
index edab49a..4487c26 100644
--- a/src/api/routes/albums/link/linksGET.js
+++ b/src/api/routes/albums/link/linksGET.js
@@ -14,7 +14,7 @@ class linkPOST extends Route {
return res.json({
message: 'Successfully retrieved links',
- links
+ links,
});
}
}
diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js
index 205737a..5c7730c 100644
--- a/src/api/routes/auth/loginPOST.js
+++ b/src/api/routes/auth/loginPOST.js
@@ -1,7 +1,7 @@
-const Route = require('../../structures/Route');
const bcrypt = require('bcrypt');
const moment = require('moment');
const JWT = require('jsonwebtoken');
+const Route = require('../../structures/Route');
class loginPOST extends Route {
constructor() {
@@ -36,7 +36,7 @@ class loginPOST extends Route {
const jwt = JWT.sign({
iss: 'lolisafe',
sub: user.id,
- iat: moment.utc().valueOf()
+ iat: moment.utc().valueOf(),
}, process.env.SECRET, { expiresIn: '30d' });
return res.json({
@@ -45,10 +45,10 @@ class loginPOST extends Route {
id: user.id,
username: user.username,
apiKey: user.apiKey,
- isAdmin: user.isAdmin
+ isAdmin: user.isAdmin,
},
token: jwt,
- apiKey: user.apiKey
+ apiKey: user.apiKey,
});
}
}
diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js
index feeb360..e2ac018 100644
--- a/src/api/routes/auth/registerPOST.js
+++ b/src/api/routes/auth/registerPOST.js
@@ -1,7 +1,7 @@
-const Route = require('../../structures/Route');
-const log = require('../../utils/Log');
const bcrypt = require('bcrypt');
const moment = require('moment');
+const Route = require('../../structures/Route');
+const log = require('../../utils/Log');
class registerPOST extends Route {
constructor() {
@@ -9,7 +9,7 @@ class registerPOST extends Route {
}
async run(req, res, db) {
- if (process.env.USER_ACCOUNTS == 'false') return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
+ if (process.env.USER_ACCOUNTS === 'false') return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { username, password } = req.body;
if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
@@ -50,7 +50,7 @@ class registerPOST extends Route {
createdAt: now,
editedAt: now,
enabled: true,
- isAdmin: false
+ isAdmin: false,
});
return res.json({ message: 'The account was created successfully' });
}
diff --git a/src/api/routes/files/albumAddPOST.js b/src/api/routes/files/albumAddPOST.js
index af39caa..a88e636 100644
--- a/src/api/routes/files/albumAddPOST.js
+++ b/src/api/routes/files/albumAddPOST.js
@@ -24,7 +24,8 @@ class albumAddPOST extends Route {
}
return res.json({
- message: 'Successfully added file to album'
+ message: 'Successfully added file to album',
+ data: { fileId, album: { id: album.id, name: album.name } },
});
}
}
diff --git a/src/api/routes/files/albumDelPOST.js b/src/api/routes/files/albumDelPOST.js
index 9a4b87b..6e4d576 100644
--- a/src/api/routes/files/albumDelPOST.js
+++ b/src/api/routes/files/albumDelPOST.js
@@ -25,7 +25,8 @@ class albumDelPOST extends Route {
}
return res.json({
- message: 'Successfully removed file from album'
+ message: 'Successfully removed file from album',
+ data: { fileId, album: { id: album.id, name: album.name } },
});
}
}
diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js
index 7f1190c..f5f2f3b 100644
--- a/src/api/routes/files/filesAlbumsGET.js
+++ b/src/api/routes/files/filesAlbumsGET.js
@@ -18,7 +18,7 @@ class filesGET extends Route {
.select('albumId');
if (albumFiles.length) {
- albumFiles = albumFiles.map(a => a.albumId);
+ albumFiles = albumFiles.map((a) => a.albumId);
albums = await db.table('albums')
.whereIn('id', albumFiles)
.select('id', 'name');
@@ -26,7 +26,7 @@ class filesGET extends Route {
return res.json({
message: 'Successfully retrieved file albums',
- albums
+ albums,
});
}
}
diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js
index 9e90633..ce1d788 100644
--- a/src/api/routes/files/filesGET.js
+++ b/src/api/routes/files/filesGET.js
@@ -36,7 +36,7 @@ class filesGET extends Route {
return res.json({
message: 'Successfully retrieved files',
files,
- count
+ count,
});
}
}
diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js
index 25467ab..07ecb18 100644
--- a/src/api/routes/files/tagAddPOST.js
+++ b/src/api/routes/files/tagAddPOST.js
@@ -14,7 +14,8 @@ class tagAddPOST extends Route {
const file = await db.table('files').where({ id: fileId, userId: user.id }).first();
if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' });
- tagNames.forEach(async tag => {
+ // eslint-disable-next-line consistent-return
+ tagNames.forEach(async (tag) => {
try {
await db.table('fileTags').insert({ fileId, tag });
} catch (error) {
@@ -23,7 +24,7 @@ class tagAddPOST extends Route {
});
return res.json({
- message: 'Successfully added file to album'
+ message: 'Successfully added file to album',
});
}
}
diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js
index b653066..3c6a2f8 100644
--- a/src/api/routes/service/configGET.js
+++ b/src/api/routes/service/configGET.js
@@ -15,11 +15,11 @@ class configGET extends Route {
maxUploadSize: parseInt(process.env.MAX_SIZE, 10),
filenameLength: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10),
albumLinkLength: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10),
- generateThumbnails: process.env.GENERATE_THUMBNAILS == 'true' ? true : false,
- generateZips: process.env.GENERATE_ZIPS == 'true' ? true : false,
- publicMode: process.env.PUBLIC_MODE == 'true' ? true : false,
- enableAccounts: process.env.USER_ACCOUNTS == 'true' ? true : false
- }
+ generateThumbnails: process.env.GENERATE_THUMBNAILS === 'true',
+ generateZips: process.env.GENERATE_ZIPS === 'true',
+ publicMode: process.env.PUBLIC_MODE === 'true',
+ enableAccounts: process.env.USER_ACCOUNTS === 'true',
+ },
});
}
}
diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js
index b6ec395..856e0d4 100644
--- a/src/api/routes/tags/tagPOST.js
+++ b/src/api/routes/tags/tagPOST.js
@@ -1,5 +1,5 @@
-const Route = require('../../structures/Route');
const moment = require('moment');
+const Route = require('../../structures/Route');
class tagPOST extends Route {
constructor() {
@@ -22,7 +22,7 @@ class tagPOST extends Route {
name,
userId: user.id,
createdAt: now,
- editedAt: now
+ editedAt: now,
});
return res.json({ message: 'The tag was created successfully' });
diff --git a/src/api/routes/tags/tagsGET.js b/src/api/routes/tags/tagsGET.js
index 871148e..848e08d 100644
--- a/src/api/routes/tags/tagsGET.js
+++ b/src/api/routes/tags/tagsGET.js
@@ -1,5 +1,4 @@
const Route = require('../../structures/Route');
-const Util = require('../../utils/Util');
class tagsGET extends Route {
constructor() {
@@ -20,7 +19,7 @@ class tagsGET extends Route {
return res.json({
message: 'Successfully retrieved tags',
- tags
+ tags,
});
} catch (error) {
return super.error(res, error);
diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js
index 013c0d6..a9baf55 100644
--- a/src/api/routes/uploads/chunksPOST.js
+++ b/src/api/routes/uploads/chunksPOST.js
@@ -1,27 +1,27 @@
-const Route = require('../../structures/Route');
const path = require('path');
-const Util = require('../../utils/Util');
const jetpack = require('fs-jetpack');
const randomstring = require('randomstring');
+const Util = require('../../utils/Util');
+const Route = require('../../structures/Route');
class uploadPOST extends Route {
constructor() {
super('/upload/chunks', 'post', {
bypassAuth: true,
- canApiKey: true
+ canApiKey: true,
});
}
- async run(req, res, db) {
+ async run(req, res) {
const filename = Util.getUniqueFilename(randomstring.generate(32));
// console.log('Files', req.body.files);
const info = {
size: req.body.files[0].size,
- url: `${process.env.DOMAIN}/`
+ url: `${process.env.DOMAIN}/`,
};
for (const chunk of req.body.files) {
- const { uuid, count } = chunk;
+ const { uuid } = chunk;
// console.log('Chunk', chunk);
const chunkOutput = path.join(__dirname,
@@ -65,7 +65,7 @@ class uploadPOST extends Route {
return res.status(201).send({
message: 'Sucessfully merged the chunk(s).',
- ...info
+ ...info,
/*
name: `${filename}${ext || ''}`,
size: exists.size,
diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js
index 6c01dd3..99f5ee5 100644
--- a/src/api/routes/uploads/uploadPOST.js
+++ b/src/api/routes/uploads/uploadPOST.js
@@ -1,17 +1,18 @@
-const Route = require('../../structures/Route');
const path = require('path');
-const Util = require('../../utils/Util');
const jetpack = require('fs-jetpack');
const multer = require('multer');
const moment = require('moment');
+const Util = require('../../utils/Util');
+const Route = require('../../structures/Route');
+
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000),
- files: 1
+ files: 1,
},
- fileFilter: (req, file, cb) => {
- // TODO: Enable blacklisting of files/extensions
+ fileFilter: (req, file, cb) =>
+ // TODO: Enable blacklisting of files/extensions
/*
if (options.blacklist.mimes.includes(file.mimetype)) {
return cb(new Error(`${file.mimetype} is a blacklisted filetype.`));
@@ -19,35 +20,34 @@ const upload = multer({
return cb(new Error(`${path.extname(file.originalname).toLowerCase()} is a blacklisted extension.`));
}
*/
- return cb(null, true);
- }
+ cb(null, true)
+ ,
}).array('files[]');
/*
TODO: If source has transparency generate a png thumbnail, otherwise a jpg.
TODO: If source is a gif, generate a thumb of the first frame and play the gif on hover on the frontend.
- TODO: If source is a video, generate a thumb of the first frame and save the video length to the file?
- Another possible solution would be to play a gif on hover that grabs a few chunks like youtube.
TODO: Think if its worth making a folder with the user uuid in uploads/ and upload the pictures there so
that this way at least not every single file will be in 1 directory
- - Addendum to this: Now that the default behaviour is to serve files with node, we can actually pull this off. Before this, having files in
- subfolders meant messing with nginx and the paths, but now it should be fairly easy to re-arrange the folder structure with express.static
- I see great value in this, open to suggestions.
+ XXX: Now that the default behaviour is to serve files with node, we can actually pull this off.
+ Before this, having files in subfolders meant messing with nginx and the paths,
+ but now it should be fairly easy to re-arrange the folder structure with express.static
+ I see great value in this, open to suggestions.
*/
class uploadPOST extends Route {
constructor() {
super('/upload', 'post', {
bypassAuth: true,
- canApiKey: true
+ canApiKey: true,
});
}
async run(req, res, db) {
const user = await Util.isAuthorized(req);
- if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' });
+ if (!user && process.env.PUBLIC_MODE === 'false') return res.status(401).json({ message: 'Not authorized to use this resource' });
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' });
@@ -56,12 +56,13 @@ class uploadPOST extends Route {
if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
}
- return upload(req, res, async err => {
+ return upload(req, res, async (err) => {
if (err) console.error(err.message);
let uploadedFile = {};
let insertedId;
+ // eslint-disable-next-line no-underscore-dangle
const remappedKeys = this._remapKeys(req.body);
const file = req.files[0];
@@ -83,10 +84,7 @@ class uploadPOST extends Route {
if (remappedKeys && remappedKeys.uuid) {
const chunkOutput = path.join(__dirname,
- '..',
- '..',
- '..',
- '..',
+ '../../../../',
process.env.UPLOAD_FOLDER,
'chunks',
remappedKeys.uuid,
@@ -94,10 +92,7 @@ class uploadPOST extends Route {
await jetpack.writeAsync(chunkOutput, file.buffer);
} else {
const output = path.join(__dirname,
- '..',
- '..',
- '..',
- '..',
+ '../../../../',
process.env.UPLOAD_FOLDER,
filename);
await jetpack.writeAsync(output, file.buffer);
@@ -105,7 +100,7 @@ class uploadPOST extends Route {
name: filename,
hash,
size: file.buffer.length,
- url: filename
+ url: filename,
};
}
@@ -124,7 +119,7 @@ class uploadPOST extends Route {
return res.status(201).send({
message: 'Sucessfully uploaded the file.',
- ...uploadedFile
+ ...uploadedFile,
});
});
}
@@ -137,7 +132,7 @@ class uploadPOST extends Route {
size: exists.size,
url: `${process.env.DOMAIN}/${exists.name}`,
deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}`,
- repeated: true
+ repeated: true,
});
return Util.deleteFile(filename);
@@ -145,7 +140,7 @@ class uploadPOST extends Route {
async checkIfFileExists(db, user, hash) {
const exists = await db.table('files')
- .where(function() { // eslint-disable-line func-names
+ .where(function () { // eslint-disable-line func-names
if (user) this.where('userId', user.id);
else this.whereNull('userId');
})
@@ -186,7 +181,7 @@ class uploadPOST extends Route {
hash: file.hash,
ip: req.ip,
createdAt: now,
- editedAt: now
+ editedAt: now,
});
} else {
insertedId = await db.table('files').insert({
@@ -198,7 +193,7 @@ class uploadPOST extends Route {
hash: file.hash,
ip: req.ip,
createdAt: now,
- editedAt: now
+ editedAt: now,
}, 'id');
}
return insertedId;
@@ -220,6 +215,7 @@ class uploadPOST extends Route {
}
return body;
}
+ return keys;
}
}
diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js
index a87d98d..a63f0c0 100644
--- a/src/api/routes/user/apiKey.js
+++ b/src/api/routes/user/apiKey.js
@@ -1,7 +1,7 @@
-const Route = require('../../structures/Route');
const randomstring = require('randomstring');
const moment = require('moment');
const { dump } = require('dumper.js');
+const Route = require('../../structures/Route');
class apiKeyPOST extends Route {
constructor() {
@@ -17,7 +17,7 @@ class apiKeyPOST extends Route {
.where({ id: user.id })
.update({
apiKey,
- apiKeyEditedAt: now
+ apiKeyEditedAt: now,
});
} catch (error) {
dump(error);
@@ -26,7 +26,7 @@ class apiKeyPOST extends Route {
return res.json({
message: 'Successfully created new api key',
- apiKey
+ apiKey,
});
}
}
diff --git a/src/api/routes/user/changePasswordPOST.js b/src/api/routes/user/changePasswordPOST.js
index 9cd621e..1b3a27a 100644
--- a/src/api/routes/user/changePasswordPOST.js
+++ b/src/api/routes/user/changePasswordPOST.js
@@ -1,7 +1,7 @@
-const Route = require('../../structures/Route');
-const log = require('../../utils/Log');
const bcrypt = require('bcrypt');
const moment = require('moment');
+const Route = require('../../structures/Route');
+const log = require('../../utils/Log');
class changePasswordPOST extends Route {
constructor() {
@@ -36,7 +36,7 @@ class changePasswordPOST extends Route {
const now = moment.utc().toDate();
await db.table('users').where('id', user.id).update({
password: hash,
- passwordEditedAt: now
+ passwordEditedAt: now,
});
return res.json({ message: 'The password was changed successfully' });
diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js
index fe46fd4..6f179a9 100644
--- a/src/api/routes/user/userGET.js
+++ b/src/api/routes/user/userGET.js
@@ -11,8 +11,9 @@ class usersGET extends Route {
user: {
id: user.id,
username: user.username,
- isAdmin: user.isAdmin
- }
+ isAdmin: user.isAdmin,
+ apiKey: user.apiKey,
+ },
});
}
}
diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js
index 2f370e8..107c20a 100644
--- a/src/api/routes/verifyGET.js
+++ b/src/api/routes/verifyGET.js
@@ -11,8 +11,8 @@ class verifyGET extends Route {
user: {
id: user.id,
username: user.username,
- isAdmin: user.isAdmin
- }
+ isAdmin: user.isAdmin,
+ },
});
}
}
diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js
index 8956c24..6be0dc7 100644
--- a/src/api/structures/Route.js
+++ b/src/api/structures/Route.js
@@ -7,23 +7,19 @@ const db = require('knex')({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
- filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite')
+ filename: nodePath.join(__dirname, '../../../database.sqlite'),
},
- postProcessResponse: result => {
+ postProcessResponse: (result) => {
/*
Fun fact: Depending on the database used by the user and given that I don't want
to force a specific database for everyone because of the nature of this project,
some things like different data types for booleans need to be considered like in
the implementation below where sqlite returns 1 and 0 instead of true and false.
*/
- const booleanFields = [
- 'enabled',
- 'enableDownload',
- 'isAdmin'
- ];
+ const booleanFields = ['enabled', 'enableDownload', 'isAdmin'];
- const processResponse = row => {
- Object.keys(row).forEach(key => {
+ const processResponse = (row) => {
+ Object.keys(row).forEach((key) => {
if (booleanFields.includes(key)) {
if (row[key] === 0) row[key] = false;
else if (row[key] === 1) row[key] = true;
@@ -32,11 +28,11 @@ const db = require('knex')({
return row;
};
- if (Array.isArray(result)) return result.map(row => processResponse(row));
+ if (Array.isArray(result)) return result.map((row) => processResponse(row));
if (typeof result === 'object') return processResponse(result);
return result;
},
- useNullAsDefault: process.env.DB_CLIENT === 'sqlite3' ? true : false
+ useNullAsDefault: process.env.DB_CLIENT === 'sqlite3',
});
const moment = require('moment');
const log = require('../utils/Log');
@@ -52,11 +48,15 @@ class Route {
}
async authorize(req, res) {
- const banned = await db.table('bans').where({ ip: req.ip }).first();
+ const banned = await db
+ .table('bans')
+ .where({ ip: req.ip })
+ .first();
if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' });
if (this.options.bypassAuth) return this.run(req, res, db);
- // The only reason I call it token here and not Api Key is to be backwards compatible with the uploader and sharex
+ // The only reason I call it token here and not Api Key is to be backwards compatible
+ // with the uploader and sharex
// Small price to pay.
if (req.headers.token) return this.authorizeApiKey(req, res, req.headers.token);
if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' });
@@ -72,11 +72,16 @@ class Route {
const id = decoded ? decoded.sub : '';
const iat = decoded ? decoded.iat : '';
- const user = await db.table('users').where({ id }).first();
+ const user = await db
+ .table('users')
+ .where({ id })
+ .first();
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
- if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' });
+ if (iat && iat < moment(user.passwordEditedAt).format('x')) {
+ return res.status(401).json({ message: 'Token expired' });
+ }
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
- if (this.options.adminOnly && !user.isAdmin) return res.status(401).json({ message: 'Invalid authorization' });
+ if (this.options.adminOnly && !user.isAdmin) { return res.status(401).json({ message: 'Invalid authorization' }); }
return this.run(req, res, db, user);
});
@@ -84,16 +89,17 @@ class Route {
async authorizeApiKey(req, res, apiKey) {
if (!this.options.canApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' });
- const user = await db.table('users').where({ apiKey }).first();
+ const user = await db
+ .table('users')
+ .where({ apiKey })
+ .first();
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
return this.run(req, res, db, user);
}
- run(req, res, db) { // eslint-disable-line no-unused-vars
- return;
- }
+ run() {}
error(res, error) {
log.error(error);
diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js
index a8eccd9..c8537fb 100644
--- a/src/api/structures/Server.js
+++ b/src/api/structures/Server.js
@@ -1,6 +1,5 @@
require('dotenv').config();
-const log = require('../utils/Log');
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
@@ -8,11 +7,15 @@ const RateLimit = require('express-rate-limit');
const bodyParser = require('body-parser');
const jetpack = require('fs-jetpack');
const path = require('path');
+const morgan = require('morgan');
+const log = require('../utils/Log');
+const ThumbUtil = require('../utils/ThumbUtil');
+// eslint-disable-next-line no-unused-vars
const rateLimiter = new RateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10),
max: parseInt(process.env.RATE_LIMIT_MAX, 10),
- delayMs: 0
+ delayMs: 0,
});
class Server {
@@ -32,16 +35,38 @@ class Server {
});
this.server.use(bodyParser.urlencoded({ extended: true }));
this.server.use(bodyParser.json());
+ if (process.env.NODE_ENV !== 'production') {
+ this.server.use(morgan('combined', {
+ skip(req) {
+ let ext = req.path.split('.').pop();
+ if (ext) { ext = `.${ext.toLowerCase()}`; }
+
+ if (
+ ThumbUtil.imageExtensions.indexOf(ext) > -1
+ || ThumbUtil.videoExtensions.indexOf(ext) > -1
+ || req.path.indexOf('_nuxt') > -1
+ || req.path.indexOf('favicon.ico') > -1
+ ) {
+ return true;
+ }
+ return false;
+ },
+ 'stream': {
+ write(str) { log.debug(str); },
+ },
+ }));
+ }
// this.server.use(rateLimiter);
// Serve the uploads
- this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads')));
- this.routesFolder = path.join(__dirname, '..', 'routes');
+ this.server.use(express.static(path.join(__dirname, '../../../uploads')));
+ this.routesFolder = path.join(__dirname, '../routes');
}
registerAllTheRoutes() {
- jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => {
- const RouteClass = require(path.join('..', '..', '..', routeFile));
+ jetpack.find(this.routesFolder, { matching: '*.js' }).forEach((routeFile) => {
+ // eslint-disable-next-line import/no-dynamic-require, global-require
+ const RouteClass = require(path.join('../../../', routeFile));
let routes = [RouteClass];
if (Array.isArray(RouteClass)) routes = RouteClass;
for (const File of routes) {
@@ -55,7 +80,7 @@ class Server {
serveNuxt() {
// Serve the frontend if we are in production mode
if (process.env.NODE_ENV === 'production') {
- this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist')));
+ this.server.use(express.static(path.join(__dirname, '../../../dist')));
}
/*
@@ -66,7 +91,7 @@ class Server {
*/
this.server.all('*', (_req, res) => {
try {
- res.sendFile(path.join(__dirname, '..', '..', '..', 'dist', 'index.html'));
+ res.sendFile(path.join(__dirname, '../../../dist/index.html'));
} catch (error) {
res.json({ success: false, message: 'Something went wrong' });
}
diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js
index 6753f9e..99d11e4 100644
--- a/src/api/utils/Log.js
+++ b/src/api/utils/Log.js
@@ -22,11 +22,10 @@ class Log {
else console.log(chalk.red(args)); // eslint-disable-line no-console
}
- /*
- static dump(args) {
- dump(args);
+ static debug(args) {
+ if (this.checkIfArrayOrObject(args)) dump(args);
+ else console.log(chalk.gray(args)); // eslint-disable-line no-console
}
- */
static checkIfArrayOrObject(thing) {
if (typeof thing === typeof [] || typeof thing === typeof {}) return true;
diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js
new file mode 100644
index 0000000..98ba5c0
--- /dev/null
+++ b/src/api/utils/ThumbUtil.js
@@ -0,0 +1,108 @@
+const jetpack = require('fs-jetpack');
+const path = require('path');
+const sharp = require('sharp');
+const ffmpeg = require('fluent-ffmpeg');
+const previewUtil = require('./videoPreview/FragmentPreview');
+
+const log = require('./Log');
+
+class ThumbUtil {
+ static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
+
+ static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
+
+ static thumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs');
+
+ static squareThumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'square');
+
+ static videoPreviewPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'preview');
+
+ static generateThumbnails(filename) {
+ const ext = path.extname(filename).toLowerCase();
+ const output = `${filename.slice(0, -ext.length)}.png`;
+ const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
+
+ // eslint-disable-next-line max-len
+ if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output);
+ // eslint-disable-next-line max-len
+ if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput);
+ return null;
+ }
+
+ static async generateThumbnailForImage(filename, output) {
+ const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename);
+
+ const file = await jetpack.readAsync(filePath, 'buffer');
+ await sharp(file)
+ .resize(64, 64)
+ .toFormat('png')
+ .toFile(path.join(ThumbUtil.squareThumbPath, output));
+ await sharp(file)
+ .resize(225, null)
+ .toFormat('png')
+ .toFile(path.join(ThumbUtil.thumbPath, output));
+ }
+
+ static async generateThumbnailForVideo(filename, output) {
+ const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename);
+
+ ffmpeg(filePath)
+ .thumbnail({
+ timestamps: [0],
+ filename: '%b.png',
+ folder: ThumbUtil.squareThumbPath,
+ size: '64x64',
+ })
+ .on('error', (error) => log.error(error.message));
+
+ ffmpeg(filePath)
+ .thumbnail({
+ timestamps: [0],
+ filename: '%b.png',
+ folder: ThumbUtil.thumbPath,
+ size: '150x?',
+ })
+ .on('error', (error) => log.error(error.message));
+
+ try {
+ await previewUtil({
+ input: filePath,
+ width: 150,
+ output: path.join(ThumbUtil.videoPreviewPath, output),
+ log: log.debug,
+ });
+ } catch (e) {
+ log.error(e);
+ }
+ }
+
+ static getFileThumbnail(filename) {
+ if (!filename) return null;
+ const ext = path.extname(filename).toLowerCase();
+
+ const isImage = ThumbUtil.imageExtensions.includes(ext);
+ const isVideo = ThumbUtil.videoExtensions.includes(ext);
+
+ if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.png` };
+ if (isVideo) {
+ return {
+ thumb: `${filename.slice(0, -ext.length)}.png`,
+ preview: `${filename.slice(0, -ext.length)}.webm`,
+ };
+ }
+
+ return null;
+ }
+
+ static async removeThumbs({ thumb, preview }) {
+ if (thumb) {
+ await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumb));
+ await jetpack.removeAsync(path.join(ThumbUtil.squareThumbPath, thumb));
+ }
+ if (preview) {
+ await jetpack.removeAsync(path.join(ThumbUtil.videoPreviewPath, preview));
+ }
+ }
+}
+
+module.exports = ThumbUtil;
diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js
index 07c295d..905948a 100644
--- a/src/api/utils/Util.js
+++ b/src/api/utils/Util.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-await-in-loop */
const jetpack = require('fs-jetpack');
const randomstring = require('randomstring');
const path = require('path');
@@ -9,23 +10,23 @@ const db = require('knex')({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
- filename: path.join(__dirname, '..', '..', '..', 'database.sqlite')
+ filename: path.join(__dirname, '../../../database.sqlite'),
},
- useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false
+ useNullAsDefault: process.env.DB_CLIENT === 'sqlite',
});
const moment = require('moment');
-const log = require('../utils/Log');
const crypto = require('crypto');
-const sharp = require('sharp');
-const ffmpeg = require('fluent-ffmpeg');
const Zip = require('adm-zip');
const uuidv4 = require('uuid/v4');
-const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
-const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
+const log = require('./Log');
+const ThumbUtil = require('./ThumbUtil');
+
const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(',');
class Util {
+ static uploadPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER);
+
static uuid() {
return uuidv4();
}
@@ -34,62 +35,17 @@ class Util {
return blockedExtensions.includes(extension);
}
- static generateThumbnails(filename) {
- const ext = path.extname(filename).toLowerCase();
- const output = `${filename.slice(0, -ext.length)}.png`;
- if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output);
- if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename);
- return null;
- }
-
- static async generateThumbnailForImage(filename, output) {
- const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer');
- await sharp(file)
- .resize(64, 64)
- .toFormat('png')
- .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output));
- await sharp(file)
- .resize(225, null)
- .toFormat('png')
- .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output));
- }
-
- static generateThumbnailForVideo(filename) {
- ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'),
- size: '64x64'
- })
- .on('error', error => log.error(error.message));
- ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'),
- size: '150x?'
- })
- .on('error', error => log.error(error.message));
- }
-
- static getFileThumbnail(filename) {
- if (!filename) return null;
- const ext = path.extname(filename).toLowerCase();
- if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null;
- return `${filename.slice(0, -ext.length)}.png`;
- }
-
static constructFilePublicLink(file) {
/*
TODO: This wont work without a reverse proxy serving both
the site and the API under the same domain. Pls fix.
*/
file.url = `${process.env.DOMAIN}/${file.name}`;
- const thumb = this.getFileThumbnail(file.name);
+ const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
if (thumb) {
file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`;
file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`;
+ file.preview = preview && `${process.env.DOMAIN}/thumbs/preview/${preview}`;
}
return file;
}
@@ -98,11 +54,11 @@ class Util {
const retry = (i = 0) => {
const filename = randomstring.generate({
length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10),
- capitalization: 'lowercase'
+ capitalization: 'lowercase',
}) + path.extname(name).toLowerCase();
// TODO: Change this to look for the file in the db instead of in the filesystem
- const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename));
+ const exists = jetpack.exists(path.join(Util.uploadPath, filename));
if (!exists) return filename;
if (i < 5) return retry(i + 1);
log.error('Couldnt allocate identifier for file');
@@ -115,14 +71,17 @@ class Util {
const retry = async (i = 0) => {
const identifier = randomstring.generate({
length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10),
- capitalization: 'lowercase'
+ capitalization: 'lowercase',
});
- const exists = await db.table('links').where({ identifier }).first();
+ const exists = await db
+ .table('links')
+ .where({ identifier })
+ .first();
if (!exists) return identifier;
/*
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
*/
- if (i < 5) return retry(++i);
+ if (i < 5) return retry(i + 1);
log.error('Couldnt allocate identifier for album');
return null;
};
@@ -130,7 +89,7 @@ class Util {
}
static async getFileHash(filename) {
- const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer');
+ const file = await jetpack.readAsync(path.join(Util.uploadPath, filename), 'buffer');
if (!file) {
log.error(`There was an error reading the file < ${filename} > for hashing`);
return null;
@@ -142,7 +101,10 @@ class Util {
}
static generateFileHash(data) {
- const hash = crypto.createHash('md5').update(data).digest('hex');
+ const hash = crypto
+ .createHash('md5')
+ .update(data)
+ .digest('hex');
return hash;
}
@@ -151,13 +113,16 @@ class Util {
}
static async deleteFile(filename, deleteFromDB = false) {
- const thumbName = this.getFileThumbnail(filename);
+ const thumbName = ThumbUtil.getFileThumbnail(filename);
try {
- await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename));
- await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName));
- await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName));
+ await jetpack.removeAsync(path.join(Util.uploadPath, filename));
+ await ThumbUtil.removeThumbs(thumbName);
+
if (deleteFromDB) {
- await db.table('files').where('name', filename).delete();
+ await db
+ .table('files')
+ .where('name', filename)
+ .delete();
}
} catch (error) {
log.error(`There was an error removing the file < ${filename} >`);
@@ -169,10 +134,13 @@ class Util {
try {
const fileAlbums = await db.table('albumsFiles').where({ albumId: id });
for (const fileAlbum of fileAlbums) {
- const file = await db.table('files')
+ const file = await db
+ .table('files')
.where({ id: fileAlbum.fileId })
.first();
+
if (!file) continue;
+
await this.deleteFile(file.name, true);
}
} catch (error) {
@@ -195,7 +163,8 @@ class Util {
try {
const fileTags = await db.table('fileTags').where({ tagId: id });
for (const fileTag of fileTags) {
- const file = await db.table('files')
+ const file = await db
+ .table('files')
.where({ id: fileTag.fileId })
.first();
if (!file) continue;
@@ -219,7 +188,10 @@ class Util {
const id = decoded ? decoded.sub : '';
const iat = decoded ? decoded.iat : '';
- const user = await db.table('users').where({ id }).first();
+ const user = await db
+ .table('users')
+ .where({ id })
+ .first();
if (!user || !user.enabled) return false;
if (iat && iat < moment(user.passwordEditedAt).format('x')) return false;
@@ -231,13 +203,25 @@ class Util {
try {
const zip = new Zip();
for (const file of files) {
- zip.addLocalFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file));
+ zip.addLocalFile(path.join(Util.uploadPath, file));
}
- zip.writeZip(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`));
+ zip.writeZip(
+ path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ process.env.UPLOAD_FOLDER,
+ 'zips',
+ `${album.userId}-${album.id}.zip`,
+ ),
+ );
} catch (error) {
log.error(error);
}
}
+
+ static generateThumbnails = ThumbUtil.generateThumbnails;
}
module.exports = Util;
diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js
new file mode 100644
index 0000000..bf623c1
--- /dev/null
+++ b/src/api/utils/videoPreview/FragmentPreview.js
@@ -0,0 +1,88 @@
+/* eslint-disable no-bitwise */
+const ffmpeg = require('fluent-ffmpeg');
+const probe = require('ffmpeg-probe');
+
+const noop = () => {};
+
+const getRandomInt = (min, max) => {
+ const minInt = Math.ceil(min);
+ const maxInt = Math.floor(max);
+
+ // eslint-disable-next-line no-mixed-operators
+ return Math.floor(Math.random() * (maxInt - minInt + 1) + minInt);
+};
+
+const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPercent) => {
+ // by subtracting the fragment duration we can be sure that the resulting
+ // start time + fragment duration will be less than the video duration
+ const safeVDuration = vDuration - fDuration;
+
+ // if the fragment duration is longer than the video duration
+ if (safeVDuration <= 0) {
+ return 0;
+ }
+
+ return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration);
+};
+
+module.exports = async (opts) => {
+ const {
+ log = noop,
+
+ // general output options
+ quality = 2,
+ width,
+ height,
+ input,
+ output,
+
+ fragmentDurationSecond = 3,
+ ignoreBeforePercent = 0.25,
+ ignoreAfterPercent = 0.75,
+ } = opts;
+
+ const info = await probe(input);
+
+ let { duration } = info.format;
+ duration = parseInt(duration, 10);
+
+ const startTime = getStartTime(duration, fragmentDurationSecond, ignoreBeforePercent, ignoreAfterPercent);
+
+ const result = { startTime, duration };
+
+ await new Promise((resolve, reject) => {
+ let scale = null;
+
+ if (width && height) {
+ result.width = width | 0;
+ result.height = height | 0;
+ scale = `scale=${width}:${height}`;
+ } else if (width) {
+ result.width = width | 0;
+ result.height = ((info.height * width) / info.width) | 0;
+ scale = `scale=${width}:-1`;
+ } else if (height) {
+ result.height = height | 0;
+ result.width = ((info.width * height) / info.height) | 0;
+ scale = `scale=-1:${height}`;
+ } else {
+ result.width = info.width;
+ result.height = info.height;
+ }
+
+ return ffmpeg()
+ .input(input)
+ .inputOptions([`-ss ${startTime}`])
+ .outputOptions(['-vsync', 'vfr'])
+ .outputOptions(['-q:v', quality, '-vf', scale])
+ .outputOptions([`-t ${fragmentDurationSecond}`])
+ .noAudio()
+ .output(output)
+ .on('start', (cmd) => log && log({ cmd }))
+ .on('end', resolve)
+ .on('error', reject)
+ .run();
+ });
+
+ return result;
+};
diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js
new file mode 100644
index 0000000..8c5f1c3
--- /dev/null
+++ b/src/api/utils/videoPreview/FrameIntervalPreview.js
@@ -0,0 +1,73 @@
+/* eslint-disable no-bitwise */
+const ffmpeg = require('fluent-ffmpeg');
+const probe = require('ffmpeg-probe');
+
+const noop = () => {};
+
+module.exports = async (opts) => {
+ const {
+ log = noop,
+
+ // general output options
+ quality = 2,
+ width,
+ height,
+ input,
+ output,
+
+ numFrames,
+ numFramesPercent = 0.05,
+ } = opts;
+
+ const info = await probe(input);
+ // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10);
+ const { avg_frame_rate: avgFrameRate, duration } = info.streams[0];
+ const [frames, time] = avgFrameRate.split('/').map((e) => parseInt(e, 10));
+
+ const numFramesTotal = (frames / time) * duration;
+
+ let numFramesToCapture = numFrames || numFramesPercent * numFramesTotal;
+ numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0;
+ const nthFrame = (numFramesTotal / numFramesToCapture) | 0;
+
+ const result = {
+ output,
+ numFrames: numFramesToCapture,
+ };
+
+ await new Promise((resolve, reject) => {
+ let scale = null;
+
+ if (width && height) {
+ result.width = width | 0;
+ result.height = height | 0;
+ scale = `scale=${width}:${height}`;
+ } else if (width) {
+ result.width = width | 0;
+ result.height = ((info.height * width) / info.width) | 0;
+ scale = `scale=${width}:-1`;
+ } else if (height) {
+ result.height = height | 0;
+ result.width = ((info.width * height) / info.height) | 0;
+ scale = `scale=-1:${height}`;
+ } else {
+ result.width = info.width;
+ result.height = info.height;
+ }
+
+ const filter = [`select=not(mod(n\\,${nthFrame}))`, scale].filter(Boolean).join(',');
+
+ ffmpeg(input)
+ .outputOptions(['-vsync', 'vfr'])
+ .outputOptions(['-q:v', quality, '-vf', filter])
+ .noAudio()
+ .outputFormat('webm')
+ .output(output)
+ .on('start', (cmd) => log && log({ cmd }))
+ .on('end', () => resolve())
+ .on('error', (err) => reject(err))
+ .run();
+ });
+
+ return result;
+};
diff --git a/src/setup.js b/src/setup.js
index 080320f..91eaa1c 100644
--- a/src/setup.js
+++ b/src/setup.js
@@ -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`;
}
diff --git a/src/site/assets/styles/_bulma_colors_extender.scss b/src/site/assets/styles/_bulma_colors_extender.scss
new file mode 100644
index 0000000..69dbd1e
--- /dev/null
+++ b/src/site/assets/styles/_bulma_colors_extender.scss
@@ -0,0 +1,16 @@
+// Import the initial variables
+@import "../../../node_modules/bulma/sass/utilities/initial-variables";
+@import "../../../node_modules/bulma/sass/utilities/functions";
+
+// Setup our custom colors
+$lolisafe: #323846;
+$lolisafe-invert: findColorInvert($lolisafe);
+
+// XXX: EXPERIMENTAL, CHECK IF WE NEED ORIGINAL PRIMARY ANYWHERE
+// $primary: $lolisafe;
+// $primary-invert: $lolisafe-invert;
+
+// declare custom colors
+$custom-colors: (
+ "lolisafe":($lolisafe, $lolisafe-invert)
+);
diff --git a/src/site/assets/styles/_colors.scss b/src/site/assets/styles/_colors.scss
index a141447..0bc7c5e 100644
--- a/src/site/assets/styles/_colors.scss
+++ b/src/site/assets/styles/_colors.scss
@@ -1,15 +1,19 @@
// $basePink: #EC1A55;
$base-1: #292e39;
-$base-2: #2E3440;
-$base-3: #3B4252;
-$base-4: #434C5E;
-$base-5: #4C566A;
+$base-2: #2e3440;
+$base-3: #3b4252;
+$base-4: #434c5e;
+$base-5: #4c566a;
$background: #1e2430;
$backgroundAccent: #20222b;
-$backgroundAccentLighter: #53555E;
+$backgroundAccentLighter: #53555e;
$backgroundLight1: #f5f6f8;
+$scheme-main: $background;
+$scheme-main-bis: $backgroundAccent;
+$scheme-main-ter: $backgroundAccentLighter;
+
// customize navbar
$navbar-background-color: $backgroundAccent;
$navbar-item-color: #f5f6f8;
@@ -23,16 +27,15 @@ $input-hover-color: $textColor;
$basePink: #ff015b;
$basePinkHover: rgb(196, 4, 71);
-$baseBlue: #30A9ED;
+$baseBlue: #30a9ed;
$baseBlueHover: rgb(21, 135, 201);
$uploaderDropdownColor: #797979;
-$boxShadow: 0 10px 15px rgba(4,39,107,0.2);
+$boxShadow: 0 10px 15px rgba(4, 39, 107, 0.2);
$boxShadowLight: rgba(15, 17, 21, 0.35) 0px 6px 9px 0px;
// pagination
-
$pagination-color: $defaultTextColor;
$pagination-focus-color: $textColorHighlight;
@@ -45,4 +48,24 @@ $pagination-hover-color: $textColorHighlight;
$pagination-hover-border-color: $textColorHighlight;
$pagination-current-background-color: $base-3;
-$pagination-current-border-color: $base-2; \ No newline at end of file
+$pagination-current-border-color: $base-2;
+
+// loading
+$loading-background: rgba(0, 0, 0, 0.8);
+$loading-background: rgba(40, 40, 40, 0.66);
+
+// dialogs
+$modal-card-title-color: $textColor;
+$modal-card-body-background-color: $background;
+$modal-card-head-background-color: $backgroundAccent;
+$modal-card-head-border-bottom: 1px solid rgba(255, 255, 255, 0.1098);
+$modal-card-foot-border-top: 1px solid rgba(255, 255, 255, 0.1098);
+
+// sidebar
+$sidebar-background: $base-1;
+$sidebar-box-shadow: none;
+
+//
+$menu-item-color: $textColor;
+$menu-item-hover-color: $textColorHighlight;
+$menu-item-active-background-color: $backgroundAccent;
diff --git a/src/site/assets/styles/style.scss b/src/site/assets/styles/style.scss
index 69d15d3..202e02a 100644
--- a/src/site/assets/styles/style.scss
+++ b/src/site/assets/styles/style.scss
@@ -1,15 +1,19 @@
// Let's first take care of having the customized colors ready.
-@import "./_colors.scss";
+@import './_bulma_colors_extender.scss';
+@import './_colors.scss';
// Bulma/Buefy customization
-@import "../../../node_modules/bulma/sass/utilities/_all.sass";
+@import '../../../node_modules/bulma/sass/utilities/_all.sass';
$body-size: 14px !default;
-$family-primary: 'Nunito', BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+$family-primary: 'Nunito', BlinkMacSystemFont, -apple-system, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
+ 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
$size-normal: 1rem;
-@import "../../../node_modules/bulma/bulma.sass";
-@import "../../../node_modules/buefy/src/scss/buefy.scss";
+@import '../../../node_modules/bulma/bulma.sass';
+@import '../../../node_modules/buefy/src/scss/buefy.scss';
+
+@import '@mdi/font/css/materialdesignicons.css';
html {
// font-size: 100%;
@@ -18,9 +22,9 @@ html {
}
a {
- color: #5E81AC;
+ color: #5e81ac;
&:hover {
- color: #81A1C1;
+ color: #81a1c1;
text-decoration: underline;
}
}
@@ -43,10 +47,18 @@ h4 {
}
@for $i from 1 through 10 {
- .mt#{$i} { margin-top: $i + em !important; }
- .mb#{$i} { margin-bottom: $i + em !important; }
- .ml#{$i} { margin-left: $i + em !important; }
- .mr#{$i} { margin-right: $i + em !important; }
+ .mt#{$i} {
+ margin-top: $i + em !important;
+ }
+ .mb#{$i} {
+ margin-bottom: $i + em !important;
+ }
+ .ml#{$i} {
+ margin-left: $i + em !important;
+ }
+ .mr#{$i} {
+ margin-right: $i + em !important;
+ }
}
.text-center {
@@ -58,8 +70,12 @@ hr {
height: 1px;
}
// Bulma color changes.
-.tooltip.is-top.is-primary:before { border-top: 5px solid #20222b; }
-.tooltip.is-primary:after { background: #20222b; }
+.tooltip.is-top.is-primary:before {
+ border-top: 5px solid #20222b;
+}
+.tooltip.is-primary:after {
+ background: #20222b;
+}
div#drag-overlay {
position: fixed;
@@ -93,7 +109,6 @@ div#drag-overlay {
}
}
-
section.hero {
&.dashboard {
// background-color: $backgroundLight1 !important;
@@ -103,10 +118,12 @@ section.hero {
}
}
-section input, section a.button {
+section input,
+section a.button {
font-size: 14px !important;
}
-section input, section p.control a.button {
+section input,
+section p.control a.button {
border-left: 0px !important;
border-top: 0px !important;
border-right: 0px !important;
@@ -114,13 +131,15 @@ section input, section p.control a.button {
box-shadow: 0 0 0 !important;
}
-section p.control a.button { margin-left: 10px !important; }
+section p.control a.button {
+ margin-left: 10px !important;
+}
section p.control button {
height: 100%;
font-size: 12px;
}
-.switch input[type=checkbox] + .check:before {
+.switch input[type='checkbox'] + .check:before {
background: #fbfbfb;
}
@@ -128,7 +147,8 @@ section p.control button {
Register and Login forms
*/
-section.hero.is-login, section.hero.is-register {
+section.hero.is-login,
+section.hero.is-register {
a {
font-size: 1.25em;
line-height: 2.5em;
@@ -174,18 +194,22 @@ section#register a.is-text {
font-size: 1.25em;
line-height: 2.5em;
}
-*/
+
.modal-card-head, .modal-card-foot {
background: $backgroundLight1;
}
+*/
.switch {
margin-top: 5px;
}
-.input, .taginput .taginput-container.is-focusable, .textarea, .select select {
+.input,
+.taginput .taginput-container.is-focusable,
+.textarea,
+.select select {
border: 2px solid #21252d;
- border-radius: .3em !important;
+ border-radius: 0.3em !important;
background: rgba(0, 0, 0, 0.15);
padding: 1rem;
color: $textColor;
@@ -198,21 +222,21 @@ section#register a.is-text {
color: $textColor;
}
}
-button.button.is-primary {
+/* button.button.is-primary {
background-color: #323846;
border: 2px solid #21252d;
color: $textColor;
font-size: 1rem;
- border-top: 0;
- border-left: 0;
- border-right: 0;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
&:hover {
background-color: $base-2;
}
&.big {
font-size: 1.25rem;
}
-}
+} */
svg.waves {
display: block;
bottom: -1px;
@@ -224,13 +248,16 @@ svg.waves {
user-select: none;
overflow: hidden;
}
-div.field-body > div.field { text-align: left; }
+div.field-body > div.field {
+ text-align: left;
+}
table.table {
background: $base-2;
color: $textColor;
border: 0;
thead {
- th, td {
+ th,
+ td {
color: $textColor;
}
}
@@ -244,60 +271,55 @@ table.table {
}
}
}
- th, td {
+ th,
+ td {
border-color: #ffffff1c;
}
}
// vue-bar
.vb > .vb-dragger {
- z-index: 5;
- width: 12px;
- right: 0;
+ z-index: 5;
+ width: 12px;
+ right: 0;
}
.vb > .vb-dragger > .vb-dragger-styler {
- -webkit-backface-visibility: hidden;
- backface-visibility: hidden;
- -webkit-transform: rotate3d(0,0,0,0);
- transform: rotate3d(0,0,0,0);
- -webkit-transition:
- background-color 100ms ease-out,
- margin 100ms ease-out,
- height 100ms ease-out;
- transition:
- background-color 100ms ease-out,
- margin 100ms ease-out,
- height 100ms ease-out;
- background-color: $backgroundAccent;
- margin: 5px 5px 5px 0;
- border-radius: 20px;
- height: calc(100% - 10px);
- display: block;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ -webkit-transform: rotate3d(0, 0, 0, 0);
+ transform: rotate3d(0, 0, 0, 0);
+ -webkit-transition: background-color 100ms ease-out, margin 100ms ease-out, height 100ms ease-out;
+ transition: background-color 100ms ease-out, margin 100ms ease-out, height 100ms ease-out;
+ background-color: $backgroundAccent;
+ margin: 5px 5px 5px 0;
+ border-radius: 20px;
+ height: calc(100% - 10px);
+ display: block;
}
.vb.vb-scrolling-phantom > .vb-dragger > .vb-dragger-styler {
- background-color: $backgroundAccentLighter;
+ background-color: $backgroundAccentLighter;
}
.vb > .vb-dragger:hover > .vb-dragger-styler {
- background-color: $backgroundAccentLighter;
- margin: 0px;
- height: 100%;
+ background-color: $backgroundAccentLighter;
+ margin: 0px;
+ height: 100%;
}
.vb.vb-dragging > .vb-dragger > .vb-dragger-styler {
- background-color: $backgroundAccentLighter;
- margin: 0px;
- height: 100%;
+ background-color: $backgroundAccentLighter;
+ margin: 0px;
+ height: 100%;
}
.vb.vb-dragging-phantom > .vb-dragger > .vb-dragger-styler {
- background-color: $backgroundAccentLighter;
+ background-color: $backgroundAccentLighter;
}
-.vb-content{
- overflow: auto !important
+.vb-content {
+ overflow: auto !important;
}
// helpers
@@ -313,7 +335,13 @@ table.table {
height: max-content;
}
-.pagination a, .pagination a:hover {
+.pagination a,
+.pagination a:hover {
text-decoration: none;
}
+// fix control icons
+.control.has-icons-left .icon, .control.has-icons-right .icon {
+ height: 3rem;
+ padding-right: 1rem;
+}
diff --git a/src/site/components/album/AlbumDetails.vue b/src/site/components/album/AlbumDetails.vue
new file mode 100644
index 0000000..b411f13
--- /dev/null
+++ b/src/site/components/album/AlbumDetails.vue
@@ -0,0 +1,211 @@
+<template>
+ <div class="details">
+ <h2>Public links for this album:</h2>
+
+ <b-table
+ :data="details.links || []"
+ :mobile-cards="true">
+ <template slot-scope="props">
+ <b-table-column
+ field="identifier"
+ label="Link"
+ centered>
+ <a
+ :href="`${config.URL}/a/${props.row.identifier}`"
+ target="_blank">
+ {{ props.row.identifier }}
+ </a>
+ </b-table-column>
+
+ <b-table-column
+ field="views"
+ label="Views"
+ centered>
+ {{ props.row.views }}
+ </b-table-column>
+
+ <b-table-column
+ field="enableDownload"
+ label="Allow download"
+ centered>
+ <b-switch
+ v-model="props.row.enableDownload"
+ @input="updateLinkOptions(albumId, props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ field="enabled"
+ numeric>
+ <button
+ :class="{ 'is-loading': isDeleting(props.row.identifier) }"
+ class="button is-danger"
+ :disabled="isDeleting(props.row.identifier)"
+ @click="promptDeleteAlbumLink(albumId, props.row.identifier)">
+ Delete link
+ </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="level is-paddingless">
+ <div class="level-left">
+ <div class="level-item">
+ <button
+ :class="{ 'is-loading': isCreatingLink }"
+ class="button is-primary"
+ style="float: left"
+ @click="createLink(albumId)">
+ Create new link
+ </button>
+ </div>
+ <div class="level-item">
+ <span class="has-text-default">{{ details.links.length }} / {{ config.maxLinksPerAlbum }} links created</span>
+ </div>
+ </div>
+
+ <div class="level-right">
+ <div class="level-item">
+ <button
+ class="button is-danger"
+ style="float: right"
+ @click="promptDeleteAlbum(albumId)">
+ Delete album
+ </button>
+ </div>
+ </div>
+ </div>
+ </template>
+ </b-table>
+ </div>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex';
+
+export default {
+ props: {
+ albumId: {
+ type: Number,
+ default: 0,
+ },
+ details: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ isCreatingLink: false,
+ isDeletingLinks: [],
+ };
+ },
+ computed: mapState(['config']),
+ methods: {
+ ...mapActions({
+ deleteAlbumAction: 'albums/deleteAlbum',
+ deleteAlbumLinkAction: 'albums/deleteLink',
+ updateLinkOptionsAction: 'albums/updateLinkOptions',
+ createLinkAction: 'albums/createLink',
+ alert: 'alert/set',
+ }),
+ promptDeleteAlbum(id) {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to delete this album?',
+ onConfirm: () => this.deleteAlbum(id),
+ });
+ },
+ promptDeleteAlbumLink(albumId, identifier) {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to delete this album link?',
+ onConfirm: () => this.deleteAlbumLink(albumId, identifier),
+ });
+ },
+ async deleteAlbum(id) {
+ try {
+ const response = await this.deleteAlbumAction(id);
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ }
+ },
+ async deleteAlbumLink(albumId, identifier) {
+ this.isDeletingLinks.push(identifier);
+ try {
+ const response = await this.deleteAlbumLinkAction({ albumId, identifier });
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ } finally {
+ this.isDeletingLinks = this.isDeletingLinks.filter((e) => e !== identifier);
+ }
+ },
+ async createLink(albumId) {
+ this.isCreatingLink = true;
+ try {
+ const response = await this.createLinkAction(albumId);
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ } finally {
+ this.isCreatingLink = false;
+ }
+ },
+ async updateLinkOptions(albumId, linkOpts) {
+ try {
+ const response = await this.updateLinkOptionsAction({ albumId, linkOpts });
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ }
+ },
+ isDeleting(identifier) {
+ return this.isDeletingLinks.indexOf(identifier) > -1;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+ @import '~/assets/styles/_colors.scss';
+
+ div.details {
+ flex: 0 1 100%;
+ padding-left: 2em;
+ padding-top: 1em;
+ min-height: 50px;
+
+ .b-table {
+ padding: 2em 0em;
+
+ .table-wrapper {
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+ }
+ }
+</style>
+
+<style lang="scss">
+ @import '~/assets/styles/_colors.scss';
+
+ .b-table {
+ .table-wrapper {
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+ }
+</style>
diff --git a/src/site/components/album/AlbumEntry.vue b/src/site/components/album/AlbumEntry.vue
new file mode 100644
index 0000000..2723b49
--- /dev/null
+++ b/src/site/components/album/AlbumEntry.vue
@@ -0,0 +1,187 @@
+<template>
+ <div class="album">
+ <div
+ class="arrow-container"
+ @click="toggleDetails(album)">
+ <i
+ :class="{ active: isExpanded }"
+ class="icon-arrow" />
+ </div>
+ <div class="thumb">
+ <figure class="image is-64x64 thumb">
+ <img src="~/assets/images/blank_darker.png">
+ </figure>
+ </div>
+ <div class="info">
+ <h4>
+ <router-link :to="`/dashboard/albums/${album.id}`">
+ {{ album.name }}
+ </router-link>
+ </h4>
+ <span>
+ Created <span class="is-inline has-text-weight-semibold"><timeago :since="album.createdAt" /></span>
+ </span>
+ <span>{{ album.fileCount || 0 }} files</span>
+ </div>
+ <div class="latest is-hidden-mobile">
+ <template v-if="album.fileCount > 0">
+ <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>
+ </template>
+ <template v-else>
+ <span class="no-files">Nothing to show here</span>
+ </template>
+ </div>
+
+ <AlbumDetails
+ v-if="isExpanded"
+ :details="getDetails(album.id)"
+ :albumId="album.id" />
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import AlbumDetails from '~/components/album/AlbumDetails.vue';
+
+export default {
+ components: {
+ AlbumDetails,
+ },
+ props: {
+ album: {
+ type: Object,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapGetters({
+ isExpandedGetter: 'albums/isExpanded',
+ getDetails: 'albums/getDetails',
+ }),
+ isExpanded() {
+ return this.isExpandedGetter(this.album.id);
+ },
+ },
+ methods: {
+ async toggleDetails(album) {
+ if (!this.isExpanded) {
+ await this.$store.dispatch('albums/fetchDetails', album.id);
+ }
+ this.$store.commit('albums/toggleExpandedState', album.id);
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+ @import '~/assets/styles/_colors.scss';
+
+ div.album {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+
+ div.arrow-container {
+ width: 2em;
+ height: 64px;
+ position: relative;
+ cursor: pointer;
+
+ i {
+ border: 2px solid $defaultTextColor;
+ border-right: 0;
+ border-top: 0;
+ display: block;
+ height: 1em;
+ position: absolute;
+ transform: rotate(-135deg);
+ transform-origin: center;
+ width: 1em;
+ z-index: 4;
+ top: 22px;
+
+ -webkit-transition: transform 0.1s linear;
+ -moz-transition: transform 0.1s linear;
+ -ms-transition: transform 0.1s linear;
+ -o-transition: transform 0.1s linear;
+ transition: transform 0.1s linear;
+
+ &.active {
+ transform: rotate(-45deg);
+ }
+ }
+ }
+
+ div.thumb {
+ width: 64px;
+ height: 64px;
+ -webkit-box-shadow: $boxShadowLight;
+ box-shadow: $boxShadowLight;
+ }
+
+ div.info {
+ margin-left: 15px;
+ text-align: left;
+ h4 {
+ font-size: 1.5rem;
+ a {
+ color: $defaultTextColor;
+ font-weight: 400;
+ &:hover { text-decoration: underline; }
+ }
+ }
+ span { display: block; }
+ span:nth-child(3) {
+ font-size: 0.9rem;
+ }
+ }
+
+ div.latest {
+ flex-grow: 1;
+ justify-content: flex-end;
+ display: flex;
+ margin-left: 15px;
+
+ span.no-files {
+ font-size: 1.5em;
+ color: #b1b1b1;
+ padding-top: 17px;
+ }
+
+ div.more {
+ width: 64px;
+ height: 64px;
+ background: white;
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ text-align: center;
+ a {
+ line-height: 1rem;
+ color: $defaultTextColor;
+ &:hover { text-decoration: underline; }
+ }
+ }
+ }
+ }
+
+ div.no-background { background: none !important; }
+</style>
diff --git a/src/site/components/footer/Footer.vue b/src/site/components/footer/Footer.vue
index 49f622c..19d18f2 100644
--- a/src/site/components/footer/Footer.vue
+++ b/src/site/components/footer/Footer.vue
@@ -1,12 +1,18 @@
<template>
+ <!-- eslint-disable max-len -->
<footer>
- <svg viewBox="0 0 1920 250"
+ <svg
+ viewBox="0 0 1920 250"
class="waves">
- <path d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239"
+
+ <path
+ d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239"
class="wave-1" />
- <path d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z"
+ <path
+ d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z"
class="wave-2" />
- <path d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z"
+ <path
+ d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z"
class="wave-3" />
</svg>
<div>
@@ -15,7 +21,8 @@
<div class="column is-narrow">
<h4>lolisafe</h4>
<span>© 2017-2020
- <a href="https://github.com/pitu"
+ <a
+ href="https://github.com/pitu"
class="no-block">Pitu</a>
</span><br>
<span>v{{ version }}</span>
@@ -24,20 +31,33 @@
<div class="columns is-gapless">
<div class="column" />
<div class="column">
- <nuxt-link to="/">Home</nuxt-link>
- <nuxt-link to="/faq">FAQ</nuxt-link>
+ <nuxt-link to="/">
+ Home
+ </nuxt-link>
+ <nuxt-link to="/faq">
+ FAQ
+ </nuxt-link>
</div>
<div class="column">
- <nuxt-link to="/dashboard">Dashboard</nuxt-link>
- <nuxt-link to="/dashboard">Files</nuxt-link>
- <nuxt-link to="/dashboard/albums">Albums</nuxt-link>
- <nuxt-link to="/dashboard/account">Account</nuxt-link>
+ <nuxt-link to="/dashboard">
+ Dashboard
+ </nuxt-link>
+ <nuxt-link to="/dashboard">
+ Files
+ </nuxt-link>
+ <nuxt-link to="/dashboard/albums">
+ Albums
+ </nuxt-link>
+ <nuxt-link to="/dashboard/account">
+ Account
+ </nuxt-link>
</div>
<div class="column">
<a href="https://github.com/weebdev/lolisafe">GitHub</a>
</div>
<div class="column">
- <a v-if="loggedIn"
+ <a
+ v-if="loggedIn"
@click="createShareXThing">ShareX Config</a>
<a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">Chrome Extension</a>
</div>
@@ -48,27 +68,32 @@
</div>
</footer>
</template>
+
<script>
+/* eslint-disable no-restricted-globals */
+
+import { mapState, mapGetters } from 'vuex';
import { saveAs } from 'file-saver';
+
export default {
computed: {
- loggedIn() {
- return this.$store.state.loggedIn;
- },
- version() {
- return this.$store.state.config.version;
- }
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn' }),
+ ...mapState({
+ version: (state) => state.config.version,
+ serviceName: (state) => state.config.serviceName,
+ token: (state) => state.auth.token,
+ }),
},
methods: {
createShareXThing() {
const sharexFile = `{
- "Name": "${this.$store.state.config.serviceName}",
+ "Name": "${this.serviceName}",
"DestinationType": "ImageUploader, FileUploader",
"RequestType": "POST",
"RequestURL": "${location.origin}/api/upload",
"FileFormName": "files[]",
"Headers": {
- "authorization": "Bearer ${this.$store.state.token}",
+ "authorization": "Bearer ${this.token}",
"accept": "application/vnd.lolisafe.json"
},
"ResponseType": "Text",
@@ -77,10 +102,11 @@ export default {
}`;
const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' });
saveAs(sharexBlob, `${location.hostname}.sxcu`);
- }
- }
+ },
+ },
};
</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
footer {
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue
index b6615be..90c196b 100644
--- a/src/site/components/grid/Grid.vue
+++ b/src/site/components/grid/Grid.vue
@@ -1,152 +1,136 @@
<template>
<div>
- <div v-if="enableToolbar"
- class="toolbar">
- <div class="block">
- <b-radio v-model="showList"
- name="name"
- :native-value="true">
- List
- </b-radio>
- <b-radio v-model="showList"
- name="name"
- :native-value="false">
- Grid
- </b-radio>
+ <nav class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <slot name="pagination" />
+ </div>
</div>
- </div>
+ <div v-if="enableToolbar" class="level-right toolbar">
+ <div class="level-item">
+ <div class="block">
+ <b-radio v-model="showList" name="name" :native-value="true">
+ List
+ </b-radio>
+ <b-radio v-model="showList" name="name" :native-value="false">
+ Grid
+ </b-radio>
+ </div>
+ </div>
+ </div>
+ </nav>
<template v-if="!showList">
- <Waterfall v-if="showWaterfall"
+ <Waterfall
+ v-if="showWaterfall"
:gutterWidth="10"
- :gutterHeight="4">
- <!--
- TODO: Implement search based on originalName, albumName and tags
- <input v-if="enableSearch"
- v-model="searchTerm"
- type="text"
- placeholder="Search..."
- @input="search()"
- @keyup.enter="search()">
- -->
-
- <!-- TODO: Implement pagination -->
-
- <WaterfallItem v-for="(item, index) in gridFiles"
- :key="item.id"
- :width="width"
- move-class="item-move">
+ :gutterHeight="4"
+ :options="{fitWidth: true}"
+ :itemWidth="width"
+ :items="gridFiles">
+ <template v-slot="{item}">
<template v-if="isPublic">
- <a :href="`${item.url}`"
- target="_blank">
+ <a
+ :href="`${item.url}`"
+ class="preview-container"
+ target="_blank"
+ @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)"
+ @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)">
+
<img :src="item.thumb ? item.thumb : blank">
- <span v-if="!item.thumb && item.name"
- class="extension">{{ item.name.split('.').pop() }}</span>
+ <div v-if="item.preview && isHovered(item.id)" class="preview">
+ <video ref="video" class="preview" autoplay loop muted>
+ <source :src="item.preview" type="video/mp4">
+ </video>
+ </div>
+
+ <span v-if="!item.thumb && item.name" class="extension">{{
+ item.name.split('.').pop()
+ }}</span>
</a>
</template>
<template v-else>
<img :src="item.thumb ? item.thumb : blank">
- <span v-if="!item.thumb && item.name"
- class="extension">{{ item.name.split('.').pop() }}</span>
- <div v-if="!isPublic"
+ <div v-if="item.preview && isHovered(item.id)" class="preview">
+ <video ref="video" class="preview" autoplay loop muted>
+ <source :src="item.preview" type="video/mp4">
+ </video>
+ </div>
+
+ <span v-if="!item.thumb && item.name" class="extension">{{ item.name.split('.').pop() }}</span>
+ <div
+ v-if="!isPublic"
:class="{ fixed }"
- class="actions">
- <b-tooltip label="Link"
- position="is-top">
- <a :href="`${item.url}`"
- target="_blank"
- class="btn">
+ class="actions"
+ @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)"
+ @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)">
+ <b-tooltip label="Link" position="is-top">
+ <a :href="`${item.url}`" target="_blank" class="btn">
<i class="icon-web-code" />
</a>
</b-tooltip>
- <b-tooltip label="Albums"
- position="is-top">
- <a class="btn"
- @click="openAlbumModal(item)">
- <i class="icon-interface-window" />
+ <b-tooltip label="Tags" position="is-top">
+ <a class="btn" @click="false && manageTags(item)">
+ <i class="icon-ecommerce-tag-c" />
</a>
</b-tooltip>
- <!--
- <b-tooltip label="Tags"
- position="is-top">
- <a @click="manageTags(item)">
- <i class="icon-ecommerce-tag-c" />
+ <b-tooltip label="Albums" position="is-top">
+ <a class="btn" @click="openAlbumModal(item)">
+ <i class="icon-interface-window" />
</a>
</b-tooltip>
- -->
- <b-tooltip label="Delete"
- position="is-top">
- <a class="btn"
- @click="deleteFile(item, index)">
+ <b-tooltip label="Delete" position="is-top">
+ <a class="btn" @click="deleteFile(item)">
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
- <b-tooltip v-if="user && user.isAdmin"
- label="More info"
- position="is-top"
- class="more">
+ <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more">
<nuxt-link :to="`/dashboard/admin/file/${item.id}`">
<i class="icon-interface-more" />
</nuxt-link>
</b-tooltip>
</div>
</template>
- </WaterfallItem>
+ </template>
</Waterfall>
</template>
<div v-else>
- <b-table
- :data="gridFiles || []"
- :mobile-cards="true">
+ <b-table :data="gridFiles || []" :mobile-cards="true">
<template slot-scope="props">
<template v-if="!props.row.hideFromList">
- <b-table-column field="url"
- label="URL">
- <a :href="props.row.url"
- target="_blank">{{ props.row.url }}</a>
+ <b-table-column field="url" label="URL">
+ <a :href="props.row.url" target="_blank">{{ props.row.url }}</a>
</b-table-column>
- <b-table-column field="albums"
- label="Albums"
- centered>
+ <b-table-column field="albums" label="Albums" centered>
<template v-for="(album, index) in props.row.albums">
- <nuxt-link :key="index"
- :to="`/dashboard/albums/${album.id}`">
+ <nuxt-link :key="index" :to="`/dashboard/albums/${album.id}`">
{{ album.name }}
</nuxt-link>
- <template v-if="index < props.row.albums.length - 1">, </template>
+ <template v-if="index < props.row.albums.length - 1">
+ ,
+ </template>
</template>
{{ props.row.username }}
</b-table-column>
- <b-table-column field="uploaded"
- label="Uploaded"
- centered>
+ <b-table-column field="uploaded" label="Uploaded" centered>
<span><timeago :since="props.row.createdAt" /></span>
</b-table-column>
- <b-table-column field="purge"
- centered>
- <b-tooltip label="Albums"
- position="is-top">
- <a class="btn"
- @click="openAlbumModal(props.row)">
+ <b-table-column field="purge" centered>
+ <b-tooltip label="Albums" position="is-top">
+ <a class="btn" @click="openAlbumModal(props.row)">
<i class="icon-interface-window" />
</a>
</b-tooltip>
- <b-tooltip label="Delete"
- position="is-top"
- class="is-danger">
- <a class="is-danger"
- @click="deleteFile(props.row)">
+ <b-tooltip label="Delete" position="is-top" class="is-danger">
+ <a class="is-danger" @click="deleteFile(props.row)">
<i class="icon-editorial-trash-a-l" />
</a>
</b-tooltip>
- <b-tooltip v-if="user && user.isAdmin"
- label="More info"
- position="is-top"
- class="more">
+ <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more">
<nuxt-link :to="`/dashboard/admin/file/${props.row.id}`">
<i class="icon-interface-more" />
</nuxt-link>
@@ -164,26 +148,28 @@
</template>
<template slot="footer">
<div class="has-text-right has-text-default">
- {{ files.length }} files
+ Showing {{ files.length }} files ({{ total }} total)
</div>
</template>
</b-table>
</div>
- <b-modal :active.sync="isAlbumsModalActive"
- :width="640"
- scroll="keep">
+ <b-modal :active.sync="isAlbumsModalActive" :width="640" scroll="keep">
<div class="card albumsModal">
<div class="card-content">
<div class="content">
- <h3 class="subtitle">Select the albums this file should be a part of</h3>
+ <h3 class="subtitle">
+ Select the albums this file should be a part of
+ </h3>
<hr>
+
<div class="albums-container">
- <div v-for="(album, index) in albums"
- :key="index"
- class="album">
+ <div v-for="album in albums" :key="album.id" class="album">
<div class="field">
- <b-checkbox :value="isAlbumSelected(album.id)"
- @input="albumCheckboxClicked($event, album.id)">{{ album.name }}</b-checkbox>
+ <b-checkbox
+ :value="isAlbumSelected(album.id)"
+ @input="albumCheckboxClicked($event, album.id)">
+ {{ album.name }}
+ </b-checkbox>
</div>
</div>
</div>
@@ -195,243 +181,297 @@
</template>
<script>
+import { mapState } from 'vuex';
+
import Waterfall from './waterfall/Waterfall.vue';
-import WaterfallItem from './waterfall/WaterfallItem.vue';
export default {
components: {
Waterfall,
- WaterfallItem
},
props: {
files: {
type: Array,
- default: () => []
+ default: () => [],
+ },
+ total: {
+ type: Number,
+ default: 0,
},
fixed: {
type: Boolean,
- default: false
+ default: false,
},
isPublic: {
type: Boolean,
- default: false
+ default: false,
},
width: {
type: Number,
- default: 150
+ default: 150,
},
enableSearch: {
type: Boolean,
- default: true
+ default: true,
},
enableToolbar: {
type: Boolean,
- default: true
- }
+ default: true,
+ },
},
data() {
return {
showWaterfall: true,
searchTerm: null,
showList: false,
- albums: [],
+ hoveredItems: [],
isAlbumsModalActive: false,
showingModalForFile: null,
filesOffsetWaterfall: 0,
filesOffsetEndWaterfall: 50,
- filesPerPageWaterfall: 50
+ filesPerPageWaterfall: 50,
};
},
computed: {
- user() {
- return this.$store.state.user;
- },
+ ...mapState({
+ user: (state) => state.auth.user,
+ albums: (state) => state.albums.tinyDetails,
+ images: (state) => state.images,
+ }),
blank() {
+ // eslint-disable-next-line global-require, import/no-unresolved
return require('@/assets/images/blank.png');
},
gridFiles() {
return this.files;
},
},
+ created() {
+ this.getAlbums();
+ },
methods: {
async search() {
- const data = await this.$search.do(this.searchTerm, [
- 'name',
- 'original',
- 'type',
- 'albums:name'
- ]);
- console.log('> Search result data', data);
+ const data = await this.$search.do(this.searchTerm, ['name', 'original', 'type', 'albums:name']);
+ console.log('> Search result data', data); // eslint-disable-line no-console
},
- deleteFile(file, index) {
+ 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) {
- file.hideFromList = true;
- this.$forceUpdate();
- } else {
- this.showWaterfall = false;
- this.files.splice(index, 1);
- this.$nextTick(() => {
- this.showWaterfall = true;
- });
+ try {
+ const response = await this.$store.dispatch('images/deleteFile', file.id);
+
+ this.$buefy.toast.open(response.message);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
}
- return this.$buefy.toast.open(response.message);
- }
+ },
});
},
isAlbumSelected(id) {
- if (!this.showingModalForFile) return;
- const found = this.showingModalForFile.albums.find(el => el.id === id);
- return found ? found.id ? true : false : false;
+ if (!this.showingModalForFile) return false;
+ const found = this.showingModalForFile.albums.find((el) => el.id === id);
+ return !!(found && found.id);
},
async openAlbumModal(file) {
+ const { id } = file;
this.showingModalForFile = file;
this.showingModalForFile.albums = [];
- this.isAlbumsModalActive = true;
- const response = await this.$axios.$get(`file/${file.id}/albums`);
- this.showingModalForFile.albums = response.albums;
+ try {
+ await this.$store.dispatch('images/getFileAlbums', id);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ this.showingModalForFile.albums = this.images.fileAlbumsMap[id];
- this.getAlbums();
+ this.isAlbumsModalActive = true;
},
- async albumCheckboxClicked(value, id) {
- const response = await this.$axios.$post(`file/album/${value ? 'add' : 'del'}`, {
- albumId: id,
- fileId: this.showingModalForFile.id
- });
- this.$buefy.toast.open(response.message);
+ async albumCheckboxClicked(add, id) {
+ try {
+ let response;
+ if (add) {
+ response = await this.$store.dispatch('images/addToAlbum', {
+ albumId: id,
+ fileId: this.showingModalForFile.id,
+ });
+ } else {
+ response = await this.$store.dispatch('images/removeFromAlbum', {
+ albumId: id,
+ fileId: this.showingModalForFile.id,
+ });
+ }
- // Not the prettiest solution to refetch on each click but it'll do for now
- this.$parent.getFiles();
+ this.$buefy.toast.open(response.message);
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
},
async getAlbums() {
- const response = await this.$axios.$get(`albums/dropdown`);
- this.albums = response.albums;
- this.$forceUpdate();
- }
- }
+ try {
+ await this.$store.dispatch('albums/getTinyDetails');
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ },
+ mouseOver(id) {
+ const foundIndex = this.hoveredItems.indexOf(id);
+ if (foundIndex > -1) return;
+ this.hoveredItems.push(id);
+ },
+ mouseOut(id) {
+ const foundIndex = this.hoveredItems.indexOf(id);
+ if (foundIndex > -1) this.hoveredItems.splice(foundIndex, 1);
+ },
+ isHovered(id) {
+ return this.hoveredItems.includes(id);
+ },
+ },
};
</script>
<style lang="scss" scoped>
- @import '~/assets/styles/_colors.scss';
- .item-move {
- transition: all .25s cubic-bezier(.55,0,.1,1);
- }
+@import '~/assets/styles/_colors.scss';
+.item-move {
+ transition: all 0.25s cubic-bezier(0.55, 0, 0.1, 1);
+}
- div.toolbar {
- padding: 1rem;
+div.toolbar {
+ padding: 1rem;
- .block {
- text-align: right;
- }
+ .block {
+ text-align: right;
}
+}
- span.extension {
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: 0;
- top: 0;
- left: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 2rem;
- pointer-events: none;
- opacity: .75;
- max-width: 150px;
- }
+span.extension {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ pointer-events: none;
+ opacity: 0.75;
+ max-width: 150px;
+}
- div.actions {
- opacity: 0;
- -webkit-transition: opacity 0.1s linear;
- -moz-transition: opacity 0.1s linear;
- -ms-transition: opacity 0.1s linear;
- -o-transition: opacity 0.1s linear;
- transition: opacity 0.1s linear;
- position: absolute;
- top: 0px;
- left: 0px;
- width: 100%;
- height: calc(100% - 6px);
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
-
- span {
- padding: 3px;
- &.more {
- position: absolute;
- top: 0;
- right: 0;
- }
+div.preview {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: calc(100% - 6px);
+ overflow: hidden;
+}
- &:nth-child(1), &:nth-child(2) {
- align-items: flex-end;
- }
+.preview-container {
+ display: inline-block;
+}
- &:nth-child(1), &:nth-child(3) {
- justify-content: flex-end;
- }
- a {
+div.actions {
+ opacity: 0;
+ -webkit-transition: opacity 0.1s linear;
+ -moz-transition: opacity 0.1s linear;
+ -ms-transition: opacity 0.1s linear;
+ -o-transition: opacity 0.1s linear;
+ transition: opacity 0.1s linear;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: calc(100% - 6px);
+ // background: rgba(0, 0, 0, 0.5);
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 60px),
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 45px);
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+
+ span {
+ padding: 3px;
+
+ &.more {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ &:nth-child(1),
+ &:nth-child(2) {
+ align-items: flex-end;
+ padding-bottom: 10px;
+ }
+
+ &:nth-child(3),
+ &:nth-child(4) {
+ justify-content: flex-end;
+ padding-bottom: 10px;
+ }
+
+ a {
+ width: 30px;
+ height: 30px;
+ color: white;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ &.btn:before {
+ content: '';
width: 30px;
height: 30px;
- color: white;
- justify-content: center;
- align-items: center;
- display: flex;
- &.btn:before {
- content: '';
- width: 30px;
- height: 30px;
- border: 1px solid white;
- border-radius: 50%;
- position: absolute;
- }
+ border: 1px solid white;
+ border-radius: 50%;
+ position: absolute;
}
}
+ }
- &.fixed {
- position: relative;
- opacity: 1;
- background: none;
-
- a {
- width: auto;
- height: auto;
- color: $defaultTextColor;
- &:before {
- display: none;
- }
- }
+ &.fixed {
+ position: relative;
+ opacity: 1;
+ background: none;
+ a {
+ width: auto;
+ height: auto;
+ color: $defaultTextColor;
+ &:before {
+ display: none;
+ }
}
}
+}
- .albums-container {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- .album {
- flex-basis: 33%;
- text-align: left;
- }
+.albums-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ .album {
+ flex-basis: 33%;
+ text-align: left;
}
-</style>
+}
-<style lang="scss">
- .waterfall-item:hover {
- div.actions {
- opacity: 1
- }
+.hidden {
+ display: none;
+}
+
+.waterfall {
+ margin: 0 auto;
+}
+
+.waterfall-item:hover {
+ div.actions {
+ opacity: 1;
}
+}
</style>
diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue
index 8631ea5..79a330a 100644
--- a/src/site/components/grid/waterfall/Waterfall.vue
+++ b/src/site/components/grid/waterfall/Waterfall.vue
@@ -1,180 +1,133 @@
-<style>
- .waterfall {
- position: relative;
- }
-</style>
<template>
- <div class="waterfall">
- <slot />
+ <div ref="waterfall" class="waterfall">
+ <WaterfallItem
+ v-for="(item, index) in items"
+ :key="item.id"
+ :style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }"
+ :width="itemWidth">
+ <slot :item="item" />
+ </WaterfallItem>
</div>
</template>
<script>
-// import {quickSort, getMinIndex, _, sum} from './util'
-
-const quickSort = (arr, type) => {
- const left = [];
- const right = [];
- if (arr.length <= 1) {
- return arr;
- }
- const povis = arr[0];
- for (let i = 1; i < arr.length; i++) {
- if (arr[i][type] < povis[type]) {
- left.push(arr[i]);
- } else {
- right.push(arr[i]);
- }
- }
- return quickSort(left, type).concat(povis, quickSort(right, type))
-};
-
-const getMinIndex = arr => {
- let pos = 0;
- for (let i = 0; i < arr.length; i++) {
- if (arr[pos] > arr[i]) {
- pos = i;
- }
- }
- return pos;
-};
+import WaterfallItem from './WaterfallItem.vue';
-const _ = {
- on(el, type, func, capture = false) {
- el.addEventListener(type, func, capture);
- },
- off(el, type, func, capture = false) {
- el.removeEventListener(type, func, capture);
- }
-};
+const isBrowser = typeof window !== 'undefined';
+const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null;
+const imagesloaded = isBrowser ? require('imagesloaded') : null;
-const sum = arr => arr.reduce((sum, val) => sum + val);
export default {
name: 'Waterfall',
+ components: {
+ WaterfallItem,
+ },
props: {
- gutterWidth: {
- type: Number,
- default: 0
- },
- gutterHeight: {
- type: Number,
- default: 0
- },
- resizable: {
- type: Boolean,
- default: true
+ options: {
+ type: Object,
+ default: () => {},
},
- align: {
- type: String,
- default: 'center'
+ items: {
+ type: Array,
+ default: () => [],
},
- fixWidth: {
- type: Number
+ itemWidth: {
+ type: Number,
+ default: 150,
},
- minCol: {
+ gutterWidth: {
type: Number,
- default: 1
+ default: 10,
},
- maxCol: {
- type: Number
+ gutterHeight: {
+ type: Number,
+ default: 4,
},
- percent: {
- type: Array
- }
},
- data() {
- return {
- timer: null,
- colNum: 0,
- lastWidth: 0,
- percentWidthArr: []
- };
+ mounted() {
+ this.initializeMasonry();
+ this.imagesLoaded();
},
- created() {
- this.$on('itemRender', () => {
- if (this.timer) {
- clearTimeout(this.timer);
- }
- this.timer = setTimeout(() => {
- this.render();
- }, 0);
- });
+ updated() {
+ this.performLayout();
+ this.imagesLoaded();
},
- mounted() {
- this.resizeHandle();
- this.$watch('resizable', this.resizeHandle);
+ unmounted() {
+ this.masonry.destroy();
},
methods: {
- calulate(arr) {
- let pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
- // 百分比布局计算
- if (this.percent) {
- this.colNum = this.percent.length;
- const total = sum(this.percent);
- this.percentWidthArr = this.percent.map(value => (value / total) * pageWidth);
- this.lastWidth = 0;
- // 正常布局计算
- } else {
- this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth));
- if (this.minCol && this.colNum < this.minCol) {
- this.colNum = this.minCol;
- this.lastWidth = 0;
- } else if (this.maxCol && this.colNum > this.maxCol) {
- this.colNum = this.maxCol;
- this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
- } else {
- this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
- }
+ imagesLoaded() {
+ const node = this.$refs.waterfall;
+ imagesloaded(
+ node,
+ () => {
+ this.masonry.layout();
+ },
+ );
+ },
+ performLayout() {
+ const diff = this.diffDomChildren();
+ if (diff.removed.length > 0) {
+ this.masonry.remove(diff.removed);
+ this.masonry.reloadItems();
+ }
+ if (diff.appended.length > 0) {
+ this.masonry.appended(diff.appended);
+ this.masonry.reloadItems();
+ }
+ if (diff.prepended.length > 0) {
+ this.masonry.prepended(diff.prepended);
}
+ if (diff.moved.length > 0) {
+ this.masonry.reloadItems();
+ }
+ this.masonry.layout();
+ },
+ diffDomChildren() {
+ const oldChildren = this.domChildren.filter((element) => !!element.parentNode);
+ const newChildren = this.getNewDomChildren();
+ const removed = oldChildren.filter((oldChild) => !newChildren.includes(oldChild));
+ const domDiff = newChildren.filter((newChild) => !oldChildren.includes(newChild));
+ const prepended = domDiff.filter((newChild, index) => newChildren[index] === newChild);
+ const appended = domDiff.filter((el) => !prepended.includes(el));
+ let moved = [];
+ if (removed.length === 0) {
+ moved = oldChildren.filter((child, index) => index !== newChildren.indexOf(child));
+ }
+ this.domChildren = newChildren;
+ return {
+ old: oldChildren,
+ new: newChildren,
+ removed,
+ appended,
+ prepended,
+ moved,
+ };
},
- resizeHandle() {
- if (this.resizable) {
- _.on(window, 'resize', this.render, false);
- } else {
- _.off(window, 'resize', this.render, false);
+ initializeMasonry() {
+ if (!this.masonry) {
+ this.masonry = new Masonry(
+ this.$refs.waterfall,
+ {
+ columnWidth: this.itemWidth,
+ gutter: this.gutterWidth,
+ ...this.options,
+ },
+ );
+ this.domChildren = this.getNewDomChildren();
}
},
- render() {
- // 重新排序
- let childArr = [];
- childArr = this.$children.map(child => child.getMeta());
- childArr = quickSort(childArr, 'order');
- // 计算列数
- this.calulate(childArr[0])
- let offsetArr = Array(this.colNum).fill(0);
- // 渲染
- childArr.forEach(child => {
- let position = getMinIndex(offsetArr);
- // 百分比布局渲染
- if (this.percent) {
- let left = 0;
- child.el.style.width = `${this.percentWidthArr[position]}px`;
- if (position === 0) {
- left = 0;
- } else {
- for (let i = 0; i < position; i++) {
- left += this.percentWidthArr[i];
- }
- }
- child.el.style.left = `${left}px`;
- // 正常布局渲染
- } else {
- if (this.align === 'left') { // eslint-disable-line no-lonely-if
- child.el.style.left = `${position * (child.width + this.gutterWidth)}px`;
- } else if (this.align === 'right') {
- child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`;
- } else {
- child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`;
- }
- }
- if (child.height === 0) {
- return;
- }
- child.el.style.top = `${offsetArr[position]}px`;
- offsetArr[position] += (child.height + this.gutterHeight);
- this.$el.style.height = `${Math.max.apply(Math, offsetArr)}px`;
- });
- this.$emit('rendered', this);
- }
- }
+ getNewDomChildren() {
+ const node = this.$refs.waterfall;
+ const children = this.options && this.options.itemSelector
+ ? node.querySelectorAll(this.options.itemSelector) : node.children;
+ return Array.prototype.slice.call(children);
+ },
+ },
};
</script>
+
+<style lang="scss" scoped>
+.wfi {
+
+}
+</style>
diff --git a/src/site/components/grid/waterfall/WaterfallItem.vue b/src/site/components/grid/waterfall/WaterfallItem.vue
index a02ea1f..c5cade1 100644
--- a/src/site/components/grid/waterfall/WaterfallItem.vue
+++ b/src/site/components/grid/waterfall/WaterfallItem.vue
@@ -1,60 +1,10 @@
-<style>
- .waterfall-item {
- position: absolute;
- }
-</style>
<template>
<div class="waterfall-item">
<slot />
</div>
</template>
<script>
-import imagesLoaded from 'imagesloaded';
export default {
name: 'WaterfallItem',
- props: {
- order: {
- type: Number,
- default: 0
- },
- width: {
- type: Number,
- default: 150
- }
- },
- data() {
- return {
- itemWidth: 0,
- height: 0
- };
- },
- created() {
- this.$watch(() => this.height, this.emit);
- },
- mounted() {
- this.$el.style.display = 'none';
- this.$el.style.width = `${this.width}px`;
- this.emit();
- imagesLoaded(this.$el, () => {
- this.$el.style.left = '-9999px';
- this.$el.style.top = '-9999px';
- this.$el.style.display = 'block';
- this.height = this.$el.offsetHeight;
- this.itemWidth = this.$el.offsetWidth;
- });
- },
- methods: {
- emit() {
- this.$parent.$emit('itemRender');
- },
- getMeta() {
- return {
- el: this.$el,
- height: this.height,
- width: this.itemWidth,
- order: this.order
- };
- }
- }
-}
+};
</script>
diff --git a/src/site/components/imageInfo/ImageInfo.vue b/src/site/components/imageInfo/ImageInfo.vue
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/site/components/imageInfo/ImageInfo.vue
diff --git a/src/site/components/loading/BulmaLoading.vue b/src/site/components/loading/BulmaLoading.vue
new file mode 100644
index 0000000..37cc5a5
--- /dev/null
+++ b/src/site/components/loading/BulmaLoading.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="loader-wrapper">
+ <div class="loader is-loading" />
+ </div>
+</template>
+
+<style lang="scss" scoped>
+ .loader-wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background: #fff;
+ opacity: 0;
+ z-index: -1;
+ transition: opacity .3s;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 6px;
+
+ .loader {
+ height: 80px;
+ width: 80px;
+ }
+
+ &.is-active {
+ opacity: 1;
+ z-index: 1;
+ }
+ }
+</style>
diff --git a/src/site/components/loading/CubeShadow.vue b/src/site/components/loading/CubeShadow.vue
index af31dac..bbfdb52 100644
--- a/src/site/components/loading/CubeShadow.vue
+++ b/src/site/components/loading/CubeShadow.vue
@@ -1,5 +1,6 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--cube-shadow" />
</template>
@@ -8,16 +9,16 @@ export default {
props: {
size: {
type: String,
- default: '60px'
+ default: '60px',
},
background: {
type: String,
- default: '#9C27B0'
+ default: '#9C27B0',
},
duration: {
type: String,
- default: '1.8s'
- }
+ default: '1.8s',
+ },
},
computed: {
styles() {
@@ -25,10 +26,10 @@ export default {
width: this.size,
height: this.size,
backgroundColor: this.background,
- animationDuration: this.duration
+ animationDuration: this.duration,
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/loading/Origami.vue b/src/site/components/loading/Origami.vue
index d1b523d..cd1c087 100644
--- a/src/site/components/loading/Origami.vue
+++ b/src/site/components/loading/Origami.vue
@@ -1,7 +1,9 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner-origami">
- <div :style="innerStyles"
+ <div
+ :style="innerStyles"
class="spinner-inner loading">
<span class="slice" />
<span class="slice" />
@@ -18,21 +20,21 @@ export default {
props: {
size: {
type: String,
- default: '40px'
- }
+ default: '40px',
+ },
},
computed: {
innerStyles() {
- let size = parseInt(this.size);
+ const size = parseInt(this.size, 10);
return { transform: `scale(${(size / 60)})` };
},
styles() {
return {
width: this.size,
- height: this.size
+ height: this.size,
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/loading/PingPong.vue b/src/site/components/loading/PingPong.vue
index ac33e28..d562e9f 100644
--- a/src/site/components/loading/PingPong.vue
+++ b/src/site/components/loading/PingPong.vue
@@ -1,12 +1,14 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--ping-pong">
- <div :style="innerStyles"
+ <div
+ :style="innerStyles"
class="spinner-inner">
<div class="board">
- <div class="left"/>
- <div class="right"/>
- <div class="ball"/>
+ <div class="left" />
+ <div class="right" />
+ <div class="ball" />
</div>
</div>
</div>
@@ -17,22 +19,22 @@ export default {
props: {
size: {
type: String,
- default: '60px'
- }
+ default: '60px',
+ },
},
computed: {
innerStyles() {
- let size = parseInt(this.size);
+ const size = parseInt(this.size, 10);
return { transform: `scale(${size / 250})` };
},
styles() {
return {
width: this.size,
- height: this.size
+ height: this.size,
};
- }
- }
-}
+ },
+ },
+};
</script>
<style lang="scss" scoped>
diff --git a/src/site/components/loading/RotateSquare.vue b/src/site/components/loading/RotateSquare.vue
index 4da8300..089e01a 100644
--- a/src/site/components/loading/RotateSquare.vue
+++ b/src/site/components/loading/RotateSquare.vue
@@ -1,5 +1,6 @@
<template>
- <div :style="styles"
+ <div
+ :style="styles"
class="spinner spinner--rotate-square-2" />
</template>
@@ -8,18 +9,18 @@ export default {
props: {
size: {
type: String,
- default: '40px'
- }
+ default: '40px',
+ },
},
computed: {
styles() {
return {
width: this.size,
height: this.size,
- display: 'inline-block'
+ display: 'inline-block',
};
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/components/navbar/Navbar.vue b/src/site/components/navbar/Navbar.vue
index 47f90cb..fb78631 100644
--- a/src/site/components/navbar/Navbar.vue
+++ b/src/site/components/navbar/Navbar.vue
@@ -1,5 +1,6 @@
<template>
- <b-navbar :class="{ isWhite }"
+ <b-navbar
+ :class="{ isWhite }"
transparent>
<template slot="end">
<b-navbar-item tag="div">
@@ -65,32 +66,31 @@
</template>
<script>
+import { mapState, mapGetters } from 'vuex';
+
export default {
props: {
isWhite: {
type: Boolean,
- default: false
- }
+ default: false,
+ },
},
data() {
return { hamburger: false };
},
computed: {
- loggedIn() {
- return this.$store.state.loggedIn;
- },
- config() {
- return this.$store.state.config;
- }
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn' }),
+ ...mapState(['config']),
},
methods: {
- logOut() {
- this.$store.dispatch('logout');
+ async logOut() {
+ await this.$store.dispatch('auth/logout');
this.$router.replace('/login');
- }
- }
+ },
+ },
};
</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
nav.navbar {
diff --git a/src/site/components/sidebar/Sidebar.vue b/src/site/components/sidebar/Sidebar.vue
index a2ad3f4..d586122 100644
--- a/src/site/components/sidebar/Sidebar.vue
+++ b/src/site/components/sidebar/Sidebar.vue
@@ -1,64 +1,80 @@
<template>
- <div class="dashboard-menu">
- <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/account">
- <i class="icon-ecommerce-tag-c" />Account
- </router-link>
- <template v-if="user && user.isAdmin">
- <router-link to="/dashboard/admin/users">
- <i class="icon-setting-gear-a" />Users
- </router-link>
- <!--
- TODO: Dont wanna deal with this now
- <router-link to="/dashboard/admin/settings">
- <i class="icon-setting-gear-a" />Settings
- </router-link>
- -->
- </template>
- </div>
+ <b-menu class="dashboard-menu">
+ <b-menu-list label="Menu">
+ <b-menu-item
+ class="item"
+ icon="information-outline"
+ label="Dashboard"
+ tag="nuxt-link"
+ to="/dashboard"
+ exact />
+ <b-menu-item
+ class="item"
+ icon="image-multiple-outline"
+ label="Albums"
+ tag="nuxt-link"
+ to="/dashboard/albums"
+ exact />
+ <b-menu-item
+ class="item"
+ icon="tag-outline"
+ label="Tags"
+ tag="nuxt-link"
+ to="/dashboard/tags"
+ exact />
+ <b-menu-item icon="settings" expanded>
+ <template slot="label" slot-scope="props">
+ Administration
+ <b-icon class="is-pulled-right" :icon="props.expanded ? 'menu-down' : 'menu-up'" />
+ </template>
+ <b-menu-item icon="account" label="Users" tag="nuxt-link" to="/dashboard/admin/users" exact />
+ <b-menu-item icon="cog-outline" label="Settings" tag="nuxt-link" to="/dashboard/admin/settings" exact />
+ </b-menu-item>
+ <b-menu-item
+ class="item"
+ icon="account-cog-outline"
+ label="My account"
+ tag="nuxt-link"
+ to="/dashboard/account"
+ exact />
+ </b-menu-list>
+ <b-menu-list label="Actions">
+ <b-menu-item icon="exit-to-app" label="Logout" tag="nuxt-link" to="/logout" exact />
+ </b-menu-list>
+ </b-menu>
</template>
<script>
+import { mapState } from 'vuex';
+
export default {
- computed: {
- user() {
- return this.$store.state.user;
- }
- }
+ computed: mapState({
+ user: (state) => state.auth.user,
+ }),
+ methods: {
+ isRouteActive(id) {
+ if (this.$route.path.includes(id)) {
+ return true;
+ }
+ return false;
+ },
+ },
};
+
</script>
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
.dashboard-menu {
- padding: 2rem;
- border-radius: 8px;
+ ::v-deep a:hover {
+ cursor: pointer;
+ text-decoration: none;
+ }
- a {
- display: block;
- font-weight: 700;
- color: $textColor;
- position: relative;
- padding-left: 40px;
- height: 35px;
- &:hover{
- color: white;
- }
+ ::v-deep .icon {
+ margin-right: 0.5rem;
+ }
- i {
- position: absolute;
- font-size: 1.5em;
- top: -4px;
- left: 5px;
- }
+ ::v-deep .icon.is-pulled-right {
+ margin-right: 0;
}
hr { margin-top: 0.6em; }
diff --git a/src/site/components/uploader/Uploader.vue b/src/site/components/uploader/Uploader.vue
index 1b03ff8..7e2d446 100644
--- a/src/site/components/uploader/Uploader.vue
+++ b/src/site/components/uploader/Uploader.vue
@@ -1,7 +1,9 @@
<template>
- <div :class="{ 'has-files': alreadyAddedFiles }"
+ <div
+ :class="{ 'has-files': alreadyAddedFiles }"
class="uploader-wrapper">
- <b-select v-if="loggedIn"
+ <b-select
+ v-if="loggedIn"
v-model="selectedAlbum"
placeholder="Upload to album"
size="is-medium"
@@ -13,7 +15,8 @@
{{ album.name }}
</option>
</b-select>
- <dropzone v-if="showDropzone"
+ <dropzone
+ v-if="showDropzone"
id="dropzone"
ref="el"
:options="dropzoneOptions"
@@ -25,16 +28,22 @@
Add or drop more files
</label>
- <div id="template"
+ <div
+ id="template"
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="openLink">
- <a class="link"
+ <a
+ class="link"
target="_blank">
Link
</a>
@@ -43,14 +52,16 @@
<div class="error">
<div>
<span>
- <span class="error-message"
+ <span
+ class="error-message"
data-dz-errormessage />
<i class="icon-web-warning" />
</span>
</div>
</div>
<div class="dz-progress">
- <span class="dz-upload"
+ <span
+ class="dz-upload"
data-dz-uploadprogress />
</div>
<!--
@@ -64,6 +75,8 @@
</template>
<script>
+import { mapState, mapGetters } from 'vuex';
+
import Dropzone from 'nuxt-dropzone';
import '~/assets/styles/dropzone.scss';
@@ -75,20 +88,15 @@ export default {
files: [],
dropzoneOptions: {},
showDropzone: false,
- albums: [],
- selectedAlbum: null
+ selectedAlbum: null,
};
},
computed: {
- config() {
- return this.$store.state.config;
- },
- token() {
- return this.$store.state.token;
- },
- loggedIn() {
- return this.$store.state.loggedIn;
- }
+ ...mapState({
+ config: (state) => state.config,
+ albums: (state) => state.albums.tinyDetails,
+ }),
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn', token: 'auth/getToken' }),
},
watch: {
loggedIn() {
@@ -96,7 +104,7 @@ export default {
},
selectedAlbum() {
this.updateDropzoneConfig();
- }
+ },
},
mounted() {
this.dropzoneOptions = {
@@ -119,7 +127,7 @@ export default {
maxFilesize: this.config.maxFileSize,
previewTemplate: this.$refs.template.innerHTML,
dictDefaultMessage: 'Drag & Drop your files or click to browse',
- headers: { Accept: 'application/vnd.lolisafe.json' }
+ headers: { Accept: 'application/vnd.lolisafe.json' },
};
this.showDropzone = true;
if (this.loggedIn) this.getAlbums();
@@ -129,8 +137,11 @@ export default {
Get all available albums so the user can upload directly to one (or several soon™) of them.
*/
async getAlbums() {
- const response = await this.$axios.$get(`albums/dropdown`);
- this.albums = response.albums;
+ try {
+ await this.$store.dispatch('albums/getTinyDetails');
+ } catch (e) {
+ this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
this.updateDropzoneConfig();
},
@@ -143,14 +154,14 @@ export default {
this.$refs.el.setOption('headers', {
Accept: 'application/vnd.lolisafe.json',
Authorization: this.token ? `Bearer ${this.token}` : '',
- albumId: this.selectedAlbum ? this.selectedAlbum : null
+ albumId: this.selectedAlbum ? this.selectedAlbum : null,
});
},
/*
Dropzone stuff
*/
- dropzoneFilesAdded(files) {
+ dropzoneFilesAdded() {
this.alreadyAddedFiles = true;
},
dropzoneSuccess(file, response) {
@@ -159,8 +170,9 @@ export default {
dropzoneError(file, message, xhr) {
this.$store.dispatch('alert', {
text: 'There was an error uploading this file. Check the console.',
- error: true
+ error: true,
});
+ // eslint-disable-next-line no-console
console.error(file, message, xhr);
},
async dropzoneChunksUploaded(file, done) {
@@ -170,12 +182,11 @@ export default {
original: file.name,
size: file.size,
type: file.type,
- count: file.upload.totalChunkCount
- }]
+ count: file.upload.totalChunkCount,
+ }],
});
this.processResult(file, data);
- this.$forceUpdate();
return done();
},
@@ -194,8 +205,8 @@ export default {
this.$clipboard(response.url);
});
*/
- }
- }
+ },
+ },
};
</script>
<style lang="scss" scoped>
diff --git a/src/site/constants/alertTypes.js b/src/site/constants/alertTypes.js
new file mode 100644
index 0000000..1b830bc
--- /dev/null
+++ b/src/site/constants/alertTypes.js
@@ -0,0 +1,10 @@
+export default {
+ PRIMARY: 'is-primary',
+ INFO: 'is-info',
+ SUCCESS: 'is-success',
+ WARNING: 'is-warning',
+ ERROR: 'is-danger',
+ DARK: 'is-dark',
+ LIGHT: 'is-light',
+ WHITE: 'is-white',
+};
diff --git a/src/site/layouts/default.vue b/src/site/layouts/default.vue
index 61a257e..0049b88 100644
--- a/src/site/layouts/default.vue
+++ b/src/site/layouts/default.vue
@@ -1,69 +1,84 @@
<template>
- <div v-bar
+ <div
+ v-bar
class="scroll-area">
<div class="default-body">
<Navbar :isWhite="true" />
- <nuxt-child id="app"
+ <nuxt-child
+ id="app"
class="nuxt-app is-height-max-content" />
<Footer />
</div>
</div>
</template>
<script>
+import { mapState } from 'vuex';
import Navbar from '~/components/navbar/Navbar.vue';
-import Footer from '~/components/footer/Footer';
+import Footer from '~/components/footer/Footer.vue';
+
export default {
components: {
- Navbar,
- Footer
- },
- computed: {
- config() {
- return this.$store.state.config;
- },
- alert() {
- return this.$store.state.alert;
- }
+ Navbar,
+ Footer,
},
- watch: {
- alert() {
- if (!this.alert) return;
+ computed: mapState(['config', 'alert']),
+ created() {
+ this.$store.watch((state) => state.alert.message, () => {
+ const { message, type, snackbar } = this.alert;
+
+ if (!message) return;
- this.$buefy.toast.open({
- duration: 3500,
- message: this.alert.text,
- position: 'is-bottom',
- type: this.alert.error ? 'is-danger' : 'is-success'
- });
+ if (snackbar) {
+ this.$buefy.snackbar.open({
+ duration: 3500,
+ position: 'is-bottom',
+ message,
+ type,
+ });
+ } else {
+ this.$buefy.toast.open({
+ duration: 3500,
+ position: 'is-bottom',
+ message,
+ type,
+ });
+ }
- setTimeout(() => {
- this.$store.dispatch('alert', null);
- }, 3500);
- }
+ this.$store.dispatch('alert/clear', null);
+ });
},
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');
- }
+ // eslint-disable-next-line no-console
+ 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',
+ );
+ },
};
</script>
+
<style lang="scss">
- html { overflow: hidden !important; }
- .is-fullheight {
- min-height: 100vh !important;
- height: max-content;
- }
- .nuxt-app > .section {
- min-height: auto !important;
- height: auto !important;
- }
- @import "~/assets/styles/style.scss";
- @import "~/assets/styles/icons.min.css";
+html {
+ overflow: hidden !important;
+}
+.is-fullheight {
+ min-height: 100vh !important;
+ height: max-content;
+}
+.nuxt-app > .section {
+ min-height: auto !important;
+ height: auto !important;
+}
+@import '~/assets/styles/style.scss';
+@import '~/assets/styles/icons.min.css';
</style>
<style lang="scss" scoped>
- .default-body {
- align-items: baseline !important;
- }
- .scroll-area {
- height: 100vh;
- }
+.default-body {
+ align-items: baseline !important;
+}
+.scroll-area {
+ height: 100vh;
+}
</style>
diff --git a/src/site/layouts/error.vue b/src/site/layouts/error.vue
index 77d188f..3f12c46 100644
--- a/src/site/layouts/error.vue
+++ b/src/site/layouts/error.vue
@@ -23,6 +23,6 @@
import Navbar from '~/components/navbar/Navbar.vue';
export default {
- components: { Navbar }
+ components: { Navbar },
};
</script>
diff --git a/src/site/middleware/admin.js b/src/site/middleware/admin.js
index fcac9c6..1a94b75 100644
--- a/src/site/middleware/admin.js
+++ b/src/site/middleware/admin.js
@@ -1,5 +1,6 @@
-export default function({ store, redirect }) {
+export default function ({ store, redirect }) {
// If the user is not authenticated
- if (!store.state.user) return redirect('/login');
- if (!store.state.user.isAdmin) return redirect('/dashboard');
+ if (!store.state.auth.user) return redirect('/login');
+ if (!store.state.auth.user.isAdmin) return redirect('/dashboard');
+ return true;
}
diff --git a/src/site/middleware/auth.js b/src/site/middleware/auth.js
index 58a372e..b2ecc68 100644
--- a/src/site/middleware/auth.js
+++ b/src/site/middleware/auth.js
@@ -1,6 +1,7 @@
-export default function({ store, redirect }) {
+export default function ({ store, redirect }) {
// If the user is not authenticated
- if (!store.state.loggedIn) {
+ if (!store.state.auth.loggedIn) {
return redirect('/login');
}
+ return true;
}
diff --git a/src/site/pages/a/_identifier.vue b/src/site/pages/a/_identifier.vue
index 24d172b..3746cc6 100644
--- a/src/site/pages/a/_identifier.vue
+++ b/src/site/pages/a/_identifier.vue
@@ -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>
diff --git a/src/site/pages/dashboard/account.vue b/src/site/pages/dashboard/account.vue
index 121b2b3..31ec8af 100644
--- a/src/site/pages/dashboard/account.vue
+++ b/src/site/pages/dashboard/account.vue
@@ -6,57 +6,84 @@
<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 v-model="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="user.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="user.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="user.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>
+ <b-button
+ type="is-lolisafe"
+ @click="changePassword">
+ Change password
+ </b-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-input v-model="user.apiKey"
- expanded
- disabled />
+ <b-field expanded>
+ <b-input
+ :value="apiKey"
+ expanded
+ disabled />
+ <p class="control">
+ <b-button
+ type="is-lolisafe"
+ @click="copyKey">
+ Copy
+ </b-button>
+ </p>
+ </b-field>
</b-field>
<div class="mb2 mt2 text-center">
- <button class="button is-primary"
- @click="promptNewAPIKey">Request new API key</button>
+ <b-button
+ type="is-lolisafe"
+ @click="promptNewAPIKey">
+ Request new API key
+ </b-button>
</div>
</div>
</div>
@@ -65,65 +92,78 @@
</template>
<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
- Sidebar
+ Sidebar,
},
- middleware: 'auth',
+ middleware: ['auth', ({ store }) => {
+ store.dispatch('auth/fetchCurrentUser');
+ }],
data() {
return {
- user: {}
+ password: '',
+ newPassword: '',
+ reNewPassword: '',
};
},
+ computed: {
+ ...mapGetters({ 'apiKey': 'auth/getApiKey' }),
+ ...mapState({
+ user: (state) => state.auth.user,
+ }),
+ },
metaInfo() {
return { title: 'Account' };
},
- mounted() {
- this.getUserSetttings();
- },
methods: {
- async getUserSetttings() {
- const response = await this.$axios.$get(`users/me`);
- this.user = response.user;
- },
+ ...mapActions({
+ getUserSetttings: 'auth/fetchCurrentUser',
+ }),
async changePassword() {
- if (!this.user.password || !this.user.newPassword || !this.user.reNewPassword) {
- this.$store.dispatch('alert', {
+ const { password, newPassword, reNewPassword } = this;
+
+ if (!password || !newPassword || !reNewPassword) {
+ this.$store.dispatch('alert/set', {
text: 'One or more fields are missing',
- error: true
+ error: true,
});
return;
}
- if (this.user.newPassword !== this.user.reNewPassword) {
- this.$store.dispatch('alert', {
+ if (newPassword !== reNewPassword) {
+ this.$store.dispatch('alert/set', {
text: 'Passwords don\'t match',
- error: true
+ error: true,
});
return;
}
- const response = await this.$axios.$post(`user/password/change`,
- {
- password: this.user.password,
- newPassword: this.user.newPassword
- });
- this.$buefy.toast.open(response.message);
+ const response = await this.$store.dispatch('auth/changePassword', {
+ password,
+ newPassword,
+ });
+
+ if (response) {
+ this.$buefy.toast.open(response.message);
+ }
},
promptNewAPIKey() {
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(),
});
},
+ copyKey() {
+ this.$clipboard(this.apiKey);
+ this.$notifier.success('API key copied to clipboard');
+ },
async requestNewAPIKey() {
- const response = await this.$axios.$post(`user/apikey/change`);
- this.user.apiKey = response.apiKey;
- this.$forceUpdate();
+ const response = await this.$store.dispatch('auth/requestAPIKey');
this.$buefy.toast.open(response.message);
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/pages/dashboard/admin/file/_id.vue b/src/site/pages/dashboard/admin/file/_id.vue
index 5821292..89afa47 100644
--- a/src/site/pages/dashboard/admin/file/_id.vue
+++ b/src/site/pages/dashboard/admin/file/_id.vue
@@ -9,87 +9,111 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">File details</h2>
+ <h2 class="subtitle">
+ File details
+ </h2>
<hr>
<div class="columns">
<div class="column is-6">
- <b-field label="ID"
+ <b-field
+ label="ID"
horizontal>
- <span>{{ file.id }}</span>
+ <span>{{ admin.file.id }}</span>
</b-field>
- <b-field label="Name"
+ <b-field
+ label="Name"
horizontal>
- <span>{{ file.name }}</span>
+ <span>{{ admin.file.name }}</span>
</b-field>
- <b-field label="Original Name"
+ <b-field
+ label="Original Name"
horizontal>
- <span>{{ file.original }}</span>
+ <span>{{ admin.file.original }}</span>
</b-field>
- <b-field label="IP"
+ <b-field
+ label="IP"
horizontal>
- <span class="underline">{{ file.ip }}</span>
+ <span class="underline">{{ admin.file.ip }}</span>
</b-field>
- <b-field label="Link"
+ <b-field
+ label="Link"
horizontal>
- <a :href="file.url"
- target="_blank">{{ file.url }}</a>
+ <a
+ :href="admin.file.url"
+ target="_blank">{{ admin.file.url }}</a>
</b-field>
- <b-field label="Size"
+ <b-field
+ label="Size"
horizontal>
- <span>{{ formatBytes(file.size) }}</span>
+ <span>{{ formatBytes(admin.file.size) }}</span>
</b-field>
- <b-field label="Hash"
+ <b-field
+ label="Hash"
horizontal>
- <span>{{ file.hash }}</span>
+ <span>{{ admin.file.hash }}</span>
</b-field>
- <b-field label="Uploaded"
+ <b-field
+ label="Uploaded"
horizontal>
- <span><timeago :since="file.createdAt" /></span>
+ <span><timeago :since="admin.file.createdAt" /></span>
</b-field>
</div>
<div class="column is-6">
- <b-field label="User Id"
+ <b-field
+ label="User Id"
horizontal>
- <span>{{ user.id }}</span>
+ <span>{{ admin.user.id }}</span>
</b-field>
- <b-field label="Username"
+ <b-field
+ label="Username"
horizontal>
- <span>{{ user.username }}</span>
+ <span>{{ admin.user.username }}</span>
</b-field>
- <b-field label="Enabled"
+ <b-field
+ label="Enabled"
horizontal>
- <span>{{ user.enabled }}</span>
+ <span>{{ admin.user.enabled }}</span>
</b-field>
- <b-field label="Registered"
+ <b-field
+ label="Registered"
horizontal>
- <span><timeago :since="user.createdAt" /></span>
+ <span><timeago :since="admin.user.createdAt" /></span>
</b-field>
- <b-field label="Files"
+ <b-field
+ label="Files"
horizontal>
<span>
- <nuxt-link :to="`/dashboard/admin/user/${user.id}`">{{ user.fileCount }}</nuxt-link>
+ <nuxt-link :to="`/dashboard/admin/user/${admin.user.id}`">{{ admin.user.fileCount }}</nuxt-link>
</span>
</b-field>
</div>
</div>
<div class="mb2 mt2 text-center">
- <button class="button is-danger"
- @click="promptBanIP">Ban IP</button>
- <button class="button is-danger"
- @click="promptDisableUser">Disable user</button>
+ <b-button
+ v-if="admin.user.id !== auth.user.id"
+ type="is-danger"
+ @click="promptBanIP">
+ Ban IP
+ </b-button>
+ <b-button
+ v-if="admin.user.id !== auth.user.id"
+ type="is-danger"
+ @click="promptDisableUser">
+ Disable user
+ </b-button>
</div>
</div>
</div>
@@ -98,59 +122,42 @@
</template>
<script>
+import { mapState } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
- Sidebar
+ Sidebar,
},
- middleware: ['auth', 'admin'],
- data() {
- return {
- options: {},
- file: null,
- user: null
- };
- },
- async asyncData({ $axios, route }) {
+ middleware: ['auth', 'admin', ({ route, store }) => {
try {
- const response = await $axios.$get(`file/${route.params.id}`);
- return {
- file: response.file ? response.file : null,
- user: response.user ? response.user : null
- };
- } catch (error) {
- console.error(error);
- return {
- file: null,
- user: null
- };
+ store.dispatch('admin/fetchFile', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
}
- },
+ }],
+ computed: mapState(['admin', 'auth']),
methods: {
promptDisableUser() {
this.$buefy.dialog.confirm({
+ type: 'is-danger',
message: 'Are you sure you want to disable the account of the user that uploaded this file?',
- onConfirm: () => this.disableUser()
+ onConfirm: () => this.disableUser(),
});
},
- async disableUser() {
- const response = await this.$axios.$post('admin/users/disable', {
- id: this.user.id
- });
- this.$buefy.toast.open(response.message);
+ disableUser() {
+ this.$handler.executeAction('admin/disableUser', this.user.id);
},
promptBanIP() {
this.$buefy.dialog.confirm({
+ type: 'is-danger',
message: 'Are you sure you want to ban the IP this file was uploaded from?',
- onConfirm: () => this.banIP()
+ onConfirm: () => this.banIP(),
});
},
- async banIP() {
- const response = await this.$axios.$post('admin/ban/ip', {
- ip: this.file.ip
- });
- this.$buefy.toast.open(response.message);
+ banIP() {
+ this.$handler.executeAction('admin/banIP', this.file.ip);
},
formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
@@ -161,8 +168,8 @@ export default {
const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
- }
- }
+ return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
+ },
+ },
};
</script>
diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue
index 2d59fff..7f6a0fe 100644
--- a/src/site/pages/dashboard/admin/settings.vue
+++ b/src/site/pages/dashboard/admin/settings.vue
@@ -6,87 +6,112 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">Service settings</h2>
+ <h2 class="subtitle">
+ Service settings
+ </h2>
<hr>
- <b-field label="Service name"
+ <b-field
+ label="Service name"
message="Please enter the name which this service is gonna be identified as"
horizontal>
- <b-input v-model="options.serviceName"
+ <b-input
+ v-model="settings.serviceName"
expanded />
</b-field>
- <b-field label="Upload folder"
+ <b-field
+ label="Upload folder"
message="Where to store the files relative to the working directory"
horizontal>
- <b-input v-model="options.uploadFolder"
+ <b-input
+ v-model="settings.uploadFolder"
expanded />
</b-field>
- <b-field label="Links per album"
+ <b-field
+ label="Links per album"
message="Maximum links allowed per album"
horizontal>
- <b-input v-model="options.linksPerAlbum"
+ <b-input
+ v-model="settings.linksPerAlbum"
type="number"
expanded />
</b-field>
- <b-field label="Max upload size"
+ <b-field
+ label="Max upload size"
message="Maximum allowed file size in MB"
horizontal>
- <b-input v-model="options.maxUploadSize"
+ <b-input
+ v-model="settings.maxUploadSize"
expanded />
</b-field>
- <b-field label="Filename length"
+ <b-field
+ label="Filename length"
message="How many characters long should the generated filenames be"
horizontal>
- <b-input v-model="options.filenameLength"
+ <b-input
+ v-model="settings.filenameLength"
expanded />
</b-field>
- <b-field label="Album link length"
+ <b-field
+ label="Album link length"
message="How many characters a link for an album should have"
horizontal>
- <b-input v-model="options.albumLinkLength"
+ <b-input
+ v-model="settings.albumLinkLength"
expanded />
</b-field>
- <b-field label="Generate thumbnails"
+ <b-field
+ label="Generate thumbnails"
message="Generate thumbnails when uploading a file if possible"
horizontal>
- <b-switch v-model="options.generateThumbnails"
+ <b-switch
+ v-model="settings.generateThumbnails"
:true-value="true"
:false-value="false" />
</b-field>
- <b-field label="Generate zips"
+ <b-field
+ label="Generate zips"
message="Allow generating zips to download entire albums"
horizontal>
- <b-switch v-model="options.generateZips"
+ <b-switch
+ v-model="settings.generateZips"
:true-value="true"
:false-value="false" />
</b-field>
- <b-field label="Public mode"
+ <b-field
+ label="Public mode"
message="Enable anonymous uploades"
horizontal>
- <b-switch v-model="options.publicMode"
+ <b-switch
+ v-model="settings.publicMode"
:true-value="true"
:false-value="false" />
</b-field>
- <b-field label="Enable creating account"
+ <b-field
+ label="Enable creating account"
message="Enable creating new accounts in the platform"
horizontal>
- <b-switch v-model="options.enableAccounts"
+ <b-switch
+ v-model="settings.enableAccounts"
:true-value="true"
:false-value="false" />
</b-field>
<div class="mb2 mt2 text-center">
- <button class="button is-primary"
- @click="promptRestartService">Save and restart service</button>
+ <button
+ class="button is-primary"
+ @click="promptRestartService">
+ Save and restart service
+ </button>
</div>
</div>
</div>
@@ -95,39 +120,37 @@
</template>
<script>
+import { mapState } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
- Sidebar
- },
- middleware: ['auth', 'admin'],
- data() {
- return {
- options: {}
- };
+ Sidebar,
},
+ middleware: ['auth', 'admin', ({ store }) => {
+ try {
+ store.dispatch('admin/fetchSettings');
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
metaInfo() {
return { title: 'Settings' };
},
- mounted() {
- this.getSettings();
- },
+ computed: mapState({
+ settings: (state) => state.admin.settings,
+ }),
methods: {
- async getSettings() {
- const response = await this.$axios.$get(`service/config`);
- this.options = response.config;
- },
promptRestartService() {
this.$buefy.dialog.confirm({
message: 'Keep in mind that restarting only works if you have PM2 or something similar set up. Continue?',
- onConfirm: () => this.restartService()
+ onConfirm: () => this.restartService(),
});
},
- async restartService() {
- const response = await this.$axios.$post(`service/restart`);
- this.$buefy.toast.open(response.message);
- }
- }
+ restartService() {
+ this.$handler.executeAction('admin/restartService');
+ },
+ },
};
</script>
diff --git a/src/site/pages/dashboard/admin/user/_id.vue b/src/site/pages/dashboard/admin/user/_id.vue
index 2a56c34..7814468 100644
--- a/src/site/pages/dashboard/admin/user/_id.vue
+++ b/src/site/pages/dashboard/admin/user/_id.vue
@@ -9,41 +9,59 @@
<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>
+ <span>{{ user.files.length }}</span>
</b-field>
<div class="mb2 mt2 text-center">
- <button class="button is-danger"
- @click="promptDisableUser">Disable user</button>
+ <b-button
+ v-if="user.enabled"
+ type="is-danger"
+ @click="promptDisableUser">
+ Disable user
+ </b-button>
+ <b-button
+ v-if="!user.enabled"
+ type="is-success"
+ @click="promptEnableUser">
+ Enable user
+ </b-button>
</div>
- <Grid v-if="files.length"
- :files="files" />
+ <Grid
+ v-if="user.files.length"
+ :files="user.files" />
</div>
</div>
</div>
@@ -51,50 +69,52 @@
</template>
<script>
+import { mapState } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
import Grid from '~/components/grid/Grid.vue';
export default {
components: {
Sidebar,
- Grid
+ Grid,
},
- middleware: ['auth', 'admin'],
+ middleware: ['auth', 'admin', ({ route, store }) => {
+ try {
+ store.dispatch('admin/fetchUser', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
data() {
return {
options: {},
- files: null,
- user: null
};
},
- async asyncData({ $axios, route }) {
- try {
- const response = await $axios.$get(`/admin/users/${route.params.id}`);
- return {
- files: response.files ? response.files : null,
- user: response.user ? response.user : null
- };
- } catch (error) {
- console.error(error);
- return {
- files: null,
- user: null
- };
- }
- },
+ computed: mapState({
+ user: (state) => state.admin.user,
+ }),
methods: {
promptDisableUser() {
this.$buefy.dialog.confirm({
- message: 'Are you sure you want to disable the account of the user that uploaded this file?',
- onConfirm: () => this.disableUser()
+ type: 'is-danger',
+ message: 'Are you sure you want to disable the account of this user?',
+ onConfirm: () => this.disableUser(),
});
},
- async disableUser() {
- const response = await this.$axios.$post('admin/users/disable', {
- id: this.user.id
+ promptEnableUser() {
+ this.$buefy.dialog.confirm({
+ type: 'is-danger',
+ message: 'Are you sure you want to enable the account of this user?',
+ onConfirm: () => this.enableUser(),
});
- this.$buefy.toast.open(response.message);
- }
- }
+ },
+ disableUser() {
+ this.$handler.executeAction('admin/disableUser', this.user.id);
+ },
+ enableUser() {
+ this.$handler.executeAction('admin/enableUser', this.user.id);
+ },
+ },
};
</script>
diff --git a/src/site/pages/dashboard/admin/users.vue b/src/site/pages/dashboard/admin/users.vue
index 695cf0b..bed4c2b 100644
--- a/src/site/pages/dashboard/admin/users.vue
+++ b/src/site/pages/dashboard/admin/users.vue
@@ -1,3 +1,137 @@
+<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
+ :value="props.row.enabled"
+ @input="changeEnabledStatus(props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ field="isAdmin"
+ label="Admin"
+ centered>
+ <b-switch
+ :value="props.row.isAdmin"
+ @input="changeIsAdmin(props.row)" />
+ </b-table-column>
+
+ <b-table-column
+ field="purge"
+ centered>
+ <b-button
+ type="is-danger"
+ @click="promptPurgeFiles(props.row)">
+ Purge files
+ </b-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 { mapState } from 'vuex';
+import Sidebar from '~/components/sidebar/Sidebar.vue';
+
+export default {
+ components: {
+ Sidebar,
+ },
+ middleware: ['auth', 'admin', ({ route, store }) => {
+ try {
+ store.dispatch('admin/fetchUsers', route.params.id);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }],
+ computed: mapState({
+ users: (state) => state.admin.users,
+ config: (state) => state.config,
+ }),
+ metaInfo() {
+ return { title: 'Uploads' };
+ },
+ methods: {
+ async changeEnabledStatus(row) {
+ if (row.enabled) {
+ this.$handler.executeAction('admin/disableUser', row.id);
+ } else {
+ this.$handler.executeAction('admin/enableUser', row.id);
+ }
+ },
+ async changeIsAdmin(row) {
+ if (row.isAdmin) {
+ this.$handler.executeAction('admin/demoteUser', row.id);
+ } else {
+ this.$handler.executeAction('admin/promoteUser', row.id);
+ }
+ },
+ promptPurgeFiles(row) {
+ this.$buefy.dialog.confirm({
+ message: 'Are you sure you want to delete this user\'s files?',
+ onConfirm: () => this.purgeFiles(row),
+ });
+ },
+ async purgeFiles(row) {
+ this.$handler.executeAction('admin/purgeUserFiles', row.id);
+ },
+ },
+};
+</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
div.view-container {
@@ -107,9 +241,6 @@
}
div.column > h2.subtitle { padding-top: 1px; }
-</style>
-<style lang="scss">
- @import '~/assets/styles/_colors.scss';
.b-table {
.table-wrapper {
@@ -118,130 +249,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>
diff --git a/src/site/pages/dashboard/albums/_id.vue b/src/site/pages/dashboard/albums/_id.vue
index 47e7057..a909e75 100644
--- a/src/site/pages/dashboard/albums/_id.vue
+++ b/src/site/pages/dashboard/albums/_id.vue
@@ -10,25 +10,59 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">Files</h2>
- <hr>
+ <nav class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title is-3">
+ {{ images.name }}
+ </h1>
+ </div>
+ <div class="level-item">
+ <h2 class="subtitle is-5">
+ ({{ totalFiles }} files)
+ </h2>
+ </div>
+ </div>
+ <div class="level-right">
+ <div class="level-item">
+ <b-field>
+ <b-input
+ placeholder="Search"
+ type="search" />
+ <p class="control">
+ <b-button type="is-lolisafe">
+ Search
+ </b-button>
+ </p>
+ </b-field>
+ </div>
+ </div>
+ </nav>
- <Grid v-if="files.length"
- :files="files" />
+ <hr>
- <b-pagination
- v-if="count > perPage"
- :total="count"
- :per-page="perPage"
- :current.sync="current"
- class="pagination"
- icon-prev="icon-interface-arrow-left"
- icon-next="icon-interface-arrow-right"
- icon-pack="icon"
- aria-next-label="Next page"
- aria-previous-label="Previous page"
- aria-page-label="Page"
- aria-current-label="Current page" />
+ <Grid
+ v-if="totalFiles"
+ :files="images.files"
+ :total="totalFiles">
+ <template v-slot:pagination>
+ <b-pagination
+ v-if="shouldPaginate"
+ :total="totalFiles"
+ :per-page="limit"
+ :current.sync="current"
+ range-before="2"
+ range-after="2"
+ class="pagination-slot"
+ icon-prev="icon-interface-arrow-left"
+ icon-next="icon-interface-arrow-right"
+ icon-pack="icon"
+ aria-next-label="Next page"
+ aria-previous-label="Previous page"
+ aria-page-label="Page"
+ aria-current-label="Current page" />
+ </template>
+ </Grid>
</div>
</div>
</div>
@@ -36,53 +70,65 @@
</template>
<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+
import Sidebar from '~/components/sidebar/Sidebar.vue';
import Grid from '~/components/grid/Grid.vue';
export default {
components: {
Sidebar,
- Grid
+ Grid,
},
- middleware: 'auth',
+ middleware: ['auth', ({ route, store }) => {
+ store.commit('images/resetState');
+ store.dispatch('images/fetchByAlbumId', { id: route.params.id });
+ }],
data() {
return {
- name: null,
- files: [],
- count: 0,
current: 1,
- perPage: 30
};
},
+ computed: {
+ ...mapGetters({
+ totalFiles: 'images/getTotalFiles',
+ shouldPaginate: 'images/shouldPaginate',
+ limit: 'images/getLimit',
+ }),
+ ...mapState(['images']),
+ id() {
+ return this.$route.params.id;
+ },
+ },
metaInfo() {
return { title: 'Album' };
},
watch: {
- current: 'getFiles'
- },
- async asyncData({ $axios, route }) {
- const perPage = 30;
- const current = 1; // current page
-
- try {
- const response = await $axios.$get(`album/${route.params.id}/full`, { params: { page: current, limit: perPage }});
- return {
- files: response.files || [],
- count: response.count || 0,
- current,
- perPage
- };
- } catch (error) {
- console.error(error);
- return { files: [] };
- }
+ current: 'fetchPaginate',
},
methods: {
- async getFiles() {
- const response = await this.$axios.$get(`album/${this.$route.params.id}/full`, { params: { page: this.current, limit: this.perPage }});
- this.files = response.files;
- this.count = response.count;
- }
+ ...mapActions({
+ fetch: 'images/fetchByAlbumId',
+ }),
+ fetchPaginate() {
+ this.fetch({ id: this.id, page: this.current });
+ },
},
};
</script>
+
+<style lang="scss" scoped>
+ div.grid {
+ margin-bottom: 1rem;
+ }
+
+ .pagination-slot {
+ padding: 1rem 0;
+ }
+</style>
+
+<style lang="scss">
+ .pagination-slot > .pagination-previous, .pagination-slot > .pagination-next {
+ display: none !important;
+ }
+</style>
diff --git a/src/site/pages/dashboard/albums/index.vue b/src/site/pages/dashboard/albums/index.vue
index 065667a..a2ba522 100644
--- a/src/site/pages/dashboard/albums/index.vue
+++ b/src/site/pages/dashboard/albums/index.vue
@@ -7,136 +7,35 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">Manage your albums</h2>
+ <h2 class="subtitle">
+ Manage your albums
+ </h2>
<hr>
<div class="search-container">
<b-field>
- <b-input v-model="newAlbumName"
+ <b-input
+ v-model="newAlbumName"
placeholder="Album name..."
type="text"
@keyup.enter.native="createAlbum" />
<p class="control">
- <button outlined
- class="button is-primary"
- @click="createAlbum">Create album</button>
+ <button
+ outlined
+ class="button is-black"
+ :disabled="isCreatingAlbum"
+ @click="createAlbum">
+ Create album
+ </button>
</p>
</b-field>
</div>
<div class="view-container">
- <div v-for="album in albums"
+ <AlbumEntry
+ v-for="album in albums.list"
:key="album.id"
- class="album">
- <div class="arrow-container"
- @click="fetchAlbumDetails(album)">
- <i :class="{ active: album.isDetailsOpen }"
- class="icon-arrow" />
- </div>
- <div class="thumb">
- <figure class="image is-64x64 thumb">
- <img src="~/assets/images/blank_darker.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 is-hidden-mobile">
- <template v-if="album.fileCount > 0">
- <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>
- </template>
- <template v-else>
- <span class="no-files">Nothing to show here</span>
- </template>
- </div>
-
- <div v-if="album.isDetailsOpen"
- class="details">
- <h2>Public links for this album:</h2>
-
- <b-table
- :data="album.links.length ? album.links : []"
- :mobile-cards="true">
- <template slot-scope="props">
- <b-table-column field="identifier"
- label="Link"
- centered>
- <a :href="`${config.URL}/a/${props.row.identifier}`"
- target="_blank">
- {{ props.row.identifier }}
- </a>
- </b-table-column>
-
- <b-table-column field="views"
- label="Views"
- centered>
- {{ props.row.views }}
- </b-table-column>
-
- <b-table-column field="enableDownload"
- label="Allow download"
- centered>
- <b-switch v-model="props.row.enableDownload"
- @input="linkOptionsChanged(props.row)" />
- </b-table-column>
-
- <b-table-column field="enabled"
- numeric>
- <button class="button is-danger"
- @click="promptDeleteAlbumLink(props.row.identifier)">Delete link</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="level is-paddingless">
- <div class="level-left">
- <div class="level-item">
- <button :class="{ 'is-loading': album.isCreatingLink }"
- class="button is-primary"
- style="float: left"
- @click="createLink(album)">Create new link</button>
- </div>
- <div class="level-item">
- <span class="has-text-default">{{ album.links.length }} / {{ config.maxLinksPerAlbum }} links created</span>
- </div>
- </div>
-
- <div class="level-right">
- <div class="level-item">
- <button class="button is-danger"
- style="float: right"
- @click="promptDeleteAlbum(album.id)">Delete album</button>
- </div>
- </div>
- </div>
- </template>
- </b-table>
- </div>
- </div>
+ :album="album" />
</div>
</div>
</div>
@@ -146,105 +45,55 @@
</template>
<script>
+import { mapState, mapActions } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
+import AlbumEntry from '~/components/album/AlbumEntry.vue';
export default {
components: {
- Sidebar
+ Sidebar,
+ AlbumEntry,
},
- middleware: 'auth',
+ middleware: ['auth', ({ store }) => {
+ try {
+ store.dispatch('albums/fetch');
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
+ }
+ }],
data() {
return {
- albums: [],
- newAlbumName: null
+ newAlbumName: null,
+ isCreatingAlbum: false,
};
},
- computed: {
- config() {
- return this.$store.state.config;
- }
- },
+ computed: mapState(['config', 'albums']),
metaInfo() {
return { title: 'Uploads' };
},
- mounted() {
- this.getAlbums();
- },
methods: {
- async fetchAlbumDetails(album) {
- const response = await this.$axios.$get(`album/${album.id}/links`);
- album.links = response.links;
- album.isDetailsOpen = !album.isDetailsOpen;
- this.$forceUpdate();
- },
- promptDeleteAlbum(id) {
- this.$buefy.dialog.confirm({
- message: 'Are you sure you want to delete this album?',
- onConfirm: () => this.deleteAlbum(id)
- });
- },
- async deleteAlbum(id) {
- const response = await this.$axios.$delete(`album/${id}`);
- this.getAlbums();
- return this.$buefy.toast.open(response.message);
- },
- promptDeleteAlbumLink(identifier) {
- this.$buefy.dialog.confirm({
- message: 'Are you sure you want to delete this album link?',
- onConfirm: () => this.deleteAlbumLink(identifier)
- });
- },
- async deleteAlbumLink(identifier) {
- const response = await this.$axios.$delete(`album/link/delete/${identifier}`);
- return this.$buefy.toast.open(response.message);
- },
- async linkOptionsChanged(link) {
- const response = await this.$axios.$post(`album/link/edit`,
- {
- identifier: link.identifier,
- enableDownload: link.enableDownload,
- enabled: link.enabled
- });
- this.$buefy.toast.open(response.message);
- },
- async createLink(album) {
- album.isCreatingLink = true;
- // Since we actually want to change the state even if the call fails, use a try catch
+ ...mapActions({
+ 'alert': 'alert/set',
+ }),
+ async createAlbum() {
+ if (!this.newAlbumName || this.newAlbumName === '') return;
+
+ this.isCreatingAlbum = true;
try {
- const response = await this.$axios.$post(`album/link/new`,
- { albumId: album.id });
- this.$buefy.toast.open(response.message);
- album.links.push({
- identifier: response.identifier,
- views: 0,
- enabled: true,
- enableDownload: true,
- expiresAt: null
- });
- } catch (error) {
- //
+ const response = await this.$store.dispatch('albums/createAlbum', this.newAlbumName);
+
+ this.alert({ text: response.message, error: false });
+ } catch (e) {
+ this.alert({ text: e.message, error: true });
} finally {
- album.isCreatingLink = false;
+ this.isCreatingAlbum = false;
+ this.newAlbumName = null;
}
},
- async createAlbum() {
- if (!this.newAlbumName || this.newAlbumName === '') return;
- const response = await this.$axios.$post(`album/new`,
- { name: this.newAlbumName });
- this.newAlbumName = null;
- this.$buefy.toast.open(response.message);
- this.getAlbums();
- },
- async getAlbums() {
- const response = await this.$axios.$get(`albums/mini`);
- for (const album of response.albums) {
- album.isDetailsOpen = false;
- }
- this.albums = response.albums;
- }
- }
+ },
};
</script>
+
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
div.view-container {
@@ -256,121 +105,5 @@ export default {
background-color: $base-2;
}
- div.album {
- display: flex;
- flex-wrap: wrap;
- margin-bottom: 10px;
-
- div.arrow-container {
- width: 2em;
- height: 64px;
- position: relative;
- cursor: pointer;
-
- i {
- border: 2px solid $defaultTextColor;
- border-right: 0;
- border-top: 0;
- display: block;
- height: 1em;
- position: absolute;
- transform: rotate(-135deg);
- transform-origin: center;
- width: 1em;
- z-index: 4;
- top: 22px;
-
- -webkit-transition: transform 0.1s linear;
- -moz-transition: transform 0.1s linear;
- -ms-transition: transform 0.1s linear;
- -o-transition: transform 0.1s linear;
- transition: transform 0.1s linear;
-
- &.active {
- transform: rotate(-45deg);
- }
- }
- }
- div.thumb {
- width: 64px;
- height: 64px;
- -webkit-box-shadow: $boxShadowLight;
- box-shadow: $boxShadowLight;
- }
-
- div.info {
- margin-left: 15px;
- text-align: left;
- h4 {
- font-size: 1.5rem;
- a {
- color: $defaultTextColor;
- font-weight: 400;
- &:hover { text-decoration: underline; }
- }
- }
- span { display: block; }
- span:nth-child(3) {
- font-size: 0.9rem;
- }
- }
-
- div.latest {
- flex-grow: 1;
- justify-content: flex-end;
- display: flex;
- margin-left: 15px;
-
- span.no-files {
- font-size: 1.5em;
- color: #b1b1b1;
- padding-top: 17px;
- }
-
- div.more {
- width: 64px;
- height: 64px;
- background: white;
- display: flex;
- align-items: center;
- padding: 10px;
- text-align: center;
- a {
- line-height: 1rem;
- color: $defaultTextColor;
- &:hover { text-decoration: underline; }
- }
- }
- }
-
- div.details {
- flex: 0 1 100%;
- padding-left: 2em;
- padding-top: 1em;
- min-height: 50px;
-
- .b-table {
- padding: 2em 0em;
-
- .table-wrapper {
- -webkit-box-shadow: $boxShadowLight;
- box-shadow: $boxShadowLight;
- }
- }
- }
- }
-
div.column > h2.subtitle { padding-top: 1px; }
-
- div.no-background { background: none !important; }
-</style>
-<style lang="scss">
- @import '~/assets/styles/_colors.scss';
-
- .b-table {
- .table-wrapper {
- -webkit-box-shadow: $boxShadowLight;
- box-shadow: $boxShadowLight;
- }
- }
</style>
diff --git a/src/site/pages/dashboard/index.vue b/src/site/pages/dashboard/index.vue
index 0eb9532..08c5166 100644
--- a/src/site/pages/dashboard/index.vue
+++ b/src/site/pages/dashboard/index.vue
@@ -6,27 +6,56 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">Your uploaded files</h2>
+ <nav class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h2 class="subtitle">
+ Your uploaded files
+ </h2>
+ </div>
+ </div>
+ <div class="level-right">
+ <div class="level-item">
+ <b-field>
+ <b-input
+ placeholder="Search"
+ type="search" />
+ <p class="control">
+ <b-button type="is-lolisafe">
+ Search
+ </b-button>
+ </p>
+ </b-field>
+ </div>
+ </div>
+ </nav>
<hr>
- <Grid v-if="count"
- :files="files"
- :enableSearch="false"
- class="grid" />
+ <!-- <b-loading :active="images.isLoading" /> -->
- <b-pagination
- v-if="count > perPage"
- :total="count"
- :per-page="perPage"
- :current.sync="current"
- class="pagination"
- icon-prev="icon-interface-arrow-left"
- icon-next="icon-interface-arrow-right"
- icon-pack="icon"
- aria-next-label="Next page"
- aria-previous-label="Previous page"
- aria-page-label="Page"
- aria-current-label="Current page" />
+ <Grid
+ v-if="totalFiles && !isLoading"
+ :files="images.files"
+ :enableSearch="false"
+ class="grid">
+ <template v-slot:pagination>
+ <b-pagination
+ v-if="shouldPaginate"
+ :total="totalFiles"
+ :per-page="limit"
+ :current.sync="current"
+ range-before="2"
+ range-after="2"
+ class="pagination-slot"
+ icon-prev="icon-interface-arrow-left"
+ icon-next="icon-interface-arrow-right"
+ icon-pack="icon"
+ aria-next-label="Next page"
+ aria-previous-label="Previous page"
+ aria-page-label="Page"
+ aria-current-label="Current page" />
+ </template>
+ </Grid>
</div>
</div>
</div>
@@ -34,54 +63,50 @@
</template>
<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+
import Sidebar from '~/components/sidebar/Sidebar.vue';
import Grid from '~/components/grid/Grid.vue';
export default {
components: {
Sidebar,
- Grid
+ Grid,
},
- middleware: 'auth',
+ middleware: ['auth', ({ store }) => {
+ store.commit('images/resetState');
+ store.dispatch('images/fetch');
+ }],
data() {
return {
- files: [],
- count: 0,
current: 1,
- perPage: 30
+ isLoading: false,
};
},
+ computed: {
+ ...mapGetters({
+ totalFiles: 'images/getTotalFiles',
+ shouldPaginate: 'images/shouldPaginate',
+ limit: 'images/getLimit',
+ }),
+ ...mapState(['images']),
+ },
metaInfo() {
return { title: 'Uploads' };
},
watch: {
- current: 'getFiles'
- },
- async asyncData({ $axios, route }) {
- const perPage = 30;
- const current = 1; // current page
-
- try {
- const response = await $axios.$get(`files`, { params: { page: current, limit: perPage }});
- return {
- files: response.files || [],
- count: response.count || 0,
- current,
- perPage
- };
- } catch (error) {
- console.error(error);
- return { files: [] };
- }
+ current: 'fetchPaginate',
},
methods: {
- async getFiles() {
- // TODO: Cache a few pages once fetched
- const response = await this.$axios.$get(`files`, { params: { page: this.current, limit: this.perPage }});
- this.files = response.files;
- this.count = response.count;
- }
- }
+ ...mapActions({
+ fetch: 'images/fetch',
+ }),
+ async fetchPaginate() {
+ this.isLoading = true;
+ await this.fetch(this.current);
+ this.isLoading = false;
+ },
+ },
};
</script>
@@ -89,4 +114,14 @@ export default {
div.grid {
margin-bottom: 1rem;
}
-</style> \ No newline at end of file
+
+ .pagination-slot {
+ padding: 1rem 0;
+ }
+</style>
+
+<style lang="scss">
+ .pagination-slot > .pagination-previous, .pagination-slot > .pagination-next {
+ display: none !important;
+ }
+</style>
diff --git a/src/site/pages/dashboard/tags/index.vue b/src/site/pages/dashboard/tags/index.vue
index 7c295b7..a9c5756 100644
--- a/src/site/pages/dashboard/tags/index.vue
+++ b/src/site/pages/dashboard/tags/index.vue
@@ -123,7 +123,6 @@
}
</style>
-
<template>
<section class="section is-fullheight dashboard">
<div class="container">
@@ -132,27 +131,35 @@
<Sidebar />
</div>
<div class="column">
- <h2 class="subtitle">Manage your tags</h2>
+ <h2 class="subtitle">
+ Manage your tags
+ </h2>
<hr>
<div class="search-container">
<b-field>
- <b-input v-model="newTagName"
+ <b-input
+ v-model="newTagName"
placeholder="Tag name..."
type="text"
@keyup.enter.native="createTag" />
<p class="control">
- <button class="button is-primary"
- @click="createTag">Create tags</button>
+ <b-button
+ type="is-lolisafe"
+ @click="createTag">
+ Create tags
+ </b-button>
</p>
</b-field>
</div>
<div class="view-container">
- <div v-for="tag in tags"
+ <div
+ v-for="tag in tags"
:key="tag.id"
class="album">
- <div class="arrow-container"
+ <div
+ class="arrow-container"
@click="promptDeleteTag">
<i class="icon-arrow" />
</div>
@@ -165,7 +172,9 @@
-->
<div class="info">
<h4>
- <router-link :to="`/dashboard/tags/${tag.id}`">{{ tag.name }}</router-link>
+ <router-link :to="`/dashboard/tags/${tag.id}`">
+ {{ tag.name }}
+ </router-link>
</h4>
<span>{{ tag.count || 0 }} files</span>
</div>
@@ -205,19 +214,19 @@ import Sidebar from '~/components/sidebar/Sidebar.vue';
export default {
components: {
- Sidebar
+ Sidebar,
},
middleware: 'auth',
data() {
return {
tags: [],
- newTagName: null
+ newTagName: null,
};
},
computed: {
config() {
return this.$store.state.config;
- }
+ },
},
metaInfo() {
return { title: 'Tags' };
@@ -228,17 +237,19 @@ export default {
methods: {
promptDeleteTag(id) {
this.$buefy.dialog.confirm({
+ type: 'is-danger',
message: 'Are you sure you want to delete this tag?',
- onConfirm: () => this.promptPurgeTag(id)
+ onConfirm: () => this.promptPurgeTag(id),
});
},
promptPurgeTag(id) {
this.$buefy.dialog.confirm({
+ type: 'is-danger',
message: 'Would you like to delete every file associated with this tag?',
cancelText: 'No',
confirmText: 'Yes',
onConfirm: () => this.deleteTag(id, true),
- onCancel: () => this.deleteTag(id, false)
+ onCancel: () => this.deleteTag(id, false),
});
},
async deleteTag(id, purge) {
@@ -248,19 +259,19 @@ export default {
},
async createTag() {
if (!this.newTagName || this.newTagName === '') return;
- const response = await this.$axios.$post(`tag/new`,
+ const response = await this.$axios.$post('tag/new',
{ name: this.newTagName });
this.newTagName = null;
this.$buefy.toast.open(response.message);
this.getTags();
},
async getTags() {
- const response = await this.$axios.$get(`tags`);
+ const response = await this.$axios.$get('tags');
for (const tag of response.tags) {
tag.isDetailsOpen = false;
}
this.tags = response.tags;
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/pages/faq.vue b/src/site/pages/faq.vue
index bb84a7d..85e8744 100644
--- a/src/site/pages/faq.vue
+++ b/src/site/pages/faq.vue
@@ -1,34 +1,45 @@
<template>
+ <!-- eslint-disable max-len -->
<div class="container has-text-left">
- <h2 class="subtitle">What is lolisafe?</h2>
+ <h2 class="subtitle">
+ What is lolisafe?
+ </h2>
<article class="message">
<div class="message-body">
lolisafe is an easy to use, open source and completely free file upload service. We accept your files, photos, documents, anything, and give you back a shareable link for you to send to others.
</div>
</article>
- <h2 class="subtitle">Can I run my own lolisafe?</h2>
+ <h2 class="subtitle">
+ Can I run my own lolisafe?
+ </h2>
<article class="message">
<div class="message-body">
Definitely. Head to <a target="_blank" href="https://github.com/WeebDev/lolisafe">our GitHub repo</a> and follow the instructions to clone, build and deploy it by yourself. It's super easy too!
</div>
</article>
- <h2 class="subtitle">How can I keep track of my uploads?</h2>
+ <h2 class="subtitle">
+ How can I keep track of my uploads?
+ </h2>
<article class="message">
<div class="message-body">
Simply create a user on the site and every upload will be associated with your account, granting you access to your uploaded files through our dashboard.
</div>
</article>
- <h2 class="subtitle">What are albums?</h2>
+ <h2 class="subtitle">
+ What are albums?
+ </h2>
<article class="message">
<div class="message-body">
Albums are a simple way of sorting uploads together. Right now you can create albums through the dashboard and use them only with <a target="_blank" href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">our chrome extension</a> which will enable you to <strong>right click -&gt; send to lolisafe</strong> or to a desired album if you have any.
</div>
</article>
- <h2 class="subtitle">Why should I use this?</h2>
+ <h2 class="subtitle">
+ Why should I use this?
+ </h2>
<article class="message">
<div class="message-body">
There are too many file upload services out there, and a lot of them rely on the foundations of pomf which is ancient. In a desperate and unsuccessful attempt of finding a good file uploader that's easily extendable, lolisafe was born. We give you control over your files, we give you a way to sort your uploads into albums for ease of access and we give you an api to use with ShareX or any other thing that let's you make POST requests.
@@ -45,7 +56,7 @@ export default {
},
metaInfo() {
return { title: 'Faq' };
- }
+ },
};
</script>
diff --git a/src/site/pages/index.vue b/src/site/pages/index.vue
index 707ae67..a28eea4 100644
--- a/src/site/pages/index.vue
+++ b/src/site/pages/index.vue
@@ -11,6 +11,7 @@
<div class="content-wrapper">
<h4>Blazing fast file uploader. <br>For real.</h4>
<p>
+ <!-- eslint-disable-next-line max-len -->
A <strong>modern</strong> and <strong>self-hosted</strong> 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.
</p>
</div>
@@ -19,7 +20,8 @@
</div>
<div class="container uploader">
<Uploader v-if="config.publicMode || (!config.publicMode && loggedIn)" />
- <div v-else
+ <div
+ v-else
class="has-text-centered is-size-4 has-text-danger">
This site has disabled public uploads. You need an account.
</div>
@@ -28,6 +30,8 @@
</div>
</template>
<script>
+import { mapState, mapGetters } from 'vuex';
+
import Logo from '~/components/logo/Logo.vue';
import Uploader from '~/components/uploader/Uploader.vue';
import Links from '~/components/home/links/Links.vue';
@@ -37,19 +41,15 @@ export default {
components: {
Logo,
Uploader,
- Links
+ Links,
},
data() {
return { albums: [] };
},
computed: {
- loggedIn() {
- return this.$store.state.loggedIn;
- },
- config() {
- return this.$store.state.config;
- }
- }
+ ...mapGetters({ loggedIn: 'auth/isLoggedIn' }),
+ ...mapState(['config']),
+ },
};
</script>
<style lang="scss" scoped>
diff --git a/src/site/pages/login.vue b/src/site/pages/login.vue
index 514cbc5..569e9d9 100644
--- a/src/site/pages/login.vue
+++ b/src/site/pages/login.vue
@@ -10,27 +10,47 @@
<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
@keyup.enter.native="login" />
</b-field>
- <p class="control has-addons is-pulled-right">
- <router-link v-if="config.userAccounts"
- to="/register"
- 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>
- </p>
+ <p class="control has-addons is-pulled-right" />
+
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <router-link
+ v-if="config.userAccounts"
+ to="/register"
+ class="is-text">
+ Don't have an account?
+ </router-link>
+ <span v-else>Registration is closed at the moment</span>
+ </div>
+ </div>
+
+ <div class="level-right">
+ <p class="level-item">
+ <b-button
+ size="is-medium"
+ type="is-lolisafe"
+ @click="login">
+ Login
+ </b-button>
+ </p>
+ </div>
+ </div>
</div>
</div>
</div>
@@ -63,6 +83,8 @@
</template>
<script>
+import { mapState } from 'vuex';
+
export default {
name: 'Login',
data() {
@@ -71,41 +93,36 @@ export default {
password: null,
mfaCode: null,
isMfaModalActive: false,
- isLoading: false
+ isLoading: false,
};
},
- computed: {
- config() {
- return this.$store.state.config;
- }
- },
+ computed: mapState(['config', 'auth']),
metaInfo() {
return { title: 'Login' };
},
+ created() {
+ if (this.auth.loggedIn) {
+ this.redirect();
+ }
+ },
methods: {
async login() {
if (this.isLoading) return;
- if (!this.username || !this.password) {
- this.$store.dispatch('alert', {
- text: 'Please fill both fields before attempting to log in.',
- error: true
- });
+
+ const { username, password } = this;
+ if (!username || !password) {
+ this.$notifier.error('Please fill both fields before attempting to log in.');
return;
}
- this.isLoading = true;
try {
- const data = await this.$axios.$post(`auth/login`, {
- username: this.username,
- password: this.password
- });
- this.$axios.setToken(data.token, 'Bearer');
- document.cookie = `token=${encodeURIComponent(data.token)}`;
- this.$store.dispatch('login', { token: data.token, user: data.user });
-
- this.redirect();
- } catch (error) {
- //
+ this.isLoading = true;
+ await this.$store.dispatch('auth/login', { username, password });
+ if (this.auth.loggedIn) {
+ this.redirect();
+ }
+ } catch (e) {
+ this.$notifier.error(e.message);
} finally {
this.isLoading = false;
}
@@ -124,15 +141,14 @@ export default {
this.isLoading = false;
this.$onPromiseError(err);
});
- },*/
+ }, */
redirect() {
- this.$store.commit('loggedIn', true);
if (typeof this.$route.query.redirect !== 'undefined') {
this.$router.push(this.$route.query.redirect);
return;
}
this.$router.push('/dashboard');
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/pages/logout.vue b/src/site/pages/logout.vue
new file mode 100644
index 0000000..7124ea5
--- /dev/null
+++ b/src/site/pages/logout.vue
@@ -0,0 +1,8 @@
+<script>
+export default {
+ async created() {
+ await this.$store.dispatch('auth/logout');
+ this.$router.replace('/login');
+ },
+};
+</script>
diff --git a/src/site/pages/register.vue b/src/site/pages/register.vue
index c102abd..7cf4573 100644
--- a/src/site/pages/register.vue
+++ b/src/site/pages/register.vue
@@ -10,31 +10,51 @@
<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" />
</b-field>
<b-field>
- <b-input v-model="password"
+ <b-input
+ v-model="password"
type="password"
placeholder="Password"
password-reveal />
</b-field>
<b-field>
- <b-input v-model="rePassword"
+ <b-input
+ v-model="rePassword"
type="password"
placeholder="Re-type Password"
password-reveal
@keyup.enter.native="register" />
</b-field>
- <p class="control has-addons is-pulled-right">
- <router-link to="/login"
- class="is-text">Already have an account?</router-link>
- <button class="button is-primary big ml1"
- :disabled="isLoading"
- @click="register">Register</button>
- </p>
+ <div class="level">
+ <!-- Left side -->
+ <div class="level-left">
+ <div class="level-item">
+ <router-link
+ to="/login"
+ class="is-text">
+ Already have an account?
+ </router-link>
+ </div>
+ </div>
+ <!-- Right side -->
+ <div class="level-right">
+ <p class="level-item">
+ <b-button
+ size="is-medium"
+ type="is-lolisafe"
+ :disabled="isLoading"
+ @click="register">
+ Register
+ </b-button>
+ </p>
+ </div>
+ </div>
</div>
</div>
</div>
@@ -42,6 +62,8 @@
</template>
<script>
+import { mapState } from 'vuex';
+
export default {
name: 'Register',
data() {
@@ -49,50 +71,42 @@ export default {
username: null,
password: null,
rePassword: null,
- isLoading: false
+ isLoading: false,
};
},
- computed: {
- config() {
- return this.$store.state.config;
- }
- },
+ computed: mapState(['config', 'auth']),
metaInfo() {
return { title: 'Register' };
},
methods: {
async register() {
if (this.isLoading) return;
+
if (!this.username || !this.password || !this.rePassword) {
- this.$store.dispatch('alert', {
- text: 'Please fill all fields before attempting to register.',
- error: true
- });
+ this.$notifier.error('Please fill all fields before attempting to register.');
return;
}
if (this.password !== this.rePassword) {
- this.$store.dispatch('alert', {
- text: 'Passwords don\'t match',
- error: true
- });
+ this.$notifier.error('Passwords don\'t match');
return;
}
this.isLoading = true;
try {
- const response = await this.$axios.$post(`auth/register`, {
+ const response = await this.$store.dispatch('auth/register', {
username: this.username,
- password: this.password
+ password: this.password,
});
- this.$store.dispatch('alert', { text: response.message });
- return this.$router.push('/login');
+ this.$notifier.success(response.message);
+ this.$router.push('/login');
+ return;
} catch (error) {
- //
+ this.$notifier.error(error.message);
} finally {
this.isLoading = false;
}
- }
- }
+ },
+ },
};
</script>
diff --git a/src/site/plugins/axios.js b/src/site/plugins/axios.js
index 843a258..0308d39 100644
--- a/src/site/plugins/axios.js
+++ b/src/site/plugins/axios.js
@@ -1,22 +1,32 @@
-export default function({ $axios, store }) {
+export default function ({ $axios, store }) {
$axios.setHeader('accept', 'application/vnd.lolisafe.json');
- $axios.onRequest(config => {
- if (store.state.token) {
- config.headers.common['Authorization'] = `bearer ${store.state.token}`;
+
+ $axios.onRequest((config) => {
+ if (store.state.auth.token) {
+ config.headers.common.Authorization = `bearer ${store.state.auth.token}`;
}
});
- $axios.onError(error => {
- if (process.env.development) console.error('[AXIOS Error]', error);
+ $axios.onError((error) => {
+ if (process.env.NODE_ENV !== 'production') console.error('[AXIOS Error]', error);
if (process.browser) {
- store.dispatch('alert', {
- text: error.response.data.message,
- error: true
- });
-
- if (error.response.data.message.indexOf('Token expired') !== -1) {
- store.dispatch('logout');
+ if (process.env.NODE_ENV !== 'production') {
+ if (error.response?.data?.message) {
+ store.dispatch('alert/set', {
+ text: error.response.data.message,
+ error: true,
+ });
+ } else {
+ store.dispatch('alert/set', {
+ text: `[AXIOS]: ${error.message}`,
+ error: true,
+ });
+ }
}
+
+ /* if (error.response?.data?.message.indexOf('Token expired') !== -1) {
+ store.dispatch('auth/logout');
+ } */
}
});
}
diff --git a/src/site/plugins/flexsearch.js b/src/site/plugins/flexsearch.js
index 595b180..8640232 100644
--- a/src/site/plugins/flexsearch.js
+++ b/src/site/plugins/flexsearch.js
@@ -1,16 +1,17 @@
import Vue from 'vue';
import FlexSearch from 'flexsearch';
+
const search = new FlexSearch('speed');
// https://github.com/nextapps-de/flexsearch
Vue.prototype.$search = {
- items: async items => {
+ items: async (items) => {
await search.clear();
await search.add(items);
},
do: async (term, field) => {
const results = await search.search(term, { field });
return results;
- }
+ },
};
diff --git a/src/site/plugins/handler.js b/src/site/plugins/handler.js
new file mode 100644
index 0000000..3d85b15
--- /dev/null
+++ b/src/site/plugins/handler.js
@@ -0,0 +1,25 @@
+import AlertTypes from '~/constants/alertTypes';
+
+export default ({ store }, inject) => {
+ inject('handler', {
+ async executeAction(action, param) {
+ try {
+ const response = await store.dispatch(action, param);
+
+ store.commit('alert/set', {
+ message: response?.message ?? 'Executed sucesfully',
+ type: AlertTypes.SUCCESS,
+ });
+
+ return response;
+ } catch (e) {
+ store.commit('alert/set', {
+ message: e.message,
+ type: AlertTypes.ERROR,
+ });
+
+ return null;
+ }
+ },
+ });
+};
diff --git a/src/site/plugins/notifier.js b/src/site/plugins/notifier.js
new file mode 100644
index 0000000..4fe1262
--- /dev/null
+++ b/src/site/plugins/notifier.js
@@ -0,0 +1,25 @@
+import AlertTypes from '~/constants/alertTypes';
+
+export default ({ store }, inject) => {
+ inject('notifier', {
+ showMessage({ message = '', type = '', snackbar = false }) {
+ store.commit('alert/set', { message, type, snackbar });
+ },
+ message(message, snackbar) {
+ this.showMessage({ message, type: AlertTypes.PRIMARY, snackbar });
+ },
+ info(message, snackbar) {
+ this.showMessage({ message, type: AlertTypes.INFO, snackbar });
+ },
+ warning(message, snackbar) {
+ this.showMessage({ message, type: AlertTypes.WARNING, snackbar });
+ },
+ success(message, snackbar) {
+ this.showMessage({ message, type: AlertTypes.SUCCESS, snackbar });
+ },
+ error(message, snackbar) {
+ this.showMessage({ message, type: AlertTypes.ERROR, snackbar });
+ },
+ types: AlertTypes,
+ });
+};
diff --git a/src/site/plugins/nuxt-client-init.js b/src/site/plugins/nuxt-client-init.js
index 4b10dcd..01f33ff 100644
--- a/src/site/plugins/nuxt-client-init.js
+++ b/src/site/plugins/nuxt-client-init.js
@@ -1,3 +1,3 @@
-export default async ctx => {
+export default async (ctx) => {
await ctx.store.dispatch('nuxtClientInit', ctx);
};
diff --git a/src/site/plugins/vue-isyourpasswordsafe.js b/src/site/plugins/vue-isyourpasswordsafe.js
index 6172ca0..68f313a 100644
--- a/src/site/plugins/vue-isyourpasswordsafe.js
+++ b/src/site/plugins/vue-isyourpasswordsafe.js
@@ -3,5 +3,5 @@ import VueIsYourPasswordSafe from 'vue-isyourpasswordsafe';
Vue.use(VueIsYourPasswordSafe, {
minLength: 6,
- maxLength: 64
+ maxLength: 64,
});
diff --git a/src/site/plugins/vue-timeago.js b/src/site/plugins/vue-timeago.js
index 28f3c6d..81e6e75 100644
--- a/src/site/plugins/vue-timeago.js
+++ b/src/site/plugins/vue-timeago.js
@@ -4,5 +4,5 @@ import VueTimeago from 'vue-timeago';
Vue.use(VueTimeago, {
name: 'timeago',
locale: 'en-US',
- locales: { 'en-US': require('vue-timeago/locales/en-US.json') }
+ locales: { 'en-US': require('vue-timeago/locales/en-US.json') },
});
diff --git a/src/site/store/.eslintrc.json b/src/site/store/.eslintrc.json
new file mode 100644
index 0000000..052e3ef
--- /dev/null
+++ b/src/site/store/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "no-shadow": ["error", { "allow": ["state"] }]
+ }
+}
diff --git a/src/site/store/admin.js b/src/site/store/admin.js
new file mode 100644
index 0000000..b31c446
--- /dev/null
+++ b/src/site/store/admin.js
@@ -0,0 +1,122 @@
+export const state = () => ({
+ users: [],
+ user: {
+ id: null,
+ username: null,
+ enabled: false,
+ createdAt: null,
+ editedAt: null,
+ apiKeyEditedAt: null,
+ isAdmin: null,
+ files: [],
+ },
+ file: {},
+ settings: {},
+});
+
+export const actions = {
+ async fetchSettings({ commit }) {
+ const response = await this.$axios.$get('service/config');
+ commit('setSettings', response);
+
+ return response;
+ },
+ async fetchUsers({ commit }) {
+ const response = await this.$axios.$get('admin/users');
+ commit('setUsers', response);
+
+ return response;
+ },
+ async fetchUser({ commit }, id) {
+ const response = await this.$axios.$get(`admin/users/${id}`);
+ commit('setUserInfo', response);
+
+ return response;
+ },
+ async fetchFile({ commit }, id) {
+ const response = await this.$axios.$get(`file/${id}`);
+ commit('setFile', response);
+ commit('setUserInfo', response);
+
+ return response;
+ },
+ async banIP(_, ip) {
+ const response = await this.$axios.$post('admin/ban/ip', { ip });
+
+ return response;
+ },
+ async enableUser({ commit }, id) {
+ const response = await this.$axios.$post('admin/users/enable', { id });
+
+ commit('changeUserState', { userId: id, enabled: true });
+
+ return response;
+ },
+ async disableUser({ commit }, id) {
+ const response = await this.$axios.$post('admin/users/disable', { id });
+
+ commit('changeUserState', { userId: id, enabled: false });
+
+ return response;
+ },
+ async promoteUser({ commit }, id) {
+ const response = await this.$axios.$post('admin/users/promote', { id });
+
+ commit('changeUserState', { userId: id, isAdmin: true });
+
+ return response;
+ },
+ async demoteUser({ commit }, id) {
+ const response = await this.$axios.$post('admin/users/demote', { id });
+
+ commit('changeUserState', { userId: id, isAdmin: false });
+
+ return response;
+ },
+ async purgeUserFiles(_, id) {
+ const response = await this.$axios.$post('admin/users/purge', { id });
+
+ return response;
+ },
+ async restartService() {
+ const response = await this.$axios.$post('service/restart');
+
+ return response;
+ },
+};
+
+export const mutations = {
+ setSettings(state, { config }) {
+ state.settings = config;
+ },
+ setUsers(state, { users }) {
+ state.users = users;
+ },
+ setUserInfo(state, { user, files }) {
+ state.user = { ...state.user, ...user };
+ state.user.files = files || [];
+ },
+ setFile(state, { file }) {
+ state.file = file || {};
+ },
+ changeUserState(state, { userId, enabled, isAdmin }) {
+ const foundIndex = state.users.findIndex(({ id }) => id === userId);
+ if (foundIndex > -1) {
+ if (enabled !== undefined) {
+ state.users[foundIndex].enabled = enabled;
+ }
+ if (isAdmin !== undefined) {
+ state.users[foundIndex].isAdmin = isAdmin;
+ }
+ }
+
+ if (state.user.id === userId) {
+ if (enabled !== undefined) {
+ state.user.enabled = enabled;
+ }
+ if (isAdmin !== undefined) {
+ state.user.isAdmin = isAdmin;
+ }
+ }
+ },
+};
diff --git a/src/site/store/albums.js b/src/site/store/albums.js
new file mode 100644
index 0000000..8441182
--- /dev/null
+++ b/src/site/store/albums.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+
+export const state = () => ({
+ list: [],
+ isListLoading: false,
+ albumDetails: {},
+ expandedAlbums: [],
+ tinyDetails: [],
+});
+
+export const getters = {
+ isExpanded: (state) => (id) => state.expandedAlbums.indexOf(id) > -1,
+ getDetails: (state) => (id) => state.albumDetails[id] || {},
+};
+
+export const actions = {
+ async fetch({ commit }) {
+ commit('albumsRequest');
+ const response = await this.$axios.$get('albums/mini');
+
+ commit('setAlbums', response.albums);
+
+ return response;
+ },
+ async fetchDetails({ commit }, albumId) {
+ const response = await this.$axios.$get(`album/${albumId}/links`);
+
+ commit('setDetails', {
+ id: albumId,
+ details: {
+ links: response.links,
+ },
+ });
+
+ return response;
+ },
+ async createAlbum({ commit }, name) {
+ const response = await this.$axios.$post('album/new', { name });
+
+ commit('addAlbum', response.data);
+
+ return response;
+ },
+ async deleteAlbum({ commit }, albumId) {
+ const response = await this.$axios.$delete(`album/${albumId}`);
+
+ commit('removeAlbum', albumId);
+
+ return response;
+ },
+ async createLink({ commit }, albumId) {
+ const response = await this.$axios.$post('album/link/new', { albumId });
+
+ commit('addAlbumLink', { albumId, data: response.data });
+
+ return response;
+ },
+ async updateLinkOptions({ commit }, { albumId, linkOpts }) {
+ const response = await this.$axios.$post('album/link/edit', {
+ identifier: linkOpts.identifier,
+ enableDownload: linkOpts.enableDownload,
+ enabled: linkOpts.enabled,
+ });
+
+ commit('updateAlbumLinkOpts', { albumId, linkOpts: response.data });
+
+ return response;
+ },
+ async deleteLink({ commit }, { albumId, identifier }) {
+ const response = await this.$axios.$delete(`album/link/delete/${identifier}`);
+
+ commit('removeAlbumLink', { albumId, identifier });
+
+ return response;
+ },
+ async getTinyDetails({ commit }) {
+ const response = await this.$axios.$get('albums/dropdown');
+
+ commit('setTinyDetails', response);
+
+ return response;
+ },
+};
+
+export const mutations = {
+ albumsRequest(state) {
+ state.isLoading = true;
+ },
+ setAlbums(state, albums) {
+ state.list = albums;
+ state.isLoading = false;
+ },
+ addAlbum(state, album) {
+ state.list.unshift(album);
+ },
+ removeAlbum(state, albumId) {
+ // state.list = state.list.filter(({ id }) => id !== albumId);
+ const foundIndex = state.list.findIndex(({ id }) => id === albumId);
+ state.list.splice(foundIndex, 1);
+ },
+ setDetails(state, { id, details }) {
+ Vue.set(state.albumDetails, id, details);
+ },
+ addAlbumLink(state, { albumId, data }) {
+ state.albumDetails[albumId].links.push(data);
+ },
+ updateAlbumLinkOpts(state, { albumId, linkOpts }) {
+ const foundIndex = state.albumDetails[albumId].links.findIndex(
+ ({ identifier }) => identifier === linkOpts.identifier,
+ );
+ const link = state.albumDetails[albumId].links[foundIndex];
+ state.albumDetails[albumId].links[foundIndex] = { ...link, ...linkOpts };
+ },
+ removeAlbumLink(state, { albumId, identifier }) {
+ const foundIndex = state.albumDetails[albumId].links.findIndex(({ identifier: id }) => id === identifier);
+ if (foundIndex > -1) state.albumDetails[albumId].links.splice(foundIndex, 1);
+ },
+ toggleExpandedState(state, id) {
+ const foundIndex = state.expandedAlbums.indexOf(id);
+ if (foundIndex > -1) {
+ state.expandedAlbums.splice(foundIndex, 1);
+ } else {
+ state.expandedAlbums.push(id);
+ }
+ },
+ setTinyDetails(state, { albums }) {
+ state.tinyDetails = albums;
+ },
+};
diff --git a/src/site/store/alert.js b/src/site/store/alert.js
new file mode 100644
index 0000000..580dcc8
--- /dev/null
+++ b/src/site/store/alert.js
@@ -0,0 +1,33 @@
+import AlertTypes from '~/constants/alertTypes';
+
+const getDefaultState = () => ({
+ message: null,
+ type: null,
+ snackbar: false,
+});
+
+export const state = getDefaultState;
+
+export const actions = {
+ set({ commit }, data) {
+ // Only exists for backwards compatibility, remove one day
+ if (data.error === true) data.type = AlertTypes.ERROR;
+ if (data.text !== undefined) data.message = data.text;
+
+ commit('set', data);
+ },
+ clear({ commit }) {
+ commit('clear');
+ },
+};
+
+export const mutations = {
+ set(state, { message, type, snackbar }) {
+ state.message = message;
+ state.type = type;
+ state.snackbar = snackbar || false;
+ },
+ clear(state) {
+ Object.assign(state, getDefaultState());
+ },
+};
diff --git a/src/site/store/auth.js b/src/site/store/auth.js
new file mode 100644
index 0000000..96631e2
--- /dev/null
+++ b/src/site/store/auth.js
@@ -0,0 +1,106 @@
+const getDefaultState = () => ({
+ loggedIn: false,
+ user: {
+ id: null,
+ isAdmin: false,
+ username: null,
+ },
+ token: null,
+});
+
+export const state = getDefaultState;
+
+export const getters = {
+ isLoggedIn: (state) => state.loggedIn,
+ getApiKey: (state) => state.user?.apiKey,
+ getToken: (state) => state.token,
+};
+
+export const actions = {
+ async verify({ commit, dispatch }) {
+ try {
+ const response = await this.$axios.$get('verify');
+ commit('loginSuccess', response);
+ } catch (e) {
+ dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ },
+ async login({ commit }, { username, password }) {
+ commit('loginRequest');
+
+ const data = await this.$axios.$post('auth/login', { username, password });
+ this.$axios.setToken(data.token, 'Bearer');
+
+ commit('setToken', data.token);
+ commit('loginSuccess', { token: data.token, user: data.user });
+ },
+ async register(_, { username, password }) {
+ return this.$axios.$post('auth/register', {
+ username,
+ password,
+ });
+ },
+ async fetchCurrentUser({ commit, dispatch }) {
+ try {
+ const data = await this.$axios.$get('users/me');
+ commit('setUser', data.user);
+ } catch (e) {
+ dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+ },
+ async changePassword({ dispatch }, { password, newPassword }) {
+ try {
+ const response = await this.$axios.$post('user/password/change', {
+ password,
+ newPassword,
+ });
+
+ return response;
+ } catch (e) {
+ dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+
+ return null;
+ },
+ async requestAPIKey({ commit, dispatch }) {
+ try {
+ const response = await this.$axios.$post('user/apikey/change');
+ commit('setApiKey', response.apiKey);
+
+ return response;
+ } catch (e) {
+ dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+
+ return null;
+ },
+ logout({ commit }) {
+ commit('logout');
+ },
+};
+
+export const mutations = {
+ setToken(state, token) {
+ state.token = token;
+ },
+ setApiKey(state, apiKey) {
+ state.user.apiKey = apiKey;
+ },
+ setUser(state, user) {
+ state.user = user;
+ },
+ loginRequest(state) {
+ state.isLoading = true;
+ },
+ loginSuccess(state, { user }) {
+ this.$cookies.set('token', state.token, { path: '/' });
+ state.user = user;
+ state.loggedIn = true;
+ state.isLoading = false;
+ },
+ logout(state) {
+ this.$cookies.remove('token', { path: '/' });
+ // reset state to default
+ Object.assign(state, getDefaultState());
+ },
+};
diff --git a/src/site/store/config.js b/src/site/store/config.js
new file mode 100644
index 0000000..f52fc0f
--- /dev/null
+++ b/src/site/store/config.js
@@ -0,0 +1,18 @@
+export const state = () => ({
+ development: true,
+ version: '4.0.0',
+ URL: 'http://localhost:8080',
+ baseURL: 'http://localhost:8080/api',
+ serviceName: '',
+ maxFileSize: 100,
+ chunkSize: 90,
+ maxLinksPerAlbum: 5,
+ publicMode: false,
+ userAccounts: false,
+});
+
+export const mutations = {
+ set(state, config) {
+ Object.assign(state, config);
+ },
+};
diff --git a/src/site/store/images.js b/src/site/store/images.js
new file mode 100644
index 0000000..acac286
--- /dev/null
+++ b/src/site/store/images.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+
+export const getDefaultState = () => ({
+ files: [],
+ isLoading: false,
+ pagination: {
+ page: 1,
+ limit: 30,
+ totalFiles: 0,
+ },
+ name: null,
+ downloadEnabled: false,
+ fileAlbumsMap: {}, // map of file ids with a list of album objects the file is in
+ filesTags: {}, // map of file ids with a list of tag objects for the file
+});
+
+export const state = getDefaultState;
+
+export const getters = {
+ getTotalFiles: ({ pagination }) => pagination.totalFiles,
+ getFetchedCount: ({ files }) => files.length,
+ shouldPaginate: ({ pagination }) => pagination.totalFiles > pagination.limit,
+ getLimit: ({ pagination }) => pagination.limit,
+ getName: ({ name }) => name,
+};
+
+export const actions = {
+ async fetch({ commit, dispatch, state }, page) {
+ commit('setIsLoading');
+
+ page = page || 1;
+
+ try {
+ const response = await this.$axios.$get('files', { params: { limit: state.pagination.limit, page } });
+
+ commit('setFilesAndMeta', { ...response, page });
+
+ return response;
+ } catch (e) {
+ dispatch('alert/set', { text: e.message, error: true }, { root: true });
+ }
+
+ return null;
+ },
+ async fetchByAlbumId({ commit, state }, { id, page }) {
+ commit('setIsLoading');
+
+ page = page || 1;
+
+ const response = await this.$axios.$get(`album/${id}/full`, {
+ params: { limit: state.pagination.limit, page },
+ });
+
+ commit('setFilesAndMeta', { ...response, page });
+
+ return response;
+ },
+ async getFileAlbums({ commit }, fileId) {
+ const response = await this.$axios.$get(`file/${fileId}/albums`);
+
+ commit('setFileAlbums', { ...response, fileId });
+
+ return response;
+ },
+ async addToAlbum({ commit }, { fileId, albumId }) {
+ const response = await this.$axios.$post('file/album/add', { fileId, albumId });
+
+ commit('addAlbumToFile', { fileId, albumId, ...response.data });
+
+ return response;
+ },
+ async removeFromAlbum({ commit }, { fileId, albumId }) {
+ const response = await this.$axios.$post('file/album/del', { fileId, albumId });
+
+ commit('removeAlbumFromFile', { fileId, albumId });
+
+ return response;
+ },
+ async deleteFile({ commit }, fileId) {
+ const response = await this.$axios.$delete(`file/${fileId}`);
+
+ commit('removeFile', fileId);
+
+ return response;
+ },
+};
+
+export const mutations = {
+ setIsLoading(state) {
+ state.isLoading = true;
+ },
+ setFilesAndMeta(state, {
+ files, name, page, count,
+ }) {
+ state.files = files || [];
+ state.name = name ?? null;
+ state.isLoading = false;
+ state.pagination.page = page || 1;
+ state.pagination.totalFiles = count || 0;
+ },
+ removeFile(state, fileId) {
+ const foundIndex = state.files.findIndex(({ id }) => id === fileId);
+ if (foundIndex > -1) {
+ state.files.splice(foundIndex, 1);
+ state.pagination.totalFiles -= 1;
+ }
+ },
+ setFileAlbums(state, { fileId, albums }) {
+ Vue.set(state.fileAlbumsMap, fileId, albums);
+ },
+ addAlbumToFile(state, { fileId, album }) {
+ if (!state.fileAlbumsMap[fileId]) return;
+
+ state.fileAlbumsMap[fileId].push(album);
+ },
+ removeAlbumFromFile(state, { fileId, albumId }) {
+ if (!state.fileAlbumsMap[fileId]) return;
+
+ const foundIndex = state.fileAlbumsMap[fileId].findIndex(({ id }) => id === albumId);
+ if (foundIndex > -1) {
+ state.fileAlbumsMap[fileId].splice(foundIndex, 1);
+ }
+ },
+ resetState(state) {
+ Object.assign(state, getDefaultState());
+ },
+};
diff --git a/src/site/store/index.js b/src/site/store/index.js
index 1fc2272..c0faffb 100644
--- a/src/site/store/index.js
+++ b/src/site/store/index.js
@@ -1,66 +1,21 @@
import config from '../../../dist/config.json';
-export const state = () => ({
- loggedIn: false,
- user: null,
- token: null,
- config: null,
- alert: null
-});
-
-/* eslint-disable no-shadow */
-export const mutations = {
- loggedIn(state, payload) {
- state.loggedIn = payload;
- },
- user(state, payload) {
- state.user = payload;
- },
- token(state, payload) {
- state.token = payload;
- },
- config(state, payload) {
- state.config = payload;
- },
- alert(state, payload) {
- state.alert = payload;
- }
-};
+// eslint-disable-next-line import/prefer-default-export
export const actions = {
- async nuxtClientInit({ commit, dispatch }, { app, req }) {
- commit('config', config);
+ async nuxtClientInit({ commit, dispatch }) {
+ commit('config/set', config);
const cookies = this.$cookies.getAll();
- if (!cookies.token) return dispatch('logout');
+ if (!cookies.token) return dispatch('auth/logout');
- commit('token', cookies.token);
- try {
- const response = await this.$axios.$get('verify');
- dispatch('login', {
- token: cookies.token,
- user: response.user
- });
- } catch (error) {
- // dispatch('logout');
- }
- },
- login({ commit }, { token, user }) {
- this.$cookies.set('token', token);
- commit('token', token);
- commit('user', user);
- commit('loggedIn', true);
- },
- logout({ commit }) {
- this.$cookies.remove('token');
- commit('token', null);
- commit('user', null);
- commit('loggedIn', false);
+ commit('auth/setToken', cookies.token);
+ return dispatch('auth/verify');
},
- alert({ commit }, payload) {
+ /* alert({ commit }, payload) {
if (!payload) return commit('alert', null);
commit('alert', {
text: payload.text,
error: payload.error
});
- }
+ } */
};