unit View.Login;

interface

uses
  System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
  Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
  JS, XData.Web.Connection, WEBLib.ExtCtrls,
  App.Types, ConnectionModule, XData.Web.Client;

type
  TFViewLogin = class(TWebForm)
    WebLabel1: TWebLabel;
    edtUsername: TWebEdit;
    edtPassword: TWebEdit;
    btnLogin: TWebButton;
    pnlMessage: TWebPanel;
    lblMessage: TWebLabel;
    btnCloseNotification: TWebButton;
    XDataWebClient: TXDataWebClient;
    lucbAgency: TWebLookupComboBox;
    procedure btnLoginClick(Sender: TObject);
    procedure btnCloseNotificationClick(Sender: TObject);
    procedure WebFormCreate(Sender: TObject);
  private
    FLoginProc: TSuccessProc;
    FMessage: string;
    procedure ShowNotification(Notification: string);
    procedure HideNotification;
    procedure GetAgencyConfigList;
    procedure SetBusy(ABusy: Boolean);
    procedure DoWebAuthnGet(AUser, APassword, AAgency, ACredentialId,
                            AChallenge, AChallengeToken: string);
  public
    class procedure Display(LoginProc: TSuccessProc); overload;
    class procedure Display(LoginProc: TSuccessProc; AMsg: string); overload;
  end;

var
  FViewLogin: TFViewLogin;

implementation

uses
  Auth.Service,
  View.ErrorPage;

{$R *.dfm}

class procedure TFViewLogin.Display(LoginProc: TSuccessProc);
begin
  TFViewLogin.Display(LoginProc, '');
end;

class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AMsg: string);

  procedure FormCreate(AForm: TObject);
  begin
    TFViewLogin(AForm).FMessage := AMsg;
  end;

begin
  if Assigned(FViewLogin) then
    FViewLogin.Free;
  FViewLogin := TFViewLogin.CreateNew(@FormCreate);
  FViewLogin.FLoginProc := LoginProc;
end;

procedure TFViewLogin.WebFormCreate(Sender: TObject);
begin
  GetAgencyConfigList;
  if FMessage <> '' then
    ShowNotification(FMessage)
  else
    HideNotification;
end;

procedure TFViewLogin.SetBusy(ABusy: Boolean);
begin
  asm
    var btn = document.getElementById('view.login.btnlogin');
    if (btn) {
      btn.disabled = ABusy;
      btn.textContent = ABusy ? 'Verifying...' : 'Login';
    }
  end;
end;

procedure TFViewLogin.btnLoginClick(Sender: TObject);
var
  user, password, agency, credentialId: string;

  procedure OnBeginOK(AChallenge, AChallengeToken: string);
  begin
    DoWebAuthnGet(user, password, agency, credentialId, AChallenge, AChallengeToken);
  end;

  procedure OnBeginError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification('Login Error: ' + AMsg);
  end;

begin
  user     := edtUsername.Text;
  password := edtPassword.Text;
  agency   := lucbAgency.Value;

  if (user = '') or (password = '') or (agency = '') then
  begin
    ShowNotification('Please enter a username, password, and agency.');
    Exit;
  end;

  credentialId := AuthService.GetCredentialId;
  if credentialId = '' then
  begin
    ShowNotification('Device not registered. Please restart the app to register this device.');
    Exit;
  end;

  SetBusy(True);
  HideNotification;

  AuthService.BeginAuthentication(credentialId, @OnBeginOK, @OnBeginError);
end;

procedure TFViewLogin.DoWebAuthnGet(AUser, APassword, AAgency, ACredentialId,
  AChallenge, AChallengeToken: string);
var
  user, password, agency, credentialId, challenge, challengeToken: string;

  procedure OnLoginOK;
  begin
    FLoginProc;
  end;

  procedure OnLoginError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification('Login Error: ' + AMsg);
  end;

  procedure OnAssertion(AAuthData, AClientDataJSON, ASignature: string);
  begin
    AuthService.LoginWithAssertion(
      user, password, agency, credentialId, challengeToken,
      AAuthData, AClientDataJSON, ASignature,
      @OnLoginOK, @OnLoginError
    );
  end;

  procedure OnWebAuthnError(AMsg: string);
  begin
    SetBusy(False);
    ShowNotification('Login Error: ' + AMsg);
  end;

begin
  user          := AUser;
  password      := APassword;
  agency        := AAgency;
  credentialId  := ACredentialId;
  challenge     := AChallenge;
  challengeToken := AChallengeToken;

  asm
    (function() {
      function b64urlToArr(b64) {
        b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
        while (b64.length % 4) b64 += '=';
        var bin = atob(b64);
        var arr = new Uint8Array(bin.length);
        for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
        return arr;
      }
      function arrToB64url(buf) {
        var bin = String.fromCharCode.apply(null, new Uint8Array(buf));
        return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
      }

      navigator.credentials.get({
        publicKey: {
          challenge: b64urlToArr(challenge),
          rpId: window.location.hostname,
          allowCredentials: [{
            id: b64urlToArr(credentialId),
            type: 'public-key'
          }],
          timeout: 60000,
          userVerification: 'required'
        }
      }).then(function(assertion) {
        var authData = arrToB64url(assertion.response.authenticatorData);
        var cdJson   = arrToB64url(assertion.response.clientDataJSON);
        var sig      = arrToB64url(assertion.response.signature);
        OnAssertion(authData, cdJson, sig);
      }).catch(function(err) {
        OnWebAuthnError('WebAuthn error: ' + err.message);
      });
    })();
  end;
end;

procedure TFViewLogin.GetAgencyConfigList;

  procedure OnLoad(Response: TXDataClientResponse);
  var
    jsResponse: TJSObject;
    jsArray: TJSArray;
    jsObject: TJSObject;
    agency: string;
    name: string;
    i: Integer;
  begin
    jsResponse := TJSObject(Response.Result);
    jsArray    := TJSArray(TJSObject(Response.Result)['data']);
    lucbAgency.LookupValues.Clear;
    for i := 0 to jsArray.Length - 1 do
    begin
      jsObject := TJSObject(jsArray[i]);
      agency   := string(jsObject['agency']);
      name     := string(jsObject['name']);
      lucbAgency.LookupValues.AddPair(agency, agency + ' - ' + name);
    end;
  end;

  procedure OnError(Error: TXDataClientError);
  begin
    ShowNotification('GetAgencyConfigList: ' + Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
  end;

begin
  XDataWebClient.RawInvoke(
    'IAuthService.GetAgencyConfigList', [],
    @OnLoad, @OnError
  );
end;

procedure TFViewLogin.ShowNotification(Notification: string);
begin
  if Notification <> '' then
  begin
    lblMessage.Caption := Notification;
    pnlMessage.ElementHandle.hidden := False;
  end;
end;

procedure TFViewLogin.HideNotification;
begin
  pnlMessage.ElementHandle.hidden := True;
end;

procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
  HideNotification;
end;

end.
