diff --git a/app/models/Invite.js b/app/models/Invite.js index 2a6dfee..dd5bcec 100644 --- a/app/models/Invite.js +++ b/app/models/Invite.js @@ -14,4 +14,8 @@ var InviteSchema = mongoose.Schema({ exp: Date }); +InviteSchema.methods.use = function(canonicalname, cb) { + return this.model('Invite').updateOne({code: this.code}, {recipient: canonicalname, used: Date.now()}, cb); +}; + module.exports = mongoose.model('Invite', InviteSchema); \ No newline at end of file diff --git a/app/models/Upload.js b/app/models/Upload.js index 4b77826..3b43c6a 100644 --- a/app/models/Upload.js +++ b/app/models/Upload.js @@ -11,6 +11,7 @@ var UploadSchema = mongoose.Schema({ default: 0 }, uploader: String, + uploadKey: String, date: Date, file: Object }); diff --git a/app/models/User.js b/app/models/User.js index f979948..5e4c514 100644 --- a/app/models/User.js +++ b/app/models/User.js @@ -7,6 +7,11 @@ var UserSchema = mongoose.Schema({ unique: true, required: true }, + canonicalname: { + type: String, + unique: true, + required: true + }, scope: [String], uploadCount: { type: Number, @@ -19,6 +24,6 @@ var UserSchema = mongoose.Schema({ date: Date }); -UserSchema.plugin(passportLocalMongoose); +UserSchema.plugin(passportLocalMongoose, {usernameField: 'canonicalname'}); module.exports = mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/public/css/form.css b/app/public/css/form.css index 08f34b0..b344f2d 100644 --- a/app/public/css/form.css +++ b/app/public/css/form.css @@ -58,8 +58,7 @@ fieldset:before { border: 1px solid #999; width: 226px; padding: 12px 12px; - margin: auto; - margin-bottom: 5px; + margin: auto auto 5px; } button { @@ -70,9 +69,8 @@ button { cursor: pointer; display: block; padding: 10px 30px; - margin: auto; - margin-top: 20px; - transition: background 0.25s; + margin: 20px auto auto; + transition: background 0.25s, border-color 0.25s; } button:hover { @@ -81,3 +79,31 @@ button:hover { text-decoration: none; outline: none; } + +button.shake { + background: #ff6666; + border-color: #ff6666; + color: #fff; + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; +} + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} diff --git a/app/public/css/index.css b/app/public/css/index.css index 4461762..2bcbff1 100644 --- a/app/public/css/index.css +++ b/app/public/css/index.css @@ -8,7 +8,7 @@ body { a { position: absolute; - top: 5px; + top: 40px; left: 48%; opacity: 0.1; height: 30px; @@ -34,8 +34,6 @@ video { left: 50%; min-width: 100%; min-height: 100%; - width: 1920px; - height: 760px; z-index: -100; transform: translateX(-50%) translateY(-50%); background-size: cover; diff --git a/app/public/panel/controllers/ApiCtrl.js b/app/public/panel/controllers/ApiCtrl.js index b728361..7700aca 100644 --- a/app/public/panel/controllers/ApiCtrl.js +++ b/app/public/panel/controllers/ApiCtrl.js @@ -1,57 +1,48 @@ var angular = require('angular'); angular.module('ApiCtrl', ['ApiSvc', 'AuthSvc']).controller('ApiController', ['$scope', 'ApiService', 'AuthService', function ($scope, ApiService, AuthService) { + // Transforms an array of period-separated properties ex. ["file.upload", "user.view", "user.ban"] + // to json ex. { "file": "upload", "user": ["view", "ban"] } function splitScope(scope) { var res = {}; for (var i in scope) { - var perm = scope[i]; - var prefix = perm.substr(0, perm.indexOf('.')); - var postfix = perm.substr(perm.indexOf('.') + 1); - if (!res[prefix]) res[prefix] = []; - res[prefix].push({name: postfix}); + if (scope.hasOwnProperty(i)) { + var perm = scope[i]; + var prefix = perm.substr(0, perm.indexOf('.')); + var postfix = perm.substr(perm.indexOf('.') + 1); + if (!res[prefix]) res[prefix] = []; + res[prefix].push({name: postfix}); + } } return res; } - $scope.checkCkPerm = function(prefix, perm) { - var index = $scope.scopeObj[prefix].indexOf(perm); - if ($scope.scopeObj[prefix][index].isChecked) { - $scope.ckScope.push(prefix + '.' + perm.name); - } else { - var index = $scope.ckScope.indexOf(prefix + '.' + perm.name); - $scope.ckScope.splice(index, 1); - } - }; - + // Called on init, retrieves the user's scope from the server. $scope.parseScope = function () { AuthService.currentUser(function (res) { $scope.scopeObj = splitScope(res.scope); - $scope.ckScope = []; + $scope.currKeyScope = []; }) }; + // Triggered when a checkbox for a permission changes. + // Updates the currKeyScope object with the addition or removal. + $scope.updateCurrKeyPerm = function(prefix, perm) { + var index = $scope.scopeObj[prefix].indexOf(perm); + if ($scope.scopeObj[prefix][index].isChecked) { + $scope.currKeyScope.push(prefix + '.' + perm.name); + } else { + index = $scope.currKeyScope.indexOf(prefix + '.' + perm.name); + $scope.currKeyScope.splice(index, 1); + } + }; + $scope.getKeys = function () { - ApiService.getAll(function (keys) { + ApiService.getAllKeys(function (keys) { $scope.keys = keys; }); }; - $scope.hideNewKey = function () { - $scope.nModalShow = false; - }; - $scope.showNewKey = function () { - $scope.nModalShow = true; - }; - - $scope.hideKeyInfo = function () { - $scope.kModalShow = false; - }; - $scope.showKeyInfo = function (key) { - $scope.kModalShow = true; - $scope.currKey = key; - $scope.currKey.scopeObj = splitScope($scope.currKey.scope); - }; - $scope.deleteKey = function (key) { ApiService.deleteKey(key, function () { var index = $scope.keys.indexOf(key); @@ -62,17 +53,35 @@ angular.module('ApiCtrl', ['ApiSvc', 'AuthSvc']).controller('ApiController', ['$ }; $scope.createKey = function () { - if ($scope.ckScope.length === 0 || !$scope.ckIdentifier) + if ($scope.currKeyScope.length === 0 || !$scope.currKeyIdentifier) return; ApiService.createKey({ - identifier: $scope.ckIdentifier, - scope: JSON.stringify($scope.ckScope) + identifier: $scope.currKeyIdentifier, + scope: JSON.stringify($scope.currKeyScope) }, function (res) { if (res.key) { $scope.hideNewKey(); $scope.getKeys(); } }); - } + }; + + // Hide/show new key modal dialog + $scope.hideNewKey = function () { + $scope.nModalShow = false; + }; + $scope.showNewKey = function () { + $scope.nModalShow = true; + }; + + // Hide/show key info modal dialog + $scope.hideKeyInfo = function () { + $scope.kModalShow = false; + }; + $scope.showKeyInfo = function (key) { + $scope.kModalShow = true; + $scope.currKey = key; + $scope.currKey.scopeObj = splitScope($scope.currKey.scope); + }; }]); \ No newline at end of file diff --git a/app/public/panel/controllers/InviteCtrl.js b/app/public/panel/controllers/InviteCtrl.js new file mode 100644 index 0000000..62125a0 --- /dev/null +++ b/app/public/panel/controllers/InviteCtrl.js @@ -0,0 +1,64 @@ +var angular = require('angular'); + +angular.module('InviteCtrl', ['InviteSvc', 'AuthSvc']).controller('InviteController', ['$scope', 'InviteService', 'AuthService', function($scope, InviteService, AuthService) { + // Transforms an array of period-separated properties ex. ["file.upload", "user.view", "user.ban"] + // to json ex. { "file": "upload", "user": ["view", "ban"] } + function splitScope(scope) { + var res = {}; + for (var i in scope) { + if (scope.hasOwnProperty(i)) { + var perm = scope[i]; + var prefix = perm.substr(0, perm.indexOf('.')); + var postfix = perm.substr(perm.indexOf('.') + 1); + if (!res[prefix]) + res[prefix] = []; + res[prefix].push({name: postfix}); + } + } + return res; + } + + // Called on init, retrieves the user's scope from the server. + $scope.parseScope = function () { + AuthService.currentUser(function (res) { + $scope.scopeObj = splitScope(res.scope); + $scope.currInvScope = []; + }) + }; + + // Triggered when a checkbox for a permission changes. + // Updates the currInvScope object with the addition or removal. + $scope.updateCurrInvPerm = function(prefix, perm) { + var index = $scope.scopeObj[prefix].indexOf(perm); + if ($scope.scopeObj[prefix][index].isChecked) { + $scope.currInvScope.push(prefix + '.' + perm.name); + } else { + index = $scope.currInvScope.indexOf(prefix + '.' + perm.name); + $scope.currInvScope.splice(index, 1); + } + }; + + $scope.getInvites = function() { + InviteService.getAllInvites(function(invites) { + $scope.invites = invites; + }); + }; + + $scope.deleteInvite = function(invite) { + InviteService.deleteInvite(function() { + var index = $scope.invites.indexOf(invite); + $scope.invites.splice(index, 1); + }); + }; + + $scope.createInvite = function() { + InviteService.createInvite({ + scope: JSON.stringify($scope.currInvScope), + exp: JSON.stringify($scope.currInvExpiry) + }, function(res) { + if (res.code) { + $scope.getInvites(); + } + }); + }; +}]); \ No newline at end of file diff --git a/app/public/panel/controllers/NavCtrl.js b/app/public/panel/controllers/NavCtrl.js index 20fb1d0..07891ea 100644 --- a/app/public/panel/controllers/NavCtrl.js +++ b/app/public/panel/controllers/NavCtrl.js @@ -3,7 +3,7 @@ var angular = require('angular'); angular.module('NavCtrl', ['AuthSvc']).controller('NavController', ['$scope', '$window', 'AuthService', function($scope, $window, AuthService) { $scope.user = {}; AuthService.currentUser(function(user) { - $scope.user = user; + $scope.user = username; }); $scope.logout = AuthService.logout; @@ -12,5 +12,4 @@ angular.module('NavCtrl', ['AuthSvc']).controller('NavController', ['$scope', '$ if (!$scope.user.scope) return false; return $scope.user.scope.indexOf(permission) !== -1; }; - }]); \ No newline at end of file diff --git a/app/public/panel/controllers/UserCtrl.js b/app/public/panel/controllers/UserCtrl.js new file mode 100644 index 0000000..432a8bd --- /dev/null +++ b/app/public/panel/controllers/UserCtrl.js @@ -0,0 +1,9 @@ +var angular = require('angular'); + +angular.module('UserCtrl', ['UserSvc']).controller('UserController', ['$scope', 'UserService', function($scope, UserService) { + $scope.getUsers = function() { + UserService.getAllUsers(function(users) { + $scope.users = users; + }); + }; +}]); \ No newline at end of file diff --git a/app/public/panel/shimapan-panel.js b/app/public/panel/shimapan-panel.js index cc974f2..e92d5fb 100644 --- a/app/public/panel/shimapan-panel.js +++ b/app/public/panel/shimapan-panel.js @@ -1,6 +1,6 @@ var angular = require('angular'); var uirouter = require('angular-ui-router'); -var app = angular.module('shimapan-panel', ['ui.router', 'AuthSvc', 'ApiSvc', 'ApiCtrl', 'NavCtrl', 'PanelRoutes']); +var app = angular.module('shimapan-panel', ['ui.router', 'AuthSvc', 'ApiSvc', 'InviteSvc', 'UserSvc', 'ApiCtrl', 'InviteCtrl', 'UserCtrl', 'NavCtrl', 'PanelRoutes']); app.run(['$rootScope', '$state', '$stateParams', function($rootScope, $state, $stateParams) { $rootScope.$state = $state; diff --git a/app/public/services/ApiSvc.js b/app/public/services/ApiSvc.js index e70028a..a981f22 100644 --- a/app/public/services/ApiSvc.js +++ b/app/public/services/ApiSvc.js @@ -1,6 +1,6 @@ var angular = require('angular'); -angular.module('ApiSvc', []).service('ApiService', ['$http', '$window', function ($http, $window) { +angular.module('ApiSvc', []).service('ApiService', ['$http', function ($http) { this.getKey = function (identifier, cb) { $http({ method: 'GET', @@ -11,7 +11,7 @@ angular.module('ApiSvc', []).service('ApiService', ['$http', '$window', function }); }; - this.getAll = function (cb) { + this.getAllKeys = function (cb) { $http({ method: 'GET', url: '/api/keys/get' @@ -28,7 +28,8 @@ angular.module('ApiSvc', []).service('ApiService', ['$http', '$window', function transformRequest: function (obj) { var str = []; for (var p in obj) - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); return str.join("&"); }, data: {key: key.key} @@ -45,7 +46,8 @@ angular.module('ApiSvc', []).service('ApiService', ['$http', '$window', function transformRequest: function (obj) { var str = []; for (var p in obj) - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); return str.join("&"); }, data: key diff --git a/app/public/services/AuthSvc.js b/app/public/services/AuthSvc.js index e387e93..4e17d02 100644 --- a/app/public/services/AuthSvc.js +++ b/app/public/services/AuthSvc.js @@ -9,13 +9,14 @@ angular.module('AuthSvc', []).service('AuthService', ['$http', '$window', functi transformRequest: function(obj) { var str = []; for (var p in obj) - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); return str.join("&"); }, data: user }).then(function(res) { - if (res.status === 401) return false; - $window.location.href = '/home'; + if (res.status === 200) + $window.location.href = '/home'; }) }; @@ -36,13 +37,14 @@ angular.module('AuthSvc', []).service('AuthService', ['$http', '$window', functi transformRequest: function(obj) { var str = []; for (var p in obj) - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); return str.join("&"); }, data: user }).then(function(res) { - if (res.status === 401) return false; - $window.location.href = '/home'; + if (res.status === 200) + $window.location.href = '/home'; }); }; diff --git a/app/public/services/InviteSvc.js b/app/public/services/InviteSvc.js new file mode 100644 index 0000000..935f59d --- /dev/null +++ b/app/public/services/InviteSvc.js @@ -0,0 +1,58 @@ +var angular = require('angular'); + +angular.module('InviteSvc', []).service('InviteService', ['$http', function ($http) { + this.getInvite = function (code, cb) { + $http({ + method: 'GET', + url: '/api/invites/get', + params: {code: code} + }).then(function (res) { + cb(res.data); + }); + }; + + this.getAllInvites = function (cb) { + $http({ + method: 'GET', + url: '/api/invites/get' + }).then(function (res) { + cb(res.data); + }); + }; + + this.deleteInvite = function (invite, cb) { + $http({ + method: 'POST', + url: '/api/invites/delete', + headers: {'Content-Type': 'application/x-www-form-urlencode'}, + transformRequest: function (obj) { + var str = []; + for (var p in obj) + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: {code: invite.code} + }).then(function (res) { + cb(res.data); + }); + }; + + this.createInvite = function (invite, cb) { + $http({ + method: 'POST', + url: '/api/invites/create', + headers: {'Content-Type': 'application/x-www-form-urlencode'}, + transformRequest: function (obj) { + var str = []; + for (var p in obj) + if (obj.hasOwnProperty(p)) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: invite + }).then(function(res) { + cb(res.data); + }); + }; +}]); \ No newline at end of file diff --git a/app/public/services/UserSvc.js b/app/public/services/UserSvc.js new file mode 100644 index 0000000..99d61c8 --- /dev/null +++ b/app/public/services/UserSvc.js @@ -0,0 +1,22 @@ +var angular = require('angular'); + +angular.module('UserSvc', []).service('UserService', ['$http', function($http) { + this.getUser = function(username, cb) { + $http({ + method: 'GET', + url: '/api/users/get', + params: {username: username} + }).then(function(res) { + cb(res.data); + }); + }; + + this.getAllUsers = function(cb) { + $http({ + method: 'GET', + url: '/api/users/get' + }).then(function(res) { + cb(res.data); + }); + }; +}]); \ No newline at end of file diff --git a/app/public/shimapan/components/LoginComp.js b/app/public/shimapan/components/LoginComp.js index 103e79e..68baa47 100644 --- a/app/public/shimapan/components/LoginComp.js +++ b/app/public/shimapan/components/LoginComp.js @@ -2,11 +2,16 @@ var angular = require('angular'); angular.module('LoginComp', ['AuthSvc']).component('loginComponent', { templateUrl: '/views/shimapan/login-form.html', - controller: ['$scope', 'AuthService', function ($scope, AuthService) { + controller: ['$scope', '$timeout', 'AuthService', function ($scope, $timeout, AuthService) { $scope.login = function () { AuthService.login({ username: $scope.username, password: $scope.password + }).catch(function() { + $scope.error = true; + $timeout(function() { + $scope.error = false; + }, 820); }); }; }] diff --git a/app/public/shimapan/components/RegisterComp.js b/app/public/shimapan/components/RegisterComp.js index 8bb5c94..fb1c321 100644 --- a/app/public/shimapan/components/RegisterComp.js +++ b/app/public/shimapan/components/RegisterComp.js @@ -2,12 +2,17 @@ var angular = require('angular'); angular.module('RegisterComp', ['AuthSvc']).component('registerComponent', { templateUrl: '/views/shimapan/register-form.html', - controller: ['$scope', 'AuthService', function ($scope, AuthService) { + controller: ['$scope', '$timeout', 'AuthService', function ($scope, $timeout, AuthService) { $scope.register = function () { AuthService.register({ username: $scope.username, password: $scope.password, invite: $scope.invite + }).catch(function() { + $scope.error = true; + $timeout(function() { + $scope.error = false + }, 820); }); }; }] diff --git a/app/public/shimapan/components/UploadComp.js b/app/public/shimapan/components/UploadComp.js index 2617498..3a58d8a 100644 --- a/app/public/shimapan/components/UploadComp.js +++ b/app/public/shimapan/components/UploadComp.js @@ -2,7 +2,7 @@ var angular = require('angular'); angular.module('UploadComp', ['ngFileUpload', 'AuthSvc']).component('uploadComponent', { templateUrl: '/views/shimapan/upload-form.html', - controller: ['$scope', 'Upload', '$timeout', 'AuthService', function ($scope, Upload, $timeout, AuthService) { + controller: ['$scope', 'Upload', '$timeout', function ($scope, Upload, $timeout) { $scope.errToString = function (err) { if (err === 'maxSize') return "File too large."; diff --git a/app/routes/auth.js b/app/routes/auth.js index 99b46b1..f2d73da 100644 --- a/app/routes/auth.js +++ b/app/routes/auth.js @@ -1,6 +1,3 @@ -var fs = require('fs'); -var path = require('path'); - var express = require('express'); var router = express.Router(); @@ -9,57 +6,124 @@ var Invite = require('../models/Invite.js'); var passport = require('passport'); -function checkInvite(code, callback) { +var async = require('async'); + +// Normalizes, decomposes, and lowercases a unicode string +function canonicalize(username) { + return username.normalize('NFKD').toLowerCase(); +} + +// Checks if an invite code is valid +// Returns the invite object if valid +function checkInvite(code, cb) { Invite.findOne({code: code}, function (err, invite) { - if (err) return callback(err); - if (!invite || invite.used || invite.exp < new Date()) - callback(null, false); + if (err) + cb(err); + else if (!invite) + cb('Invalid invite code.'); + else if (invite.used) + cb('Invite already used.'); + else if (invite.exp < Date.now()) + cb('Invite expired.'); else - callback(null, true, invite); + cb(null, invite); }); } -function useInvite(code, username) { - Invite.updateOne({code: code}, {recipient: username, used: new Date()}, function (err) { - if (err) throw err; +// Validates the username, then registers the user in the database using the given invite. +function registerUser(username, password, invite, sanitizeFn, cb) { + async.series([ + function (cb) { + // Canonicalize and sanitize the username, checking for HTML + var canonicalName = canonicalize(username); + var sanitizedName = sanitizeFn(canonicalName); + + if (sanitizedName !== canonicalName) + cb('Username failed sanitization check.'); + else if (canonicalName.length > 36) + cb('Username too long.'); + else + cb(null); + }, + function (cb) { + User.register(new User({ + username: username, + canonicalname: canonicalize(username), + scope: invite.scope, + date: Date.now() + }), password, cb); + }, + function (cb) { + invite.use(canonicalize(username), cb); + } + ], function (err) { + cb(err); }); } -router.post('/register', function (req, res, next) { - // Validate the invite code, then hand off to passport - checkInvite(req.body.invite, function (err, valid, invite) { - if (valid) { - User.register( - new User({username: req.body.username, scope: invite.scope, date: Date.now()}), - req.body.password, - function (err) { - if (err) return res.status(403).json({'message': err.message}); - passport.authenticate('local')(req, res, function () { - req.session.save(function(err) { - if (err) return next(err); - useInvite(req.body.invite, req.body.username); - req.session.username = req.body.username; - res.status(200).json({'message': 'Registered.'}); - }); - }); - } - ); +// Authenticates and creates the required session variables +function setupSession(username, req, res, cb) { + // Body needs to contain canonical name for proper authentication + req.body.canonicalname = canonicalize(req.body.username); + + passport.authenticate('local')(req, res, function () { + req.session.save(function (err) { + if (!err) { + req.session.passport.username = username; + req.session.passport.canonicalname = canonicalize(username); + } + cb(err); + }); + }); +} + +router.post('/register', function (req, res) { + async.waterfall([ + function (cb) { + checkInvite(req.body.invite, cb); + }, + function (invite, cb) { + registerUser(req.body.username, req.body.password, invite, req.sanitize, cb); + }, + function (cb) { + setupSession(req.body.username, req, res, cb); + } + ], function (err) { + if (err) { + res.status(401).json({'message': err}); } else { - res.status(401).json({'message': 'Invalid invite code.'}); + res.status(200).json({'message': 'Registration successful.'}); } }); }); router.post('/login', function (req, res, next) { - passport.authenticate('local', function(err, user, info) { - if (err) return next(err); - if (!user) return res.status(401).json({'message': info}); - req.logIn(user, function(err) { - if (err) return next(err); - req.session.username = user; - res.status(200).json({'message': 'Logged in.'}); - }); - })(req, res, next); + // Take 'username' from the form and canonicalize it for authentication. + req.body.canonicalname = canonicalize(req.body.username); + + async.waterfall([ + function (cb) { + passport.authenticate('local', function(err, user, info) { + cb(err, user, info); + })(req, res, next); + }, + function (user, info, cb) { + if (!user) + cb(info); + else + req.logIn(user, cb); + }, + function (cb) { + req.session.passport.username = req.body.username; + req.session.passport.canonicalname = canonicalize(req.body.username); + cb(); + } + ], function (err) { + if (err) + res.status(401).json({'message': err}); + else + res.status(200).json({'message': 'Login successful.'}); + }); }); router.get('/logout', function (req, res) { @@ -67,17 +131,19 @@ router.get('/logout', function (req, res) { res.status(200).json({'message': 'Logged out.'}); }); -router.get('/session', function(req, res) { - if (req.session.passport.user) { - User.findOne({username: req.session.passport.user}, function(err, user) { - res.status(200).json({ - user: user.username, - scope: user.scope - }); - }); - } else { - res.status(401).json({'message': 'Unauthorized.'}); - } +router.get('/session', function (req, res) { + console.log(req.session.passport); + if (req.session.passport.canonicalname) { + User.findOne({canonicalname: req.session.passport.canonicalname}, function (err, user) { + res.status(200).json({ + username: user.username, + canonicalname: user.canonicalname, + scope: user.scope + }); + }); + } else { + res.status(401).json({'message': 'Unauthorized.'}); + } }); module.exports = router; \ No newline at end of file diff --git a/app/routes/invites.js b/app/routes/invites.js new file mode 100644 index 0000000..55b1583 --- /dev/null +++ b/app/routes/invites.js @@ -0,0 +1,78 @@ +var express = require('express'); +var router = express.Router(); + +var Invite = require('../models/Invite.js'); + +var requireScope = function (perm) { + return function(req, res, next) { + User.findOne({username: req.session.passport.user}, function(err, user) { + if (err) throw err; + if (user.scope.indexOf(perm) === -1) + res.status(400).json({'message': 'No permission.'}); + else + next(); + }); + } +}; + +router.post('/create', function (req, res) { + if (!req.body.scope) { + res.status(400).json({'message': 'Bad request.'}); + return; + } + + var scope; + try { + scope = JSON.parse(req.body.scope); + } catch (e) { + res.status(500).json({'message': e.name + ': ' + e.message}); + return; + } + + var expiry = req.body.exp; + if (!expiry || expiry < Date.now()) + expiry = 0; + + var entry = { + code: crypto.randomBytes(12).toString('hex'), + scope: scope, + issuer: req.session.passport.user, + issued: Date.now(), + exp: expiry + }; + + Invite.create(entry, function (err) { + if (err) { + throw err; + } else { + res.status(200).json({ + code: entry.code, + scope: entry.scope + }); + } + }) +}); + +router.get('/get', function (req, res, next) { + var query = {issuer: req.session.passport.user}; + + if (req.body.code) + query.code = req.body.code; + + Invite.find(query, function (err, invites) { + if (err) { + next(err); + } else { + res.status(200).json(invites); + } + }) +}); + +router.post('/delete', function (req, res, next) { + Invite.deleteOne({code: req.body.code}, function (err) { + if (err) next(err); + else res.status(200).json({'message': 'Successfully deleted.'}); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/app/routes/keys.js b/app/routes/keys.js index f2414b7..299a578 100644 --- a/app/routes/keys.js +++ b/app/routes/keys.js @@ -2,9 +2,22 @@ var express = require('express'); var router = express.Router(); var crypto = require('crypto'); +var User = require('../models/User.js'); var Key = require('../models/Key.js'); -router.post('/create', function (req, res) { +var requireScope = function (perm) { + return function(req, res, next) { + User.findOne({username: req.session.passport.user}, function(err, user) { + if (err) throw err; + if (user.scope.indexOf(perm) === -1) + res.status(400).json({'message': 'No permission.'}); + else + next(); + }); + } +}; + +router.post('/create', requireScope('api.create'), function (req, res) { if (!req.body.identifier || !req.body.scope) { res.status(400).json({'message': 'Bad request.'}); return; @@ -20,7 +33,7 @@ router.post('/create', function (req, res) { try { scope = JSON.parse(req.body.scope); } catch (e) { - res.status(400).json({'message': e.name + ': ' + e.message}); + res.status(500).json({'message': e.name + ': ' + e.message}); return; } @@ -64,7 +77,7 @@ router.get('/get', function (req, res, next) { }) }); -router.post('/delete', function(req, res, next) { +router.post('/delete', requireScope('api.delete'), function(req, res, next) { Key.deleteOne({key: req.body.key}, function(err) { if (err) next(err); else res.status(200).json({'message': 'Successfully deleted.'}); diff --git a/app/routes/login.js b/app/routes/login.js index 221d87b..1ba29a2 100644 --- a/app/routes/login.js +++ b/app/routes/login.js @@ -2,7 +2,7 @@ var express = require('express'); var router = express.Router(); var path = require('path'); -router.get('/', function(req, res, next) { +router.get('/', function(req, res) { res.sendFile(path.join(__dirname, '../../public/views', 'login.html')); }); diff --git a/app/routes/panel.js b/app/routes/panel.js index a62517b..f1ebd4a 100644 --- a/app/routes/panel.js +++ b/app/routes/panel.js @@ -2,7 +2,7 @@ var express = require('express'); var router = express.Router(); var path = require('path'); -router.get('/', function(req, res, next) { +router.get('/', function(req, res) { res.sendFile(path.join(__dirname, '../../public/views', 'panel.html')); }); diff --git a/app/routes/register.js b/app/routes/register.js index e04f90a..0411eac 100644 --- a/app/routes/register.js +++ b/app/routes/register.js @@ -2,7 +2,7 @@ var express = require('express'); var router = express.Router(); var path = require('path'); -router.get('/', function(req, res, next) { +router.get('/', function(req, res) { res.sendFile(path.join(__dirname, '../../public/views', 'register.html')); }); diff --git a/app/routes/routes.js b/app/routes/routes.js index 5ff6175..6f927dd 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -7,30 +7,57 @@ var register = require('./register.js'); var login = require('./login.js'); var panel = require('./panel.js'); var keys = require('./keys.js'); +var invites = require('./invites.js'); +var users = require('./users.js'); -var fs = require('fs'); -var path = require('path'); +var Key = require('../models/Key.js'); -var requireLogin = function(req, res, next) { +var checkApiKey = function (key, cb) { + Key.find({key: key}, function (err, res) { + if (err) throw err; + cb(res.length === 1); + }); +}; + +var requireLogin = function (req, res, next) { if (!req.session || !req.session.passport) return res.redirect('/login'); else return next(); }; -module.exports = function(app) { +var requireLoginApi = function(req, res, next) { + if (!req.session || !req.session.passport) { + if (!req.body.apikey) { + return res.redirect('/login'); + } else { + checkApiKey(res.body.apikey, function(valid) { + if (!valid) + return res.sendStatus(401); + else + return next(); + }); + } + } else { + return next(); + } +}; + +module.exports = function (app) { app.use('/', index); app.use('/home', requireLogin, home); app.use('/v', view); app.use('/api/upload', upload); app.use('/api/auth', auth); app.use('/api/keys', requireLogin, keys); + app.use('/api/invites', requireLogin, invites); + app.use('/api/users', requireLogin, users); app.use('/register', register); app.use('/login', login); app.use('/panel', requireLogin, panel); app.use('/panel*', requireLogin, panel); - app.use(function(err, req, res, next) { + app.use(function (err, req, res) { if (err.name === 'UnauthorizedError') { res.status(401); res.json({"message": err.name + ": " + err.message}); diff --git a/app/routes/upload.js b/app/routes/upload.js index b5138ff..7f6b8c3 100644 --- a/app/routes/upload.js +++ b/app/routes/upload.js @@ -4,6 +4,7 @@ var router = express.Router(); var mongoose = require('mongoose'); var User = require('../models/User.js'); var Upload = require('../models/Upload.js'); +var Key = require('../models/Key.js'); var multer = require('multer'); var dest = multer({dest: 'uploads/'}); @@ -14,12 +15,6 @@ function fileNameExists(name) { }); } -function updateUserStats(user, size) { - User.updateOne({username: user}, {$inc: {uploadCount: 1, uploadSize: size}}, function (err, res) { - if (err) throw err; - }); -} - function genFileName() { var charset = "abcdefghijklmnopqrstuvwxyz"; do { @@ -30,32 +25,90 @@ function genFileName() { return chars.join(''); } -router.post('/', dest.single('file'), function (req, res) { - // Size must be below 128 Megabytes (1024*1024*128 Bytes) - if (req.file.size >= 134217728) { - res.status(413).json({'message': 'File too large.'}); - return; +function updateStats(type, id, size) { + if (type === 'session') { + User.updateOne({username: id}, {$inc: {uploadCount: 1, uploadSize: size}}, function (err) { + if (err) throw err; + }); + } else if (type === 'apikey') { + Key.updateOne({key: id}, {$inc: {uploadCount: 1, uploadSize: size}}, function (err) { + if (err) throw err; + }); } +} - updateUserStats(req.session.passport.user, req.file.size); +var checkApiKey = function (key, cb) { + Key.find({key: key}, function (err, res) { + if (err) throw err; + cb(res.length === 1, res); + }); +}; - var entry = { - name: genFileName(), - uploader: req.session.passport.user, - created: Date.now(), - file: req.file - }; +var checkScope = function (type, id, perm, cb) { + if (type === 'session') { + User.findOne({username: id}, function (err, user) { + if (err) throw err; + cb(user.scope.indexOf(perm) !== -1); + }); + } else { + Key.findOne({key: id}, function (err, key) { + if (err) throw err; + cb(key.scope.indexOf(perm) !== -1); + }); + } +}; - Upload.create(entry, function (err, next) { - if (err) { - next(err); - } else { - res.send({ +function uploadFile(req, res, type, key) { + if (!req.file) + return res.status(400).json({'message': 'No file specified.'}); + + // Size must be below 128 Megabytes (1024*1024*128 Bytes) + if (req.file.size >= 134217728) + return res.status(413).json({'message': 'File too large.'}); + + var uploader = type === 'session' ? req.session.passport.user : key[0].username; + var uploadKey = type === 'apikey' ? key[0].key : null; + var id = type === 'session' ? req.session.passport.user : key[0].key; + + checkScope(type, id, 'file.upload', function (valid) { + if (!valid) + return res.status(403).json({'message': 'No permission.'}); + + var entry = { + name: genFileName(), + uploader: uploader, + uploadKey: uploadKey, + date: Date.now(), + file: req.file + }; + + updateStats(type, id, req.file.size); + + Upload.create(entry, function (err) { + if (err) throw err; + res.status(200).json({ name: entry.name, url: 'https://shimapan.rocks/v/' + entry.name }); - } + }); }); +} + +router.post('/', dest.single('file'), function (req, res) { + if (!req.session || !req.session.passport) { + if (!req.body.apikey) { + return res.sendStatus(401); + } else { + checkApiKey(req.body.apikey, function (valid, key) { + if (!valid) + return res.sendStatus(401); + else + uploadFile(req, res, 'apikey', key); + }); + } + } else { + uploadFile(req, res, 'session'); + } }); module.exports = router; diff --git a/app/routes/users.js b/app/routes/users.js new file mode 100644 index 0000000..c5270a6 --- /dev/null +++ b/app/routes/users.js @@ -0,0 +1,33 @@ +var express = require('express'); +var router = express.Router(); + +var User = require('../models/User.js'); + +var requireScope = function (perm) { + return function(req, res, next) { + User.findOne({username: req.session.passport.user}, function(err, user) { + if (err) throw err; + if (user.scope.indexOf(perm) === -1) + res.status(400).json({'message': 'No permission.'}); + else + next(); + }); + } +}; + +router.get('/get', requireScope('users.view'), function (req, res, next) { + var query = {}; + + if (req.body.username) + query.username = req.body.username; + + User.find(query, function (err, users) { + if (err) { + next(err) + } else { + res.status(200).json(users); + } + }) +}); + +module.exports = router; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 81fbfc7..5f1a701 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -105,7 +105,9 @@ gulp.task('concatjs', function () { var tasks = files.map(function (entry) { return gulp.src(entry.src) .pipe(concat(entry.name)) - .pipe(uglify()) + .pipe(uglify().on('error', function(err) { + console.log(err.toString()); + })) .pipe(gulp.dest('public/js')); }); diff --git a/public/img/edge.webm b/public/img/edge.webm new file mode 100644 index 0000000..ffe506f Binary files /dev/null and b/public/img/edge.webm differ diff --git a/public/views/index.html b/public/views/index.html index 0a3373f..99d4e8c 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -15,9 +15,7 @@