Commit 00d30059 by Mac Stephens

Implement URL-based JWT login (VCL-issued codes), migrate server/client to etask…

Implement URL-based JWT login (VCL-issued codes), migrate server/client to etask + web_tasks/web_tasks_url, add task-id API flow with blank-row creation, and update Web Core app to load/save grid via GetTaskItems without the login form. Still testing.
parent d3897a78
......@@ -18,3 +18,9 @@ emT3webClient/Win32/Debug/
*.local
*.identcache
emT3VCLDemo/__history/
emT3VCLDemo/Win64x/Debug/
kgOrdersServer/bin/static/
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <tchar.h>
//---------------------------------------------------------------------------
USEFORM("uMain.cpp", fMain);
//---------------------------------------------------------------------------
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
try
{
Application->Initialize();
Application->MainFormOnTaskBar = true;
Application->CreateForm(__classid(TfMain), &fMain);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
catch (...)
{
try
{
throw Exception("");
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
}
return 0;
}
//---------------------------------------------------------------------------
#include <vcl.h>
#include <tchar.h>
//---------------------------------------------------------------------------
#include <vcl.h>
#include <Shellapi.h>
#include <System.NetEncoding.hpp>
#pragma hdrstop
#include "uMain.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma link "MySQLUniProvider"
#pragma link "UniProvider"
#pragma resource "*.dfm"
TfMain *fMain;
//---------------------------------------------------------------------------
__fastcall TfMain::TfMain(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TfMain::FormCreate(TObject *Sender)
{
Randomize();
LogLine("App started");
}
//---------------------------------------------------------------------------
void __fastcall TfMain::btnOpenTaskItemsClick(TObject *Sender)
{
const UnicodeString userId = edtUserId->Text.Trim();
const UnicodeString taskId = edtTaskId->Text.Trim();
const UnicodeString baseUrl = edtWebUrl->Text.Trim();
const int expSeconds = StrToIntDef(edtExpSeconds->Text.Trim(), 60);
if (userId.IsEmpty() || taskId.IsEmpty() || baseUrl.IsEmpty())
{
LogLine("Missing required input (userId, taskId, or webUrl)");
return;
}
const int codeInt = 100000 + Random(900000);
const UnicodeString urlCode = IntToStr(codeInt);
LogLine("Generated URL_CODE=" + urlCode);
try
{
if (!ucETask->Connected)
{
LogLine("Connecting to MariaDB...");
ucETask->Connect();
LogLine("Connected");
}
LogLine("Inserting row into web_tasks_url...");
uqWebTasksUrl->Close();
uqWebTasksUrl->SQL->Text =
"insert into web_tasks_url (USER_ID, TASK_ID, URL_CODE, URL_TIME, URL_TIME_EXP) "
"values (:USER_ID, :TASK_ID, :URL_CODE, NOW(), :URL_TIME_EXP)";
uqWebTasksUrl->ParamByName("USER_ID")->AsString = userId;
uqWebTasksUrl->ParamByName("TASK_ID")->AsString = taskId;
uqWebTasksUrl->ParamByName("URL_CODE")->AsString = urlCode;
uqWebTasksUrl->ParamByName("URL_TIME_EXP")->AsInteger = expSeconds;
uqWebTasksUrl->ExecSQL();
LogLine("Insert OK");
}
catch (const Exception &e)
{
LogLine("DB ERROR: " + e.Message);
return;
}
const UnicodeString launchUrl = BuildLaunchUrl(baseUrl, userId, taskId, urlCode);
if (launchUrl.IsEmpty())
{
LogLine("Launch URL build failed");
return;
}
LogLine("Launching browser:");
LogLine(launchUrl);
ShellExecute(0, L"open", launchUrl.w_str(), 0, 0, SW_SHOWNORMAL);
}
//---------------------------------------------------------------------------
void TfMain::LogLine(const String &message)
{
const String stamp = FormatDateTime("yyyy-mm-dd hh:nn:ss.zzz", Now());
memoLog->Lines->Add(stamp + " " + message);
memoLog->SelStart = memoLog->Text.Length();
}
//---------------------------------------------------------------------------
String TfMain::BuildLaunchUrl(const String &baseUrl, const String &userId, const String &taskId, const String &code)
{
String cleanBaseUrl = baseUrl.Trim();
if (cleanBaseUrl.IsEmpty())
return "";
String sep = cleanBaseUrl.Pos("?") > 0 ? "&" : "?";
String qUserId = TNetEncoding::URL->Encode(userId);
String qTaskId = TNetEncoding::URL->Encode(taskId);
String qCode = TNetEncoding::URL->Encode(code);
return cleanBaseUrl + sep + "user_id=" + qUserId + "&task_id=" + qTaskId + "&code=" + qCode;
}
object fMain: TfMain
Left = 0
Top = 0
Caption = 'fMain'
ClientHeight = 441
ClientWidth = 624
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
OnCreate = FormCreate
TextHeight = 15
object lblWCBaseUrl: TLabel
Left = 20
Top = 14
Width = 131
Height = 15
Caption = 'Web Core Client Base Url'
end
object lblUserId: TLabel
Left = 368
Top = 14
Width = 36
Height = 15
Caption = 'User Id'
end
object lblTaskId: TLabel
Left = 434
Top = 14
Width = 36
Height = 15
HelpType = htKeyword
Caption = 'Task Id'
end
object Label2: TLabel
Left = 500
Top = 14
Width = 73
Height = 15
HelpType = htKeyword
Caption = 'Exp (Seconds)'
end
object btnOpenTaskItems: TButton
Left = 20
Top = 64
Width = 109
Height = 25
Caption = 'Open Task Items'
TabOrder = 0
OnClick = btnOpenTaskItemsClick
end
object memoLog: TMemo
Left = 20
Top = 102
Width = 581
Height = 311
Lines.Strings = (
'')
TabOrder = 1
end
object edtUserId: TEdit
Left = 368
Top = 32
Width = 60
Height = 23
TabOrder = 2
end
object edtTaskId: TEdit
Left = 434
Top = 32
Width = 60
Height = 23
TabOrder = 3
end
object edtWebUrl: TEdit
Left = 20
Top = 32
Width = 337
Height = 23
TabOrder = 4
Text = 'http://127.0.0.1:8000/emT3webClient/index.html'
end
object edtExpSeconds: TEdit
Left = 500
Top = 32
Width = 79
Height = 23
NumbersOnly = True
TabOrder = 5
end
object ucETask: TUniConnection
ProviderName = 'MySQL'
Database = 'eTask'
SpecificOptions.Strings = (
'MySQL.ConnectionTimeout=15'
'MySQL.UseUnicode=False'
'MySQL.Compress=False'
'MySQL.Protocol=mpDefault'
'MySQL.Embedded=False'
'MySQL.IPVersion=ivIPv4'
'MySQL.Interactive=False'
'MySQL.AnsiQuotesMode=aqDefault'
'MySQL.HttpAuthenticationType=atBasic'
'MySQL.HttpTrustServerCertificate=False'
'MySQL.ProxyPort=0')
Username = 'root'
Server = '192.168.102.129'
LoginPrompt = False
Left = 390
Top = 342
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object uqWebTasksUrl: TUniQuery
Connection = ucETask
Left = 478
Top = 342
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 438
Top = 284
end
end
//---------------------------------------------------------------------------
#ifndef uMainH
#define uMainH
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Uni.hpp>
#include <MemDS.hpp>
#include <DBAccess.hpp>
#include <Data.DB.hpp>
#include "MySQLUniProvider.hpp"
#include "UniProvider.hpp"
//---------------------------------------------------------------------------
class TfMain : public TForm
{
__published: // IDE-managed Components
TButton *btnOpenTaskItems;
TMemo *memoLog;
TEdit *edtUserId;
TEdit *edtTaskId;
TEdit *edtWebUrl;
TEdit *edtExpSeconds;
TLabel *lblWCBaseUrl;
TLabel *lblUserId;
TLabel *lblTaskId;
TLabel *Label2;
TUniConnection *ucETask;
TUniQuery *uqWebTasksUrl;
TMySQLUniProvider *MySQLUniProvider1;
void __fastcall FormCreate(TObject *Sender);
void __fastcall btnOpenTaskItemsClick(TObject *Sender);
private:
void LogLine(const String &message);
String BuildLaunchUrl(const String &baseUrl, const String &userId, const String &taskId, const String &code);
public: // User declarations
__fastcall TfMain(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TfMain *fMain;
//---------------------------------------------------------------------------
#endif
......@@ -7,13 +7,11 @@ uses
XData.Web.Client;
const
TOKEN_NAME = 'KG_ORDERS_WEB_TOKEN';
TOKEN_NAME = 'EMT3_WEB_TOKEN';
type
TOnLoginSuccess = reference to procedure;
TOnLoginError = reference to procedure(AMsg: string);
TOnProfileSuccess = reference to procedure;
TOnProfileError = reference to procedure(AMsg: string);
TAuthService = class
private
......@@ -23,8 +21,9 @@ type
public
constructor Create; reintroduce;
destructor Destroy; override;
procedure Login(AUser, APassword: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure Login(const userId, taskId, urlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure Logout;
function GetToken: string;
function Authenticated: Boolean;
......@@ -42,7 +41,7 @@ type
class function DecodePayload(AToken: string): string;
end;
function AuthService: TAuthService;
function AuthService: TAuthService;
implementation
......@@ -55,9 +54,7 @@ var
function AuthService: TAuthService;
begin
if not Assigned(_AuthService) then
begin
_AuthService := TAuthService.Create;
end;
Result := _AuthService;
end;
......@@ -91,8 +88,7 @@ begin
Result := window.localStorage.getItem(TOKEN_NAME);
end;
procedure TAuthService.Login(AUser, APassword: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure TAuthService.Login(const userId, taskId, urlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse);
var
......@@ -109,14 +105,14 @@ procedure TAuthService.Login(AUser, APassword: string; ASuccess: TOnLoginSuccess
end;
begin
if (AUser = '') or (APassword = '') then
if (userId = '') or (taskId = '') or (urlCode = '') then
begin
AError('Please enter a username and a password');
AError('Missing URL parameters. Please reopen from emt3.');
Exit;
end;
FClient.RawInvoke(
'IAuthService.Login', [AUser, APassword],
'IAuthService.Login', [userId, taskId, urlCode],
@OnLoad, @OnError
);
end;
......@@ -124,6 +120,10 @@ end;
procedure TAuthService.Logout;
begin
DeleteToken;
window.localStorage.removeItem('EMT3_USER_ID');
window.localStorage.removeItem('EMT3_TASK_ID');
window.localStorage.removeItem('EMT3_CODE');
end;
procedure TAuthService.SetToken(AToken: string);
......@@ -140,17 +140,9 @@ begin
ExpirationDate := TJwtHelper.TokenExpirationDate(GetToken);
Result := EncodeDate(
ExpirationDate.FullYear,
ExpirationDate.Month + 1,
ExpirationDate.Date
) +
EncodeTime(
ExpirationDate.Hours,
ExpirationDate.Minutes,
ExpirationDate.Seconds,
0
);
Result :=
EncodeDate(ExpirationDate.FullYear, ExpirationDate.Month + 1, ExpirationDate.Date) +
EncodeTime(ExpirationDate.Hours, ExpirationDate.Minutes, ExpirationDate.Seconds, 0);
end;
function TAuthService.TokenExpired: Boolean;
......@@ -176,8 +168,7 @@ begin
Result := '';
asm
const parts = AToken.split('.');
if (parts.length === 3) { // <- strict compare
// JWTs use url-safe base64; convert before atob
if (parts.length === 3) {
Result = atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'));
}
end;
......
......@@ -17,11 +17,13 @@ type
procedure AuthConnectionError(Error: TXDataWebConnectionError);
private
FUnauthorizedAccessProc: TUnauthorizedAccessProc;
FUserIdParam: string;
FTaskIdParam: string;
FCodeParam: string;
public
const clientVersion = '0.0.1';
procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure InitApp(SuccessProc: TSuccessProc; UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure SetClientConfig(Callback: TVersionCheckCallback);
end;
......@@ -52,11 +54,10 @@ begin
Args.Request.Headers.SetValue('Authorization', 'Bearer ' + AuthService.GetToken);
end;
procedure TDMConnection.ApiConnectionResponse(
Args: TXDataWebConnectionResponse);
procedure TDMConnection.ApiConnectionResponse(Args: TXDataWebConnectionResponse);
begin
if Args.Response.StatusCode = 401 then
FUnauthorizedAccessProc(Format('%d: %s',[Args.Response.StatusCode, Args.Response.ContentAsText]));
if (Args.Response.StatusCode = 401) and Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(Format('%d: %s', [Args.Response.StatusCode, Args.Response.ContentAsText]));
end;
procedure TDMConnection.AuthConnectionError(Error: TXDataWebConnectionError);
......@@ -83,13 +84,12 @@ begin
LoadConfig(@ConfigLoaded);
end;
procedure TDMConnection.SetClientConfig(Callback: TVersionCheckCallback);
begin
XDataWebClient1.Connection := AuthConnection;
XDataWebClient1.RawInvoke('IAuthService.VerifyVersion', [clientVersion],
XDataWebClient1.RawInvoke(
'IAuthService.VerifyVersion', [clientVersion],
procedure(Response: TXDataClientResponse)
var
jsonResult: TJSObject;
......@@ -106,8 +106,12 @@ begin
Callback(False, error)
else
Callback(True, '');
end);
end,
procedure(Error: TXDataClientError)
begin
Callback(False, Error.ErrorMessage);
end
);
end;
end.
......@@ -46596,7 +46596,6 @@ object FViewLogin: TFViewLogin
Role = 'null'
TabOrder = 2
WidthPercent = 100.000000000000000000
OnClick = btnLoginClick
end
object pnlMessage: TWebPanel
Left = 240
......@@ -20,7 +20,6 @@ type
XDataWebClient: TXDataWebClient;
WebImageControl1: TWebImageControl;
lblClientVersion: TWebLabel;
procedure btnLoginClick(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormShow(Sender: TObject);
private
......@@ -43,26 +42,6 @@ uses
{$R *.dfm}
procedure TFViewLogin.btnLoginClick(Sender: TObject);
procedure LoginSuccess;
begin
FLoginProc;
end;
procedure LoginError(AMsg: string);
begin
ShowNotification('Login Error: ' + AMsg);
end;
begin
AuthService.Login(
edtUsername.Text, edtPassword.Text,
@LoginSuccess,
@LoginError
);
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc);
begin
TFViewLogin.Display(LoginProc, '');
......
......@@ -64,7 +64,7 @@
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_restart" class="btn btn-primary">
Back to Orders
Restart
</button>
</div>
</div>
......
......@@ -45,7 +45,6 @@ implementation
uses
Auth.Service,
View.Login,
View.Tasks,
View.TasksHTML,
View.TasksDataGrid,
......@@ -62,11 +61,11 @@ begin
end;
procedure TFViewMain.WebFormCreate(Sender: TObject);
var
userName: string;
test: boolean;
begin
console.log('TFViewMain.WebFormCreate fired');
FChildForm := nil;
console.log('About to ShowForm(TFTasksHTML), host=' + WebPanel1.ElementID);
ShowForm(TFTasksHTML);
lblAppTitle.Caption := 'emT3web';
lblVersion.Caption := 'v' + DMConnection.clientVersion;
......@@ -89,7 +88,7 @@ end;
procedure TFViewMain.ConfirmLogout;
begin
ShowConfirmationModal(
'Are you sure you want to log out?',
'End this session?.',
'Yes',
'No',
procedure(confirmed: Boolean)
......
......@@ -3,7 +3,7 @@ object FTasksHTML: TFTasksHTML
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
OnShow = WebFormShow
object btnReload: TWebButton
Left = 78
Top = 88
......
......@@ -30,9 +30,10 @@ type
xdwdsTaskstaskItemId: TStringField;
procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
procedure WebFormShow(Sender: TObject);
private
[async] procedure LoadTasks(const AProjectId: string);
FTaskId: string;
[async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable;
procedure BindTableEditors;
......@@ -44,7 +45,7 @@ type
procedure GotoRowIndex(AIndex: Integer);
function HtmlEncode(const s: string): string;
procedure SetProjectLabel(const AProjectId: string);
procedure SetTaskLabel(const ATaskId: string);
[async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent);
......@@ -61,9 +62,20 @@ uses
{$R *.dfm}
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
procedure TFTasksHTML.WebFormShow(Sender: TObject);
begin
LoadTasks('WPR0001');
FTaskId := window.localStorage.getItem('EMT3_TASK_ID');
console.log('The task id is: ' + FTaskId);
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing task_id. Please reopen from emt3.');
Exit;
end;
btnAddRow.Enabled := False;
LoadTasks(FTaskId);
end;
......@@ -191,23 +203,19 @@ end;
procedure TFTasksHTML.btnAddRowClick(Sender: TObject);
begin
if not xdwdsTasks.Active then
Exit;
xdwdsTasks.Append;
xdwdsTaskstaskId.AsString := 'NEW_TASK_ID';
xdwdsTasksreportedBy.AsString := '';
xdwdsTasksassignedTo.AsString := '';
xdwdsTasksstatus.AsString := '';
xdwdsTasks.Post;
RenderTable;
Utils.ShowErrorModal('Add row is not enabled yet.');
end;
procedure TFTasksHTML.btnReloadClick(Sender: TObject);
begin
LoadTasks('WPR0001');
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing Task Id. Update url params or resend from emT3.');
Exit;
end;
LoadTasks(FTaskId);
end;
procedure TFTasksHTML.EnableAutoGrowTextAreas;
......@@ -225,29 +233,37 @@ begin
end;
end;
procedure TFTasksHTML.SetProjectLabel(const AProjectId: string);
procedure TFTasksHTML.SetTaskLabel(const ATaskId: string);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_project_name'));
if Assigned(el) then
el.innerText := 'Tasks - ' + AProjectId;
el.innerText := 'Tasks - ' + ATaskId;
end;
[async] procedure TFTasksHTML.LoadTasks(const AProjectId: string);
[async] procedure TFTasksHTML.LoadTasks(const ATaskId: string);
var
response: TXDataClientResponse;
resultObj, taskObj: TJSObject;
tasksArray, itemsArray, flatItems: TJSArray;
taskIndex, itemIndex: Integer;
begin
SetProjectLabel(AProjectId);
SetTaskLabel(ATaskId);
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.GetProjectTasks', [AProjectId]
'IApiService.GetTaskItems', [ATaskId]
));
except
on E: EXDataClientRequestException do
begin
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
Exit;
end;
end;
if not Assigned(response.Result) then
Exit;
......@@ -275,6 +291,7 @@ begin
end;
end;
procedure TFTasksHTML.RenderTable;
var
host: TJSHTMLElement;
......@@ -566,5 +583,7 @@ end;
end.
{
"AuthUrl" : "http://localhost:2004/kgOrders/auth/",
"ApiUrl" : "http://localhost:2004/kgOrders/api/"
"AuthUrl" : "http://localhost:2004/emsys/emt3/auth/",
"ApiUrl" : "http://localhost:2004/emsys/emt3/api/"
}
......@@ -2,6 +2,9 @@ program emT3webClient;
uses
Vcl.Forms,
System.SysUtils,
JS,
Web,
XData.Web.Connection,
WEBLib.Dialogs,
Auth.Service in 'Auth.Service.pas',
......@@ -19,7 +22,57 @@ uses
{$R *.res}
procedure DisplayLoginView(AMessage: string = ''); forward;
procedure DisplayAccessDeniedModal(const ErrorMessage: string);
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "500px";
dlg.style.width = "90%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>emT3web</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + ErrorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='actionBtn' class='btn btn-primary'></button></div>";
document.body.appendChild(dlg);
dlg.showModal();
var btn = document.getElementById("actionBtn");
if (
(ErrorMessage.indexOf("Version mismatch") >= 0) ||
(ErrorMessage.indexOf("old version") >= 0)
) {
btn.textContent = "Reload";
btn.addEventListener("click", function () {
location.reload(true);
});
} else {
btn.textContent = "Close";
btn.addEventListener("click", function () {
dlg.close();
dlg.remove();
});
}
end;
end;
procedure DisplayLoginView(AMessage: string = '');
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
if AMessage = '' then
DisplayAccessDeniedModal('Access requires a valid emt3 link. Please reopen from emt3.')
else
DisplayAccessDeniedModal(AMessage);
end;
procedure DisplayMainView;
......@@ -27,6 +80,7 @@ procedure DisplayMainView;
begin
if Assigned(FViewLogin) then
FViewLogin.Free;
TFViewMain.Display(@DisplayLoginView);
end;
......@@ -37,25 +91,33 @@ begin
ConnectProc;
end;
procedure DisplayLoginView(AMessage: string);
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
TFViewLogin.Display(@DisplayMainView, AMessage);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
DisplayLoginView(AMessage);
end;
procedure SaveUrlParamsToStorage(const userId, taskId, code: string);
begin
if userId <> '' then
window.localStorage.setItem('EMT3_USER_ID', userId);
if taskId <> '' then
window.localStorage.setItem('EMT3_TASK_ID', taskId);
if code <> '' then
window.localStorage.setItem('EMT3_CODE', code);
end;
procedure StartApplication;
var
UserIdParam: string;
TaskIdParam: string;
CodeParam: string;
begin
UserIdParam := Application.Parameters.Values['user_id'];
TaskIdParam := Application.Parameters.Values['task_id'];
CodeParam := Application.Parameters.Values['code'];
SaveUrlParamsToStorage(UserIdParam, TaskIdParam, CodeParam);
DMConnection.InitApp(
procedure
begin
......@@ -64,33 +126,30 @@ begin
begin
if Success then
begin
if (not AuthService.Authenticated) or AuthService.TokenExpired then
DisplayLoginView
else
if (UserIdParam <> '') and (TaskIdParam <> '') and (CodeParam <> '') then
begin
AuthService.Login(
UserIdParam, TaskIdParam, CodeParam,
procedure
begin
DisplayMainView;
end,
procedure(LoginError: string)
begin
DisplayLoginView('Invalid or expired link.' + sLineBreak + LoginError);
end
);
Exit;
end;
if AuthService.Authenticated and (not AuthService.TokenExpired) then
DisplayMainView
else
DisplayLoginView;
end
else
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "500px";
dlg.style.width = "90%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>kgOrders web app</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + ErrorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='refreshBtn' class='btn btn-primary'>Reload</button></div>";
document.body.appendChild(dlg);
dlg.showModal();
document.getElementById("refreshBtn").addEventListener("click", function () {
location.reload(true); // Hard refresh
});
end;
DisplayAccessDeniedModal(ErrorMessage);
end;
end);
end,
......@@ -98,9 +157,6 @@ begin
);
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
......
......@@ -141,27 +141,22 @@
<DCCReference Include="Utils.pas"/>
<DCCReference Include="View.Tasks.pas">
<Form>FTasks</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksDataGrid.pas">
<Form>FTasksDataGrid</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksTabulator.pas">
<Form>FTasksTabulator</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksDBGrid.pas">
<Form>FTasksDBGrid</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<None Include="index.html"/>
......
......@@ -6,7 +6,7 @@
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>EM Systems webKGOrders App</title>
<title>Em Systems - emT3 Web</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
......
......@@ -2,9 +2,9 @@ object ApiDatabase: TApiDatabase
OnCreate = DataModuleCreate
Height = 358
Width = 519
object ucEmT3: TUniConnection
object ucETaskApi: TUniConnection
ProviderName = 'MySQL'
Database = 'emt3_web_db'
Database = 'eTask'
Username = 'root'
Server = '192.168.102.129'
Connected = True
......@@ -18,7 +18,7 @@ object ApiDatabase: TApiDatabase
Top = 66
end
object uqUsers: TUniQuery
Connection = ucEmT3
Connection = ucETaskApi
SQL.Strings = (
'SELECT USER_ID, NAME, STATUS from users ORDER BY NAME')
OnCalcFields = uqUsersCalcFields
......@@ -42,84 +42,10 @@ object ApiDatabase: TApiDatabase
Calculated = True
end
end
object uqProjectTasks: TUniQuery
Connection = ucEmT3
SQL.Strings = (
'SELECT *'
'FROM task_items'
'WHERE PROJECT_ID = :PROJECT_ID'
'ORDER BY TASK_ID, TASK_ITEM_ID;')
Active = True
Left = 308
Top = 142
ParamData = <
item
DataType = ftUnknown
Name = 'PROJECT_ID'
Value = nil
end>
object uqProjectTasksTASK_ITEM_ID: TStringField
FieldName = 'TASK_ITEM_ID'
Required = True
Size = 7
end
object uqProjectTasksTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqProjectTasksPROJECT_ID: TStringField
FieldName = 'PROJECT_ID'
Required = True
Size = 7
end
object uqProjectTasksAPPLICATION: TStringField
FieldName = 'APPLICATION'
Size = 255
end
object uqProjectTasksAPP_VERSION: TStringField
FieldName = 'APP_VERSION'
Size = 50
end
object uqProjectTasksTASK_DATE: TDateField
FieldName = 'TASK_DATE'
end
object uqProjectTasksREPORTED_BY: TStringField
FieldName = 'REPORTED_BY'
Size = 50
end
object uqProjectTasksASSIGNED_TO: TStringField
FieldName = 'ASSIGNED_TO'
Size = 50
end
object uqProjectTasksSTATUS: TStringField
FieldName = 'STATUS'
Size = 100
end
object uqProjectTasksSTATUS_DATE: TDateField
FieldName = 'STATUS_DATE'
end
object uqProjectTasksFIXED_VERSION: TStringField
FieldName = 'FIXED_VERSION'
Size = 50
end
object uqProjectTasksFORM_SECTION: TStringField
FieldName = 'FORM_SECTION'
Size = 255
end
object uqProjectTasksISSUE: TStringField
FieldName = 'ISSUE'
Size = 1000
end
object uqProjectTasksNOTES: TStringField
FieldName = 'NOTES'
Size = 1000
end
end
object uqSaveTaskRow: TUniQuery
Connection = ucEmT3
Connection = ucETaskApi
SQL.Strings = (
'UPDATE task_items'
'UPDATE web_tasks'
'SET'
' APPLICATION = :APPLICATION,'
' APP_VERSION = :APP_VERSION,'
......@@ -133,8 +59,8 @@ object ApiDatabase: TApiDatabase
' ISSUE = :ISSUE,'
' NOTES = :NOTES'
'WHERE TASK_ITEM_ID = :TASK_ITEM_ID')
Left = 308
Top = 208
Left = 306
Top = 198
ParamData = <
item
DataType = ftUnknown
......@@ -197,4 +123,149 @@ object ApiDatabase: TApiDatabase
Value = nil
end>
end
object uqWebTasks: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' TASK_ITEM_ID,'
' TASK_ID,'
' PROJECT_ID,'
' APPLICATION,'
' APP_VERSION,'
' TASK_DATE,'
' STATUS_DATE,'
' REPORTED_BY,'
' ASSIGNED_TO,'
' STATUS,'
' FIXED_VERSION,'
' FORM_SECTION,'
' ISSUE,'
' NOTES'
'from web_tasks'
'where TASK_ID = :TASK_ID'
'order by TASK_ITEM_ID')
Active = True
Left = 306
Top = 134
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqWebTasksTASK_ITEM_ID: TStringField
FieldName = 'TASK_ITEM_ID'
Required = True
Size = 7
end
object uqWebTasksTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqWebTasksPROJECT_ID: TStringField
FieldName = 'PROJECT_ID'
Required = True
Size = 7
end
object uqWebTasksAPPLICATION: TStringField
FieldName = 'APPLICATION'
Required = True
Size = 255
end
object uqWebTasksAPP_VERSION: TStringField
FieldName = 'APP_VERSION'
Required = True
Size = 50
end
object uqWebTasksTASK_DATE: TDateField
FieldName = 'TASK_DATE'
Required = True
end
object uqWebTasksSTATUS_DATE: TDateField
FieldName = 'STATUS_DATE'
Required = True
end
object uqWebTasksREPORTED_BY: TStringField
FieldName = 'REPORTED_BY'
Required = True
Size = 50
end
object uqWebTasksASSIGNED_TO: TStringField
FieldName = 'ASSIGNED_TO'
Required = True
Size = 50
end
object uqWebTasksSTATUS: TStringField
FieldName = 'STATUS'
Required = True
Size = 100
end
object uqWebTasksFIXED_VERSION: TStringField
FieldName = 'FIXED_VERSION'
Required = True
Size = 50
end
object uqWebTasksFORM_SECTION: TStringField
FieldName = 'FORM_SECTION'
Required = True
Size = 255
end
object uqWebTasksISSUE: TStringField
FieldName = 'ISSUE'
Required = True
Size = 1000
end
object uqWebTasksNOTES: TStringField
FieldName = 'NOTES'
Required = True
Size = 1000
end
end
object uqEnsureBlankRow: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert ignore into web_tasks ('
' TASK_ITEM_ID,'
' TASK_ID,'
' PROJECT_ID,'
' APPLICATION,'
' APP_VERSION,'
' TASK_DATE,'
' STATUS_DATE,'
' REPORTED_BY,'
' ASSIGNED_TO,'
' STATUS,'
' FIXED_VERSION,'
' FORM_SECTION,'
' ISSUE,'
' NOTES'
')'
'values ('
' :TASK_ID,'
' :TASK_ID,'
' coalesce((select PROJECT_ID from tasks where TASK_ID = :TASK_I' +
'D), '#39#39'),'
' '#39#39','
' '#39#39','
' curdate(),'
' curdate(),'
' '#39#39','
' '#39#39','
' '#39#39','
' '#39#39','
' '#39#39','
' '#39#39','
' '#39#39
')')
Left = 306
Top = 254
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
end
end
......@@ -9,29 +9,30 @@ uses
type
TApiDatabase = class(TDataModule)
ucEmT3: TUniConnection;
ucETaskApi: TUniConnection;
MySQLUniProvider1: TMySQLUniProvider;
uqUsers: TUniQuery;
uqUsersUSER_ID: TIntegerField;
uqUsersNAME: TStringField;
uqUsersSTATUS: TStringField;
uqUsersREPRESENTATIVE: TStringField;
uqProjectTasks: TUniQuery;
uqProjectTasksTASK_ITEM_ID: TStringField;
uqProjectTasksTASK_ID: TStringField;
uqProjectTasksPROJECT_ID: TStringField;
uqProjectTasksAPPLICATION: TStringField;
uqProjectTasksAPP_VERSION: TStringField;
uqProjectTasksTASK_DATE: TDateField;
uqProjectTasksREPORTED_BY: TStringField;
uqProjectTasksASSIGNED_TO: TStringField;
uqProjectTasksSTATUS: TStringField;
uqProjectTasksSTATUS_DATE: TDateField;
uqProjectTasksFIXED_VERSION: TStringField;
uqProjectTasksFORM_SECTION: TStringField;
uqProjectTasksISSUE: TStringField;
uqProjectTasksNOTES: TStringField;
uqSaveTaskRow: TUniQuery;
uqWebTasks: TUniQuery;
uqWebTasksTASK_ITEM_ID: TStringField;
uqWebTasksTASK_ID: TStringField;
uqWebTasksPROJECT_ID: TStringField;
uqWebTasksAPPLICATION: TStringField;
uqWebTasksAPP_VERSION: TStringField;
uqWebTasksTASK_DATE: TDateField;
uqWebTasksSTATUS_DATE: TDateField;
uqWebTasksREPORTED_BY: TStringField;
uqWebTasksASSIGNED_TO: TStringField;
uqWebTasksSTATUS: TStringField;
uqWebTasksFIXED_VERSION: TStringField;
uqWebTasksFORM_SECTION: TStringField;
uqWebTasksISSUE: TStringField;
uqWebTasksNOTES: TStringField;
uqEnsureBlankRow: TUniQuery;
procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet);
private
......@@ -54,9 +55,9 @@ uses
procedure TApiDatabase.DataModuleCreate(Sender: TObject);
begin
Logger.Log( 5, 'TApiDatabase.DataModuleCreate' );
LoadDatabaseSettings( ucEmT3, 'emT3webServer.ini' );
LoadDatabaseSettings( ucETaskApi, 'emT3webServer.ini' );
try
ucEmT3.Connect;
ucETaskApi.Connect;
except
on E: Exception do
begin
......
......@@ -10,13 +10,16 @@ object ApiServerModule: TApiServerModule
ModelName = 'Api'
EntitySetPermissions = <>
SwaggerOptions.Enabled = True
SwaggerOptions.AuthMode = Jwt
SwaggerUIOptions.Enabled = True
SwaggerUIOptions.ShowFilter = True
SwaggerUIOptions.TryItOutEnabled = True
Left = 80
Top = 142
object XDataServerJWT: TSparkleJwtMiddleware
ForbidAnonymousAccess = True
OnGetSecret = XDataServerJWTGetSecret
OnForbidRequest = XDataServerJWTForbidRequest
end
object XDataServerCompress: TSparkleCompressMiddleware
end
......
......@@ -25,6 +25,8 @@ type
XDataServerCompress: TSparkleCompressMiddleware;
XDataServerCORS: TSparkleCorsMiddleware;
XDataServerLogging: TSparkleLoggingMiddleware;
procedure XDataServerJWTForbidRequest(Sender: TObject; Context:
THttpServerContext; var Forbid: Boolean);
procedure XDataServerLoggingMiddlewareCreate(Sender: TObject;
var Middleware: IHttpServerMiddleware);
procedure XDataServerJWTGetSecret(Sender: TObject; var Secret: string);
......@@ -86,6 +88,27 @@ begin
Logger.Log(1, Format('Api server module listening at "%s"', [XDataServer.BaseUrl]));
end;
procedure TApiServerModule.XDataServerJWTForbidRequest(Sender: TObject;
Context: THttpServerContext; var Forbid: Boolean);
var
Path: string;
begin
Path := Context.Request.Uri.Path;
if SameText(Context.Request.Method, 'OPTIONS') then
Forbid := False;
if Path.Contains('/swaggerui') then
Forbid := False;
if Path.Contains('/openapi/swagger.json') then
Forbid := False;
if Forbid then
Logger.Log(1, '[JWT] ForbidRequest fired (token missing/invalid/expired?)');
end;
procedure TApiServerModule.XDataServerLoggingMiddlewareCreate(Sender: TObject;
var Middleware: IHttpServerMiddleware);
begin
......
......@@ -77,7 +77,7 @@ type
[ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
[HttpGet] function GetProjectTasks(projectId: string): TTasksList;
function GetTaskItems(taskId: string): TTasksList;
[HttpPost] function SaveTaskRow(const Item: TTaskRowSave): Boolean;
end;
......
......@@ -3,9 +3,14 @@ unit Api.ServiceImpl;
interface
uses
XData.Server.Module, System.Generics.Collections,
XData.Service.Common, System.Variants, System.DateUtils,
Api.Service, Api.Database, Common.Logging, System.SysUtils;
XData.Server.Module,
XData.Service.Common,
System.Variants,
System.DateUtils,
Api.Service,
Api.Database,
Common.Logging,
System.SysUtils;
type
[ServiceImplementation]
......@@ -13,104 +18,133 @@ type
strict private
ApiDB: TApiDatabase;
private
procedure EnsureBlankWebTaskRow(const taskId: string);
function SaveTaskRow(const Item: TTaskRowSave): Boolean;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function GetProjectTasks(projectId: string): TTasksList;
function GetTaskItems(taskId: string): TTasksList;
end;
implementation
procedure TApiService.AfterConstruction;
begin
inherited;
ApiDB := TApiDatabase.Create(nil);
Logger.Log(4, 'ApiService.AfterConstruction - ApiDB created');
end;
procedure TApiService.BeforeDestruction;
begin
Logger.Log(4, 'ApiService.BeforeDestruction - freeing ApiDB');
ApiDB.Free;
inherited;
end;
function TApiService.GetProjectTasks(projectId: string): TTasksList;
function TApiService.GetTaskItems(taskId: string): TTasksList;
var
taskMap: TDictionary<string, TTask>;
task: TTask;
item: TTaskItem;
taskId: string;
begin
Logger.Log(4, Format('ApiService.GetTaskItems - TASK_ID="%s"', [taskId]));
Result := TTasksList.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
taskMap := TDictionary<string, TTask>.Create;
try
ApiDB.uqProjectTasks.Close;
ApiDB.uqProjectTasks.ParamByName('PROJECT_ID').AsString := projectId;
ApiDB.uqProjectTasks.Open;
ApiDB.uqWebTasks.Close;
ApiDB.uqWebTasks.ParamByName('TASK_ID').AsString := taskId;
ApiDB.uqWebTasks.Open;
while not ApiDB.uqProjectTasks.Eof do
if ApiDB.uqWebTasks.IsEmpty then
begin
taskId := ApiDB.uqProjectTasksTASK_ID.AsString;
Logger.Log(4, Format('ApiService.GetTaskItems - no rows for TASK_ID="%s", ensuring blank row', [taskId]));
EnsureBlankWebTaskRow(taskId);
ApiDB.uqWebTasks.Close;
ApiDB.uqWebTasks.ParamByName('TASK_ID').AsString := taskId;
ApiDB.uqWebTasks.Open;
end;
if not taskMap.TryGetValue(taskId, task) then
if ApiDB.uqWebTasks.IsEmpty then
begin
Logger.Log(2, Format('ApiService.GetTaskItems - still no rows after ensure blank for TASK_ID="%s"', [taskId]));
Result.count := 0;
Exit;
end;
task := TTask.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(task);
task.taskId := taskId;
task.projectId := ApiDB.uqProjectTasksPROJECT_ID.AsString;
task.projectId := ApiDB.uqWebTasksPROJECT_ID.AsString;
taskMap.Add(taskId, task);
Result.data.Add(task);
end;
while not ApiDB.uqWebTasks.Eof do
begin
item := TTaskItem.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(item);
item.taskItemId := ApiDB.uqProjectTasksTASK_ITEM_ID.AsString;
item.taskId := ApiDB.uqProjectTasksTASK_ID.AsString;
item.projectId := ApiDB.uqProjectTasksPROJECT_ID.AsString;
item.taskItemId := ApiDB.uqWebTasksTASK_ITEM_ID.AsString;
item.taskId := ApiDB.uqWebTasksTASK_ID.AsString;
item.projectId := ApiDB.uqWebTasksPROJECT_ID.AsString;
item.application := ApiDB.uqProjectTasksAPPLICATION.AsString;
item.version := ApiDB.uqProjectTasksAPP_VERSION.AsString;
item.application := ApiDB.uqWebTasksAPPLICATION.AsString;
item.version := ApiDB.uqWebTasksAPP_VERSION.AsString;
if ApiDB.uqProjectTasksTASK_DATE.IsNull then
if ApiDB.uqWebTasksTASK_DATE.IsNull then
item.taskDate := 0
else
item.taskDate := ApiDB.uqProjectTasksTASK_DATE.AsDateTime;
item.taskDate := ApiDB.uqWebTasksTASK_DATE.AsDateTime;
item.reportedBy := ApiDB.uqProjectTasksREPORTED_BY.AsString;
item.assignedTo := ApiDB.uqProjectTasksASSIGNED_TO.AsString;
item.reportedBy := ApiDB.uqWebTasksREPORTED_BY.AsString;
item.assignedTo := ApiDB.uqWebTasksASSIGNED_TO.AsString;
item.status := ApiDB.uqProjectTasksSTATUS.AsString;
item.status := ApiDB.uqWebTasksSTATUS.AsString;
if ApiDB.uqProjectTasksSTATUS_DATE.IsNull then
if ApiDB.uqWebTasksSTATUS_DATE.IsNull then
item.statusDate := Null
else
item.statusDate := ApiDB.uqProjectTasksSTATUS_DATE.AsDateTime;
item.statusDate := ApiDB.uqWebTasksSTATUS_DATE.AsDateTime;
item.fixedVersion := ApiDB.uqProjectTasksFIXED_VERSION.AsString;
item.formSection := ApiDB.uqProjectTasksFORM_SECTION.AsString;
item.issue := ApiDB.uqProjectTasksISSUE.AsString;
item.notes := ApiDB.uqProjectTasksNOTES.AsString;
item.fixedVersion := ApiDB.uqWebTasksFIXED_VERSION.AsString;
item.formSection := ApiDB.uqWebTasksFORM_SECTION.AsString;
item.issue := ApiDB.uqWebTasksISSUE.AsString;
item.notes := ApiDB.uqWebTasksNOTES.AsString;
task.items.Add(item);
ApiDB.uqProjectTasks.Next;
ApiDB.uqWebTasks.Next;
end;
Result.count := Result.data.Count;
finally
taskMap.Free;
Logger.Log(4, Format('ApiService.GetTaskItems - returned %d task(s)', [Result.count]));
except
on E: Exception do
begin
Logger.Log(2, 'ApiService.GetTaskItems - ERROR: ' + E.Message);
raise;
end;
end;
end;
procedure TApiService.EnsureBlankWebTaskRow(const taskId: string);
begin
Logger.Log(4, Format('ApiService.EnsureBlankWebTaskRow - TASK_ID="%s"', [taskId]));
try
ApiDB.uqEnsureBlankRow.Close;
ApiDB.uqEnsureBlankRow.ParamByName('TASK_ID').AsString := taskId;
ApiDB.uqEnsureBlankRow.ExecSQL;
except
on E: Exception do
begin
Logger.Log(2, 'ApiService.EnsureBlankWebTaskRow - ERROR: ' + E.Message);
raise;
end;
end;
end;
function TApiService.SaveTaskRow(const Item: TTaskRowSave): Boolean;
......@@ -124,6 +158,9 @@ function TApiService.SaveTaskRow(const Item: TTaskRowSave): Boolean;
var
d: TDateTime;
begin
Logger.Log(4, Format('ApiService.SaveTaskRow - TASK_ITEM_ID="%s"', [Item.taskItemId]));
try
ApiDB.uqSaveTaskRow.Close;
ApiDB.uqSaveTaskRow.ParamByName('TASK_ITEM_ID').AsString := Item.taskItemId;
......@@ -144,7 +181,7 @@ begin
if ParseDateOrZero(Item.statusDate, d) then
ApiDB.uqSaveTaskRow.ParamByName('STATUS_DATE').AsDateTime := d
else
ApiDB.uqSaveTaskRow.ParamByName('STATUS_DATE').Clear;
ApiDB.uqSaveTaskRow.ParamByName('STATUS_DATE').AsDateTime := Date;
ApiDB.uqSaveTaskRow.ParamByName('FIXED_VERSION').AsString := Item.fixedVersion;
ApiDB.uqSaveTaskRow.ParamByName('FORM_SECTION').AsString := Item.formSection;
......@@ -154,11 +191,19 @@ begin
ApiDB.uqSaveTaskRow.ExecSQL;
Result := True;
Logger.Log(4, 'ApiService.SaveTaskRow - OK');
except
on E: Exception do
begin
Logger.Log(2, 'ApiService.SaveTaskRow - ERROR: ' + E.Message);
raise;
end;
end;
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
RegisterServiceType(TApiService);
end.
......@@ -3,31 +3,116 @@ object AuthDatabase: TAuthDatabase
OnDestroy = DataModuleDestroy
Height = 249
Width = 433
object uq: TUniQuery
Connection = ucKG
object uqWebTasksUrl: TUniQuery
Connection = ucETaskAuth
SQL.Strings = (
'select * from users')
'select'
' u.USER_ID,'
' u.USER_NAME,'
' u.NAME,'
' u.STATUS,'
' u.EMAIL,'
' u.ACCESS_LEVEL,'
' u.TASK_RIGHTS,'
' u.PERSPECTIVE_ID,'
' u.LAST_NAME,'
' u.FIRST_NAME,'
' w.URL_TIME,'
' w.URL_TIME_EXP'
'from web_tasks_url w'
'join users u on u.USER_ID = w.USER_ID'
'where w.USER_ID = :USER_ID'
' and w.TASK_ID = :TASK_ID'
' and w.URL_CODE = :URL_CODE'
' and TIMESTAMPDIFF(SECOND, w.URL_TIME, NOW()) between 0 and w.U' +
'RL_TIME_EXP'
'order by w.URL_TIME desc'
'limit 1')
FetchRows = 100
Active = True
Left = 162
Top = 45
ParamData = <
item
DataType = ftUnknown
Name = 'USER_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end
item
DataType = ftUnknown
Name = 'URL_CODE'
Value = nil
end>
object uqWebTasksUrlUSER_ID: TStringField
FieldName = 'USER_ID'
Required = True
Size = 7
end
object uqWebTasksUrlUSER_NAME: TStringField
FieldName = 'USER_NAME'
Required = True
Size = 12
end
object uqWebTasksUrlNAME: TStringField
FieldName = 'NAME'
Size = 40
end
object uqWebTasksUrlSTATUS: TStringField
FieldName = 'STATUS'
Size = 7
end
object uqWebTasksUrlEMAIL: TStringField
FieldName = 'EMAIL'
Size = 50
end
object uqWebTasksUrlACCESS_LEVEL: TIntegerField
FieldName = 'ACCESS_LEVEL'
end
object uqWebTasksUrlTASK_RIGHTS: TIntegerField
FieldName = 'TASK_RIGHTS'
end
object uqWebTasksUrlPERSPECTIVE_ID: TStringField
FieldName = 'PERSPECTIVE_ID'
Size = 45
end
object uqWebTasksUrlLAST_NAME: TStringField
FieldName = 'LAST_NAME'
Size = 35
end
object uqWebTasksUrlFIRST_NAME: TStringField
FieldName = 'FIRST_NAME'
Size = 25
end
object uqWebTasksUrlURL_TIME: TDateTimeField
FieldName = 'URL_TIME'
ReadOnly = True
Required = True
end
object uqWebTasksUrlURL_TIME_EXP: TIntegerField
FieldName = 'URL_TIME_EXP'
ReadOnly = True
Required = True
end
object uqMisc: TUniQuery
FetchRows = 100
Left = 249
Top = 45
end
object ucKG: TUniConnection
object ucETaskAuth: TUniConnection
ProviderName = 'MySQL'
Database = 'emt3_web_db'
Database = 'eTask'
Username = 'root'
Server = '192.168.102.129'
Connected = True
LoginPrompt = False
Left = 67
Top = 131
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 230
Top = 140
Left = 194
Top = 132
end
end
......@@ -10,10 +10,21 @@ uses
type
TAuthDatabase = class(TDataModule)
uq: TUniQuery;
uqMisc: TUniQuery;
ucKG: TUniConnection;
uqWebTasksUrl: TUniQuery;
ucETaskAuth: TUniConnection;
MySQLUniProvider1: TMySQLUniProvider;
uqWebTasksUrlUSER_ID: TStringField;
uqWebTasksUrlUSER_NAME: TStringField;
uqWebTasksUrlNAME: TStringField;
uqWebTasksUrlSTATUS: TStringField;
uqWebTasksUrlEMAIL: TStringField;
uqWebTasksUrlACCESS_LEVEL: TIntegerField;
uqWebTasksUrlTASK_RIGHTS: TIntegerField;
uqWebTasksUrlPERSPECTIVE_ID: TStringField;
uqWebTasksUrlLAST_NAME: TStringField;
uqWebTasksUrlFIRST_NAME: TStringField;
uqWebTasksUrlURL_TIME: TDateTimeField;
uqWebTasksUrlURL_TIME_EXP: TIntegerField;
procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject);
private
......@@ -40,9 +51,9 @@ uses
procedure TAuthDatabase.DataModuleCreate(Sender: TObject);
begin
Logger.Log( 5, 'TAuthDatabase.DataModuleCreate' );
LoadDatabaseSettings( ucKG, 'kgOrdersServer.ini' );
LoadDatabaseSettings( ucETaskAuth, 'emT3WebServer.ini' );
try
ucKG.Connect;
ucETaskAuth.Connect;
except
on E: Exception do
begin
......@@ -53,7 +64,7 @@ end;
procedure TAuthDatabase.DataModuleDestroy(Sender: TObject);
begin
ucKG.Connected := false;
ucETaskAuth.Connected := false;
end;
......
......@@ -18,7 +18,7 @@ type
[ServiceContract, Model(AUTH_MODEL)]
IAuthService = interface(IInvokable)
['{9CFD59B2-A832-4F82-82BB-9A25FC93F305}']
function Login(const user, password: string): string;
function Login(const userId, taskId, urlCode: string): string;
function VerifyVersion(ClientVersion: string): TJSONObject;
end;
......
......@@ -14,22 +14,25 @@ type
TAuthService = class(TInterfacedObject, IAuthService)
strict private
authDB: TAuthDatabase;
function GetQuery: TUniQuery;
private
userId: string;
userName: string;
userFullName: string;
userId: string;
userPerspectiveID: string;
userQBID: string;
userAccessType: string;
userEmail: string;
userStatus: string;
qbEnabled: boolean;
userEmail: string;
userAccessLevel: string;
userTaskRights: string;
userPerspectiveId: string;
userFirstName: string;
userLastName: string;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function CheckUser(const user, password: string): Integer;
function CheckUrlLogin(const userId, taskId, urlCode: string): Integer;
procedure LoadUserFromUrlLoginQuery;
public
function Login(const user, password: string): string;
function Login(const userId, taskId, urlCode: string): string;
function VerifyVersion(ClientVersion: string): TJSONObject;
end;
......@@ -38,16 +41,12 @@ implementation
uses
System.SysUtils,
System.DateUtils,
System.Generics.Collections,
Bcl.JOSE.Core.Builder,
Bcl.JOSE.Core.JWT,
Aurelius.Global.Utils,
XData.Sys.Exceptions,
Common.Logging,
Common.Config,
uLibrary;
{ TLoginService }
Common.Config;
procedure TAuthService.AfterConstruction;
begin
......@@ -58,7 +57,7 @@ begin
on E: Exception do
begin
Logger.Log(1, 'Error when creating the Auth database: ' + E.Message);
raise EXDataHttpException.Create(500, 'Unable to create Auth database: A KGOrders Server Error has occured!');
raise EXDataHttpException.Create(500, 'Unable to create Auth database: A Server Error has occured!');
end;
end;
end;
......@@ -69,11 +68,6 @@ begin
inherited;
end;
function TAuthService.GetQuery: TUniQuery;
begin
Result := authDB.uq;
end;
function TAuthService.VerifyVersion(ClientVersion: string): TJSONObject;
var
iniFile: TIniFile;
......@@ -86,14 +80,14 @@ begin
try
webClientVersion := iniFile.ReadString('Settings', 'webClientVersion', '');
Result.AddPair('webClientVersion', webClientVersion);
qbEnabled := iniFile.ReadBool('Quickbooks', 'Enabled', false);
if webClientVersion = '' then
begin
Result.AddPair('error', 'webClientVersion is not configured.');
Exit;
end;
if clientVersion <> webClientVersion then
if ClientVersion <> webClientVersion then
begin
Result.AddPair('error',
'Your browser is running an old version of the app.' + sLineBreak +
......@@ -104,111 +98,103 @@ begin
end;
end;
function TAuthService.CheckUrlLogin(const userId, taskId, urlCode: string): Integer;
begin
Result := 0;
authDB.uqWebTasksUrl.Close;
authDB.uqWebTasksUrl.ParamByName('USER_ID').AsString := userId;
authDB.uqWebTasksUrl.ParamByName('TASK_ID').AsString := taskId;
authDB.uqWebTasksUrl.ParamByName('URL_CODE').AsString := urlCode;
authDB.uqWebTasksUrl.Open;
if authDB.uqWebTasksUrl.IsEmpty then
Exit;
function TAuthService.Login(const user, password: string): string;
if authDB.uqWebTasksUrl.FieldByName('STATUS').AsString <> 'ACTIVE' then
begin
Result := 2;
Exit;
end;
LoadUserFromUrlLoginQuery;
Result := 3;
end;
procedure TAuthService.LoadUserFromUrlLoginQuery;
var
nameValue: string;
begin
Self.userId := authDB.uqWebTasksUrl.FieldByName('USER_ID').AsString;
userName := authDB.uqWebTasksUrl.FieldByName('USER_NAME').AsString;
userStatus := authDB.uqWebTasksUrl.FieldByName('STATUS').AsString;
userEmail := authDB.uqWebTasksUrl.FieldByName('EMAIL').AsString;
userAccessLevel := authDB.uqWebTasksUrl.FieldByName('ACCESS_LEVEL').AsString;
userTaskRights := authDB.uqWebTasksUrl.FieldByName('TASK_RIGHTS').AsString;
userPerspectiveId := authDB.uqWebTasksUrl.FieldByName('PERSPECTIVE_ID').AsString;
userLastName := authDB.uqWebTasksUrl.FieldByName('LAST_NAME').AsString;
userFirstName := authDB.uqWebTasksUrl.FieldByName('FIRST_NAME').AsString;
nameValue := Trim(authDB.uqWebTasksUrl.FieldByName('NAME').AsString);
if nameValue <> '' then
userFullName := nameValue
else
userFullName := Trim(userFirstName + ' ' + userLastName);
end;
function TAuthService.Login(const userId, taskId, urlCode: string): string;
var
userState: Integer;
iniFile: TIniFile;
JWT: TJWT;
jwt: TJWT;
begin
Logger.Log(3, Format( 'AuthService.Login - User: "%s"', [User]));
userState := CheckUser( user, password );
Logger.Log(3, Format('AuthService.Login - UserID: "%s", TaskID: "%s"', [userId, taskId]));
try
userState := CheckUser(user, password);
userState := CheckUrlLogin(userId, taskId, urlCode);
except
on E: Exception do
begin
Logger.Log(1, 'Login failed due to database error: ' + E.Message);
Logger.Log(1, 'URL Login failed due to database error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Login failed: Unable to connect to the database.');
end;
end;
if userState = 0 then
begin
raise EXDataHttpUnauthorized.Create('Invalid username or password');
logger.Log(2, 'Login Error: Invalid username or password');
end
else if userState = 1 then
begin
raise EXDataHttpUnauthorized.Create('User does not exist!');
logger.Log(2, 'Login Error: User does not exist!');
end
else if userState = 2 then
begin
raise EXDataHttpUnauthorized.Create('User not active!');
logger.Log(2, 'Login Error: User not active!');
Logger.Log(2, 'Login Error: Invalid code or expired link');
raise EXDataHttpUnauthorized.Create('Invalid code or expired link');
end;
iniFile := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
try
qbEnabled := iniFile.ReadBool('Quickbooks', 'Enabled', false);
finally
iniFile.Free;
if userState = 2 then
begin
Logger.Log(2, 'Login Error: User not active!');
raise EXDataHttpUnauthorized.Create('User not active!');
end;
JWT := TJWT.Create;
jwt := TJWT.Create;
try
JWT.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
JWT.Claims.IssuedAt := Now;
JWT.Claims.Expiration := IncHour(Now, 24);
JWT.Claims.SetClaimOfType<string>('user_name', userName);
JWT.Claims.SetClaimOfType<string>('user_fullname', userFullName);
JWT.Claims.SetClaimOfType<string>('user_id', userId);
JWT.Claims.SetClaimOfType<string>('user_perspective_id', userPerspectiveID);
JWT.Claims.SetClaimOfType<string>('user_status', userStatus);
JWT.Claims.SetClaimOfType<string>('user_email', userEmail);
JWT.Claims.SetClaimOfType<string>('user_qb_id', userQBID);
JWT.Claims.SetClaimOfType<string>('user_access_type', userAccessType);
JWT.Claims.SetClaimOfType<boolean>('qb_enabled', qbEnabled);
Result := TJOSE.SHA256CompactToken(serverConfig.jwtTokenSecret, JWT);
jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
jwt.Claims.IssuedAt := Now;
jwt.Claims.Expiration := IncHour(Now, 24);
jwt.Claims.SetClaimOfType<string>('user_id', Self.userId);
jwt.Claims.SetClaimOfType<string>('user_name', userName);
jwt.Claims.SetClaimOfType<string>('user_fullname', userFullName);
jwt.Claims.SetClaimOfType<string>('user_status', userStatus);
jwt.Claims.SetClaimOfType<string>('user_email', userEmail);
jwt.Claims.SetClaimOfType<string>('user_access_level', userAccessLevel);
jwt.Claims.SetClaimOfType<string>('task_rights', userTaskRights);
jwt.Claims.SetClaimOfType<string>('user_perspective_id', userPerspectiveId);
jwt.Claims.SetClaimOfType<string>('first_name', userFirstName);
jwt.Claims.SetClaimOfType<string>('last_name', userLastName);
Result := TJOSE.SHA256CompactToken(serverConfig.jwtTokenSecret, jwt);
finally
JWT.Free;
end;
end;
function TAuthService.CheckUser(const user, password: string): Integer;
var
userStr: string;
SQL: string;
name: string;
checkString: string;
begin
Result := 0;
Logger.Log(1, Format('AuthService.CheckUser - User: "%s"', [user]) );
SQL := 'select * from users where USER_NAME = ' + QuotedStr(user);
DoQuery(authDB.uq, SQL);
if authDB.uq.IsEmpty then
begin
Result := 1; //user does not exist, replace this with 0 for more security
end
else if ( authDB.uq.FieldByName('STATUS').AsString <> 'ACTIVE' ) then
Result := 2 // user is not active
else
begin
name := authDB.uq.FieldByName('NAME').AsString;
checkString := THashSHA2.GetHashString(name + password, THashSHA2.TSHA2Version.SHA512).ToUpper;
if authDB.uq.FieldByName('PASSWORD').AsString = checkString then
begin
userName := user;
userFullName:= authDB.uq.FieldByName('NAME').AsString;;
userId := authDB.uq.FieldByName('USER_ID').AsString;
userStatus := authDB.uq.FieldByName('STATUS').AsString;
userPerspectiveID := authDB.uq.FieldByName('PERSPECTIVE_ID').AsString;
userEmail := authDB.uq.FieldByName('EMAIL').AsString;
userQBID := authDB.uq.FieldByName('QB_ID').AsString;
userAccessType := authDB.uq.FieldByName('ACCESS_TYPE').AsString;
Logger.Log(1, Format('AuthDB.SetLoginAuditEntry: "%s"', [user]) );
Result := 3; // Succcess
end
else
Result := 0; // invalid password
jwt.Free;
end;
end;
initialization
RegisterServiceType(TAuthService);
end.
......@@ -3,7 +3,7 @@ unit Common.Config;
interface
const
defaultServerUrl = 'http://localhost:2004/kgOrders/';
defaultServerUrl = 'http://localhost:2004/emsys/emt3';
type
TServerConfig = class
......
......@@ -9,7 +9,7 @@ LogFileNum=175
Server=192.168.102.129
--Server=192.168.75.133
--Server=192.168.159.10
Database=emt3_web_db
Database=eTask
Username=root
Password=emsys01
--Password=emsys!012
......
{
"url": "http://localhost:2004/kgOrders/",
"url": "http://localhost:2004/emsys/emt3",
"jwtTokenSecret": "super_secret0123super_secret4567",
"adminPassword": "whatisthisusedfor",
"webAppFolder": "static",
......
......@@ -97,7 +97,7 @@ begin
if not DirectoryExists(logsDir) then
CreateDir(logsDir);
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini' );
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'emT3webServer.ini' );
try
fileNum := iniFile.ReadInteger( 'Settings', 'LogFileNum', 0 );
FLogFile := logsDir + AFilename + Format( '%.4d', [fileNum] ) + '.log';
......@@ -160,7 +160,7 @@ begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TFMain, FMain);
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini' );
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'emT3webServer.ini' );
try
memoLogLevel := iniFile.ReadInteger( 'Settings', 'memoLogLevel', 3 );
fileLogLevel := iniFile.ReadInteger( 'Settings', 'memoLogLevel', 4 );
......@@ -168,6 +168,5 @@ begin
iniFile.Free;
end;
Logger.AddAppender(TMemoLogAppender.Create( memoLogLevel, FMain.memoinfo ));
Logger.AddAppender(TFileLogAppender.Create( fileLogLevel, 'kgOrdersServer' ));
Application.Run;
end.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment