Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
Lionel Laské committed Nov 21, 2021
2 parents 9bfb805 + 93cebc7 commit 4493d80
Show file tree
Hide file tree
Showing 36 changed files with 16,715 additions and 2,236 deletions.
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2021-11-21
### Added
- Two factor authentication
- Expose presence and server on the same port #232
- Add privacy settings

### Changed
- node.js minimal version is now 10+

### Fixed
- Unit tests are broken for activities #273
- Warning: Accessing non-existent property with node.js #272
- Error retrieving content when stored in UTF-16 #286


## [1.3.0] - 2020-10-11
### Added
- Provide a way to remove multiple users at the same time #217
Expand Down
44 changes: 29 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[Sugarizer](https://github.com/llaske/sugarizer) is the open source learning platform based on Sugar that began in the famous One Laptop Per Child project.

Sugarizer Server allows the deployment of Sugarizer on a local server, for example on a school server, so expose locally Sugarizer as a Web Application. Sugarizer Server can also be used to provide collaboration features for Sugarizer Application on the network. Sugarizer Server could be deployed in a Docker container or on any computer with Node.js 6+ and MongoDB 2.6+.
Sugarizer Server allows the deployment of Sugarizer on a local server, for example on a school server, so expose locally Sugarizer as a Web Application. Sugarizer Server can also be used to provide collaboration features for Sugarizer Application on the network. Sugarizer Server could be deployed in a Docker container or on any computer with Node.js 10+ and MongoDB 2.6+.


## Running Sugarizer Server
Expand Down Expand Up @@ -40,13 +40,20 @@ Following is the typical content of Sugarizer Server settings file:
port = 8080

[security]
min_password_size = 4
max_age = 172800000
https = false
certificate_file = ../server.crt
key_file = ../server.key
strict_ssl = false
no_signup_mode = false
min_password_size = 4
max_age = 172800000
max_age_TFA = 180000
https = false
certificate_file = ../server.crt
key_file = ../server.key
strict_ssl = false
no_signup_mode = false
service_name = Sugarizer Server
secret = super.sugarizer.server.key

[privacy]
consent_need = false
policy = https://sugarizer.org/policy.html

[client]
path = ../sugarizer/
Expand Down Expand Up @@ -84,12 +91,16 @@ The **[information]** section is for describing your server. It could be useful

The **[web]** section describes the settings of the node.js process. By default, the web server is on the port 8080.

The **[security]** section regroup security settings. `min_password_size` is the minimum number of characters for the password. `max_age` is the expiration time in milliseconds of a session with the client. At the expiration of the session, the client should reenter its password. Default time is 172800000 (48 hours). Parameters `https`, `certificate_file`, `key_file` and `strict_ssl` are explain above.
The **[security]** section regroup security settings. `min_password_size` is the minimum number of characters for the password. `max_age` is the expiration time in milliseconds of a session with the client. At the expiration of the session, the client should reenter its password. Default time is 172800000 (48 hours). Similarly, `max_age_TFA` is is the expiration time in milliseconds of a session with the client. At the expiration of the session, the client should reenter its password. The default time is 180000 (30 mins).Parameters `https`, `certificate_file`, `key_file` and `strict_ssl` are explain above.
It `no_signup_mode` is true, account creation is allowed only by an administrator or a teacher (no direct sign-up allowed by a student).
The `service_name` is the issuer parameter, a string value indicating the provider or service this account is associated with, URL-encoded according to [RFC 3986](http://tools.ietf.org/html/rfc3986).
The `secret` is the JWT Secret which is used to encrypt JSON Web Token. It should be replaced with a unique value to keep the SSP Server secure.

The **[privacy]** section describe privacy settings. When `consent_need` is set to true, the Sugarizer client will ask a consent to user before they will be allowed to do their first connection to the server. `policy` is the URL that Sugarizer client shown in consent popup displayed to user.

The **[client]** indicate the place where is located Sugarizer Client. Sugarizer Client is need by the server.

The **[presence]** section describes the settings of the presence server. By default, a web socket is created on port 8039. You need to change this value if you want to use another port.
The **[presence]** section describes the settings of the presence server. By default, a web socket is created on port 8039. You need to change this value if you want to use another port. You could use the same value than the one in the `web` port.

The **[database]** and **[collections]** sections are for MongoDB settings. You could update the server name (by default MongoDB run locally) and the server port. Names of the database and collections had no reason to be changed. The `waitdb` parameter allow you to force server to wait for the database. Optionally, the `replicaset` parameter can be set to `true` to enable MongoDB Replicaset support, in this case the server name becomes the replicaset connection string.

Expand Down Expand Up @@ -142,6 +153,7 @@ To implement the above functionalities, the sugarizer backend exposes an API. Th
#### USERS ROUTES

[POST] /auth/login
[POST] /auth/verify2FA
[POST] /auth/signup
[GET] /api/v1/users
[GET] /api/v1/users?name=tarun
Expand Down Expand Up @@ -198,6 +210,12 @@ To implement the above functionalities, the sugarizer backend exposes an API. Th
[POST] /api/v1/stats
[DELETE] /api/v1/stats

#### TWO FACTOR AUTHENTICATION ROUTES

[GET] /api/v1/dashboard/profile/enable2FA
[PUT] /api/v1/dashboard/profile/enable2FA
[PUT] /api/v1/dashboard/profile/disable2FA


A full documentation of the API is available in http://127.0.0.1:8080/docs.

Expand Down Expand Up @@ -290,11 +308,7 @@ Then launch Grunt task to minify Sugarizer JavaScript files:

grunt -v

After minification, the `build` directory will contain the optimized version of each file in the same directory as the initial one, so you could just copy files:

cp -r build/* .

Then navigate to Sugarizer-Server directory install the specific component for Sugarizer-Server by running:
Now navigate to Sugarizer-Server directory install the specific component for Sugarizer-Server by running:

npm install

Expand Down
79 changes: 73 additions & 6 deletions api/controller/auth.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
var jwt = require('jwt-simple'),
users = require('./users.js'),
mongo = require('mongodb'),
otplib = require('otplib'),
common = require('../../dashboard/helper/common');

var security;
var secret;

// Init settings
exports.init = function(settings) {
security = settings.security;
secret = settings.security.secret;
};

/**
Expand Down Expand Up @@ -110,9 +113,15 @@ exports.login = function(req, res) {
//take the first user incase of multple matches
user = users[0];

// If authentication is success, we will generate a token and dispatch it to the client
var maxAge = req.iniconfig.security.max_age;
res.send(genToken(user, maxAge));
var maxAgeTfa = req.iniconfig.security.max_age_TFA;
// If authentication is success, we will generate a token and dispatch it to the client
if (user.tfa === false || typeof user.tfa === "undefined") {
res.send(genToken(user, maxAge, false));
} else {
delete user.deployments;
res.send(genToken(user, maxAgeTfa, true)); //give users a buffer of 30 mins to verify.
}
} else {
res.status(401).send({
'error': "Invalid credentials",
Expand All @@ -122,6 +131,62 @@ exports.login = function(req, res) {
return;
});
};
exports.verify2FA = function(req, res) {

if (!req.body.userToken) {
return res.status(401).send({
'error': 'User Token not defined',
'code': 31
});
}

//token that the user entered
var uniqueToken = req.body.userToken;

var uid = req.user._id; // unique uid.

//find user by user id.
users.getAllUsers({
_id: new mongo.ObjectID(uid),
verified: {
$ne: false
}
}, {enableSecret: true}, function(users) {
if (users && users.length > 0) {

//take the first user incase of multple matches
var user = users[0];
var uniqueSecret = user.uniqueSecret;
try {
var isValid = otplib.authenticator.check(uniqueToken, uniqueSecret);
} catch (err) {
res.status(401).send({
'error': 'Could not verify OTP error in otplib',
'code': 32
});
}

var maxAge = req.iniconfig.security.max_age;
var maxAgeTfa = req.iniconfig.security.max_age_TFA;

if (isValid === true) {
delete user.uniqueSecret;
// refresh the user token and set partial to false.
res.send(genToken(user, maxAge, false));
} else {
delete user.deployments;
delete user.uniqueSecret;
res.send(genToken(user, maxAgeTfa, true));
}
} else {
res.status(401).send({
'error': "User not found",
'code': 1
});
}
return;
});
};

/**
* @api {post} auth/signup/ Signup User
Expand Down Expand Up @@ -216,7 +281,7 @@ function validateUsername(name, callback) {
callback(false);
}
});
};
}

exports.validateUser = function(uid, callback) {

Expand Down Expand Up @@ -307,16 +372,18 @@ exports.checkAdminOrLocal = function(req, res, next) {
};

// private method
function genToken(user, age) {
function genToken(user, age, partial) {
var expires = expiresIn(age);
var token = jwt.encode({
partial: partial,
exp: expires
}, require('../../config/secret')());
}, secret);

return {
token: token,
expires: expires,
user: user
user: user,
partial: partial
};
}

Expand Down
47 changes: 45 additions & 2 deletions api/controller/journal.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,46 @@ var sugarizerVersion = null;
var gridfsbucket, CHUNKS_COLL, FILES_COLL;

//- Utility functions
// Extract from https://gist.github.com/kongchen/941a652882d89bb96f87
function _toUTF8(str) {
var utf8 = [];
for (var i=0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff))
utf8.push(0xf0 | (charcode >>18),
0x80 | ((charcode>>12) & 0x3f),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
return utf8;
}
function _toUTF16(input) {
var i, str = '';

for (i = 0; i < input.length; i++) {
str += '%' + ('0' + input[i].toString(16)).slice(-2);
}
str = decodeURIComponent(str);
return str;
}

// Init database
exports.init = function(settings, database) {
Expand Down Expand Up @@ -395,7 +435,7 @@ exports.findJournalContent = function(req, res) {
if (resCount == reqCount) {
try {
var textObject = JSON.parse(items[ind].text);
items[ind].text = textObject.text;
items[ind].text = textObject.encoding ? _toUTF16(textObject.text) : textObject.text;
} catch (e) {
return res.status(500).send({'error': 'Invalid text value', 'code': 12});
}
Expand Down Expand Up @@ -662,10 +702,13 @@ exports.addEntryInJournal = function(req, res) {
// Add a new entry
if (journal.text) {
var text = journal.text;
var utftext = _toUTF8(journal.text);
var isUtf16 = (journal.text.length != utftext.length);
var filename = mongo.ObjectId();
var textContent = JSON.stringify({
text_type: typeof journal.text,
text: journal.text
text: isUtf16 ? utftext : journal.text,
encoding: isUtf16
});

streamifier.createReadStream(textContent)
Expand Down
Loading

0 comments on commit 4493d80

Please sign in to comment.