login, logout, signup

upd4.2
Bernat Romagosa 2017-09-28 13:26:46 +02:00
rodzic 51998503a3
commit 13c05e3082
3 zmienionych plików z 171 dodań i 602 usunięć

729
cloud.js
Wyświetl plik

@ -4,8 +4,10 @@
a backend API for SNAP!
written by Jens Mönig
written by Bernat Romagosa
inspired in old cloud API by Jens Mönig
Copyright (C) 2017 by Bernat Romagosa
Copyright (C) 2015 by Jens Mönig
This file is part of Snap!.
@ -27,598 +29,31 @@
// Global settings /////////////////////////////////////////////////////
/*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop,
localize*/
/*global modules, IDE_Morph, SnapSerializer, nop, localize*/
modules.cloud = '2015-December-15';
// Global stuff
var Cloud;
var SnapCloud = new Cloud(
'https://snap.apps.miosoft.com/SnapCloud'
);
// Cloud /////////////////////////////////////////////////////////////
function Cloud(url) {
this.username = null;
this.password = null; // hex_sha512 hashed
this.init(url);
};
Cloud.prototype.init = function (url) {
this.url = url;
this.session = null;
this.limo = null;
this.route = null;
this.api = {};
}
this.username = null;
this.checkCredentials();
};
Cloud.prototype.clear = function () {
this.username = null;
this.password = null;
this.session = null;
this.limo = null;
this.route = null;
this.api = {};
};
Cloud.prototype.hasProtocol = function () {
return this.url.toLowerCase().indexOf('http') === 0;
};
Cloud.prototype.setRoute = function (username) {
var routes = 20,
userNum = 0,
i;
for (i = 0; i < username.length; i += 1) {
userNum += username.charCodeAt(i);
}
userNum = userNum % routes + 1;
this.route = '.sc1m' +
(userNum < 10 ? '0' : '') +
userNum;
};
// Cloud: Snap! API
Cloud.prototype.signup = function (
username,
email,
callBack,
errorCall
) {
// both callBack and errorCall are two-argument functions
var request = new XMLHttpRequest(),
myself = this;
try {
request.open(
"GET",
(this.hasProtocol() ? '' : 'http://')
+ this.url + 'SignUp'
+ '?Username='
+ encodeURIComponent(username)
+ '&Email='
+ encodeURIComponent(email),
true
);
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
if (request.responseText.indexOf('ERROR') === 0) {
errorCall.call(
this,
request.responseText,
'Signup'
);
} else {
callBack.call(
null,
request.responseText,
'Signup'
);
}
} else {
errorCall.call(
null,
myself.url + 'SignUp',
localize('could not connect to:')
);
}
}
};
request.send(null);
} catch (err) {
errorCall.call(this, err.toString(), 'Snap!Cloud');
}
};
Cloud.prototype.getPublicProject = function (
id,
callBack,
errorCall
) {
// id is Username=username&projectName=projectname,
// where the values are url-component encoded
// callBack is a single argument function, errorCall take two args
var request = new XMLHttpRequest(),
myself = this;
try {
request.open(
"GET",
(this.hasProtocol() ? '' : 'http://')
+ this.url + 'RawPublic'
+ '?'
+ id,
true
);
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
if (request.responseText.indexOf('ERROR') === 0) {
errorCall.call(
this,
request.responseText
);
} else {
callBack.call(
null,
request.responseText
);
}
} else {
errorCall.call(
null,
myself.url + 'Public',
localize('could not connect to:')
);
}
}
};
request.send(null);
} catch (err) {
errorCall.call(this, err.toString(), 'Snap!Cloud');
}
};
Cloud.prototype.resetPassword = function (
username,
callBack,
errorCall
) {
// both callBack and errorCall are two-argument functions
var request = new XMLHttpRequest(),
myself = this;
try {
request.open(
"GET",
(this.hasProtocol() ? '' : 'http://')
+ this.url + 'ResetPW'
+ '?Username='
+ encodeURIComponent(username),
true
);
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
if (request.responseText.indexOf('ERROR') === 0) {
errorCall.call(
this,
request.responseText,
'Reset Password'
);
} else {
callBack.call(
null,
request.responseText,
'Reset Password'
);
}
} else {
errorCall.call(
null,
myself.url + 'ResetPW',
localize('could not connect to:')
);
}
}
};
request.send(null);
} catch (err) {
errorCall.call(this, err.toString(), 'Snap!Cloud');
}
};
Cloud.prototype.login = function (
username,
password,
callBack,
errorCall
) {
// both callBack and errorCall are two-argument functions
var request = new XMLHttpRequest(),
usr = JSON.stringify({'__h': password, '__u': username}),
myself = this;
this.setRoute(username);
try {
request.open(
"POST",
(this.hasProtocol() ? '' : 'http://') +
this.url +
'?SESSIONGLUE=' +
this.route,
true
);
request.setRequestHeader(
"Content-Type",
"application/json; charset=utf-8"
);
// glue this session to a route:
request.setRequestHeader('SESSIONGLUE', this.route);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
myself.api = myself.parseAPI(request.responseText);
myself.session = request.getResponseHeader('MioCracker')
.split(';')[0];
// set the cookie identifier:
myself.limo = this.getResponseHeader("miocracker")
.substring(
9,
this.getResponseHeader("miocracker").indexOf("=")
);
if (myself.api.logout) {
myself.username = username;
myself.password = password;
callBack.call(null, myself.api, 'Snap!Cloud');
} else {
errorCall.call(
null,
request.responseText,
'connection failed'
);
}
} else {
errorCall.call(
null,
myself.url,
localize('could not connect to:')
);
}
}
};
request.send(usr);
} catch (err) {
errorCall.call(this, err.toString(), 'Snap!Cloud');
}
};
Cloud.prototype.reconnect = function (
callBack,
errorCall
) {
if (!(this.username && this.password)) {
this.message('You are not logged in');
return;
}
this.login(
this.username,
this.password,
callBack,
errorCall
);
};
Cloud.prototype.saveProject = function (ide, callBack, errorCall) {
var myself = this,
pdata,
media,
size,
mediaSize;
ide.serializer.isCollectingMedia = true;
pdata = ide.serializer.serialize(ide.stage);
media = ide.hasChangedMedia ?
ide.serializer.mediaXML(ide.projectName) : null;
ide.serializer.isCollectingMedia = false;
ide.serializer.flushMedia();
mediaSize = media ? media.length : 0;
size = pdata.length + mediaSize;
if (mediaSize > 10485760) {
new DialogBoxMorph().inform(
'Snap!Cloud - Cannot Save Project',
'The media inside this project exceeds 10 MB.\n' +
'Please reduce the size of costumes or sounds.\n',
ide.world(),
ide.cloudIcon(null, new Color(180, 0, 0))
);
throw new Error('Project media exceeds 10 MB size limit');
}
// check if serialized data can be parsed back again
try {
ide.serializer.parse(pdata);
} catch (err) {
ide.showMessage('Serialization of program data failed:\n' + err);
throw new Error('Serialization of program data failed:\n' + err);
}
if (media !== null) {
try {
ide.serializer.parse(media);
} catch (err) {
ide.showMessage('Serialization of media failed:\n' + err);
throw new Error('Serialization of media failed:\n' + err);
}
}
ide.serializer.isCollectingMedia = false;
ide.serializer.flushMedia();
ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...');
myself.reconnect(
function () {
myself.callService(
'saveProject',
function (response, url) {
callBack.call(null, response, url);
myself.disconnect();
ide.hasChangedMedia = false;
},
errorCall,
[
ide.projectName,
pdata,
media,
pdata.length,
media ? media.length : 0
]
);
},
errorCall
);
};
Cloud.prototype.getProjectList = function (callBack, errorCall) {
var myself = this;
this.reconnect(
function () {
myself.callService(
'getProjectList',
function (response, url) {
callBack.call(null, response, url);
myself.disconnect();
},
errorCall
);
},
errorCall
);
};
Cloud.prototype.changePassword = function (
oldPW,
newPW,
callBack,
errorCall
) {
var myself = this;
this.reconnect(
function () {
myself.callService(
'changePassword',
function (response, url) {
callBack.call(null, response, url);
myself.disconnect();
},
errorCall,
[hex_sha512(oldPW), hex_sha512(newPW)]
);
},
errorCall
);
};
Cloud.prototype.logout = function (callBack, errorCall) {
this.clear();
this.callService(
'logout',
callBack,
errorCall
);
};
Cloud.prototype.disconnect = function () {
this.callService(
'logout',
nop,
nop
);
};
// Cloud: backend communication
Cloud.prototype.callURL = function (url, callBack, errorCall) {
// both callBack and errorCall are optional two-argument functions
var request = new XMLHttpRequest(),
stickyUrl,
myself = this;
try {
// set the Limo. Also set the glue as a query paramter for backup.
stickyUrl = url +
'&SESSIONGLUE=' +
this.route +
'&_Limo=' +
this.limo;
request.open('GET', stickyUrl, true);
request.withCredentials = true;
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
request.setRequestHeader('MioCracker', this.session);
// Set the glue as a request header.
request.setRequestHeader('SESSIONGLUE', this.route);
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
var responseList = myself.parseResponse(
request.responseText
);
callBack.call(null, responseList, url);
} else {
errorCall.call(
null,
url,
'no response from:'
);
}
}
};
request.send(null);
} catch (err) {
errorCall.call(this, err.toString(), url);
}
};
Cloud.prototype.callService = function (
serviceName,
callBack,
errorCall,
args
) {
// both callBack and errorCall are optional two-argument functions
var request = new XMLHttpRequest(),
service = this.api[serviceName],
myself = this,
stickyUrl,
postDict;
if (!this.session) {
errorCall.call(null, 'You are not connected', 'Cloud');
return;
}
if (!service) {
errorCall.call(
null,
'service ' + serviceName + ' is not available',
'API'
);
return;
}
if (args && args.length > 0) {
postDict = {};
service.parameters.forEach(function (parm, idx) {
postDict[parm] = args[idx];
});
}
try {
stickyUrl = this.url +
'/' +
service.url +
'&SESSIONGLUE=' +
this.route +
'&_Limo=' +
this.limo;
request.open(service.method, stickyUrl, true);
request.withCredentials = true;
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
request.setRequestHeader('MioCracker', this.session);
request.setRequestHeader('SESSIONGLUE', this.route);
request.onreadystatechange = function () {
if (request.readyState === 4) {
var responseList = [];
if (request.responseText &&
request.responseText.indexOf('ERROR') === 0) {
errorCall.call(
this,
request.responseText,
localize('Service:') + ' ' + localize(serviceName)
);
return;
}
if (serviceName === 'login') {
myself.api = myself.parseAPI(request.responseText);
}
if (serviceName === 'getRawProject') {
responseList = request.responseText;
} else {
responseList = myself.parseResponse(
request.responseText
);
}
callBack.call(null, responseList, service.url);
}
};
request.send(this.encodeDict(postDict));
} catch (err) {
errorCall.call(this, err.toString(), service.url);
}
};
// Cloud: payload transformation
Cloud.prototype.parseAPI = function (src) {
var api = {},
services;
services = src.split(" ");
services.forEach(function (service) {
var entries = service.split("&"),
serviceDescription = {},
parms;
entries.forEach(function (entry) {
var pair = entry.split("="),
key = decodeURIComponent(pair[0]).toLowerCase(),
val = decodeURIComponent(pair[1]);
if (key === "service") {
api[val] = serviceDescription;
} else if (key === "parameters") {
parms = val.split(",");
if (!(parms.length === 1 && !parms[0])) {
serviceDescription.parameters = parms;
}
} else {
serviceDescription[key] = val;
}
});
});
return api;
};
Cloud.prototype.parseResponse = function (src) {
var ans = [],
lines;
if (!src) {return ans; }
lines = src.split(" ");
lines.forEach(function (service) {
var entries = service.split("&"),
dict = {};
entries.forEach(function (entry) {
var pair = entry.split("="),
key = decodeURIComponent(pair[0]),
val = decodeURIComponent(pair[1]);
dict[key] = val;
});
ans.push(dict);
});
return ans;
};
// Dictionary handling
Cloud.prototype.parseDict = function (src) {
var dict = {};
@ -651,8 +86,142 @@ Cloud.prototype.encodeDict = function (dict) {
return str;
};
// Cloud: user messages (to be overridden)
// Low level functionality
Cloud.prototype.message = function (string) {
alert(string);
Cloud.prototype.get = function (path, onSuccess, onError, errorMsg) {
var request = new XMLHttpRequest(),
myself = this;
try {
request.open(
'GET',
this.url + path,
true
);
request.setRequestHeader(
'Content-Type',
'application/json; charset=utf-8'
);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
var response = JSON.parse(request.responseText);
if (response.errors) {
onError.call(
null,
response.errors[0],
errorMsg
);
} else {
onSuccess.call(null, response.message || response);
}
} else {
onError.call(
null,
myself.url,
errorMsg
);
}
}
};
request.send();
} catch (err) {
onError.call(this, err.toString(), 'Cloud Error');
}
};
Cloud.prototype.post = function (path, body, onSuccess, onError, errorMsg) {
var request = new XMLHttpRequest(),
myself = this;
try {
request.open(
'POST',
this.url + path,
true
);
request.setRequestHeader(
'Content-Type',
'application/json'
);
request.withCredentials = true;
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.responseText) {
var response = JSON.parse(request.responseText);
if (response.errors) {
onError.call(
this,
response.errors[0],
'Cloud Error'
);
} else {
onSuccess.call(null, response.message || response);
}
} else {
onError.call(
null,
myself.url + path,
localize('could not connect to:')
);
}
}
};
request.send(body);
} catch (err) {
onError.call(this, err.toString(), 'Cloud Error');
}
};
// Credentials management
Cloud.prototype.checkCredentials = function (onSuccess, onError) {
var myself = this;
this.getCurrentUser(
function (user) {
if (user.username) {
myself.username = user.username;
}
if (onSuccess) { onSuccess.call(null, user.username); }
},
onError);
};
Cloud.prototype.getCurrentUser = function (onSuccess, onError) {
this.get('/users/c', onSuccess, onError, 'Could not retrieve current user');
};
Cloud.prototype.logout = function (onSuccess, onError) {
this.post(
'/users/' + this.username + '/logout',
null,
onSuccess,
onError,
'logout failed');
};
Cloud.prototype.login = function (username, password, onSuccess, onError) {
var myself = this;
this.post(
'/users/' + username + '/login?' + this.encodeDict({ password: password }),
null,
function () {
myself.checkCredentials(onSuccess, onError);
},
onError,
'login failed');
};
Cloud.prototype.signup = function (username, password, password_repeat, email, onSuccess, onError){
this.post(
'/users/' + username + '?' + this.encodeDict({
email: email,
password: password,
password_repeat: password_repeat
}),
null,
onSuccess,
onError,
'signup failed');
};
var SnapCloud = new Cloud('http://localhost:8080');

33
gui.js
Wyświetl plik

@ -272,14 +272,16 @@ IDE_Morph.prototype.openIn = function (world) {
if (this.hasLocalStorage()) {
usr = localStorage['-snap-user'];
if (usr) {
usr = SnapCloud.parseResponse(usr)[0];
if (usr) {
SnapCloud.username = usr.username || null;
SnapCloud.password = usr.password || null;
if (SnapCloud.username) {
this.source = 'cloud';
SnapCloud.checkCredentials(
function (username) {
if (username) {
this.source = 'cloud';
}
},
function () {
delete localStorage['-snap-user'];
}
}
);
}
}
@ -5050,20 +5052,12 @@ IDE_Morph.prototype.initializeCloud = function () {
new DialogBoxMorph(
null,
function (user) {
var pwh = hex_sha512(user.password),
str;
SnapCloud.login(
user.username,
pwh,
user.password,
function () {
if (user.choice) {
str = SnapCloud.encodeDict(
{
username: user.username,
password: pwh
}
);
localStorage['-snap-user'] = str;
localStorage['-snap-user'] = user.username;
}
myself.source = 'cloud';
myself.showMessage('now connected.', 2);
@ -5098,13 +5092,14 @@ IDE_Morph.prototype.createCloudAccount = function () {
function (user) {
SnapCloud.signup(
user.username,
user.password,
user.passwordRepeat,
user.email,
function (txt, title) {
new DialogBoxMorph().inform(
title,
txt +
'.\n\nAn e-mail with your password\n' +
'has been sent to the address provided',
'.\n\nYou can now log in.',
world,
myself.cloudIcon(null, new Color(0, 180, 0))
);

Wyświetl plik

@ -2075,6 +2075,10 @@ DialogBoxMorph.prototype.promptCredentials = function (
emlLabel = labelText('foo');
inp.add(emlLabel);
inp.add(eml);
inp.add(labelText('Password:'));
inp.add(pw1);
inp.add(labelText('Repeat Password:'));
inp.add(pw2);
}
if (purpose === 'login') {
@ -2182,7 +2186,7 @@ DialogBoxMorph.prototype.promptCredentials = function (
if (purpose === 'login') {
checklist = [usr, pw1];
} else if (purpose === 'signup') {
checklist = [usr, bmn, byr, eml];
checklist = [usr, bmn, byr, eml, pw1, pw2];
} else if (purpose === 'changePassword') {
checklist = [opw, pw1, pw2];
} else if (purpose === 'resetPassword') {
@ -2205,12 +2209,12 @@ DialogBoxMorph.prototype.promptCredentials = function (
return false;
}
if (em.indexOf(' ') > -1 || em.indexOf('@') === -1
|| em.indexOf('.') === -1) {
|| em.indexOf('.') === -1 || em.length < 5) {
indicate(eml, 'please provide a valid\nemail address');
return false;
}
}
if (purpose === 'changePassword') {
if (purpose === 'changePassword' || purpose === 'signup') {
if (pw1.getValue().length < 6) {
indicate(pw1, 'password must be six\ncharacters or longer');
return false;
@ -2249,6 +2253,7 @@ DialogBoxMorph.prototype.promptCredentials = function (
email: eml.getValue(),
oldpassword: opw.getValue(),
password: pw1.getValue(),
passwordRepeat: pw2.getValue(),
choice: agree
};
};