backend requirements onthe xustomer app

This commit is contained in:
2026-09-07 10:55:11 +05:30
parent 35675d8a9b
commit 1b2690b21a
56 changed files with 13342 additions and 1071 deletions

105
internal/sms/sms.go Normal file
View File

@@ -0,0 +1,105 @@
// 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 {
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
}

140
internal/sms/sms_test.go Normal file
View File

@@ -0,0 +1,140 @@
package sms
import (
"os"
"strings"
"testing"
)
// StagingCode is a permanent skeleton key for every account on the platform if
// it ever reaches production. The guard against that is the only thing standing
// between a convenience for QA and a total auth bypass, so it is tested rather
// than trusted.
func setEnv(t *testing.T, key, value string) {
t.Helper()
previous, had := os.LookupEnv(key)
if value == "" {
_ = os.Unsetenv(key)
} else {
_ = os.Setenv(key, value)
}
t.Cleanup(func() {
if had {
_ = os.Setenv(key, previous)
} else {
_ = os.Unsetenv(key)
}
})
}
// A fixed OTP is refused in production however the environment is spelt.
func TestStagingCodeIsRefusedInProduction(t *testing.T) {
for _, env := range []string{"production", "PRODUCTION", "Production", " production "} {
setEnv(t, "CX_STAGING_OTP", "1234")
setEnv(t, "ENV", env)
if got := StagingCode(); got != "" {
t.Errorf("ENV=%q returned the fixed code %q — that is a skeleton key "+
"for every account on the platform", env, got)
}
}
}
// And is available everywhere else, which is what unblocks automated sign-in.
func TestStagingCodeIsAvailableOutsideProduction(t *testing.T) {
for _, env := range []string{"development", "staging", ""} {
setEnv(t, "CX_STAGING_OTP", "1234")
setEnv(t, "ENV", env)
if got := StagingCode(); got != "1234" {
t.Errorf("ENV=%q returned %q, want the configured staging code", env, got)
}
}
}
// Unset means unset — no accidental default.
func TestStagingCodeIsEmptyWhenNotConfigured(t *testing.T) {
setEnv(t, "ENV", "development")
setEnv(t, "CX_STAGING_OTP", "")
if got := StagingCode(); got != "" {
t.Errorf("StagingCode() = %q with nothing configured, want empty", got)
}
}
// Until a gateway is registered, Configured() must report false. Shipping while
// this quietly said true would mean nobody noticed OTP codes were only reaching
// the application log.
func TestTransportReportsThatNoGatewayIsWired(t *testing.T) {
if Configured() {
t.Error("Configured() = true with no gateway registered — " +
"the log sink must never claim to be a real transport")
}
if Transport() != "log" {
t.Errorf("Transport() = %q, want \"log\"", Transport())
}
}
// Registering a gateway flips both, and Register(nil) is ignored rather than
// silently disabling delivery.
func TestRegisterInstallsAGatewayAndIgnoresNil(t *testing.T) {
original := active
t.Cleanup(func() { active = original })
Register(nil)
if Transport() != "log" {
t.Errorf("Register(nil) changed the transport to %q", Transport())
}
fake := &recordingSender{}
Register(fake)
if !Configured() || Transport() != "test" {
t.Fatalf("after Register: configured=%v transport=%q", Configured(), Transport())
}
if err := SendOTP("+919876543210", "4821"); err != nil {
t.Fatalf("SendOTP: %v", err)
}
if fake.phone != "+919876543210" {
t.Errorf("phone = %q, want the number passed in", fake.phone)
}
if !strings.Contains(fake.message, "4821") {
t.Errorf("message %q does not carry the code", fake.message)
}
if !strings.Contains(fake.message, "Do not share") {
t.Errorf("message %q is missing the do-not-share warning", fake.message)
}
}
// An empty number is refused rather than handed to a gateway that will bill for
// it and fail.
func TestSendOTPRefusesAnEmptyNumber(t *testing.T) {
if err := SendOTP(" ", "4821"); err == nil {
t.Error("SendOTP accepted an empty phone number")
}
}
// The log sink masks the number. An OTP in a log file is already bad enough
// without the number it belongs to sitting beside it.
func TestMaskPhoneHidesTheSubscriberDigits(t *testing.T) {
got := maskPhone("+919876543210")
if strings.Contains(got, "9876543") {
t.Errorf("maskPhone = %q, still exposes the subscriber digits", got)
}
if !strings.HasSuffix(got, "10") {
t.Errorf("maskPhone = %q, should keep the last two digits so a support "+
"call can be matched", got)
}
}
type recordingSender struct {
phone string
message string
}
func (r *recordingSender) Name() string { return "test" }
func (r *recordingSender) Send(phone, message string) error {
r.phone, r.message = phone, message
return nil
}