// Implementation of Auth Serice that will eventually retrieve login information
// from the auth database.

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;
  private
    function CheckUser(const user, password: string): Integer;
  public
    function Login(const user, password: string): string;
    function VerifyVersion(ClientVersion: string): TJSONObject;
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
  end;

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 }

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 KGOrders 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
  Logger.Log( 5, 'TAuthService.VerifyVersion(clientVersion: ' + clientVersion + ')' );
  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
      Logger.Log(1, 'webApp version mismatch client ver: ' + clientVersion + ' required ver: ' + webClientVersion);
      Result.AddPair('error',
        'webApp version mismatch' + sLineBreak + '  Client version: ' + clientVersion  +
         sLineBreak + '  Server version: ' + webClientVersion +
         sLineBreak + 'Please click button to clear cache and reload.');
    end;
  finally
    iniFile.Free;
  end;
end;


function TAuthService.Login(const user, password: string): string;
var
  userState: Integer;
  iniFile: TIniFile;
  JWT: TJWT;
  qbEnabled: boolean;
begin
  Logger.Log(3, Format( 'AuthService.Login - User: "%s"', [User]));
  try
    userState := CheckUser(user, password);
  except
    on E: Exception do
    begin
      Logger.Log(1, '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 username or password');
    raise EXDataHttpUnauthorized.Create('Invalid username or password');
  end
  else if userState = 1 then
  begin
    logger.Log(2, 'Login Error: User does not exist!');
    raise EXDataHttpUnauthorized.Create('User does not exist!');
  end
  else if userState = 2 then
  begin
    logger.Log(2, 'Login Error: User not active!');
    raise EXDataHttpUnauthorized.Create('User not active!');
  end;


  iniFile := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    qbEnabled := iniFile.ReadBool('Quickbooks', 'Enabled', false);
  finally
    iniFile.Free;
  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', authDB.uq.FieldByName('USER_NAME').AsString);
    JWT.Claims.SetClaimOfType<string>('user_fullname', authDB.uq.FieldByName('NAME').AsString);
    JWT.Claims.SetClaimOfType<string>('user_id', authDB.uq.FieldByName('USER_ID').AsString);
    JWT.Claims.SetClaimOfType<string>('user_status', authDB.uq.FieldByName('STATUS').AsString);
    JWT.Claims.SetClaimOfType<string>('user_email', authDB.uq.FieldByName('EMAIL').AsString);
    JWT.Claims.SetClaimOfType<string>('user_qb_id', authDB.uq.FieldByName('QB_ID').AsString);
    JWT.Claims.SetClaimOfType<string>('user_access_type', authDB.uq.FieldByName('ACCESS_TYPE').AsString);
    JWT.Claims.SetClaimOfType<boolean>('qb_enabled', qbEnabled);

    Result := TJOSE.SHA256CompactToken(serverConfig.jwtTokenSecret, JWT);
  finally
    JWT.Free;
  end;
end;

function TAuthService.CheckUser(const user, password: string): Integer;
var
  SQL: string;
  name: string;
  checkString: string;
begin
  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
      Logger.Log(1, Format('AuthDB.SetLoginAuditEntry: "%s"', [user]) );
      Result := 3; // Succcess
    end
    else
      Result := 0; // invalid user or password
  end;
end;

initialization
  RegisterServiceType(TAuthService);
end.
