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;
    userIsAdmin: Boolean;
    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;
    // Returns True and sets AChallengeB64 if token is valid and of the expected type
    function VerifyChallengeToken(const ChallengeToken, ExpectedType: string;
      out AChallengeB64: string): Boolean;
  public
    function Login(const user, password, agency, credentialId,
                   challengeToken, authenticatorData, clientDataJSON,
                   signature: string): string;
    function GetAgencieslist(): TAgenciesList;
    function GetAgencyConfiglist: TAgencyConfigList;
    function BeginRegistration(const DeviceName: string): TJSONObject;
    function CompleteRegistration(const DeviceName, CredentialId,
                                  AttestationObject, ClientDataJSON,
                                  ChallengeToken: string): TJSONObject;
    function BeginAuthentication(const CredentialId: string): TJSONObject;
  end;

implementation

uses
  System.DateUtils,
  System.Generics.Collections,
  System.NetEncoding,
  Bcl.JOSE.Core.Builder,
  Bcl.JOSE.Core.JWT,
  Aurelius.Global.Utils,
  XData.Sys.Exceptions,
  Common.Logging,
  Common.Config,
  Sparkle.HttpServer.Context,
  Webauthn.Crypto,
  Webauthn.Cbor;

{ 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;

// ---------------------------------------------------------------------------
// Challenge token helpers
// ---------------------------------------------------------------------------

// Challenge token format: challengeB64url:type:expiryUnix:hmacB64url
// HMAC key = UTF-8 bytes of jwtTokenSecret

function TAuthService.VerifyChallengeToken(const ChallengeToken, ExpectedType: string;
  out AChallengeB64: string): Boolean;
var
  parts: TArray<string>;
  expiry: Int64;
  tokenData: string;
  keyBytes, hmacBytes: TBytes;
  computedHmac: string;
begin
  Result := False;
  AChallengeB64 := '';

  parts := ChallengeToken.Split([':'], 4);
  if Length(parts) <> 4 then Exit;

  AChallengeB64 := parts[0];
  if parts[1] <> ExpectedType then Exit;

  expiry := StrToInt64Def(parts[2], 0);
  if (expiry = 0) or (DateTimeToUnix(TTimeZone.Local.ToUniversalTime(Now)) > expiry) then
  begin
    Logger.Log(2, 'VerifyChallengeToken - token expired');
    Exit;
  end;

  tokenData := parts[0] + ':' + parts[1] + ':' + parts[2];
  keyBytes  := TEncoding.UTF8.GetBytes(ServerConfig.jwtTokenSecret);
  hmacBytes := HMACSHA256Bytes(keyBytes, TEncoding.UTF8.GetBytes(tokenData));
  computedHmac := Base64UrlEncode(hmacBytes);

  Result := (computedHmac = parts[3]);
  if not Result then
    Logger.Log(2, 'VerifyChallengeToken - HMAC mismatch');
end;

function MakeChallengeToken(const AType: string): string;
var
  challenge: TBytes;
  challengeB64, expiry, tokenData: string;
  keyBytes, hmacBytes: TBytes;
begin
  challenge    := RandomBytes(32);
  challengeB64 := Base64UrlEncode(challenge);
  expiry       := IntToStr(DateTimeToUnix(TTimeZone.Local.ToUniversalTime(IncMinute(Now, 5))));
  tokenData    := challengeB64 + ':' + AType + ':' + expiry;
  keyBytes     := TEncoding.UTF8.GetBytes(ServerConfig.jwtTokenSecret);
  hmacBytes    := HMACSHA256Bytes(keyBytes, TEncoding.UTF8.GetBytes(tokenData));
  Result       := tokenData + ':' + Base64UrlEncode(hmacBytes);
end;

// ---------------------------------------------------------------------------
// BeginRegistration
// ---------------------------------------------------------------------------

function TAuthService.BeginRegistration(const DeviceName: string): TJSONObject;
var
  token, challengeB64: string;
begin
  Logger.Log(2, 'AuthService.BeginRegistration - deviceName: "' + DeviceName + '"');

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

  token        := MakeChallengeToken('reg');
  challengeB64 := token.Split([':'], 4)[0];

  Result.AddPair('challenge',      challengeB64);
  Result.AddPair('challengeToken', token);
  Result.AddPair('rpId',           ServerConfig.rpId);
  Result.AddPair('rpName',         ServerConfig.rpName);
end;

// ---------------------------------------------------------------------------
// CompleteRegistration
// ---------------------------------------------------------------------------

function TAuthService.CompleteRegistration(const DeviceName, CredentialId,
  AttestationObject, ClientDataJSON, ChallengeToken: string): TJSONObject;
var
  challengeB64: string;
  cdJsonBytes, attObjBytes, credIdBytes: TBytes;
  cdJsonText: string;
  cdJson: TJSONObject;
  typeVal, challengeVal: string;
  authData: TBytes;
  rpIdHash, credId, pubKeyX, pubKeyY: TBytes;
  flags: Byte;
  signCount: Cardinal;
  pubKeyAlg: Integer;
  expectedRpIdHash: TBytes;
  q: TUniQuery;
  credIdB64: string;
begin
  Result := TJSONObject.Create;
  TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);

  Logger.Log(2, 'AuthService.CompleteRegistration - credId: ' + Copy(CredentialId, 1, 20) + '...');

  // 1. Verify challenge token
  if not VerifyChallengeToken(ChallengeToken, 'reg', challengeB64) then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Invalid or expired registration challenge.');
    Exit;
  end;

  // 2. Decode and parse clientDataJSON
  try
    cdJsonBytes := Base64UrlDecode(ClientDataJSON);
    cdJsonText  := TEncoding.UTF8.GetString(cdJsonBytes);
    cdJson      := TJSONObject.ParseJSONValue(cdJsonText) as TJSONObject;
  except
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Failed to parse clientDataJSON.');
    Exit;
  end;

  if not Assigned(cdJson) then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'clientDataJSON is not valid JSON.');
    Exit;
  end;

  try
    typeVal      := cdJson.GetValue<string>('type', '');
    challengeVal := cdJson.GetValue<string>('challenge', '');
  finally
    cdJson.Free;
  end;

  if typeVal <> 'webauthn.create' then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'clientDataJSON type mismatch.');
    Exit;
  end;

  if challengeVal <> challengeB64 then
  begin
    Logger.Log(2, 'CompleteRegistration - challenge mismatch');
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Challenge mismatch.');
    Exit;
  end;

  // 3. Decode attestationObject and extract authData
  try
    attObjBytes := Base64UrlDecode(AttestationObject);
  except
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Failed to decode attestationObject.');
    Exit;
  end;

  if not CborGetAuthData(attObjBytes, authData) then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Failed to parse attestationObject authData.');
    Exit;
  end;

  // 4. Parse authData
  if not ParseAuthData(authData, rpIdHash, flags, signCount,
                       credId, pubKeyX, pubKeyY, pubKeyAlg) then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Failed to parse authData.');
    Exit;
  end;

  // 5. Verify rpIdHash
  expectedRpIdHash := SHA256Bytes(TEncoding.UTF8.GetBytes(ServerConfig.rpId));
  if not CompareMem(@rpIdHash[0], @expectedRpIdHash[0], 32) then
  begin
    Logger.Log(2, 'CompleteRegistration - rpId hash mismatch');
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'rpId mismatch — check server rpId configuration.');
    Exit;
  end;

  // 6. Verify user-present flag (bit 0)
  if (flags and $01) = 0 then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'User presence flag not set.');
    Exit;
  end;

  // 7. Verify we got a valid P-256 key (alg = -7)
  if (pubKeyAlg <> -7) or (Length(pubKeyX) <> 32) or (Length(pubKeyY) <> 32) then
  begin
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Only ES256 (ECDSA P-256) credentials are supported.');
    Exit;
  end;

  // 8. The credentialId from authData must match the parameter
  credIdB64 := Base64UrlEncode(credId);
  if credIdB64 <> Trim(CredentialId) then
  begin
    Logger.Log(2, 'CompleteRegistration - credential ID mismatch');
    Result.AddPair('status', 'error');
    Result.AddPair('message', 'Credential ID mismatch.');
    Exit;
  end;

  // 9. Store credential
  q := TUniQuery.Create(nil);
  try
    q.Connection := authDB.ucLemsOCSO;

    // Check if already registered (re-registration attempt)
    q.SQL.Text :=
      'SELECT revoked_at FROM lems.device_registrations WHERE credential_id = :CID';
    q.ParamByName('CID').AsString := Trim(CredentialId);
    q.Open;

    if not q.IsEmpty then
    begin
      if not q.FieldByName('revoked_at').IsNull then
      begin
        q.Close;
        Logger.Log(2, 'CompleteRegistration - revoked credential: ' + Copy(CredentialId, 1, 20));
        Result.AddPair('status', 'revoked');
        Result.AddPair('message', 'Device access has been revoked by an administrator.');
        Exit;
      end;
      // Already active — treat as success (idempotent)
      q.Close;
      Logger.Log(3, 'CompleteRegistration - already registered');
      Result.AddPair('status', 'ok');
      Result.AddPair('message', 'Device already registered.');
      Result.AddPair('credentialId', Trim(CredentialId));
      Exit;
    end;
    q.Close;

    var ctx := THttpServerContext.Current;
    var userAgent: string := '';
    if ctx <> nil then
      userAgent := ctx.Request.Headers.Get('User-Agent');

    q.SQL.Text :=
      'INSERT INTO lems.device_registrations ' +
      '  (credential_id, device_name, user_agent, public_key_x, public_key_y, public_key_alg, sign_count) ' +
      'VALUES (:CID, :NAME, :AGENT, :KEYX, :KEYY, :ALG, :CNT)';
    q.ParamByName('CID').AsString   := Trim(CredentialId);
    q.ParamByName('NAME').AsString  := Trim(DeviceName);
    q.ParamByName('AGENT').AsString := userAgent;
    q.ParamByName('KEYX').AsBytes   := pubKeyX;
    q.ParamByName('KEYY').AsBytes   := pubKeyY;
    q.ParamByName('ALG').AsInteger  := pubKeyAlg;
    q.ParamByName('CNT').AsInteger  := Integer(signCount);
    q.ExecSQL;

  finally
    q.Free;
  end;

  Logger.Log(2, 'CompleteRegistration - stored credential for "' + DeviceName + '"');
  Result.AddPair('status', 'ok');
  Result.AddPair('message', 'Device registered successfully.');
  Result.AddPair('credentialId', Trim(CredentialId));
end;

// ---------------------------------------------------------------------------
// BeginAuthentication
// ---------------------------------------------------------------------------

function TAuthService.BeginAuthentication(const CredentialId: string): TJSONObject;
var
  q: TUniQuery;
  token, challengeB64: string;
begin
  Logger.Log(2, 'AuthService.BeginAuthentication - credId: ' + Copy(CredentialId, 1, 20));

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

  if Trim(CredentialId) = '' then
  begin
    Result.AddPair('error', 'CredentialId is required.');
    Exit;
  end;

  // Verify the credential exists and is not revoked
  q := TUniQuery.Create(nil);
  try
    q.Connection := authDB.ucLemsOCSO;
    q.SQL.Text :=
      'SELECT id FROM lems.device_registrations ' +
      'WHERE credential_id = :CID AND revoked_at IS NULL';
    q.ParamByName('CID').AsString := Trim(CredentialId);
    q.Open;
    try
      if q.IsEmpty then
      begin
        Logger.Log(2, 'BeginAuthentication - credential not found or revoked');
        Result.AddPair('error', 'Device not registered or access has been revoked.');
        Exit;
      end;
    finally
      q.Close;
    end;
  finally
    q.Free;
  end;

  token        := MakeChallengeToken('auth');
  challengeB64 := token.Split([':'], 4)[0];

  Result.AddPair('challenge',      challengeB64);
  Result.AddPair('challengeToken', token);
end;

// ---------------------------------------------------------------------------
// Login — verifies WebAuthn assertion then issues JWT
// ---------------------------------------------------------------------------

function TAuthService.Login(const user, password, agency, credentialId,
  challengeToken, authenticatorData, clientDataJSON, signature: string): string;
var
  userState: Integer;
  challengeB64: string;
  cdJsonBytes, authDataBytes, sigBytes: TBytes;
  cdJsonText: string;
  cdJson: TJSONObject;
  typeVal, challengeVal: string;
  rpIdHash, expectedRpIdHash: TBytes;
  flags: Byte;
  signCount: Cardinal;
  pubKeyX, pubKeyY: TBytes;
  message: TBytes;
  q: TUniQuery;
  storedSignCount: Int64;
  JWT: TJWT;
begin
  Logger.Log(1, Format('AuthService.Login - User: "%s" Agency: "%s"', [user, agency]));

  // 1. Verify user credentials
  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;

  // 2. Verify challenge token
  if not VerifyChallengeToken(challengeToken, 'auth', challengeB64) then
    raise EXDataHttpUnauthorized.Create('Invalid or expired authentication challenge.');

  // 3. Parse and verify clientDataJSON
  try
    cdJsonBytes := Base64UrlDecode(clientDataJSON);
    cdJsonText  := TEncoding.UTF8.GetString(cdJsonBytes);
    cdJson      := TJSONObject.ParseJSONValue(cdJsonText) as TJSONObject;
  except
    raise EXDataHttpUnauthorized.Create('Failed to parse clientDataJSON.');
  end;

  if not Assigned(cdJson) then
    raise EXDataHttpUnauthorized.Create('clientDataJSON is not valid JSON.');

  try
    typeVal      := cdJson.GetValue<string>('type', '');
    challengeVal := cdJson.GetValue<string>('challenge', '');
  finally
    cdJson.Free;
  end;

  if typeVal <> 'webauthn.get' then
    raise EXDataHttpUnauthorized.Create('clientDataJSON type mismatch.');

  if challengeVal <> challengeB64 then
    raise EXDataHttpUnauthorized.Create('Challenge mismatch.');

  // 4. Load credential from DB
  q := TUniQuery.Create(nil);
  try
    q.Connection := authDB.ucLemsOCSO;
    q.SQL.Text :=
      'SELECT public_key_x, public_key_y, sign_count ' +
      'FROM lems.device_registrations ' +
      'WHERE credential_id = :CID AND revoked_at IS NULL';
    q.ParamByName('CID').AsString := Trim(credentialId);
    q.Open;
    try
      if q.IsEmpty then
      begin
        Logger.Log(2, 'AuthService.Login - credential not found or revoked: ' + Copy(credentialId, 1, 20));
        raise EXDataHttpUnauthorized.Create('Device not registered or access has been revoked.');
      end;
      pubKeyX        := q.FieldByName('public_key_x').AsBytes;
      pubKeyY        := q.FieldByName('public_key_y').AsBytes;
      storedSignCount := q.FieldByName('sign_count').AsLargeInt;
    finally
      q.Close;
    end;
  finally
    q.Free;
  end;

  // 5. Parse authenticatorData
  try
    authDataBytes := Base64UrlDecode(authenticatorData);
  except
    raise EXDataHttpUnauthorized.Create('Failed to decode authenticatorData.');
  end;

  if Length(authDataBytes) < 37 then
    raise EXDataHttpUnauthorized.Create('authenticatorData too short.');

  // Verify rpIdHash (first 32 bytes of authData)
  SetLength(rpIdHash, 32);
  Move(authDataBytes[0], rpIdHash[0], 32);
  expectedRpIdHash := SHA256Bytes(TEncoding.UTF8.GetBytes(ServerConfig.rpId));
  if not CompareMem(@rpIdHash[0], @expectedRpIdHash[0], 32) then
    raise EXDataHttpUnauthorized.Create('rpId mismatch.');

  // Check user-present flag
  flags := authDataBytes[32];
  if (flags and $01) = 0 then
    raise EXDataHttpUnauthorized.Create('User presence flag not set.');

  // 6. Verify ECDSA signature
  // message = authenticatorData || SHA256(clientDataJSON)
  SetLength(message, Length(authDataBytes) + 32);
  Move(authDataBytes[0], message[0], Length(authDataBytes));
  var cdHash := SHA256Bytes(cdJsonBytes);
  Move(cdHash[0], message[Length(authDataBytes)], 32);

  try
    sigBytes := Base64UrlDecode(signature);
  except
    raise EXDataHttpUnauthorized.Create('Failed to decode signature.');
  end;

  if not VerifyECDSAP256(pubKeyX, pubKeyY, message, sigBytes) then
  begin
    Logger.Log(2, 'AuthService.Login - signature verification failed for credId: ' + Copy(credentialId, 1, 20));
    raise EXDataHttpUnauthorized.Create('WebAuthn signature verification failed.');
  end;

  // 7. Update sign count (replay attack protection)
  signCount := (Cardinal(authDataBytes[33]) shl 24) or
               (Cardinal(authDataBytes[34]) shl 16) or
               (Cardinal(authDataBytes[35]) shl 8)  or
               authDataBytes[36];

  if (storedSignCount > 0) and (Int64(signCount) <= storedSignCount) then
    Logger.Log(1, 'AuthService.Login - WARNING: sign count did not increase (possible cloned authenticator)');

  q := TUniQuery.Create(nil);
  try
    q.Connection := authDB.ucLemsOCSO;
    q.SQL.Text :=
      'UPDATE lems.device_registrations SET sign_count = :CNT WHERE credential_id = :CID';
    q.ParamByName('CNT').AsInteger := Integer(signCount);
    q.ParamByName('CID').AsString  := Trim(credentialId);
    q.ExecSQL;
  finally
    q.Free;
  end;

  // 8. Issue JWT
  Logger.Log(2, Format('AuthService.Login - success for User: "%s" Agency: "%s"', [user, agency]));

  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);
    JWT.Claims.SetClaimOfType<Boolean>('user_admin',      userIsAdmin);
    Result := TJOSE.SHA256CompactToken(ServerConfig.jwtTokenSecret, JWT);
  finally
    JWT.Free;
  end;
end;

// ---------------------------------------------------------------------------
// Existing methods (unchanged)
// ---------------------------------------------------------------------------

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.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;

          try
            userIsAdmin := authDB.uqAuth.FieldByName('admin').AsString = 'T';
          except
            userIsAdmin := False;
          end;

          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.
