diff --git a/delphi-backend/Handlers/PM.Handler.Auth.pas b/delphi-backend/Handlers/PM.Handler.Auth.pas index 37242c6..d8e8afa 100644 --- a/delphi-backend/Handlers/PM.Handler.Auth.pas +++ b/delphi-backend/Handlers/PM.Handler.Auth.pas @@ -1,4 +1,4 @@ -unit PM.Handler.Auth; +unit PM.Handler.Auth; (* /register POST body {username, masterPassword} -> {message,token,userId,salt,csrfToken} @@ -21,7 +21,7 @@ implementation uses System.SysUtils, System.JSON, System.Classes, FireDAC.Comp.Client, - IdCustomHTTPServer, + IdCustomHTTPServer,Data.DB, PM.Router, PM.JSON, PM.Database, PM.Crypto, PM.Session, PM.RateLimit, PM.Audit; @@ -938,8 +938,12 @@ begin LQ.ParamByName('iv').AsString := LIv; // TOTP fields are optional per entry — clear when empty so // existing-NULL rows don't get stomped with empty strings. - if LTotpSec = '' then LQ.ParamByName('ts').Clear - else LQ.ParamByName('ts').AsString := LTotpSec; + LQ.ParamByName('ts').DataType := ftString; + LQ.ParamByName('tiv').DataType := ftString; + if LTotpSec.IsEmpty then + LQ.ParamByName('ts').Clear + else + LQ.ParamByName('ts').AsString := LTotpSec; if LTotpIv = '' then LQ.ParamByName('tiv').Clear else LQ.ParamByName('tiv').AsString := LTotpIv; LQ.ExecSQL; diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index 363f7df..87cb520 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -185,6 +185,12 @@ begin LQ.ParamByName('i').AsString := LIV; LQ.ParamByName('f').AsString := LFolder; LQ.ParamByName('t').AsString := LTags; + // FireDAC needs an explicit DataType on params that are sometimes + // assigned a string and sometimes Clear()ed to NULL — without a + // prior typed assignment, .Clear raises "data type unknown" on + // SQLite. Declare ftString up front for the optional TOTP fields. + LQ.ParamByName('ts').DataType := ftString; + LQ.ParamByName('tiv').DataType := ftString; // Store empty TOTP fields as NULL so the GET endpoint emits JSON null // rather than '' — keeps client-side "has TOTP?" checks unambiguous. if LTotpSec = '' then @@ -278,6 +284,10 @@ begin LQ.ParamByName('i').AsString := LIV; LQ.ParamByName('f').AsString := LFolder; LQ.ParamByName('t').AsString := LTags; + // Declare TOTP param types so .Clear works on first use (FireDAC + // needs an inferred or explicit DataType before NULL binding). + LQ.ParamByName('ts').DataType := ftString; + LQ.ParamByName('tiv').DataType := ftString; // Clearing TOTP (user removed 2FA from this entry) is signaled by an // empty string in the request → store NULL in the DB. if LTotpSec = '' then @@ -533,6 +543,12 @@ begin '(user_id, site, username, encrypted_password, iv, encryption_method, ' + ' folder, tags, totp_secret, totp_iv, created_at, updated_at) ' + 'VALUES (:uid, :s, :u, :e, :i, ''client'', :f, :t, :ts, :tiv, :c, :c2)'; + // Declare optional TOTP param types ONCE — the prepared statement + // is reused across every imported entry, and FireDAC needs the + // type set before the first .Clear call would otherwise fail + // for a row without TOTP. + LQ.ParamByName('ts').DataType := ftString; + LQ.ParamByName('tiv').DataType := ftString; for I := 0 to LArr.Count - 1 do begin