Skip to content

Critical fixes modernization - #24

Open
MarijaGojkov wants to merge 12 commits into
english-rewritefrom
critical-fixes-modernization
Open

Critical fixes modernization#24
MarijaGojkov wants to merge 12 commits into
english-rewritefrom
critical-fixes-modernization

Conversation

@MarijaGojkov

@MarijaGojkov MarijaGojkov commented May 11, 2026

Copy link
Copy Markdown
Owner

Each change is its own commit so the PR can be read top-to-bottom.

Security

  • BCrypt password hashing (work-factor 11). Plaintext storage and
    string-equality comparison are gone. UserRepository.IsValidUser
    pulls the stored hash and calls BCrypt.Verify. Seed data ships
    pre-hashed; EBanking.TestConsole is now a one-shot utility that
    rehashes any plaintext rows in a stale dev DB.
  • Connection string externalized to App.config. Removed five
    hardcoded copies, including a dead DatabaseAccess.cs constant that
    pointed at a different database name.
  • Hardcoded login defaults removed from LoginModel.

Correctness

  • Atomic payment flow. Transfers now run inside one SqlConnection
    • SqlTransaction. The payer row is locked with WITH (UPDLOCK, ROWLOCK); balance and insufficient-funds checks happen inside the
      transaction so UI validation isn't load-bearing. Either both balance
      changes and both transaction rows land, or the transaction rolls back
      and nothing does. Previously the flow was four sequential calls with
      an async void fire-and-forget for the recipient credit.
  • Transfers to a non-existent recipient are rejected cleanly with
    rollback (was silently debiting the payer with no destination).
  • decimal for all money values (Balance, Amount,
    BalanceAfterTransaction, exchange-rate Value), end-to-end across
    DataAccess / Services / UI. Repository readers stopped round-tripping
    through decimal.ToDouble; decimal parameters are now bound via
    explicit SqlParameter with Precision = 18, Scale = 2.

WPF lifecycle

  • No more async void handlers in the UI project.
  • view.Closing lambda subscriptions that pinned ViewModels across
    child-window opens are now named handlers that detach themselves on
    first invocation.

UX polish

  • Activation flow rewording. The "Don't have an account? Activate
    it!" prompt promised self-service signup but the flow only set/reset
    the online-banking password using bank-issued credentials. Reframed
    end-to-end as "First time logging in or forgot your password? Activate
    access."
  • Currency exchange labels the source and target currencies next
    to the Amount and Converted-value fields.

Test plan

  • Log in as ana.petrovic@email.com / password123
  • Send a payment from Ana to Marko; both balances move, both
    transaction rows present in [Transaction]
  • Try to send to a made-up recipient account → clean error, no debit
  • Try to send more than the payer's balance → blocked
  • Currency exchange RSD → EUR shows correct source/target currency
    labels and accurate decimal math
  • Activate access as Jelena (PIN 9012) → log in with the new
    password

MarijaGojkov and others added 12 commits May 11, 2026 12:39
Centralizes the connection string behind DatabaseAccess.ConnectionString,
which reads the EBankingDb entry from App.config via ConfigurationManager.
Replaces five hardcoded copies (four repositories + the unused, mismatched
default in DatabaseAccess.cs that referenced a different database name).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Strips the test email/password that the login screen shipped with by
default. Reviewers cloning the repo no longer see prefilled credentials.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds BCrypt.Net-Next to the data layer and a PasswordHasher helper
(work-factor 11). UserRepository now hashes on AddUser and
UpdateUserPassword, and IsValidUser pulls the stored hash and verifies
against it instead of comparing plaintext. Also fixes a latent bug in
UpdateUserPassword that called ExecuteScalar for a non-query statement.

EBankingSystem_Seed.sql now ships pre-hashed passwords; the plaintext
demo credentials are kept readable in a comment so a reviewer can still
log in. EBanking.TestConsole — previously an empty project — is now a
one-shot utility: 'hash <pwd>' prints a hash (used to prepare the seed),
no-arg mode re-hashes any plaintext rows left over in an existing dev DB
(detected via NOT LIKE '\$2%').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Balance, Amount, BalanceAfterTransaction, and exchange-rate Value are now
decimal end to end (DataAccess models, Services models, UI models, the
IAccountRepository / IAccountService.UpdateBalance signatures, and the
service implementation). Repository readers drop the decimal.ToDouble
round-trip and read DECIMAL(18,2) directly. Writes for balance and
transaction amounts now use explicit SqlParameter with Precision=18,
Scale=2 instead of AddWithValue, which silently negotiates the wrong
precision for decimal values. Side fix: two latent ExecuteScalar calls
on UPDATE/INSERT statements (CreateAccount, UpdateBalance) are now
ExecuteNonQuery.

The DB schema is unchanged; this commit is purely about not laundering
DECIMAL through binary floating-point in the application tier.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old PaymentViewModel.Pay() inserted the payer's transaction row,
updated the payer's balance, checked the recipient, and then fired an
async-void recipient-credit before calling Close(). Any failure between
the four steps left money debited but uncredited, or an orphan
transaction row; in practice the window often closed before the
fire-and-forget credit completed.

Replaces the whole flow with ITransactionService.TransferFunds(request),
which:

  - validates Amount > 0 and payer != recipient before touching SQL,
  - opens one SqlConnection + SqlTransaction (ReadCommitted),
  - reads the payer balance with (UPDLOCK, ROWLOCK) so a concurrent
    transfer can't race the balance check,
  - throws InvalidOperationException("Insufficient funds.") inside the
    transaction — the real defense, since UI validation can be bypassed,
  - debits the payer, and if the recipient is internal credits them and
    writes their transaction row,
  - writes the payer's transaction row,
  - commits, or rolls back and rethrows on any failure.

PaymentViewModel is now a thin caller: validate, TransferFunds, Close —
or MessageBox the exception. The async-void AddTransactionToRecipient
helper is gone, and PaymentViewModel no longer needs IAccountService.

PaymentViewValidator now also blocks payer == recipient and Amount >
CurrentBalance, and drops two dead `== null` checks on non-nullable
fields.

Side fixes (same change set):
  - AccountRepository.IsValidAccount switched from SELECT * + a fragile
    `as string is not null` cast on the first column to SELECT 1 +
    `ExecuteScalar() is not null`.
  - GetAccountByAccountNumber loses its async marker; the method was
    declared async Task<Account> but did no awaiting. Propagated the
    sync signature through IAccountRepository, IAccountService, and
    AccountService.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AccountViewModel.Payment() was declared async void but did no awaiting
(left over from when PaymentViewModel.AddTransactionToRecipient was
called from here as an awaited helper). Drop the async marker; the
method is plain void now. After this commit a solution-wide search for
'async void' in EBanking.UI/ returns no hits.

The three view.Closing subscriptions — accountView.Closing in
LoginViewModel.Login(), and view.Closing for both CurrencyExchangeView
and PaymentView in AccountViewModel — were attached as anonymous
lambdas that captured 'this' and were never removed. Each child-window
open added a new subscription with no detach, pinning every prior
ViewModel and its services for the lifetime of the app. Replaced with
named local-function handlers that '-=' themselves on the first
invocation, so the closure (and the captured ViewModel chain) is
collectible after the child window closes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a README covering tech stack, the four projects, local setup,
seed credentials (now BCrypt-hashed), and a per-bullet summary of the
modernization pass that matches the six commits on this branch. Also
documents the EBanking.TestConsole utility (hash generator + idempotent
rehash for legacy dev DBs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ExecuteTransfer previously preserved the original PaymentViewModel.Pay
behavior of debiting the payer even when the recipient account number
didn't exist in [Account] (modeling a send to another bank). For a
closed-system demo this is confusing — the UI shows the payer
balance dropping with no indication that the money went anywhere.

Move the recipient lookup before the payer debit and throw
InvalidOperationException("Recipient account not found.") when it
returns null. The catch in ExecuteTransfer rolls back, so on this path
no rows are written at all. Payer-account-missing already had the
symmetric throw. PaymentViewModel.Pay catches and surfaces the message
via MessageBox, so the user gets clear feedback instead of a silent
debit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Amount and Converted-value fields gave no indication of which
currency they were in, so a user clicking exchange from an RSD account
to an EUR account would see '1000 -> 8.5' and assume the math was
broken (it wasn't: 1000 RSD * 0.0085 = 8.50 EUR).

Adds three small currency badges:
  - next to the Amount textbox: source currency (Model.Account.Currency)
  - next to the dropdown: target currency (was already there but had
    no Foreground set, so it rendered invisible on the dark gradient)
  - next to the Converted-value textbox: target currency

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The screen never created a user — it took a bank-issued email + PIN and
let the holder set their first password (UpdateUserPassword). The
wording all said 'Registration' / 'REGISTER', which led to thinking new
users were being created.

Renames the user-facing text (class names left alone to avoid churn in
the locator and navigation):
  - Title bar 'REGISTRATION' -> 'ACCOUNT ACTIVATION'
  - Model.Title 'Registration' -> 'Account Activation'
  - Submit button 'REGISTER' -> 'ACTIVATE'
  - Field labels: 'E-mail' -> 'Bank e-mail', 'User PIN' -> 'Activation
    PIN', 'Password' -> 'New password', 'Confirm password' -> 'Confirm
    new password' — the bank-issued vs user-chosen split is now obvious.

Drops the unused RegistrationModel.Label field. The 'Passwords do not
match' error was being assigned to Label, but no XAML binding existed,
so the message was silently swallowed. Surfaced inline on
ConfirmPasswordError (already shown in red below the field).

Replaces the wrong-PIN MessageBox with a form-level FormError field
bound to a new red label between the last field and the submit button.
Brings PIN-mismatch in line with the other inline errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The login screen prompted 'Don't have an account? Activate it!', which
promised self-service signup. The button actually opened a flow that
requires bank-issued credentials (email + PIN) and only sets the
online-banking password — there is no AddUser path in the UI today.

Rewords the entry point so the prompt matches what the flow actually
does. This is intentional: a real bank pre-creates users and customers
activate online access; self-service registration is out of scope for
this modernization pass.

  LoginView prompt: 'Don't have an account?' ->
                    'First time logging in or forgot your password?'
  LoginView button: 'Activate it!' -> 'Activate access'

  Activation screen titlebar:  'ACCOUNT ACTIVATION' -> 'ACTIVATE ACCESS'
  Activation screen subtitle:  generic 'Welcome' blurb ->
                               'Use the e-mail and PIN your bank issued
                                to set or reset your online banking
                                password.'
  Activation screen button:    'ACTIVATE' -> 'ACTIVATE ACCESS'
  Activation window title:     'Account Activation' -> 'Activate access'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  Adds ITransactionRepository.ExecuteExchange (UPDLOCK both rows,
  re-check funds, debit + credit + two transaction rows, commit or
  rollback) and ICurrencyExchangeService.Exchange (validate, look up
  rate, round to 2dp, call ExecuteExchange). CurrencyExchangeViewModel
  no longer open-codes the two-leg flow; AccountViewModel updated to
  the new VM signature. README updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant