unit Auth.ServiceImpl;

interface

uses
  XData.Service.Common,
  XData.Server.Module,
  Auth.Service,
  Auth.Database,
  Uni, Data.DB, System.Hash, System.IniFiles, System.JSON;

type
  [ServiceImplementation]
  TAuthService = class(TInterfacedObject, IAuthService)
  strict private
    authDB: TAuthDatabase;

    userId: string;
    userName: string;
    userFullName: string;
    userStatus: string;
    userEmail: string;
    userAccessLevel: string;
    userTaskRights: string;
    userPerspectiveId: string;
    userFirstName: string;
    userLastName: string;

    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;

    function CheckUrlLogin(userId, taskId, urlCode: string): Integer;
    function CheckUserLogin(userName, taskId, password: string): Integer;
    function HashPassword(const APassword: string): string;
    procedure LoadUserFromUrlLoginQuery;
    procedure LoadUserFromWebLoginQuery;
  public
    function Login(userId, taskId, urlCode: string): string;
    function VerifyVersion(ClientVersion: string): TJSONObject;
    function WebLogin(userName, loginType, loginValue, password: string): string;
    function CheckTimeEntriesLogin(userName, password: string): Integer;
  end;

implementation

uses
  System.SysUtils,
  System.DateUtils,
  Bcl.JOSE.Core.Builder,
  Bcl.JOSE.Core.JWT,
  Aurelius.Global.Utils,
  XData.Sys.Exceptions,
  Common.Logging,
  Common.Config;

procedure TAuthService.AfterConstruction;
begin
  inherited;
  try
    authDB := TAuthDatabase.Create(nil);
  except
    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 Server Error has occured!');
    end;
  end;
end;


procedure TAuthService.BeforeDestruction;
begin
  authDB.Free;
  inherited;
end;


function TAuthService.VerifyVersion(ClientVersion: string): TJSONObject;
var
  iniFile: TIniFile;
  webClientVersion: string;
begin
  Result := TJSONObject.Create;
  TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);

  iniFile := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    webClientVersion := iniFile.ReadString('Settings', 'webClientVersion', '');
    Result.AddPair('webClientVersion', webClientVersion);

    if webClientVersion = '' then
    begin
      Result.AddPair('error', 'webClientVersion is not configured.');
      Exit;
    end;

    if ClientVersion <> webClientVersion then
    begin
      Result.AddPair('error',
        'Your browser is running an old version of the app.' + sLineBreak +
        'Please click button to reload.' + sLineBreak + sLineBreak +
        'If error continues, empty cache and hard reload.');
    end;
  finally
    iniFile.Free;
  end;
end;


function TAuthService.CheckUrlLogin(userId, taskId, urlCode: string): Integer;
var
  sql: string;
  timeNow: TDateTime;
  timeDiff: integer;
begin
  Logger.Log(3, 'TAuthService.CheckUrlLogin(const userId, taskId, urlCode: string): Integer' );

  sql := 'select u.USER_ID, u.USER_NAME, u.NAME, u.STATUS, u.EMAIL, u.ACCESS_LEVEL, ';
  sql := sql + 'u.TASK_RIGHTS, u.PERSPECTIVE_ID, u.LAST_NAME, u.FIRST_NAME, ';
  sql := sql + 'w.URL_TIME, w.URL_TIME_EXP, w.WEB_LOGIN ';
  sql := sql + 'from web_tasks_url w ';
  sql := sql + 'join users u on u.USER_ID = w.USER_ID ';
  sql := sql + 'where w.USER_ID = :USER_ID and w.TASK_ID = :TASK_ID and w.URL_CODE = :URL_CODE ';
  //sql := sql + 'and TIMESTAMPDIFF(SECOND, w.URL_TIME, NOW()) between 0 and w.URL_TIME_EXP';

  authDB.uqWebTasksUrl.Close;
  authDB.uqWebTasksUrl.SQL.Text := sql;
  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
  begin
    Logger.Log(3, '--URL Login failed 0: authDB.uqWebTasksUrl.IsEmpty');
    Result := 0;
    Exit;
  end;

  if authDB.uqWebTasksUrlSTATUS.AsString <> 'ACTIVE' then
  begin
    Logger.Log(3, '--URL Login failed 1: authDB.uqWebTasksUrlSTATUS.AsString <> ACTIVE');
    Result := 1;
    Exit;
  end;

  timeNow := Now;
  timeDiff := SecondsBetween( timeNow, authDB.uqWebTasksUrlURL_TIME.AsDateTime );
  if timeDiff > authDB.uqWebTasksUrlURL_TIME_EXP.AsInteger then
  begin
    Logger.Log( 3, '--timeNow: ' + timeNow.ToString + ' -urlTime: ' + authDB.uqWebTasksUrlURL_TIME.AsString );
    Logger.Log( 3, '--timeDiff: ' + IntToStr(timeDiff) + ' -timeExp (authDB.uqWebTasksUrlURL_TIME_EXP.AsInteger): ' + authDB.uqWebTasksUrlURL_TIME_EXP.AsString );
    Logger.Log( 3, '--URL Login failed 2: timeDiff > timeExp' );
    Result := 2;
    Exit;
  end;

  LoadUserFromUrlLoginQuery;
  Result := 3;
end;


procedure TAuthService.LoadUserFromUrlLoginQuery;
var
  nameValue: string;
begin
  Self.userId := authDB.uqWebTasksUrlUSER_ID.AsString;
  userName := authDB.uqWebTasksUrlUSER_NAME.AsString;
  userStatus := authDB.uqWebTasksUrlSTATUS.AsString;
  userEmail := authDB.uqWebTasksUrlEMAIL.AsString;
  userAccessLevel := authDB.uqWebTasksUrlACCESS_LEVEL.AsString;
  userTaskRights := authDB.uqWebTasksUrlTASK_RIGHTS.AsString;
  userPerspectiveId := authDB.uqWebTasksUrlPERSPECTIVE_ID.AsString;
  userLastName := authDB.uqWebTasksUrlLAST_NAME.AsString;
  userFirstName := authDB.uqWebTasksUrlFIRST_NAME.AsString;

  nameValue := Trim(authDB.uqWebTasksUrlNAME.AsString);
  if nameValue <> '' then
    userFullName := nameValue
  else
    userFullName := Trim(userFirstName + ' ' + userLastName);
end;


function TAuthService.Login(userId, taskId, urlCode: string): string;
var
  userState: Integer;
  jwt: TJWT;
begin
  Logger.Log(3, Format('AuthService.Login - UserID: "%s", TaskID: "%s", Code: "%s"', [userId, taskId, urlCode]));

  try
    userState := CheckUrlLogin(userId, taskId, urlCode);
  except
    on E: Exception do
    begin
      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
    Logger.Log(2, 'Login Error: Invalid url parameters');
    raise EXDataHttpUnauthorized.Create('Invalid url parameters');
  end;

  if userState = 1 then
  begin
    Logger.Log(2, 'Login Error: User not active!');
    raise EXDataHttpUnauthorized.Create('User not active!');
  end;

  if userState = 2 then
  begin
    Logger.Log(2, 'Login Error: Expired link');
    raise EXDataHttpUnauthorized.Create('Expired link');
  end;

  jwt := TJWT.Create;
  try
    jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
    jwt.Claims.IssuedAt := Now;
    jwt.Claims.Expiration := IncHour(Now, 12);

    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.WebLogin(userName, loginType, loginValue, password: string): string;
var
  userState: Integer;
  jwt: TJWT;
begin
  Logger.Log(3, Format('AuthService.WebLogin - UserName: %s, LoginType: %s, LoginValue: %s',
    [userName, loginType, loginValue]));

  try
    if SameText(loginType, 'task_items') then
      userState := CheckUserLogin(userName, loginValue, password)
    else if SameText(loginType, 'time_entries') then
      userState := CheckTimeEntriesLogin(userName, password)
    else
    begin
      Logger.Log(2, 'Web Login Error: Invalid login type: ' + loginType);
      raise EXDataHttpUnauthorized.Create('Invalid login type');
    end;
  except
    on E: EXDataHttpUnauthorized do
      raise;
    on E: Exception do
    begin
      Logger.Log(1, 'Web 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
    if SameText(loginType, 'time_entries') then
    begin
      Logger.Log(2, 'Web Login Error: Invalid username or password');
      raise EXDataHttpUnauthorized.Create('Invalid user name or password');
    end
    else
    begin
      Logger.Log(2, 'Web Login Error: Invalid username, task id, or password');
      raise EXDataHttpUnauthorized.Create('Invalid user name, task id, or password');
    end;
  end;

  if userState = 1 then
  begin
    Logger.Log(2, 'Web Login Error: User not active!');
    raise EXDataHttpUnauthorized.Create('User not active!');
  end;

  if userState = 2 then
  begin
    Logger.Log(2, 'Web Login Error: Web login is not enabled for this task');
    raise EXDataHttpUnauthorized.Create('Web login is not enabled for this task');
  end;

  jwt := TJWT.Create;
  try
    jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
    jwt.Claims.IssuedAt := Now;
    jwt.Claims.Expiration := IncHour(Now, 12);

    jwt.Claims.SetClaimOfType<string>('user_id', Self.userId);
    jwt.Claims.SetClaimOfType<string>('user_name', Self.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.HashPassword(const APassword: string): string;
begin
  Result := LowerCase(THashSHA2.GetHashString(UpperCase(Trim(APassword))));
end;


function TAuthService.CheckUserLogin(userName, taskId, password: string): Integer;
var
  webLogin: string;
  storedPasswordHash: string;
begin
  Logger.Log(3, 'TAuthService.CheckUserLogin(userName, taskId, password: string): Integer');

  authDB.uqWebLogin.Close;
  authDB.uqWebLogin.ParamByName('USER_NAME').AsString := userName;
  authDB.uqWebLogin.ParamByName('TASK_ID').AsString := taskId;
  authDB.uqWebLogin.ParamByName('URL_CODE').AsString := '000000';
  authDB.uqWebLogin.Open;

  if authDB.uqWebLogin.IsEmpty then
  begin
    Logger.Log(3, '--Web Login failed 0: authDB.uqWebLogin.IsEmpty');
    Result := 0;
    Exit;
  end;

  storedPasswordHash := HashPassword(authDB.uqWebLogin.FieldByName('PASSWORD').AsString);
  if storedPasswordHash <> LowerCase(Trim(password)) then
  begin
    Logger.Log(3, '--Web Login failed 0: password hash mismatch');
    Result := 0;
    Exit;
  end;

  if authDB.uqWebLoginSTATUS.AsString <> 'ACTIVE' then
  begin
    Logger.Log(3, '--Web Login failed 1: authDB.uqWebLoginSTATUS.AsString <> ACTIVE');
    Result := 1;
    Exit;
  end;

  webLogin := authDB.uqWebLoginWEB_LOGIN.AsString;
  if webLogin <> 'T' then
  begin
    Logger.Log(3, '--Web Login failed 2: WEB_LOGIN <> "T"');
    Result := 2;
    Exit;
  end;

  LoadUserFromWebLoginQuery;
  Result := 3;
end;


function TAuthService.CheckTimeEntriesLogin(userName, password: string): Integer;
var
  q: TUniQuery;
  storedPasswordHash: string;
  nameValue: string;
begin
  Logger.Log(3, 'TAuthService.CheckTimeEntriesLogin(userName, password: string): Integer');

  q := TUniQuery.Create(nil);
  try
    q.Connection := authDB.uqWebLogin.Connection;
    q.SQL.Text :=
      'select ' +
      '  USER_ID, USER_NAME, NAME, STATUS, EMAIL, ACCESS_LEVEL, ' +
      '  TASK_RIGHTS, PERSPECTIVE_ID, LAST_NAME, FIRST_NAME, PASSWORD ' +
      'from users ' +
      'where USER_NAME = :USER_NAME';

    q.ParamByName('USER_NAME').AsString := userName;
    q.Open;

    if q.IsEmpty then
    begin
      Logger.Log(3, '--Time Entries Login failed 0: user not found');
      Result := 0;
      Exit;
    end;

    storedPasswordHash := HashPassword(q.FieldByName('PASSWORD').AsString);
    if storedPasswordHash <> LowerCase(Trim(password)) then
    begin
      Logger.Log(3, '--Time Entries Login failed 0: password hash mismatch');
      Result := 0;
      Exit;
    end;

    if q.FieldByName('STATUS').AsString <> 'ACTIVE' then
    begin
      Logger.Log(3, '--Time Entries Login failed 1: user is not active');
      Result := 1;
      Exit;
    end;

    Self.userId := q.FieldByName('USER_ID').AsString;
    Self.userName := q.FieldByName('USER_NAME').AsString;
    userStatus := q.FieldByName('STATUS').AsString;
    userEmail := q.FieldByName('EMAIL').AsString;
    userAccessLevel := q.FieldByName('ACCESS_LEVEL').AsString;
    userTaskRights := q.FieldByName('TASK_RIGHTS').AsString;
    userPerspectiveId := q.FieldByName('PERSPECTIVE_ID').AsString;
    userLastName := q.FieldByName('LAST_NAME').AsString;
    userFirstName := q.FieldByName('FIRST_NAME').AsString;

    nameValue := Trim(q.FieldByName('NAME').AsString);
    if nameValue <> '' then
      userFullName := nameValue
    else
      userFullName := Trim(userFirstName + ' ' + userLastName);

    Result := 3;
  finally
    q.Free;
  end;
end;


procedure TAuthService.LoadUserFromWebLoginQuery;
var
  nameValue: string;
begin
  Self.userId := authDB.uqWebLOginUSER_ID.AsString;
  userName := authDB.uqWebLoginUSER_NAME.AsString;
  userStatus := authDB.uqWebLoginSTATUS.AsString;
  userEmail := authDB.uqWebLoginEMAIL.AsString;
  userAccessLevel := authDB.uqWebLoginACCESS_LEVEL.AsString;
  userTaskRights := authDB.uqWebLoginTASK_RIGHTS.AsString;
  userPerspectiveId := authDB.uqWebLoginPERSPECTIVE_ID.AsString;
  userLastName := authDB.uqWebLoginLAST_NAME.AsString;
  userFirstName := authDB.uqWebLoginFIRST_NAME.AsString;

  nameValue := Trim(authDB.uqWebLoginNAME.AsString);
  if nameValue <> '' then
    userFullName := nameValue
  else
    userFullName := Trim(userFirstName + ' ' + userLastName);
end;


initialization
  RegisterServiceType(TAuthService);

end.
