Browse Source

Merge pull request #11 from Foltik/mean

Mean
production
Jack Foltz GitHub 6 years ago
parent
commit
d3bef194ff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 686 additions and 165 deletions
  1. +4
    -0
      app/models/Invite.js
  2. +1
    -0
      app/models/Upload.js
  3. +6
    -1
      app/models/User.js
  4. +31
    -5
      app/public/css/form.css
  5. +1
    -3
      app/public/css/index.css
  6. +46
    -37
      app/public/panel/controllers/ApiCtrl.js
  7. +64
    -0
      app/public/panel/controllers/InviteCtrl.js
  8. +1
    -2
      app/public/panel/controllers/NavCtrl.js
  9. +9
    -0
      app/public/panel/controllers/UserCtrl.js
  10. +1
    -1
      app/public/panel/shimapan-panel.js
  11. +6
    -4
      app/public/services/ApiSvc.js
  12. +8
    -6
      app/public/services/AuthSvc.js
  13. +58
    -0
      app/public/services/InviteSvc.js
  14. +22
    -0
      app/public/services/UserSvc.js
  15. +6
    -1
      app/public/shimapan/components/LoginComp.js
  16. +6
    -1
      app/public/shimapan/components/RegisterComp.js
  17. +1
    -1
      app/public/shimapan/components/UploadComp.js
  18. +117
    -51
      app/routes/auth.js
  19. +78
    -0
      app/routes/invites.js
  20. +16
    -3
      app/routes/keys.js
  21. +1
    -1
      app/routes/login.js
  22. +1
    -1
      app/routes/panel.js
  23. +1
    -1
      app/routes/register.js
  24. +32
    -5
      app/routes/routes.js
  25. +77
    -24
      app/routes/upload.js
  26. +33
    -0
      app/routes/users.js
  27. +3
    -1
      gulpfile.js
  28. BIN
      public/img/edge.webm
  29. +1
    -3
      public/views/index.html
  30. +7
    -7
      public/views/panel.html
  31. +3
    -3
      public/views/panel/api.html
  32. +14
    -0
      public/views/panel/invites.html
  33. +18
    -0
      public/views/panel/users.html
  34. +1
    -1
      public/views/shimapan/login-form.html
  35. +1
    -1
      public/views/shimapan/register-form.html
  36. +0
    -1
      server.js
  37. +11
    -0
      test/permissions.txt

+ 4
- 0
app/models/Invite.js View File

@@ -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);

+ 1
- 0
app/models/Upload.js View File

@@ -11,6 +11,7 @@ var UploadSchema = mongoose.Schema({
default: 0
},
uploader: String,
uploadKey: String,
date: Date,
file: Object
});


+ 6
- 1
app/models/User.js View File

@@ -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);

+ 31
- 5
app/public/css/form.css View File

@@ -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);
}
}

+ 1
- 3
app/public/css/index.css View File

@@ -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;


+ 46
- 37
app/public/panel/controllers/ApiCtrl.js View File

@@ -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);
};
}]);

+ 64
- 0
app/public/panel/controllers/InviteCtrl.js View File

@@ -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();
}
});
};
}]);

+ 1
- 2
app/public/panel/controllers/NavCtrl.js View File

@@ -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;
};

}]);

+ 9
- 0
app/public/panel/controllers/UserCtrl.js View File

@@ -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;
});
};
}]);

+ 1
- 1
app/public/panel/shimapan-panel.js View File

@@ -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;


+ 6
- 4
app/public/services/ApiSvc.js View File

@@ -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


+ 8
- 6
app/public/services/AuthSvc.js View File

@@ -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';
});
};



+ 58
- 0
app/public/services/InviteSvc.js View File

@@ -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);
});
};
}]);

+ 22
- 0
app/public/services/UserSvc.js View File

@@ -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);
});
};
}]);

+ 6
- 1
app/public/shimapan/components/LoginComp.js View File

@@ -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);
});
};
}]

+ 6
- 1
app/public/shimapan/components/RegisterComp.js View File

@@ -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);
});
};
}]

+ 1
- 1
app/public/shimapan/components/UploadComp.js View File

@@ -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.";


+ 117
- 51
app/routes/auth.js View File

@@ -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);
});
}

// 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);
});
}

function useInvite(code, username) {
Invite.updateOne({code: code}, {recipient: username, used: new Date()}, function (err) {
if (err) throw err;
// 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, 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.'});
});
});
}
);
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;

+ 78
- 0
app/routes/invites.js View File

@@ -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;

+ 16
- 3
app/routes/keys.js View File

@@ -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.'});


+ 1
- 1
app/routes/login.js View File

@@ -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'));
});


+ 1
- 1
app/routes/panel.js View File

@@ -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'));
});


+ 1
- 1
app/routes/register.js View File

@@ -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'));
});


+ 32
- 5
app/routes/routes.js View File

@@ -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});


+ 77
- 24
app/routes/upload.js View File

@@ -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 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);
});
}
};

var entry = {
name: genFileName(),
uploader: req.session.passport.user,
created: Date.now(),
file: req.file
};
function uploadFile(req, res, type, key) {
if (!req.file)
return res.status(400).json({'message': 'No file specified.'});

Upload.create(entry, function (err, next) {
if (err) {
next(err);
} else {
res.send({
// 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;

+ 33
- 0
app/routes/users.js View File

@@ -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;

+ 3
- 1
gulpfile.js View File

@@ -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'));
});



BIN
public/img/edge.webm View File


+ 1
- 3
public/views/index.html View File

@@ -15,9 +15,7 @@
</video>
<script>
var videos = [
"/img/edge.mp4",
"/img/endthis.mp4",
"/img/hell.mp4"
"/img/edge.webm"
];
var video = document.createElement("source");
video.setAttribute("src", videos[Math.floor(Math.random() * videos.length)]);


+ 7
- 7
public/views/panel.html View File

@@ -12,13 +12,13 @@
<div class="sidebar" ng-controller="NavController">
<div class="sidebar-title" ng-click="$state.go('home')"><span>Shimapan</span></div>
<ul class="nav">
<li><a ui-sref="dashboard" ng-class="{active: $state.$current.name=='dashboard'}">Dashboard</a></li>
<li><a ui-sref="search" ng-class="{active: $state.$current.name=='search'}">Search</a></li>
<li><a ui-sref="api" ng-class="{active: $state.$current.name=='api'}">API</a></li>
<li><a ui-sref="invites" ng-class="{active: $state.$current.name=='invites'}">Invites</a></li>
<li ng-hide="!hasPermission('users.view')"><a ui-sref="users" ng-class="{active: $state.$current.name=='users'}">Users</a></li>
<li><a ui-sref="stats" ng-class="{active: $state.$current.name=='stats'}">Statistics</a></li>
<li><a ng-click="logout()">Logout</a></li>
<li><a draggable="false" ui-sref="dashboard" ng-class="{active: $state.$current.name=='dashboard'}">Dashboard</a></li>
<li><a draggable="false" ui-sref="search" ng-class="{active: $state.$current.name=='search'}">Search</a></li>
<li><a draggable="false" ui-sref="api" ng-class="{active: $state.$current.name=='api'}">API</a></li>
<li><a draggable="false" ui-sref="invites" ng-class="{active: $state.$current.name=='invites'}">Invites</a></li>
<li ng-hide="!hasPermission('users.view')"><a draggable="false" ui-sref="users" ng-class="{active: $state.$current.name=='users'}">Users</a></li>
<li><a draggable="false" ui-sref="stats" ng-class="{active: $state.$current.name=='stats'}">Statistics</a></li>
<li><a draggable="false" ng-click="logout()">Logout</a></li>
</ul>
</div>
<div class="content" ng-class="{isOpen: open}">


+ 3
- 3
public/views/panel/api.html View File

@@ -23,7 +23,7 @@
<p>This key can be used with any 3rd party program or service to upload to and manage your account
with Shimapan.</p>
<p>For example, it can be used in a bash script to upload from the command line:</p>
<pre>APIKEY=[Your API Key Here]<br/>URL=$(curl -s -F "apikey=$APIKEY" -F "file=@$1" https://shimapan.rocks/api/upload | grep url | awk '{print $2}')<br/>echo $URL | tr -d '[\\\,"\n]'</pre>
<pre>APIKEY=[Your API Key Here]<br/>URL=$(curl -s -F "apikey=$APIKEY" -F "file=@$1" https://shimapan.rocks/api/upload | grep -Po '"'"url"'"\s*:\s*"\K([^"]*)'<br/>echo $URL</pre>
<br/>
<p>Key Permissions:</p>
<table>
@@ -54,14 +54,14 @@
</div>
<div class="modal-body">
<p>Identifier to describe the purpose/use of this key:</p>
<input id="identifier" placeholder="Identifier" class="form-control" type="text" ng-model="ckIdentifier"/>
<input id="identifier" placeholder="Identifier" class="form-control" type="text" ng-model="currKeyIdentifier"/>
<br/>
<p>Permissions the key should have:</p>
<table>
<tr ng-repeat="(prefix, perms) in scopeObj">
<th>{{prefix}}:</th>
<td ng-repeat="perm in perms">
<input type="checkbox" name="{{perm.name}}" ng-model="perm.isChecked" ng-change="checkCkPerm(prefix, perm)"/>
<input type="checkbox" title="{{perm.name}}" name="{{perm.name}}" ng-model="perm.isChecked" ng-change="updateCurrKeyPerm(prefix, perm)"/>
<label for="{{perm.name}}" ng-bind="perm.name"></label>
</td>
</tr>


+ 14
- 0
public/views/panel/invites.html View File

@@ -0,0 +1,14 @@
<div class="inner" ng-controller="InviteController" ng-init="getInvites()">
<table class="invites">
<tr>
<th>Code</th>
<th>Scope</th>
<th>Expiry</th>
</tr>
<tr ng-repeat="invite in invites">
<td>{{invite.code}}</td>
<td>{{invite.scope}}</td>
<td>{{invite.expiry}}</td>
</tr>
</table>
</div>

+ 18
- 0
public/views/panel/users.html View File

@@ -0,0 +1,18 @@
<div class="inner" ng-controller="UserController" ng-init="getUsers()">
<table class="users">
<tr>
<th>Username</th>
<th>Scope</th>
<th>Upload Count</th>
<th>Upload Size</th>
<th>Date</th>
</tr>
<tr ng-repeat="user in users">
<td>{{user.username}}</td>
<td>{{user.scope}}</td>
<td>{{user.uploadCount}}</td>
<td>{{user.uploadSize}}</td>
<td>{{user.date}}</td>
</tr>
</table>
</div>

+ 1
- 1
public/views/shimapan/login-form.html View File

@@ -4,7 +4,7 @@
<form ng-submit="login()">
<input id="username" placeholder="Username" class="form-control" type="text" ng-model="username"/>
<input id="password" placeholder="Password" class="form-control" type="password" ng-model="password"/>
<button type="submit" class="btn">Submit</button>
<button type="submit" class="btn" ng-class="{shake: error}">Submit</button>
</form>
</fieldset>
</div>

+ 1
- 1
public/views/shimapan/register-form.html View File

@@ -5,7 +5,7 @@
<input id="username" placeholder="Username" class="form-control" type="text" ng-model="username"/>
<input id="password" placeholder="Password" class="form-control" type="password" ng-model="password"/>
<input id="invite" placeholder="Invite Code" class="form-control" type="text" ng-model="invite"/>
<button type="submit" class="btn">Submit</button>
<button type="submit" class="btn" ng-class="{shake: error}">Submit</button>
</form>
</fieldset>
</div>

+ 0
- 1
server.js View File

@@ -50,7 +50,6 @@ app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text());
app.use(sanitizer());
app.use(methodOverride('X-HTTP-Method-Override'));
app.use(passport.initialize());


//app.use(favicon(__dirname + '/public/img/favicon.ico'));


+ 11
- 0
test/permissions.txt View File

@@ -0,0 +1,11 @@
users
view
edit
ban

file
upload

api
create
delete

Loading…
Cancel
Save