// 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, REST.Client,
  REST.Types, Vcl.Forms, System.NetEncoding;

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;
    function Authorize(code, realmId, state: string): string;
    function ExchangeAuthCode(code: string): string;
    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;

function TAuthService.Authorize(code, realmId, state: string): string;
var
iniFile: TIniFile;
begin
  Logger.Log(3, 'TAuthService.Authorize - begin');
  iniFile := TIniFile.Create(ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini');
  iniFile.WriteString('Quickbooks', 'CompanyID', realmId);
  iniFile.Free;
  result := ExchangeAuthCode(code);
  Logger.Log(3, 'TAuthService.Authorize - end - result: ' + result);
end;

function TAuthService.ExchangeAuthCode(code: string): string;
var
  iniFile: TIniFile;
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  Client, Secret, authString, AccessToken, RefreshToken, RedirectUri: string;
  jsonObj: TJSONObject;
begin
  logger.Log(3, 'AuthService.ExchangeAuthCode - start');

  iniFile := TIniFile.Create(ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini');
  restClient := TRESTClient.Create(nil);
  restRequest := TRESTRequest.Create(nil);
  restResponse := TRESTResponse.Create(nil);

  try
    try
      restRequest.Client := restClient;
      restRequest.Response := restResponse;

      Client := iniFile.ReadString('Quickbooks', 'ClientID', '');
      Secret := iniFile.ReadString('Quickbooks', 'ClientSecret', '');
      RedirectUri := iniFile.ReadString('Quickbooks', 'RedirectUri', '');

      authString := TNetEncoding.Base64.Encode(Client + ':' + Secret).Replace(#13,'').Replace(#10,'');

      restClient.BaseURL := 'https://oauth.platform.intuit.com';
      restRequest.Resource := '/oauth2/v1/tokens/bearer';
      restRequest.Method := rmPOST;

      // Authorization header
      param := restRequest.Params.AddItem;
      param.Name := 'Authorization';
      param.Kind := pkHTTPHEADER;
      param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
      param.Value := 'Basic ' + authString;

      // grant_type
      param := restRequest.Params.AddItem;
      param.Name := 'grant_type';
      param.Kind := pkGETorPOST;
      param.Value := 'authorization_code';

      // authorization code
      param := restRequest.Params.AddItem;
      param.Name := 'code';
      param.Kind := pkGETorPOST;
      param.Value := code;

      // redirect URI
      param := restRequest.Params.AddItem;
      param.Name := 'redirect_uri';
      param.Kind := pkGETorPOST;
      param.Value := RedirectUri;

      restRequest.Execute;

      logger.Log(5, restResponse.Content);

      jsonObj := TJSONObject.ParseJSONValue(restResponse.Content) as TJSONObject;

      if Assigned(jsonObj) then
      begin
        AccessToken := jsonObj.GetValue<string>('access_token');
        RefreshToken := jsonObj.GetValue<string>('refresh_token');

        logger.Log(5, 'Access Token: ' + AccessToken);
        logger.Log(5, 'Refresh Token: ' + RefreshToken);

        iniFile.WriteString('Quickbooks', 'AccessToken', AccessToken);
        iniFile.WriteString('Quickbooks', 'RefreshToken', RefreshToken);
        iniFile.WriteString('Quickbooks', 'LastRefresh', DateTimeToStr(Now));
        result := 'Quickbooks successfully linked!';
      end;

    except
      on E: Exception do
      begin
        result := 'The server has ran into an error please try again later...';
        logger.Log(1, 'ExchangeAuthCode Error: ' + E.Message);
      end;
    end;
  finally
    restClient.Free;
    restRequest.Free;
    restResponse.Free;
    iniFile.Free;
    logger.Log(3, 'AuthService.ExchangeAuthCode - end');
  end;
end;

initialization
  RegisterServiceType(TAuthService);
end.
