Add an 80s-style green-on-black bank terminal shell ("banking") using
charmbracelet/bubbletea for full-screen TUI rendering over SSH.
Screens: login, main menu, account summary, account detail with
transactions, wire transfer wizard (6-step form capturing routing
number, destination, beneficiary, amount, memo, auth code), transaction
history with pagination, secure messages with breadcrumb content (fake
internal IPs, vault codes), change PIN, and hidden admin access (99)
that locks after 3 failed attempts with COBOL-style error output.
All key actions (login, navigation, wire transfers, admin attempts) are
logged to the session store. Wire transfer data is the honeypot gold.
Configurable via [shell.banking] in TOML: bank_name, terminal_id, region.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
4.9 KiB
Go
195 lines
4.9 KiB
Go
package banking
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
const (
|
|
transferStepRouting = iota
|
|
transferStepDest
|
|
transferStepBeneficiary
|
|
transferStepBankName
|
|
transferStepAmount
|
|
transferStepMemo
|
|
transferStepConfirm
|
|
transferStepAuthCode
|
|
transferStepComplete
|
|
)
|
|
|
|
var transferPrompts = []string{
|
|
" ROUTING NUMBER (ABA): ",
|
|
" DESTINATION ACCOUNT: ",
|
|
" BENEFICIARY NAME: ",
|
|
" RECEIVING BANK NAME: ",
|
|
" AMOUNT (USD): ",
|
|
" MEMO / REFERENCE: ",
|
|
"",
|
|
" AUTHORIZATION CODE: ",
|
|
}
|
|
|
|
type transferModel struct {
|
|
step int
|
|
fields [8]string // indexed by step
|
|
transfer WireTransfer
|
|
confirm string // y/n input for confirm step
|
|
}
|
|
|
|
func newTransferModel() transferModel {
|
|
return transferModel{}
|
|
}
|
|
|
|
func (m transferModel) Update(msg tea.Msg) (transferModel, tea.Cmd) {
|
|
keyMsg, ok := msg.(tea.KeyMsg)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
|
|
if m.step == transferStepComplete {
|
|
return m, nil
|
|
}
|
|
|
|
if m.step == transferStepConfirm {
|
|
switch keyMsg.Type {
|
|
case tea.KeyEnter:
|
|
switch strings.ToUpper(m.confirm) {
|
|
case "Y", "YES":
|
|
m.step = transferStepAuthCode
|
|
case "N", "NO":
|
|
m.confirm = "cancelled"
|
|
}
|
|
return m, nil
|
|
case tea.KeyBackspace:
|
|
if len(m.confirm) > 0 {
|
|
m.confirm = m.confirm[:len(m.confirm)-1]
|
|
}
|
|
default:
|
|
ch := keyMsg.String()
|
|
if len(ch) == 1 && ch[0] >= 32 && ch[0] < 127 && len(m.confirm) < 3 {
|
|
m.confirm += ch
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
switch keyMsg.Type {
|
|
case tea.KeyEnter:
|
|
val := strings.TrimSpace(m.fields[m.step])
|
|
if val == "" {
|
|
return m, nil
|
|
}
|
|
if m.step == transferStepAuthCode {
|
|
m.transfer.AuthCode = val
|
|
m.step = transferStepComplete
|
|
return m, nil
|
|
}
|
|
switch m.step {
|
|
case transferStepRouting:
|
|
m.transfer.RoutingNumber = val
|
|
case transferStepDest:
|
|
m.transfer.DestAccount = val
|
|
case transferStepBeneficiary:
|
|
m.transfer.Beneficiary = val
|
|
case transferStepBankName:
|
|
m.transfer.BankName = val
|
|
case transferStepAmount:
|
|
m.transfer.Amount = val
|
|
case transferStepMemo:
|
|
m.transfer.Memo = val
|
|
}
|
|
m.step++
|
|
return m, nil
|
|
case tea.KeyBackspace:
|
|
if len(m.fields[m.step]) > 0 {
|
|
m.fields[m.step] = m.fields[m.step][:len(m.fields[m.step])-1]
|
|
}
|
|
default:
|
|
ch := keyMsg.String()
|
|
if len(ch) == 1 && ch[0] >= 32 && ch[0] < 127 && len(m.fields[m.step]) < 40 {
|
|
m.fields[m.step] += ch
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m transferModel) View() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(centerText("WIRE TRANSFER"))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(thinDivider())
|
|
b.WriteString("\n\n")
|
|
|
|
// Show completed fields.
|
|
for i := 0; i < m.step && i < len(transferPrompts); i++ {
|
|
if i == transferStepConfirm {
|
|
continue
|
|
}
|
|
prompt := transferPrompts[i]
|
|
if prompt == "" {
|
|
continue
|
|
}
|
|
b.WriteString(baseStyle.Render(prompt))
|
|
b.WriteString(baseStyle.Render(m.fields[i]))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Current field.
|
|
switch {
|
|
case m.step == transferStepConfirm:
|
|
b.WriteString("\n")
|
|
b.WriteString(titleStyle.Render(" === TRANSFER SUMMARY ==="))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" ROUTING: %s", m.transfer.RoutingNumber)))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" ACCOUNT: %s", m.transfer.DestAccount)))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" BENEFICIARY: %s", m.transfer.Beneficiary)))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" BANK: %s", m.transfer.BankName)))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" AMOUNT: $%s", m.transfer.Amount)))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" MEMO: %s", m.transfer.Memo)))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(titleStyle.Render(" CONFIRM TRANSFER? (Y/N): "))
|
|
b.WriteString(inputStyle.Render(m.confirm))
|
|
b.WriteString(inputStyle.Render("_"))
|
|
b.WriteString("\n")
|
|
case m.step == transferStepComplete:
|
|
b.WriteString("\n")
|
|
b.WriteString(thinDivider())
|
|
b.WriteString("\n\n")
|
|
b.WriteString(titleStyle.Render(" TRANSFER QUEUED FOR PROCESSING"))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(baseStyle.Render(fmt.Sprintf(" CONFIRMATION #: WR-%s-%s",
|
|
m.transfer.RoutingNumber[:4], "847291")))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(" STATUS: PENDING FEDWIRE SETTLEMENT"))
|
|
b.WriteString("\n")
|
|
b.WriteString(baseStyle.Render(" ESTIMATED COMPLETION: NEXT BUSINESS DAY"))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(dimStyle.Render(" PRESS ANY KEY TO RETURN TO MAIN MENU"))
|
|
b.WriteString("\n")
|
|
case m.step < len(transferPrompts):
|
|
prompt := transferPrompts[m.step]
|
|
b.WriteString(titleStyle.Render(prompt))
|
|
b.WriteString(inputStyle.Render(m.fields[m.step]))
|
|
b.WriteString(inputStyle.Render("_"))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if m.step < transferStepConfirm {
|
|
b.WriteString("\n")
|
|
b.WriteString(thinDivider())
|
|
b.WriteString("\n\n")
|
|
b.WriteString(dimStyle.Render(fmt.Sprintf(" STEP %d OF 6", m.step+1)))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
return b.String()
|
|
}
|