diff --git a/app/MiscUnit.js b/app/MiscUnit.js index cac18c7..a4d21e6 100644 --- a/app/MiscUnit.js +++ b/app/MiscUnit.js @@ -10,7 +10,7 @@ const Unit = require('libflitter/Unit') * other units in the stack. */ class MiscUnit extends Unit { - + /* * Initializes the unit class. * This is called OUTSIDE of a Flitter context, @@ -19,10 +19,10 @@ class MiscUnit extends Unit { */ constructor(){ super() - - + + } - + /* * Initialize the actual Unit. * This is where most of the changes will go. @@ -32,15 +32,75 @@ class MiscUnit extends Unit { * model(), etc. work. */ async go(app, context){ - + // do stuff here - + global.devbug = { + version: '0.2.0', + code: { + php: ` $what ]; +\t\t} +\t\telse { +\t\t\t$dev_outs[$group][$key] = $what; +\t\t} +\t} +\telse { +\t\t$dev_outs[$key] = $what; +\t} +} +function breakpoint($html = false, $name = null){ +\tglobal $dev_outs; +\t$devbug = "http://CHANGEME:8000/"; +\t$project_api_key = "CHANGEME"; +\t$bt = debug_backtrace(); +\t$caller = array_shift($bt); + +\tif ( !$html ){ +\t\tvar_dump([($item ? $item : $caller), 'outs' => $dev_outs]); +\t} +\telse { +\t\techo "
";
+\t\tvar_dump([($item ? $item : $caller), 'outs' => $dev_outs]);
+\t\techo "
"; +\t} + +\t// Send to devbug server +\t$ch = curl_init(); +\t$url = $devbug.'api/v1/out/'.$project_api_key; +\tvar_dump($url); +\tcurl_setopt($ch, CURLOPT_URL, $url); +\tcurl_setopt($ch, CURLOPT_POST, 1); +\tcurl_setopt($ch, CURLOPT_POSTFIELDS, [ +\t 'data' => json_encode([ +\t 'brief' => ($name ? $name : 'Breakpoint').': '.$caller['file'].': '.$caller['line'], +\t 'data' => $dev_outs, +\t ]) +\t]); + +\tcurl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +\t +\t$odata = curl_exec($ch); + +\texit(); +} +// ===========================================================` + } + } + } - + name(){ return "misc" } } -module.exports = exports = MiscUnit \ No newline at end of file +module.exports = exports = MiscUnit diff --git a/app/assets/dash_v1.css b/app/assets/dash_v1.css index ffd882d..3fbe3c6 100644 --- a/app/assets/dash_v1.css +++ b/app/assets/dash_v1.css @@ -2,7 +2,7 @@ @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro'); html { - font-family: "Source Sans Pro"; + font-family: "Source Sans Pro", sans-serif; } table, th, td { @@ -17,7 +17,7 @@ th, td { } pre, code { - font-family: "Source Code Pro"; + font-family: "Source Code Pro", monospace; font-size: 10pt; } @@ -25,11 +25,44 @@ a { color: #004d4d; } +.page-header { + background: #ccdddd; + width: 100%; + margin: 0; + padding: 0; + padding-left: 20px; + padding-right: 20px; + position: fixed; + top: 0; + left: 0; +} + +.devbug-header { + background: #509d9d; + width: 100%; + position: relative; + margin: 0; + padding: 5px; + padding-left: 20px; + margin-left: -20px; + color: white; +} + +.spacer { + min-height: 165px; +} + .navul { list-style-type: none; margin: 0; - padding: 5; - margin-bottom: 20px; + padding: 0; + padding-left: 20px; + padding-top: 10px; + padding-bottom: 10px; + margin-left: -20px; + position: relative; + width: 100%; + background: #004d4d; } .navli { @@ -41,7 +74,7 @@ a { } .navli:hover { - background: #004d4d; + background: #509d9d; } .nava { @@ -52,4 +85,11 @@ a { .nava:hover { color: #eee; -} \ No newline at end of file +} + +pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } +.string { color: darkslateblue; } +.number { color: darkorange; } +.boolean { color: blue; } +.null { color: magenta; } +.key { color: green; } diff --git a/app/assets/dash_v1.js b/app/assets/dash_v1.js new file mode 100644 index 0000000..e5daf25 --- /dev/null +++ b/app/assets/dash_v1.js @@ -0,0 +1,29 @@ +function output(inp) { + document.body.appendChild(document.createElement('pre')).innerHTML = inp; +} + +function syntaxHighlight(json) { + json = JSON.stringify(JSON.parse(json.replace(/"/g, '"')), undefined, 4) + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} + +function from_server(json_string){ + console.log(json_string) + json_string = JSON.parse(json_string.replace(/"/g, '"')) + window.devbug = json_string +} diff --git a/app/controllers/api/v1.controller.js b/app/controllers/api/v1.controller.js index 3ef5417..503d5c9 100644 --- a/app/controllers/api/v1.controller.js +++ b/app/controllers/api/v1.controller.js @@ -38,9 +38,22 @@ class v1 { error: 'Missing data.' }) } - - const data = JSON.parse(req.body.data) - + + let data + try { + data = JSON.parse(req.body.data) + } + catch (e){ + return res.status(400).send({ + success: false, + error: 'Invalid JSON.' + }) + } + + if ( !data.brief ){ + data.brief = 'Breakpoint. (No Message.)' + } + const out = new Out({ brief: data.brief, data: JSON.stringify(data.data), @@ -55,4 +68,4 @@ class v1 { } } -module.exports = exports = v1 \ No newline at end of file +module.exports = exports = v1 diff --git a/app/controllers/dash/v1.controller.js b/app/controllers/dash/v1.controller.js index 2f76580..6e1c8f4 100644 --- a/app/controllers/dash/v1.controller.js +++ b/app/controllers/dash/v1.controller.js @@ -13,6 +13,16 @@ class v1 { async main(req, res){ const projects = await Project.find({ archived: false, user_id: req.session.auth.uuid }) + + let find = { + shared_user_ids: { + $elemMatch: { + $eq: req.session.auth.uuid + } + } + } + + const shared_projects = await Project.find(find) /* * Return the main view. @@ -20,7 +30,7 @@ class v1 { * View parameters can be passed as an optional third * argument to the view() method. */ - return _flitter.view(res, 'dash_v1:main', { projects }) + return _flitter.view(res, 'dash_v1:main', { projects, shared_projects, user: req.session.auth.user }) } new_project_show(req, res, next){ @@ -30,20 +40,30 @@ class v1 { async project_edit_show(req, res, next){ const project = await Project.findById(req.params.id) if ( !project ){ - return _flitter.error(res, 404, 'Project not found with the specified ID.') + return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + } + + // check access perms + if ( !(project.user_id === req.session.auth.uuid) ){ + return _flitter.error(res, 401, {reason: 'You do not have permissions to edit this project.'}) } - return _flitter.view(res, 'dash_v1:project', { show_back: true, title: 'Update Project', project_name: project.name}) + return _flitter.view(res, 'dash_v1:project', { show_back: true, title: 'Update Project', project_name: project.name, user: req.session.auth.user }) } async project_edit_do(req, res, next){ const project = await Project.findById(req.params.id) if ( !project ){ - return _flitter.error(res, 404, 'Project not found with the specified ID.') + return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) } if ( !req.body || !req.body.name ){ - return _flitter.view(res, 'dash_v1:project', {show_back: true, title: 'Update Project', project_name: project.name, errors: ['Project name is required.']}) + return _flitter.view(res, 'dash_v1:project', {user: req.session.auth.user, show_back: true, title: 'Update Project', project_name: project.name, errors: ['Project name is required.']}) + } + + // check access perms + if ( !(project.user_id === req.session.auth.uuid) ){ + return _flitter.error(res, 401, {reason: 'Project not found with the specified ID.'}) } project.name = req.body.name @@ -54,7 +74,7 @@ class v1 { async new_project_do(req, res, next){ if ( !req.body.name ){ - return _flitter.view(res, 'dash_v1:project', {show_back: true, title: 'Create Project', errors: ['Project name is required.']}) + return _flitter.view(res, 'dash_v1:project', {user: req.session.auth.user, show_back: true, title: 'Create Project', errors: ['Project name is required.']}) } const project = new Project({ @@ -76,31 +96,78 @@ class v1 { const project = await Project.findById(req.params.id) if ( !project ){ - _flitter.error(res, 404, 'Project not found.') + return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) } const outs = await Out.find({ project_id: project.id }).sort('-created') - - return _flitter.view(res, 'dash_v1:view', { project, outs, show_back: true, title: 'View: '+project.name }) + + if ( !(project.user_id === req.session.auth.uuid) && !(project.shared_user_ids.includes(req.session.auth.uuid)) ){ + return _flitter.error(res, 401, {reason: 'You do not have permission to view this project.'}) + } + + return _flitter.view(res, 'dash_v1:view', {user: req.session.auth.user, project, outs, show_back: true, title: 'View: '+project.name }) } async out_view(req, res, next){ const out = await Out.findById(req.params.id) + + if ( !out ){ + return _flitter.error(res, 404, {reason: 'Output not found with the specified ID.'}) + } + + let pretty + try { + pretty = JSON.stringify(JSON.parse(out.data), null, 4) + } + catch (e){ + return _flitter.error(res, 500, {reason: 'Unable to parse output data. Data contains invalid JSON.'}) + } + + const project = await Project.findById(out.project_id) - console.log(out.data) - - const pretty = JSON.stringify(JSON.parse(out.data), null, 4) - + if ( !project || (!(project.user_id === req.session.auth.uuid) && !(project.shared_user_ids.includes(req.session.auth.uuid))) ){ + return _flitter.error(res, 401, {reason: 'You do not have permission to view this project.'}) + } + // TODO permission access check - return _flitter.view(res, 'dash_v1:out', {out, prettyd:pretty, show_back: true, title: out.brief, title_small: true }); + return _flitter.view(res, 'dash_v1:out', {user: req.session.auth.user, out, prettyd:pretty, show_back: true, title: out.brief, title_small: true }); } + + async out_delete(req, res, next){ + const out = await Out.findById(req.params.id) - project_delete_show(req, res, next){ - return _flitter.view(res, 'dash_v1:confirm', {show_back: true, title: 'Are you sure?', text: 'Deleting this project will remove all stored breakpoint data. This action cannot be undone.', destination: '/dash/v1/project/delete/'+req.params.id}) + const project = await Project.findById(req.params.project) + if ( !project || ( !(project.user_id === req.session.auth.uuid) ) ){ + return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'}) + } + + if ( out ){ + await out.delete() + } + + return res.redirect('/dash/v1/project/view/'+req.params.project) + } + + async project_delete_show(req, res, next){ + const project = await Project.findById(req.params.id) + if ( !project ){ + return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + } + + if ( !(project.user_id === req.session.auth.uuid) ){ + return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'}) + } + + return _flitter.view(res, 'dash_v1:confirm', {user: req.session.auth.user, project, show_back: true, title: 'Are you sure?', text: 'Deleting this project will remove all stored breakpoint data. This action cannot be undone.', destination: '/dash/v1/project/delete/'+req.params.id}) } async project_delete_do(req, res, next){ const project = await Project.findById(req.params.id) + + if ( project && ( !(project.user_id === req.session.auth.uuid) ) ){ + return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'}) + } + if ( project ){ const outs = await Out.find({project_id: project.id}) @@ -113,6 +180,90 @@ class v1 { return res.redirect('/dash/v1') } + + view_code(req, res, next){ + return _flitter.view(res, 'dash_v1:code', { user: req.session.auth.user, title: 'Inline Code Snippets' }) + } + + async project_share_show(req, res, next){ + const project = await Project.findById(req.params.id) + + if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + + if ( !(project.user_id === req.session.auth.uuid) ) return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'}) + + let find = { + uuid: { $nin: [] } + } + + find.uuid.$nin.push(req.session.auth.uuid) + find.uuid.$nin = find.uuid.$nin.concat(project.shared_user_ids) + + const to_share = await _flitter.model('User').find(find) + + find = { + uuid: { $in: find.uuid.$nin } + } + + const shared = await _flitter.model('User').find(find) + + return _flitter.view(res, 'dash_v1:share', { user: req.session.auth.user, sharing: { to_share, shared }, project, title: 'Share Project: '+project.name, show_back: true }) + } + + async project_share_do(req, res, next){ + const project = await Project.findById(req.params.id) + if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + + const target_user = await _flitter.model('User').findOne({uuid: req.params.user}) + if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'}) + + if ( !(project.user_id === req.session.auth.uuid) ) return _flitter.error(res, 401, {reason: "You do not have permission to edit this project."}) + + if ( !(project.user_id === target_user.uuid) && !(project.shared_user_ids.includes(target_user.uuid)) ){ + project.shared_user_ids.push(target_user.uuid) + await project.save() + } + + return res.redirect('/dash/v1/project/share/'+project.id) + } + + async project_share_revoke(req, res, next){ + const project = await Project.findById(req.params.id) + if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + + const target_user = await _flitter.model('User').findOne({uuid: req.params.user}) + if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'}) + + if ( !(project.user_id === req.session.auth.uuid || project.shared_user_ids.includes(req.session.auth.uuid)) ) return _flitter.error(res, 401, {reason: "You do not have permission to edit this project."}) + + const to_dash = project.shared_user_ids.includes(req.session.auth.uuid) + + if ( !(target_user.uuid === project.user_id) && (project.shared_user_ids.includes(target_user.uuid)) ){ + project.shared_user_ids.splice(project.shared_user_ids.indexOf(target_user.uuid), 1) + await project.save() + } + + if ( to_dash ) return res.redirect('/dash/v1') + + return res.redirect('/dash/v1/project/share/'+project.id) + } + + async project_share_transfer(req, res, next){ + const project = await Project.findById(req.params.id) + if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'}) + + const target_user = await _flitter.model('User').findOne({uuid: req.params.user}) + if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'}) + + if ( !project.user_id === req.session.auth.uuid ) return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'}) + + project.user_id = target_user.uuid + project.shared_user_ids.push(req.session.auth.uuid) + + await project.save() + + return res.redirect('/dash/v1') + } } -module.exports = exports = v1 \ No newline at end of file +module.exports = exports = v1 diff --git a/app/routing/routers/dash/v1.routes.js b/app/routing/routers/dash/v1.routes.js index ed0b329..0f08ebc 100644 --- a/app/routing/routers/dash/v1.routes.js +++ b/app/routing/routers/dash/v1.routes.js @@ -12,7 +12,7 @@ const v1 = { * '/login' becomes '/auth/login' */ prefix: '/dash/v1', - + /* * Define middleware that should be applied to all * routes defined in this file. Middleware should be @@ -34,15 +34,21 @@ const v1 = { * controller() calls get methods in Flitter controllers */ get: { - // '/': [ controller('Controller_Name').handler_name ], '/': [ _flitter.controller('dash:v1').main ], '/project/new': [ _flitter.controller('dash:v1').new_project_show ], '/project/view/:id': [ _flitter.controller('dash:v1').project_view ], '/project/delete/:id': [ _flitter.controller('dash:v1').project_delete_show ], '/project/edit/:id': [ _flitter.controller('dash:v1').project_edit_show ], + '/project/share/:id': [ _flitter.controller('dash:v1').project_share_show ], + '/project/share/:id/share/:user': [ _flitter.controller('dash:v1').project_share_do ], + '/project/share/:id/revoke/:user': [ _flitter.controller('dash:v1').project_share_revoke ], + '/project/share/:id/transfer/:user': [ _flitter.controller('dash:v1').project_share_transfer ], '/out/view/:id': [ _flitter.controller('dash:v1').out_view ], + '/out/delete/:id/:project': [ _flitter.controller('dash:v1').out_delete ], + + '/code': [ _flitter.controller('dash:v1').view_code ], }, /* @@ -61,4 +67,4 @@ const v1 = { }, } -module.exports = v1 \ No newline at end of file +module.exports = v1 diff --git a/app/views/dash_v1/code.pug b/app/views/dash_v1/code.pug new file mode 100644 index 0000000..348c12f --- /dev/null +++ b/app/views/dash_v1/code.pug @@ -0,0 +1,15 @@ +extends ./template +block content + p + | These code snippets are designed to be included in-line. You can store outputs using the + code out() + | function. Then, call the + code breakpoint() + | function to send those outputs to DevBug. + p + | You'll need to ensure that the DevBug server URL and Project API Key are correct. These are local variables in the + code breakpoint() + | function. + h2 PHP + pre + code #{devbug.code.php} diff --git a/app/views/dash_v1/main.pug b/app/views/dash_v1/main.pug index 1a3a884..30a26dd 100644 --- a/app/views/dash_v1/main.pug +++ b/app/views/dash_v1/main.pug @@ -17,7 +17,27 @@ block content ul(style='list-style-type: none; margin: 0; padding: 0;') li a.action(href='/dash/v1/project/view/'+project.id) View + li + a.action(href='/dash/v1/project/share/'+project.id) Share li a.action(href='/dash/v1/project/delete/'+project.id) Delete li a.action(href='/dash/v1/project/edit/'+project.id) Edit + + if shared_projects + h3 Projects Shared With Me + table + thead + tr + th(scope='col' style='min-width: 250px') Name + th(scope='col') Actions + tbody + each project in shared_projects + tr + td #{project.name} + td + ul(style='list-style-type: none; margin: 0; padding: 0;') + li + a.action(href='/dash/v1/project/view/'+project.id) View + li + a.action(href='/dash/v1/project/share/'+project.id+'/revoke/'+user.uuid) Remove diff --git a/app/views/dash_v1/out.pug b/app/views/dash_v1/out.pug index a937236..f3af085 100644 --- a/app/views/dash_v1/out.pug +++ b/app/views/dash_v1/out.pug @@ -1,5 +1,4 @@ extends ./template block content - pre - code - div #{prettyd} \ No newline at end of file + script(src='/assets/dash_v1.js') + script output(syntaxHighlight(`#{prettyd}`)); diff --git a/app/views/dash_v1/share.pug b/app/views/dash_v1/share.pug new file mode 100644 index 0000000..bb9f5dd --- /dev/null +++ b/app/views/dash_v1/share.pug @@ -0,0 +1,45 @@ +extends ./template +block content + h2 Shared With + table + thead + tr + th(scope='col' style='min-width: 250px') Username + th(scope='col') Actions + tbody + each user in sharing.shared + tr + td #{(user.uuid === project.user_id ? user.username + " (Owner)" : user.username)} + td + ul(style='list-style-type: none; margin: 0; padding: 0;') + if !(user.uuid === project.user_id) + li + a.action(href='/dash/v1/project/share/'+project.id+'/revoke/'+user.uuid) Revoke + li + a.action(href='/dash/v1/project/share/'+project.id+'/transfer/'+user.uuid) Transfer Ownership + else + li + strike Revoke + li + strike Transfer Ownership + br + h2 Share With New User + table + thead + tr + th(scope='col' style='min-width: 250px') Username + th(scope='col') Actions + tbody + each user in sharing.to_share + tr + td #{(user.uuid === project.user_id ? user.username + " (Owner)" : user.username)} + td + ul(style='list-style-type: none; margin: 0; padding: 0;') + if !(user.uuid === project.user_id) + li + a.action(href='/dash/v1/project/share/' + project.id + '/share/'+user.uuid) Share + else + li + strike Share + li + a.action(href='/dash/v1/project/share/' + project.id + '/transfer/' + user.uuid) Transfer Ownership diff --git a/app/views/dash_v1/template.pug b/app/views/dash_v1/template.pug index d8a8a33..bd48b35 100644 --- a/app/views/dash_v1/template.pug +++ b/app/views/dash_v1/template.pug @@ -2,17 +2,23 @@ html head title #{(title ? title+' | DevBug' : 'DevBug Dashboard')} link(rel='stylesheet' href='/assets/dash_v1.css') + script(src='/assets/dash_v1.js') body - if title_small - h3 #{(title ? title+' | DevBug' : 'DevBug Dashboard')} - else - h1 #{(title ? title+' | DevBug' : 'DevBug Dashboard')} - ul.navul - li.navli - a.nava(href='/dash/v1') Home - li.navli - a.nava(href='/auth/logout') Logout - if show_back + .page-header + .devbug-header DevBug | v#{devbug.version} #{(user ? " | User: "+user.username : "")} #{(project ? " | Project: "+project.name+" | API: "+project.uuid : "")} #{((_flitter.config('server.environment') === 'development') ? " | Development" : "" )} + if title_small + h3 #{(title ? title : 'Dashboard')} + else + h1 #{(title ? title : 'Dashboard')} + ul.navul li.navli - a.nava(href='javascript:window.history.back()') Back - block content \ No newline at end of file + a.nava(href='/dash/v1') Home + li.navli + a.nava(href='/dash/v1/code') Code Snippets + li.navli + a.nava(href='/auth/logout') Logout + if show_back + li.navli + a.nava(href='javascript:window.history.back()') Back + .spacer + block content diff --git a/app/views/dash_v1/view.pug b/app/views/dash_v1/view.pug index 5790aa0..3a60f3b 100644 --- a/app/views/dash_v1/view.pug +++ b/app/views/dash_v1/view.pug @@ -17,4 +17,4 @@ block content li a.action(href='/dash/v1/out/view/'+out.id) View li - a.action(href='/dash/v1/out/delete/'+out.id) Delete \ No newline at end of file + a.action(href='/dash/v1/out/delete/'+out.id+'/'+project.id) Delete diff --git a/app/views/errors/401.pug b/app/views/errors/401.pug new file mode 100644 index 0000000..b725b51 --- /dev/null +++ b/app/views/errors/401.pug @@ -0,0 +1,33 @@ +html + head + title Access Denied | Flitter + style(type="text/css"). + @import url('https://fonts.googleapis.com/css?family=Rajdhani'); + html, + body { + height: 100%; + overflow-y: hidden; + background-color: #c7dbdf; + } + + .flitter-container { + height: 60%; + display: flex; + align-items: center; + justify-content: center; + } + + .flitter-image { + height: 150px; + } + + .flitter-name { + font-family: "Rajdhani"; + font-size: 36pt; + margin-left: 35px; + color: #00323d; + } +body + .flitter-container + img.flitter-image(src="/assets/flitter.png") + p.flitter-name Access Denied: #{(reason ? reason : "Resource Not Found.")} diff --git a/app/views/errors/404.pug b/app/views/errors/404.pug index 58d3667..dea1f5c 100644 --- a/app/views/errors/404.pug +++ b/app/views/errors/404.pug @@ -23,11 +23,11 @@ html .flitter-name { font-family: "Rajdhani"; - font-size: 50pt; + font-size: 36pt; margin-left: 35px; color: #00323d; } body .flitter-container img.flitter-image(src="/assets/flitter.png") - p.flitter-name 404: Page Not Found \ No newline at end of file + p.flitter-name 404: #{(reason ? reason : "Resource Not Found.")}