From dc2cec78dcc52a1fb90665502c78a5018aac6e71 Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Mon, 24 Jun 2019 11:45:22 -0500 Subject: [PATCH] add link sharing --- app/MiscUnit.js | 3 +- app/assets/dash_v1.css | 18 +++++++ app/controllers/api/v1.controller.js | 48 +++++++++++++++++++ app/controllers/dash/v1.controller.js | 46 +++++++++++++++++- app/models/v1/Invite.model.js | 13 +++++ app/routing/Middleware.js | 6 +-- .../middleware/v1/Invite.middleware.js | 26 ++++++++++ app/routing/routers/api/v1.routes.js | 2 + app/routing/routers/dash/v1.routes.js | 6 ++- app/views/dash_v1/accept.pug | 19 ++++++++ app/views/dash_v1/invite.pug | 7 +++ app/views/dash_v1/share.pug | 3 ++ config/server.config.js | 3 +- example.env | 3 +- 14 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 app/models/v1/Invite.model.js create mode 100644 app/routing/middleware/v1/Invite.middleware.js create mode 100644 app/views/dash_v1/accept.pug create mode 100644 app/views/dash_v1/invite.pug diff --git a/app/MiscUnit.js b/app/MiscUnit.js index 8bd6ff8..e1391ea 100644 --- a/app/MiscUnit.js +++ b/app/MiscUnit.js @@ -37,7 +37,8 @@ class MiscUnit extends Unit { global.devbug = { version: '0.2.0', code: { - node: `dbsetup({ + node: `require("devbugjs") +dbsetup({ \tserver: "https://CHANGEME:8000/", // DevBug Server URL \tproject: "CHANGEME", // Project API Key })`, diff --git a/app/assets/dash_v1.css b/app/assets/dash_v1.css index 3fbe3c6..3bfa449 100644 --- a/app/assets/dash_v1.css +++ b/app/assets/dash_v1.css @@ -87,6 +87,24 @@ a { color: #eee; } +.btn { + margin: 5; + margin-bottom: 20; + padding: 5; + padding-left: 10; + padding-right: 10; + background: #509d9d; + border-radius: 7px; + text-decoration: none; + color: white; + transition: all 0.5s ease; +} + +.btn:hover { + background: #eee; + color: #307d7d; +} + pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: darkslateblue; } .number { color: darkorange; } diff --git a/app/controllers/api/v1.controller.js b/app/controllers/api/v1.controller.js index 503d5c9..9fb37cc 100644 --- a/app/controllers/api/v1.controller.js +++ b/app/controllers/api/v1.controller.js @@ -5,6 +5,7 @@ */ const Out = _flitter.model('v1:Out') const Project = _flitter.model('v1:Project') +const Invite = _flitter.model('v1:Invite') class v1 { /* @@ -66,6 +67,53 @@ class v1 { success: true }) } + + async invite_show(req, res, next){ + const invite = await Invite.findById(req.params.id) + if ( !invite ) _flitter.error(res, 404, {reason: 'Invitation not found with the specified URL.'}) + + if ( invite.used ) _flitter.error(res, 401, {reason: 'This invitation link has been used or has expired.'}) + + const project = await Project.findById(invite.project_id) + if ( !project ) _flitter.error(res, 404, {reason: 'This project no longer exists.'}) + + const user = await _flitter.model('User').findOne({uuid: invite.by_user_id}) + if ( !user ) _flitter.error(res, 500, {reason: 'This user no longer exists. Sorry.'}) + + return _flitter.view(res, 'dash_v1:accept', {invite, project, user}) + } + + async invite_accept(req, res, next){ + const invite = await Invite.findById(req.params.id) + if ( !invite ) _flitter.error(res, 404, {reason: 'Invitation not found with the specified URL.'}) + + if ( invite.used ) _flitter.error(res, 401, {reason: 'This invitation link has been used or has expired.'}) + + const project = await Project.findById(invite.project_id) + if ( !project ) _flitter.error(res, 404, {reason: 'This project no longer exists.'}) + + const user = await _flitter.model('User').findOne({uuid: invite.by_user_id}) + if ( !user ) _flitter.error(res, 500, {reason: 'This user no longer exists. Sorry.'}) + + // if we're signed in + if ( req.session.auth && req.session.auth.user ){ + if ( project.user_id !== req.session.auth.uuid && !project.shared_user_ids.includes(req.session.auth.uuid) ){ + project.shared_user_ids.push(req.session.auth.uuid) + await project.save() + } + return res.redirect('/dash/v1') + } + else { + req.session.invite = true + req.session.invite_data = { + invite: invite.id, + project: project.id, + user: user.id, + } + + return res.redirect('/dash/v1') + } + } } module.exports = exports = v1 diff --git a/app/controllers/dash/v1.controller.js b/app/controllers/dash/v1.controller.js index b29e31f..a8efe21 100644 --- a/app/controllers/dash/v1.controller.js +++ b/app/controllers/dash/v1.controller.js @@ -5,6 +5,7 @@ */ const Project = _flitter.model('v1:Project') const Out = _flitter.model('v1:Out') +const Invite = _flitter.model('v1:Invite') class v1 { /* @@ -130,7 +131,7 @@ class v1 { } // TODO permission access check - return _flitter.view(res, 'dash_v1:out', {user: req.session.auth.user, out, prettyd:pretty, show_back: true, title: out.brief, title_small: true }); + return _flitter.view(res, 'dash_v1:out', {project, user: req.session.auth.user, out, prettyd:pretty, show_back: true, title: out.brief, title_small: true }); } async out_delete(req, res, next){ @@ -264,6 +265,49 @@ class v1 { return res.redirect('/dash/v1') } + + async project_share_invite(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 share_data = { + project_id: project.id, + by_user_id: req.session.auth.uuid, + created_on: Date.now() + } + + const share = new Invite(share_data) + await share.save() + + return _flitter.view(res, 'dash_v1:invite', {share, project, title: 'Generate Invite Link', show_back: true}) + } + + async accept_invite(req, res, next){ + if ( !req.session.invite ) return res.redirect('/dash/v1') + + const invite = await Invite.findById(req.session.invite_data.invite) + if ( !invite ) return _flitter.error(res, 404, {reason: 'This invitation is no longer valid. Sorry.'}) + + const project = await Project.findById(req.session.invite_data.project) + if ( !project ) return _flitter.error(res, 404, {reason: 'This project no longer exists.'}) + + const user = await _flitter.model('User').findById(req.session.invite_data.user) + if ( !user ) return _flitter.error(res, 404, {reason: 'This user no longer exists. Sorry.'}) + + if ( !project.shared_user_ids.includes(req.session.auth.uuid) && !(project.user_id === req.session.auth.uuid) ){ + project.shared_user_ids.push(req.session.auth.uuid) + await project.save() + } + + invite.used = true + await invite.save() + + req.session.invite = false + req.session.invite_data = false + return res.redirect('/dash/v1/project/view/'+project.id) + } } module.exports = exports = v1 diff --git a/app/models/v1/Invite.model.js b/app/models/v1/Invite.model.js new file mode 100644 index 0000000..12e2a16 --- /dev/null +++ b/app/models/v1/Invite.model.js @@ -0,0 +1,13 @@ +/* + * Invite Model + * ------------------------------------------------------------- + * Put some description here! + */ +const Invite = { + project_id: String, + by_user_id: String, + created_on: Date, + used: { type: Boolean, default: false } +} + +module.exports = exports = Invite \ No newline at end of file diff --git a/app/routing/Middleware.js b/app/routing/Middleware.js index a819e07..adea072 100644 --- a/app/routing/Middleware.js +++ b/app/routing/Middleware.js @@ -9,10 +9,10 @@ * Route-specific middleware should be specified in the corresponding * routes file. */ -const Middleware = [ - - 'Debug', +let Middleware = [ ] +if ( _flitter.config('server.environment') === 'development' ) Middleware.push('Debug') + module.exports = exports = Middleware diff --git a/app/routing/middleware/v1/Invite.middleware.js b/app/routing/middleware/v1/Invite.middleware.js new file mode 100644 index 0000000..5d2f110 --- /dev/null +++ b/app/routing/middleware/v1/Invite.middleware.js @@ -0,0 +1,26 @@ +/* + * Invite Middleware + * ------------------------------------------------------------- + * Put some description here! + */ +class Invite { + + /* + * Run the middleware test. + * This method is required by all Flitter middleware. + * It should either call the next function in the stack, + * or it should handle the response accordingly. + */ + test(req, res, next, args = {}){ + if ( req.session.invite && !req.originalUrl.includes('/dash/v1/invitation/accept') ){ + return res.redirect('/dash/v1/invitation/accept') + } + + /* + * Call the next function in the stack. + */ + next() + } +} + +module.exports = Invite \ No newline at end of file diff --git a/app/routing/routers/api/v1.routes.js b/app/routing/routers/api/v1.routes.js index aaa38c6..bfb9be1 100644 --- a/app/routing/routers/api/v1.routes.js +++ b/app/routing/routers/api/v1.routes.js @@ -34,6 +34,8 @@ const v1 = { */ get: { // '/': [ controller('Controller_Name').handler_name ], + '/invitation/:id': [ _flitter.controller('api:v1').invite_show ], + '/invitation/:id/accept': [ _flitter.controller('api:v1').invite_accept ], }, /* diff --git a/app/routing/routers/dash/v1.routes.js b/app/routing/routers/dash/v1.routes.js index 0f08ebc..5ee7752 100644 --- a/app/routing/routers/dash/v1.routes.js +++ b/app/routing/routers/dash/v1.routes.js @@ -21,7 +21,8 @@ const v1 = { */ middleware: [ // mw('Middleware Name'), - _flitter.mw('auth:RequireAuth') + _flitter.mw('auth:RequireAuth'), + _flitter.mw('v1:Invite') ], /* @@ -44,11 +45,14 @@ const v1 = { '/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 ], + '/project/share/:id/invite': [ _flitter.controller('dash:v1').project_share_invite ], '/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 ], + + '/invitation/accept': [ _flitter.controller('dash:v1').accept_invite ], }, /* diff --git a/app/views/dash_v1/accept.pug b/app/views/dash_v1/accept.pug new file mode 100644 index 0000000..4fe6707 --- /dev/null +++ b/app/views/dash_v1/accept.pug @@ -0,0 +1,19 @@ +html + head + title Project Invitation | DevBug + link(rel='stylesheet' href='/assets/dash_v1.css') + script(src='/assets/dash_v1.js') + body + .page-header + .devbug-header DevBug | v#{devbug.version} #{(project ? " | Project: "+project.name+" | API: "+project.uuid : "")} + h1 Accept Invitation? + ul.navul + li.navli + a.nava(href='/dash/v1') Dashboard Login + .spacer + p You've been invited to view the debugging project "#{project.name}" by #{user.username}. + p To accept this invitation, you must have a DevBug account. You will be redirected to the registration portal. + a.btn(href='/api/v1/invitation/'+invite.id+'/accept') Accept + br + h3 What's DevBug? + p DevBug is a debugging output server used to help developers work more efficiently. Using inline-code clients, developers can output variables and data from their programs. This data is stored in a DevBug project, where it can easily be shared with others. diff --git a/app/views/dash_v1/invite.pug b/app/views/dash_v1/invite.pug new file mode 100644 index 0000000..b4a0b12 --- /dev/null +++ b/app/views/dash_v1/invite.pug @@ -0,0 +1,7 @@ +extends ./template +block content + p You can send this link to someone without a DevBug account to invite them to register. + p Once they register, #{project.name} will be shared with them. This link can only be used once. + h3 Invitation Link + pre + code #{_flitter.config('server.url')+'/api/v1/invitation/'+share.id} \ No newline at end of file diff --git a/app/views/dash_v1/share.pug b/app/views/dash_v1/share.pug index bb9f5dd..a54821f 100644 --- a/app/views/dash_v1/share.pug +++ b/app/views/dash_v1/share.pug @@ -24,6 +24,9 @@ block content strike Transfer Ownership br h2 Share With New User + a.btn(href='/dash/v1/project/share/'+project.id+'/invite') Generate Sharing Link + br + br table thead tr diff --git a/config/server.config.js b/config/server.config.js index e87fd6b..9dcb8c9 100644 --- a/config/server.config.js +++ b/config/server.config.js @@ -10,7 +10,8 @@ const server_config = { }, uploads: { destination: './uploads' - } + }, + url: process.env.SERVER_URL || "localhost", } diff --git a/example.env b/example.env index c4fad62..9175852 100644 --- a/example.env +++ b/example.env @@ -7,4 +7,5 @@ DATABASE_NAME=flitter DATABASE_AUTH=false SECRET=changeme -ENVIRONMENT=production \ No newline at end of file +ENVIRONMENT=production +SERVER_URL=http://localhost \ No newline at end of file