// Package sms delivers one-time codes to a phone number. // // There is no SMS provider wired into this backend yet — the miler app // authenticates on a PIN and the console on a password, so nothing has ever // needed to send a text. The customer app's only credential is a code sent to a // phone, which makes this the one piece of the auth flow that cannot be // finished from inside this repository. // // So this package is the seam, not the integration: a small interface, a // logging sink that lets the whole flow be exercised end to end without a // provider, and a fixed-code mode for staging. Plugging in a real gateway // (MSG91, Gupshup, Twilio) means adding one Sender and selecting it here — // nothing above this package changes. package sms import ( "fmt" "os" "strings" "doormile/utils" ) // Sender delivers a message to an E.164 phone number. type Sender interface { Send(phone, message string) error // Name identifies the transport in logs and in the readiness probe, so // "OTP not arriving" can be answered without reading code. Name() string } // logSender writes the code to the application log instead of sending it. // // This is what runs until a gateway is configured. It is deliberately loud and // deliberately marked: an OTP in a log file is a credential in a log file, and // nobody should be able to reach production with this active and not know. type logSender struct{} func (logSender) Name() string { return "log" } func (logSender) Send(phone, message string) error { // In production this is a failure, not a fallback. A code that only // reaches the application log has not been delivered, and returning nil // reports a send that did not happen: the customer waits for a text that // is never coming, and the endpoint cheerfully answers sent:true. An // error at least surfaces as a clear failure on the sign-in screen. if strings.EqualFold(strings.TrimSpace(os.Getenv("ENV")), "production") { utils.Error("SMS NOT CONFIGURED in production — refusing to write a live "+ "verification code to the log. Set SMS_GATEWAY_URL.", "phone", maskPhone(phone)) return fmt.Errorf("sms: no gateway configured") } utils.Warn("SMS NOT CONFIGURED — code written to the log instead of being sent", "phone", maskPhone(phone), "message", message) return nil } var active Sender = logSender{} // Register installs the real gateway. Call it from main() once a provider is // configured; until then the log sink stays in place. func Register(s Sender) { if s == nil { return } active = s utils.Info("SMS sender registered", "transport", s.Name()) } // Transport reports which sender is active, for the readiness probe. func Transport() string { return active.Name() } // Configured reports whether a real gateway is in place. False means codes are // only reaching the log. func Configured() bool { return active.Name() != "log" } // SendOTP delivers a login code. func SendOTP(phone, code string) error { if strings.TrimSpace(phone) == "" { return fmt.Errorf("sms: empty phone number") } msg := fmt.Sprintf("%s is your Doormile verification code. It expires in 5 minutes. Do not share it with anyone.", code) return active.Send(phone, msg) } // maskPhone keeps the country code and the last two digits so a log line can be // matched to a support call without recording the number itself. func maskPhone(phone string) string { if len(phone) < 5 { return "***" } return phone[:3] + strings.Repeat("*", len(phone)-5) + phone[len(phone)-2:] } // StagingCode returns the fixed verification code for non-production // environments, or "" when none is set. // // Automated tests and design QA cannot receive a real text, and the previous // end-to-end attempt on this system stalled for exactly that reason: customer // login needed an OTP on a real handset and could not be scripted. CX_STAGING_OTP // closes that. // // It is refused outright when ENV is production, because a fixed code is a // permanent skeleton key for every account on the platform. func StagingCode() string { code := strings.TrimSpace(os.Getenv("CX_STAGING_OTP")) if code == "" { return "" } if strings.EqualFold(strings.TrimSpace(os.Getenv("ENV")), "production") { utils.Error("CX_STAGING_OTP is set in a production environment and has been ignored — " + "a fixed verification code would accept a login for every account on the platform") return "" } return code }