You are not logged in.
Pages: 1
Another feature, I used in my project: I want inform the user if it cannot update/delete record and why.
There is my modifications (not whole modifications shown, only main points):
SQLite3Commons.pas:
function TSQLRest.RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer; Action: TSQLEvent; [b]var ErrorMsg: RawUTF8[/b]): boolean; virtual;
function TSQLRestServer.URI(const url, method, SentData: RawUTF8;
var
...
[b]ErrorMsg: RawUTF8;[/b]
begin
...
if MethodUp='DELETE' then begin // DELETE
// ModelRoot/TableName/ID to delete a member
if (Table<>nil) and (ID>0) then
if not [b]RecordCanBeUpdated(Table,ID,seDelete, ErrorMsg)[/b] then // HTTP Forbidden
begin
result.Lo := 401;
[b]Resp := JSONEncode(['errorCode','401', 'errorText', ErrorMsg]);[/b]
end
else
if not (TableIndex in RestAccessRights^.DELETE) then // check User
begin
result.Lo := 401;
[b]Resp := JSONEncode(['errorCode','401', 'errorText', 'no access']);[/b]
end
else
...
Code from my project that uses this feature:
type
TMainServer = class(TSQLRestServerDB)
protected
function RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer; Action: TSQLEvent; var ErrorMsg: RawUTF8): boolean; override;
end;
...
function TMainServer.RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer;
Action: TSQLEvent; var ErrorMsg: RawUTF8): boolean;
begin
Result := True;
if (Table = TSQLBranches) and (ID = 1) and (Action = seDelete) then
begin
ErrorMsg := 'You cannot delete main branch!';
Result := False;
end;
end;
Perhaps more information (like SentData from URI?) should be passed to RecordCanBeUpdated for a more flexible implementation of business logic?
Example client-side code (javascript):
function globalErrorHandler(jqXHR, textStatus, errorThrown) {
$.unblockUI();
if (textStatus == "timeout")
create_notify("msgs", { title:'Timeout', text:'Sorry, server is down at the moment. Try again later', icon:'server_down.gif' });
else
{
switch (jqXHR.status) {
case 401:
if (textStatus == 'no access')
{
create_notify("msgs", { title:'Access denied', text:'You do not have permission to use this function', icon:'error48.png' });
setTimeout(function() {
document.location = '/pages/main_'+getSet('SESSION_GROUP')+'.htm';
}, 2000);
}
else
{
// custom error from server (from RecordCanBeUpdated)
eval('var response = ('+jqXHR.responseText+')');
create_notify("msgs", { title:'Record cannot be updated', text:response.errorText, icon:'error48.png' });
}
break;
case 403:
create_notify("msgs", { title:'Authentication error', text:'You do not authenticated and will be redirected to login page', icon:'error48.png' });
setTimeout(function() {
document.location = '/index.htm';
}, 2000);
break;
default: create_notify("msgs", { title:'Unknown error: ' + jqXHR.status, text:'Unknown error raised: ' + textStatus + ': ' + errorThrown, icon:'error48.png' });
}
}
jqXHR.abort();
return false;
}
$.ajaxSetup({error: globalErrorHandler});
Thanks for the feedback I will do it like Sir Rufo wrote.
TSQLBranch is just a example of what I want Besides, I want a few more fields in TSQLAuthUser class, like LastName, FirstName, Birthdate, etc... I can (surely) use a dynamic array of record with these fields. But then lost OOP-flexibility. Can you give me an example of TSQLRestServer.Create() constructor modification? Only a few lines of code that I understand the direction.
And my question: is there a way to override TSQLAuthUser model (i.e. TSQLMyAuthUser, without patching source code of mORMot, just native OOP) to add relationship with TSQLBranch model? I need a "One to many" relationship between two tables: AuthUser and Branches (ID, Name, Address). I don't want to use TSQLAuthUser.Data property, because too many code for work with simply integer ID-field
Code from my working project (it uses jQuery):
config.js:
// configuration section
// URL of application server
var SERVER_URL = "http://localhost";
// model root of application server
var SERVER_ROOT = "r";
// end of configuration section
var MAIN_URL = SERVER_URL + '/' + SERVER_ROOT;
shared.js:
function d2h(d, padding) {
var hex = Number(d).toString(16);
padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;
while (hex.length < padding) {
hex = "0" + hex;
}
return hex;
}
function prf(name) {
return 'loans_' + name;
}
function getSet(name) {
return localStorage.getItem(prf(name));
}
function getSetAsInt(name) {
var c = getSet(name);
return Number(c) ? c : 0;
}
function setSet(name, value) {
return localStorage.setItem(prf(name), value);
}
function InitSession() {
localStorage.removeItem(prf('SESSION_ID'));
localStorage.removeItem(prf('SESSION_PRIVATE_KEY'));
localStorage.removeItem(prf('SESSION_LAST_TICK_COUNT'));
localStorage.removeItem(prf('SESSION_TICK_COUNT_OFFSET'));
localStorage.removeItem(prf('SESSION_USERNAME'));
return true;
}
function CloseSession() {
if (!getSetAsInt('SESSION_ID')) return;
$.ajax({
type: "GET",
dataType: "json",
url: MAIN_URL + '/auth',
data: {'session': getSetAsInt('SESSION_ID'), 'UserName': getSet('SESSION_USERNAME')},
timeout: 2000,
success: InitSession,
error: InitSession
});
}
// converted from TSQLRestClientURI.SessionSign function
function GetSessionSignature(url) {
// expected format is 'session_signature='Hexa8(SessionID)+Hexa8(TimeStamp)+
// Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
// Hexa8(TimeStamp)+url))
var d = new Date();
var Tix = d.getTime();
if (Tix < getSetAsInt('SESSION_LAST_TICK_COUNT')) // wrap around 0 after 49.7 days
setSet('SESSION_TICK_COUNT_OFFSET', getSetAsInt('SESSION_TICK_COUNT_OFFSET') + 1 << (32 - 8)); // allows 35 years timing
setSet('SESSION_LAST_TICK_COUNT', Tix);
var Nonce = d2h(Tix >>> 8 + getSetAsInt('SESSION_TICK_COUNT_OFFSET'), 8);
var sign = d2h(getSetAsInt('SESSION_ID'), 8) + Nonce + d2h(crc32(getSet('SESSION_PRIVATE_KEY') + Nonce + url), 8);
var prf = '?';
if (url.indexOf('?') > -1) prf = '&';
return prf + 'session_signature=' + sign;
}
$.ajaxPrefilter(function(options, _, jqXHR) {
// signing all sended URLs
if (getSetAsInt('SESSION_ID') > 0 && options.url.indexOf(MAIN_URL) > -1) { // if user authenticated
var new_url = options.url;
if (options.data && options.type == "GET")
{
new_url += '?' + options.data;
options.data = null; // or options.data will be added to url by JQuery
}
options.url = new_url + GetSessionSignature(new_url.substr(SERVER_URL.length + 1));
options.cache = true; // we don't want anti-cache "_" JQuery-parameter
}
});
$(function() {
if (typeof(localStorage) == 'undefined')
alert('You do not have HTML5 localStorage support in your browser. Please update or application cannot work as expected');
});
I use HTML5 LocalStorage, not cookies, for storing session information.
And this is code for user authentication:
login.js:
function AuthorizeUser() {
var name = $("#username").val();
var pwd = $("#password").val();
var servnonce = "";
CloseSession(); // try close previously opened session
var d = new Date();
var clientnonce = d.getTime() / (1000 * 60 * 5); // valid for 5*60*1000 ms = 5 minutes;
clientnonce = SHA256("" + clientnonce);
var dataString = {'UserName': name};
$.ajax({
type: "GET",
dataType: "json",
url: MAIN_URL + '/auth',
data: dataString,
timeout:2000,
beforeSend: function(jqXHR, settings) {
$.blockUI({
message: '<h1><img src="/images/busy.gif" style="height:16px;width:16px;" /> Авторизуюсь...</h1>',
css: {
border: 'none',
padding: '15px',
backgroundColor: '#000',
'-webkit-border-radius': '10px',
'-moz-border-radius': '10px',
opacity: .5,
color: '#fff'
} });
},
success: function(data, textStatus, jqXHR) {
servnonce = data.result;
// The Password parameter as sent for the 2nd request will be computed as
// ! Sha256(ModelRoot+Nonce+ClientNonce+UserName+Sha256('salt'+PassWord))
var password = SHA256(SERVER_ROOT + servnonce + clientnonce + name + SHA256('salt' + pwd));
dataString = {'UserName': name, 'Password': password, 'ClientNonce': clientnonce};
// second handshake
$.ajax({
type: "GET",
dataType: "json",
url: MAIN_URL + '/auth',
data: dataString,
timeout: 2000,
success: function(data, textStatus, jqXHR) {
var p = data.result.indexOf('+');
if (p > -1) {
setSet('SESSION_ID', data.result.substr(0, p));
setSet('SESSION_PRIVATE_KEY', data.result + SHA256('salt' + pwd));
setSet('SESSION_USERNAME', name);
// get session info
$.ajax({
type: "GET",
dataType: "json",
url: MAIN_URL + '/GetSessionInfo',
timeout: 2000,
success: function(data) {
create_notify("msgs", { title:'Welcome', text:'You are successfully authenticated', icon:'check48.png' });
setSet('SESSION_GROUP', data.group);
setSet('SESSION_FIO', data.fio);
setTimeout(function() {
document.location = '/pages/main_' + data.group + '.htm';
}, 1000);
}
});
$.unblockUI();
return true;
}
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
create_notify('msgs', {title:'Authentication error', text:'Sorry, wrong username or password', icon:'error48.png'});
$("#login section").effect("shake", 150);
$.unblockUI();
return
}
}
}); // end success of second handshake
} // end success of first handshake
});
return false; // end of AuthorizeUser
}
$(function() {
// Call the button widget method on the login button to format it.
$("#btnLogin").button().bind("click", function() {
AuthorizeUser();
return false;
});
});
Also i patch TSQLRestServer.Auth function (SQLite3Commons.pas):
Replace
...
try
User := nil; // will be freed by TAuthSession.Destroy
aResp := Session.fPrivateSalt;
if fSessions=nil then
...
with
...
try
User := nil; // will be freed by TAuthSession.Destroy
aResp := JSONEncodeResult([Session.fPrivateSalt]);
if fSessions=nil then
...
and
...
begin
// only UserName=... -> return hexadecimal nonce content valid for 5 minutes
aResp := Nonce(false);
end;
...
with
...
begin
// only UserName=... -> return hexadecimal nonce content valid for 5 minutes
aResp := JSONEncodeResult([Nonce(false)]);
end;
...
P.S. SHA256 and CRC32 routines getted from http://www.webtoolkit.info/
P.P.S. Replace string
if (typeof(crc) == "undefined") { crc = 0; }
to
crc = 0;
in webtoolkit.crc32.js or you have get wrong results from crc32 function. Or maybe code above (from ab) worked well, i don't check
P.P.P.S. Sorry for my English
Hello! Somebody tried to make the web-client for use with mORMot?
There is client-side code (just in .html file on local hard drive) for submit button on login form (uses jquery):
$(function() {
$("#signin_submit").click(function() {
var dataString = 'username=admin&password=synopse';
$.ajax({
type: "GET",
cache: false,
dataType: "jsonp",
url: "http://localhost:888/r/auth",
data: dataString,
success: function(data, textStatus, jqXHR) {
alert('success: ' + data + textStatus);
},
error: function(jqXHR, textStatus, errorThrown) {
alert("status: " + textStatus);
}
});
return false;
});
});
There is simply server-side code:
var
fModel: TSQLModel;
fDB: TSQLRestServerDB;
fServer: TSQLite3HttpServer;
procedure TfmMainServer.FormCreate(Sender: TObject);
begin
fModel := TSQLModel.Create([TSQLAuthGroup, TSQLAuthUser],'r');
fDB := TSQLRestServerDB.Create(fModel, ChangeFileExt(paramstr(0),'.db3'), True);
fDB.NoAJAXJSON := False;
fDB.CreateMissingTables(0);
fServer := TSQLite3HttpServer.Create('888',[fDB]);
fServer.OnlyJSONRequests := False;
end;
When i clicked on "Submit", browser firing up a "error" event with statusText = "parseerror". This is because mORMot HTTP server responses with content-type: application/json, but actually server return plain text string with "server nonce".
If change dataType from "jsonp" to "script", success event firing, but data parameter is "undefined" and i can't get "server nonce"
My question is: how to get "server nonce" with cross-domain AJAX (JSONP)? Or small example with mORMot RESTful authentication using browser (javascript)
P.S. Sorry for my English
Pages: 1