unit Auth.ServiceImpl;

interface

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

type
  [ServiceImplementation]
  TAuthService = class(TInterfacedObject, IAuthService)
  strict private
    authDB: TAuthDatabase;
  private
    userName: string;
    userFullName: string;
    userAgency: string;
    userBadge: string;
    userId: string;
    userPersonnelId: string;
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    function VerifyVersion(ClientVersion: string): TJSONObject;
    function CheckUser(const User, Password, Agency: string): Integer;
    function Decrypt(inStr, keyStr: AnsiString): AnsiString;
  public
    function Login(const User, Password, Agency: string): string;
    function GetAgencieslist(): TAgenciesList;
    function GetAgencyConfiglist: TAgencyConfigList;
  end;

implementation

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

{ TAuthService }

procedure TAuthService.AfterConstruction;
begin
  inherited;
  authDB := TAuthDatabase.Create(nil);
  Logger.Log(3, 'AuthDatabase created');
end;

procedure TAuthService.BeforeDestruction;
begin
  authDB.Free;
  inherited;
  Logger.Log(3, 'AuthDatabase destroyed');
end;

function TAuthService.GetAgenciesList: TAgenciesList;
var
  agency: TAgencyItem;
begin
  Logger.Log(2, 'TAuthService.GetAgenciesList - call');

  if authDB.ucLemsOCSO.Connected then
  begin
    authDB.uqAuth.Close;
    authDB.uqAuth.SQL.Text :=
      'select agencyid as agency ' +
      'from lems.agency ' +
      'where login = ''T'' ' +
      'order by agencyid';
    authDB.uqAuth.Open;

    Result := TAgenciesList.Create;
    TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);

    Result.data := TList<TAgencyItem>.Create;
    TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result.data);

    try
      while not authDB.uqAuth.Eof do
      begin
        agency := TAgencyItem.Create(authDB.uqAuth.FieldByName('agency').AsString);
        TXDataOperationContext.Current.Handler.ManagedObjects.Add(agency);
        Result.Data.Add(agency);
        authDB.uqAuth.Next;
      end;
    finally
      authDB.uqAuth.Close;
    end;

    Result.count := Result.data.count;
    Result.returned := Result.data.count;
    Logger.Log(2, 'GetAgenciesList - Count: ' + IntToStr(Result.Count) + ' Returned: ' + IntToStr(Result.Returned));
  end
  else
  begin
    Logger.Log(2, 'TAuthService.GetAgenciesList - Error: connecting to auth database!');
    raise EXDataHttpException.Create(500, 'Failed to load agencies');
  end;
end;

function TAuthService.GetAgencyConfigList: TAgencyConfigList;
var
  agencyConfig: TAgencyConfigItem;
  sql: string;
begin
  Logger.Log(2, 'AuthService.GetAgencyConfigList - call');

  authDB.uqAuth.Close;

  sql := 'select agencyid, agencyname ' +
         'from lems.agency ' +
         'where login = ''T'' ' +
         'order by agencyname';

  authDB.uqAuth.SQL.Text := sql;
  authDB.uqAuth.Open;

  Result := TAgencyConfigList.Create;
  TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);

  Result.data := TList<TAgencyConfigItem>.Create;
  TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result.data);

  try
    while not authDB.uqAuth.Eof do
    begin
      agencyConfig := TAgencyConfigItem.Create;
      TXDataOperationContext.Current.Handler.ManagedObjects.Add(agencyConfig);
      Result.data.Add(agencyConfig);

      agencyConfig.id := authDB.uqAuth.FieldByName('agencyid').AsString;
      agencyConfig.agency := authDB.uqAuth.FieldByName('agencyid').AsString;
      agencyConfig.name := authDB.uqAuth.FieldByName('agencyname').AsString;

      authDB.uqAuth.Next;
    end;
  finally
    authDB.uqAuth.Close;
  end;

  Result.count := Result.data.Count;
  Result.returned := Result.data.Count;
  Logger.Log(2, 'GetAgencyConfigList - ' + IntToStr(Result.Count));
end;


function TAuthService.VerifyVersion(ClientVersion: string): TJSONObject;
var
  iniFile: TIniFile;
  webClientVersion: string;
begin
  Logger.Log(3, 'AuthService.VerifyVersion called');

  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
      Logger.Log(2, 'AuthService.VerifyVersion: webClientVersion not configured');
      Result.AddPair('error', 'webClientVersion is not configured.');
      Exit;
    end;

    if clientVersion <> webClientVersion then
    begin
      Logger.Log(2, 'AuthService.VerifyVersion: client version mismatch');
      Result.AddPair('error',
        'Your browser is running an old version of the app.' + sLineBreak +
        'Please click below to reload.');
    end
    else
      Logger.Log(3, 'AuthService.VerifyVersion: version check passed');
  finally
    iniFile.Free;
  end;
end;


function TAuthService.Login(const User, Password, Agency: string): string;
var
  userState: Integer;
  JWT: TJWT;
begin
  Logger.Log(1, Format('AuthService.Login - User: "%s" Agency: "%s"', [User, Agency]));
  userState := CheckUser(User, Password, Agency);

  try
    userState := CheckUser(User, Password, Agency);
  except
    on E: Exception do
    begin
      Logger.Log(2, 'AuthService.Login - CheckUser error: ' + E.ClassName + ': ' + E.Message);
      raise EXDataHttpException.Create(500, 'Login failed');
    end;
  end;

  if userState = 0 then
  begin
    Logger.Log(2, Format('AuthService.Login - invalid login for User: "%s" Agency: "%s"', [User, Agency]));
    raise EXDataHttpUnauthorized.Create('Invalid user or password');
  end;

  if userState = 1 then
  begin
    Logger.Log(2, Format('AuthService.Login - inactive user: "%s" Agency: "%s"', [User, Agency]));
    raise EXDataHttpUnauthorized.Create('User not active');
  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, 24);
    JWT.Claims.SetClaimOfType<string>('user_name', userName);
    JWT.Claims.SetClaimOfType<string>('user_fullname', userFullName);
    JWT.Claims.SetClaimOfType<string>('user_agency', userAgency);
    JWT.Claims.SetClaimOfType<string>('user_badge', userBadge);
    JWT.Claims.SetClaimOfType<string>('user_id', userId);
    JWT.Claims.SetClaimOfType<string>('user_personnelid', userPersonnelId);
    Result := TJOSE.SHA256CompactToken(ServerConfig.jwtTokenSecret, JWT);
  finally
    JWT.Free;
  end;
end;

function TAuthService.CheckUser(const User, Password, Agency: string): Integer;
var
  userStr: string;
  decryptedPassword: AnsiString;
  passwordKey: AnsiString;
begin
  Logger.Log(3, Format('LoginService.CheckUser - User: "%s" Agency: "%s"', [User, Agency]));
  passwordKey := 'wx3cFo$kIf2jrk(gOmvi7uvPfk*iorE8@kfm+nvR6jfh=swDqalpokSjf';

  authDB.uqAuth.Close;
  authDB.uqAuth.SQL.Text :=
    'select u.* from lems.users u ' +
    'where upper(user_name) = :USER_NAME ' +
    'and u.dept = :AGENCY';

  authDB.uqAuth.ParamByName('USER_NAME').AsString := UpperCase(Trim(User));
  authDB.uqAuth.ParamByName('AGENCY').AsString := UpperCase(Trim(Agency));

  Logger.Log(4, 'LoginService.CheckUser - opening user lookup query');
  authDB.uqAuth.Open;
  try
    if authDB.uqAuth.IsEmpty then
      Result := 0
    else
    begin
      if authDB.uqAuth.FieldByName('active').AsString = 'F' then
        Result := 1
      else
      begin
        decryptedPassword := Uppercase(Trim(Decrypt(authDB.uqAuth.FieldByName('password').AsString, passwordKey)));
        if decryptedPassword = Uppercase(Trim(Password)) then
        begin
          userName        := authDB.uqAuth.FieldByName('user_name').AsString;
          userFullName    := authDB.uqAuth.FieldByName('firstname').AsString + ' ' + authDB.uqAuth.FieldByName('lastname').AsString;
          userAgency      := authDB.uqAuth.FieldByName('dept').AsString;
          userBadge       := authDB.uqAuth.FieldByName('badgenum').AsString;
          userId          := authDB.uqAuth.FieldByName('userid').AsString;
          userPersonnelId := authDB.uqAuth.FieldByName('personnelid').AsString;

          userStr := '?username=' + userName;
          userStr := userStr + '&fullname=' + userFullName;
          userStr := userStr + '&agency=' + userAgency;
          userStr := userStr + '&userid=' + userId;
          userStr := userStr + '&personnelid=' + userPersonnelId;
          authDB.SetLoginAuditEntry(userStr);

          Result := 2;
        end
        else
          Result := 0;
      end;
    end;
  finally
    authDB.uqAuth.Close;
  end;
end;

function TAuthService.Decrypt(inStr, keyStr: AnsiString): AnsiString;
var
  outStr: AnsiString;
  k, i: integer;
  tempKeyStr: AnsiString;
begin
  if inStr = '' then
    Exit('');

  if keyStr = '' then
    Exit('');

  k := Integer(inStr[1]);
  tempKeyStr := keyStr;
  while Length(tempKeyStr) < 256 do
    tempKeyStr := tempKeyStr + tempKeyStr;

  for i := 1 to Length(inStr) - 1 do
  begin
    k := (k + i) mod 256;
    outStr := outStr + AnsiChar(Integer(inStr[i+1]) xor Integer(tempKeyStr[k+1]));
  end;
  Result := Trim(outStr);
end;

initialization
  RegisterServiceType(TAuthService);

end.

