diff --git a/app/controllers/api/v1/FileBox.controller.js b/app/controllers/api/v1/FileBox.controller.js new file mode 100644 index 0000000..f413be5 --- /dev/null +++ b/app/controllers/api/v1/FileBox.controller.js @@ -0,0 +1,177 @@ +const { Controller } = require('libflitter') + +class FileBoxController extends Controller { + static get services() { + return [...super.services, 'models'] + } + + async create(req, res, next) { + const FileBox = this.models.get('models:files:FileBox') + + if ( !req.body.name ) { + return res.message('Missing required field: name') + .status(400) + .api() + } + + if ( req.body.rootUUID ) { + const root = await FileBox.findOne({ + active: true, + UUID: req.body.rootUUID, + }) + + if ( !root || root.pageId !== req.form.page.UUID ) { + return res.status(400) + .message('Invalid rootUUID.') + .api() + } + } + + if ( req.body.parentUUID ) { + if ( !req.body.rootUUID ) { + return res.status(400) + .message('File boxes with a parent MUST also have a root.') + .api() + } + + const parent = await FileBox.findOne({ + active: true, + UUID: req.body.parentUUID, + }) + + if ( !parent || parent.pageId !== req.form.page.UUID ) { + return res.status(400) + .message('Invalid parentUUID') + .api() + } + } + + const box = new FileBox({ + name: req.body.name, + pageId: req.form.page.UUID, + }) + + if ( req.body.rootUUID ) { + box.rootUUID = req.body.rootUUID + } + + if ( req.body.parentUUID ) { + box.parentUUID = req.body.parentUUID + } + + await box.save() + return res.api(await box.to_api()) + } + + async update(req, res, next) { + const FileBox = this.models.get('api:files:FileBox') + const { file_box, page } = req.form + + if ( req.body.name ) { + file_box.name = req.body.name + } + + if ( req.body.parentUUID && req.body.parentUUID !== file_box.parentUUID ) { + const new_parent = await FileBox.findOne({ + UUID: req.body.parentUUID, + active: true, + rootUUID: file_box.rootUUID, + pageId: page.UUID, + }) + + if ( !new_parent || new_parent.UUID === file_box.UUID ) { + return res.status(400) + .message('Invalid parentUUID.') + .api() + } + + file_box.parentUUID = new_parent.UUID + } + + await file_box.save() + return res.api(await file_box.to_api()) + } + + async update_file(req, res, next) { + const FileBox = this.models.get('api:files:FileBox') + const { file_box, file } = req.form + + if ( req.body.name ) { + file.name = req.body.name + } + + if ( req.body.parentUUID && req.body.parentUUID !== file_box.UUID ) { + const new_parent = await FileBox.findOne({ + active: true, + UUID: req.body.parentUUID, + rootUUID: file_box.rootUUID, + }) + + if ( !new_parent ) { + return res.status(400) + .message('Invalid parentUUID.') + .api() + } + + file_box.fileIds = file_box.fileIds.filter(x => `${x}` !== `${file.id}`) + new_parent.fileIds.push(file.id) + await new_parent.save() + await file_box.save() + } + + await file.save() + return res.api(this.file_to_api(file)) + } + + async upload_files(req, res, next) { + const { file_box } = req.form + + // const n_files = Object.values(req.uploads).length + + for ( const key in req.uploads ) { + if ( !req.uploads.hasOwnProperty(key) ) continue + if ( !key.startsWith('uploaded_file_') ) continue + + file_box.fileIds.push(req.uploads[key].id) + } + + await file_box.save() + return res.api() + } + + async get_box(req, res, next) { + return res.api(await req.form.file_box.to_api()) + } + + async get_files(req, res, next) { + const { file_box, page } = req.form + const files = await file_box.files() + return res.api(files.map(x => this.file_to_api(x))) + } + + async delete_box(req, res, next) { + const { file_box } = req.form + file_box.active = false; + await file_box.save() + return res.api() + } + + async delete_file(req, res, next) { + const { file_box, file } = req.form + file_box.fileIds = file_box.fileIds.filter(x => `${x}` !== `${file.id}`) + await file_box.save() + return res.api() + } + + file_to_api(file) { + return { + name: file.upload_name, + mime: file.mime_type, + uploaded: file.upload_date, + id: file.id, + _id: file._id, + } + } +} + +module.exports = exports = FileBoxController diff --git a/app/models/api/files/FileBox.model.js b/app/models/api/files/FileBox.model.js new file mode 100644 index 0000000..8268697 --- /dev/null +++ b/app/models/api/files/FileBox.model.js @@ -0,0 +1,53 @@ +const { Model } = require('flitter-orm') +const uuid = require('uuid').v4 +const { ObjectId } = require('mongodb') + +class FileBoxModel extends Model { + static get schema() { + return { + UUID: { type: String, default: uuid }, + name: String, + pageId: String, + parentUUID: String, + rootUUID: String, + fileIds: [String], + active: { type: Boolean, default: true }, + } + } + + async parent() { + return this.constructor.findOne({ + UUID: this.parentUUID, + active: true, + }) + } + + async root() { + return this.constructor.findOne({ + UUID: this.rootUUID, + active: true, + }) + } + + async files() { + const File = this.models.get('upload::File') + return await File.find({ + _id: { + $in: this.fileIds.map(x => ObjectId(x)), + } + }) + } + + async to_api() { + return { + UUID: this.UUID, + name: this.name, + pageId: this.pageId, + parentUUID: this.parentUUID, + rootUUID: this.rootUUID, + fileIds: this.fileIds || [], + } + } +} + +module.exports = exports = FileBoxModel diff --git a/app/routing/middleware/api/DataInjection.middleware.js b/app/routing/middleware/api/DataInjection.middleware.js index d3b0c24..b450d28 100644 --- a/app/routing/middleware/api/DataInjection.middleware.js +++ b/app/routing/middleware/api/DataInjection.middleware.js @@ -1,4 +1,5 @@ const { Middleware } = require('libflitter') +const { ObjectId } = require('mongodb') class DataInjectionMiddleware extends Middleware { static get services() { @@ -103,6 +104,45 @@ class DataInjectionMiddleware extends Middleware { req.form.file_group = file_group } + // Try to load in the file box + if ( req.params.FileBoxId ) { + const FileBox = this.models.get('api:files:FileBox') + const file_box = await FileBox.findOne({ + UUID: req.params.FileBoxId, + pageId: req.params.PageId, + active: true, + }) + + if ( !file_box ) { + return res.status(404) + .message('Invalid file box ID.') + .api() + } + + req.form.file_box = file_box + + if ( req.params.FileBoxFileId ) { + const File = this.models.get('upload::File') + if ( !file_box.fileIds.includes(req.params.FileBoxFileId) ) { + return res.status(400) + .message('Invalid file box file ID.') + .api() + } + + const file = await File.findOne({ + _id: ObjectId(req.params.FileBoxFileId), + }) + + if ( !file ) { + return res.status(400) + .message('Invalid file box file ID.') + .api() + } + + req.form.file = file + } + } + return next() } } diff --git a/app/routing/routers/api/v1/file-box.routes.js b/app/routing/routers/api/v1/file-box.routes.js new file mode 100644 index 0000000..895ab0f --- /dev/null +++ b/app/routing/routers/api/v1/file-box.routes.js @@ -0,0 +1,56 @@ +module.exports = exports = { + + prefix: '/api/v1/file-box', + + middleware: [], + + get: { + '/:PageId/:FileBoxId': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'view' }], + 'controller::api:v1:FileBox.get_box', + ], + '/:PageId/:FileBoxId/files': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'view' }], + 'controller::api:v1:FileBox.get_files', + ], + }, + + post: { + '/:PageId/create': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + 'controller::api:v1:FileBox.create', + ], + '/:PageId/:FileBoxId': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + 'controller::api:v1:FileBox.update', + ], + '/:PageId/:FileBoxId/files': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + ['middleware::upload:UploadFile', { tag: 'file_box_file' }], + 'controller::api:v1:FileBox.upload_files', + ], + '/:PageId/:FileBoxId/files/:FileBoxFileId': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + 'controller::api:v1:FileBox.update_file', + ], + }, + + delete: { + '/:PageId/:FileBoxId': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + 'controller::api:v1:FileBox.delete_box', + ], + '/:PageId/:FileBoxId/files/:FileBoxFileId': [ + 'middleware::auth:ApiRoute', + ['middleware::api:DataInjection', { access_level: 'update' }], + 'controller::api:v1:FileBox.delete_file', + ], + }, +}