What is your feedback with Flutter?
Is it easy to work with?
Which tutorial do you recommend?
I think Dart and Flutter is amazing and getting better with updates almost every day.
The best tutorial I saw is on AppBrewery "The Complete 2021 Flutter Development Bootcamp with Dart", Flutter/Dart changed a bit since that course, especially with null-safety, but it's not to bad to figure out what need to change. Really good progression into Dart and Flutter, covering basic design all the way to using and writing APIs, Interfaces, Services (in Delphi-speak).
The whole idea of 'Widgets' in Flutter to dynamically build the UI elements (sometimes on each frame), seems weird at first, but it is so to create dynamic, multi-platform apps. Everything, even an Integer is an Object and everything in the UI is a Widget, so the language get really expressive and compact (sometimes).
I'm not turning back, no more expensive Delphi licenses, which I only need for client-side UI apps, but thanks to mORMot, not for server![8-}>
https://appbrewery.com/courses/enrolled/548873
I have not tested/deployed a full-blown non-debug app, to see performance, etc, yet; Almost...
Good luck!
Anton E
Thank you !
]]> // - reOneSessionPerUser will force that only one session may be created
// for one user, even if connection comes from the same IP: in this case,
// you may have to set the SessionTimeOut to a small value, in case the
// session is not closed gracefully
The same user credential could be used in several places at the same time, each connection with its own session.
This is a feature of the current implementation, and changing it could break a lot of existing code.
Ok, I thought maybe I could do it by changing the value of some property I can possibly do it in some server event, any suggestions?
So a single session per user could be added as an option.
Do you still use mORMot 1?
Yes, I'm still on 1
]]>So a single session per user could be added as an option.
Do you still use mORMot 1?
Is it possible to invalidate an already created user session on the server if the same user login again on another device, browser, etc. let's say the user logs in on one computer, he has his sessionkey for signing requests, then the same user logs in on another computer and now gets another session key and is making signed requests from two places, is it possible that on the second login the first session is invalidated ?
]]>Nice to read this Dart code.
What is your feedback with Flutter?
Is it easy to work with?
Which tutorial do you recommend?
Many thanks to the posters above, I couldn't have done it without you.
FWIW, if anyone need to auth to mORMot from Dart:
import 'dart:convert';
import 'package:archive/archive.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart';
main() async {
MORMOT_client http_client = MORMOT_client('http://10.1.1.200:8092/','main/');
await http_client.login('User', 'synopse');
final List<dynamic> products = await http_client.GetList('product');
print(products);
final Map<String,dynamic> product = await http_client.GetItem('product/106');
print(product);
}
class MORMOT_client {
MORMOT_client(String this.SERVER_URL,String this.SERVER_MODEL){}
String SERVER_URL = '';
String SERVER_MODEL = '';
String SESSION_ID = '';
String SESSION_PRIVATEKEY = '';
String SESSION_USERNAME = '';
int SESSION_TICK_COUNT = 1;
int SESSION_START_TIME = 0;
login(String userName, String userPass) async {
String clientnonce = '';
String servernonce = '';
String password = '';
String hashedPassword = '';
SESSION_ID = '';
SESSION_PRIVATEKEY = '';
SESSION_USERNAME = userName;
SESSION_TICK_COUNT = 1;
SESSION_START_TIME = DateTime.now().millisecondsSinceEpoch;
clientnonce = SHA256((DateTime.now().millisecondsSinceEpoch ~/ (1000 * 60 * 5)).toString()).toString();
final Map<String, dynamic> sessionResult = await urlToMap(SERVER_URL+SERVER_MODEL+'auth?Username=$userName');
if (sessionResult.isEmpty) {return;}
servernonce = sessionResult['result'];
hashedPassword = SHA256('salt' + userPass).toString();
password = SHA256('main' + servernonce + clientnonce + userName + hashedPassword).toString();
final Map<String, dynamic> authResult = await urlToMap(SERVER_URL+SERVER_MODEL+'auth?Username=$userName&Password=$password&ClientNonce=$clientnonce');
if (authResult.isEmpty) {return;}
String session_str = authResult['result'].toString();
SESSION_ID = int.parse(session_str.split('+')[0]).toRadixString(16).padLeft(8, '0');
SESSION_PRIVATEKEY = session_str + hashedPassword;
print('SID:$SESSION_ID');
print('SPK:$SESSION_PRIVATEKEY');
}
String GetSessionSignature(String url) {
String? prefix;
String? nonce;
String? keynonce;
String? final_SIGN;
SESSION_TICK_COUNT = DateTime.now().millisecondsSinceEpoch - SESSION_START_TIME;
print('SessionTickCount:$SESSION_TICK_COUNT');
nonce = SESSION_TICK_COUNT.toRadixString(16).padLeft(8, '0');
keynonce = getCrc32(utf8.encode(SESSION_PRIVATEKEY + nonce + SERVER_MODEL+url)).toRadixString(16).padLeft(8, '0');
print('nonce :$nonce');
print('keynonce:$keynonce');
return SESSION_ID + nonce + keynonce;
}
Future<List<dynamic>> GetList(String url)async{
final String signature = GetSessionSignature(url);
final String prefix = (url.indexOf('?')>0) ? '&' : '?';
final String theUrl = SERVER_URL+SERVER_MODEL+url+prefix+'session_signature=$signature';
print('URL:$theUrl');
final List<dynamic> auth2 = await urlToList(theUrl);
print(auth2);
return auth2;
}
Future<Map<String,dynamic>> GetItem(String url)async{
final String signature = GetSessionSignature(url);
final String prefix = (url.indexOf('?')>0) ? '&' : '?';
final String theUrl = SERVER_URL+SERVER_MODEL+url+prefix+'session_signature=$signature';
print('URL:$theUrl');
final Map<String,dynamic> item = await urlToMap(theUrl);
print(item);
return item;
}
}
Digest SHA256(String value){
var bytes = utf8.encode(value);
return sha256.convert(bytes);
}
Future<Map<String,dynamic>> urlToMap(String url)async {
Response r = await get(Uri.parse(url));
if (r.statusCode != 200) {
print(r.statusCode);
print(r.body);
final Map<String,dynamic> tmp = {};
return tmp;
}
final Map<String, dynamic> res = jsonDecode(r.body);
return res;
}
Future<List<dynamic>> urlToList(String url)async {
Response r = await get(Uri.parse(url));
if (r.statusCode != 200) {
print(r.statusCode);
print(r.body);
final List<dynamic> tmp = [];
return tmp;
}
final List<dynamic> res = jsonDecode(r.body);
return res;
}
It is a very raw testing unit, needs exception handling etc.
Output an array(list) and object(map):
[{ID: 1}, {ID: 3}, {ID: 4}, {ID: 5}, {ID: 10},...
{ID: 106, Created: 0, Modified: 135522307659, CODE: SLUC40MBPS, DESCRIPTION: SLUC40MBPS...
]]>I am trying to use this js library in my application. It worked fine with sqlite db but when i use MongoDB, login funtion returned false. Server also responed to /auth parameter as "Bad Request". How could i make it work with MongoDB?
]]>No problem, repository created.
checked out and starred, thank you!
]]>GitHub repository: https://github.com/imperyal/synopse-login
/**********************************************************************************************/
/* */
/* ======= Synopse login class ======= */
/* */
/* */
/* - Based on RangerX's code ( https://synopse.info/forum/viewtopic.php?pid=2995#p2995 ) */
/* - Requires JQuery */
/* - Requires sha256 (https://github.com/emn178/js-sha256) */
/* - crc32 code included (from https://stackoverflow.com/questions/18638900/javascript-crc32) */
/* - Uses Localstorage to store session data like in the original code */
/* */
/* */
/* Example usage: */
/* */
/* -> Config variables (set before using the class) */
/* */
/* var G_SERVER_URL = "http://127.0.0.1:8080"; // Server URL */
/* var G_SERVER_ROOT = "root"; // Server root */
/* var G_MAIN_URL = G_SERVER_URL + '/' + G_SERVER_ROOT; // Main URL */
/* */
/* */
/* -> Login */
/* */
/* const APP_Login = new SYN_login; */
/* APP_Login.login(userName, userPass, F_loginResult); */
/* */
/* function F_loginResult(result) { */
/* if (result) { alert("Login OK"); } */
/* else { alert("Login ERROR"); } */
/* } */
/* */
/* -> Use $.ajax to call your interfaces, etc.. */
/* */
/**********************************************************************************************/
class SYN_login {
login(userName, usarPass, callBack) {
let servnonce;
let currDate;
let clientnonce;
let dataString;
let password;
let charPlusPos;
let self = this;
this.setAjaxPrefilter();
this.CloseSession(); // try to close previously opened session
currDate = new Date();
clientnonce = currDate.getTime() / (1000 * 60 * 5); // valid for 5*60*1000 ms = 5 minutes;
clientnonce = sha256("" + clientnonce);
dataString = {'UserName': userName};
// First request, to get the servnonce for the user
$.ajax({
type: "GET",
dataType: "json",
url: G_MAIN_URL + '/auth',
data: dataString,
timeout: 2000,
success: function(data, textStatus, jqXHR) {
servnonce = data.result;
password = sha256(G_SERVER_ROOT + servnonce + clientnonce + userName + sha256('salt' + usarPass)); // Sha256(ModelRoot+Nonce+ClientNonce+UserName+Sha256('salt'+PassWord))
dataString = {'UserName': userName, 'Password': password, 'ClientNonce': clientnonce};
// Secound request, sending required data including the spicedup password, to get a session
$.ajax({
type: "GET",
dataType: "json",
url: G_MAIN_URL + '/auth',
data: dataString,
crossDomain: true,
timeout: 2000,
success: function(data, textStatus, jqXHR) {
charPlusPos = data.result.indexOf('+');
if (charPlusPos > -1) {
// ******************************************
// Save relevant session data on localstorage
self.setNameValue('SESSION_ID', data.result.substr(0, charPlusPos));
self.setNameValue('SESSION_PRIVATE_KEY', data.result + sha256('salt' + usarPass));
self.setNameValue('SESSION_USERNAME', userName);
callBack(true);
return true;
}
},
error: function (jqXHR, textStatus, errorThrown) {
callBack(false);
return false;
if (jqXHR.status == 404) {return false;} // Not used so far
}
});
},
error: function() {
callBack(false);
return false;
}
});
}
InitSession() {
localStorage.removeItem(self.getPrefixed('SESSION_ID'));
localStorage.removeItem(self.getPrefixed('SESSION_PRIVATE_KEY'));
localStorage.removeItem(self.getPrefixed('SESSION_LAST_TICK_COUNT'));
localStorage.removeItem(self.getPrefixed('SESSION_TICK_COUNT_OFFSET'));
localStorage.removeItem(self.getPrefixed('SESSION_USERNAME'));
return true;
}
CloseSession() {
self = this;
if (!this.getValue_FromNameAsInt('SESSION_ID')) return;
$.ajax({
type: "GET",
dataType: "json",
url: G_MAIN_URL + '/auth',
data: {'session': this.getValue_FromNameAsInt('SESSION_ID'), 'UserName': this.getValue_FromName('SESSION_USERNAME')},
timeout: 2000,
success: self.InitSession,
error: self.InitSession
});
}
// converted from TSQLRestClientURI.SessionSign function
// expected format is 'session_signature='Hexa8(SessionID)+Hexa8(TimeStamp)+
// Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
// Hexa8(TimeStamp)+url))
GetSessionSignature(url) {
let currDate;
let currMsecs;
let prefix;
let nonce;
let ss_id_hex;
let ss_keyNonceUrl_crc32;
let ss_keyNonceUrl_hex;
let final_SIGN;
currDate = new Date();
currMsecs = currDate.getTime();
prefix = '?';
if (currMsecs < this.getValue_FromNameAsInt('SESSION_LAST_TICK_COUNT')) // wrap around 0 after 49.7 days
this.setNameValue('SESSION_TICK_COUNT_OFFSET', this.getValue_FromNameAsInt('SESSION_TICK_COUNT_OFFSET') + 1 << (32 - 8)); // allows 35 years timing
this.setNameValue('SESSION_LAST_TICK_COUNT', currMsecs);
nonce = currMsecs >>> 8 + this.getValue_FromNameAsInt('SESSION_TICK_COUNT_OFFSET');
nonce = this.numToHex(nonce);
ss_id_hex = this.numToHex(this.getValue_FromNameAsInt('SESSION_ID'));
ss_keyNonceUrl_crc32 = this.getValue_FromName('SESSION_PRIVATE_KEY') + nonce + url;
ss_keyNonceUrl_crc32 = this.crc32( ss_keyNonceUrl_crc32);
ss_keyNonceUrl_hex = this.numToHex(ss_keyNonceUrl_crc32);
// Final signature
final_SIGN = ss_id_hex + nonce + ss_keyNonceUrl_hex;
// Change prefix if necessary (if the URL already has variables add "&" to set another, keep "?" is this is the only one)
if (url.indexOf('?') >= 0)
prefix = '&';
return prefix + 'session_signature=' + final_SIGN;
}
// Set ajaxPrefilter function - will run on every jQuery ajax call to add the SessionSignature */
setAjaxPrefilter() {
self = this;
$.ajaxPrefilter(function(options, _, jqXHR) {
let new_url;
let session_sign;
if (self.getValue_FromNameAsInt('SESSION_ID') > 0 && options.url.indexOf(G_MAIN_URL) > -1) { // User is authenticated
new_url = options.url;
if (options.data && options.type == "GET")
{
new_url = new_url + '?' + options.data;
options.data = null; // prevents jQuery from adding these to the URL
}
session_sign = self.GetSessionSignature(new_url.substr(G_SERVER_URL.length + 1));
options.url = new_url + session_sign;
options.cache = true; // we don't want anti-cache "_" JQuery-parameter
}
});
}
// Convert number to Hex with 8 caracters
numToHex(d) {
let hex = Number(d).toString(16); // Converts to Hex (base 16)
while (hex.length < 8) {
hex = "0" + hex;
}
return hex;
}
/****************************/
/* Local Storage */
/****************************/
getPrefixed(name) { return 'syn_' + name; }
getValue_FromName(name) { return localStorage.getItem(this.getPrefixed(name)); }
setNameValue(name, value) { return localStorage.setItem(this.getPrefixed(name), value); }
getValue_FromNameAsInt(name) { return Number(this.getValue_FromName(name)) ? this.getValue_FromName(name) : 0; } // Operator "?" = if then ":"" = else
/*****************************************************************/
/* crc32 functions */
/* https://stackoverflow.com/questions/18638900/javascript-crc32 */
/*****************************************************************/
makeCRCTable() {
let c;
let crcTable = [];
for(let n =0; n < 256; n++){
c = n;
for(let k =0; k < 8; k++){
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
}
return crcTable;
}
crc32(str) {
let crcTable = window.crcTable || (window.crcTable = this.makeCRCTable());
let crc = 0 ^ (-1);
for (let i = 0; i < str.length; i++ ) {
crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
};
}
/*********************************************/
/* Check for localstorage functionality */
/*********************************************/
$(function() {
if (typeof(localStorage) == 'undefined')
alert('You do not have HTML5 localStorage support in your browser. Please update or application cannot work as expected');
});