accounts/main.go (3597 bytes)
1 // Command accounts is the sign-in, email, and private feed service for 2 // recentlywritten.com. It serves the paths under /account/ behind the web 3 // server that serves the static site, and it reads the post data that 4 // build.sh writes to postdata/ in the site directory. 5 // 6 // All settings and secrets come from environment variables. See 7 // .env.example. 8 package main 9 10 import ( 11 "context" 12 "errors" 13 "flag" 14 "fmt" 15 "log" 16 "net" 17 "net/http" 18 "net/mail" 19 "os" 20 "os/signal" 21 "strings" 22 "syscall" 23 "time" 24 ) 25 26 type config struct { 27 BaseURL string // for example https://recentlywritten.com, with no final slash 28 SiteDir string // the web root that build.sh deploys 29 DBPath string 30 Listen string 31 MailFrom *mail.Address 32 SMTPAddr string 33 SMTPUsername string 34 SMTPPassword string 35 Interval time.Duration 36 } 37 38 // loadConfig reads the configuration from getenv. In dev mode the mail 39 // settings are not necessary. The error names each missing variable. 40 func loadConfig(getenv func(string) string, dev bool) (config, error) { 41 c := config{ 42 BaseURL: strings.TrimRight(getenv("RW_BASE_URL"), "/"), 43 SiteDir: getenv("RW_SITE_DIR"), 44 DBPath: getenv("RW_DB_PATH"), 45 Listen: getenv("RW_LISTEN"), 46 SMTPUsername: getenv("SMTP_USERNAME"), 47 SMTPPassword: getenv("SMTP_PASSWORD"), 48 Interval: 15 * time.Minute, 49 } 50 if c.Listen == "" { 51 c.Listen = "127.0.0.1:8081" 52 } 53 required := []string{"RW_BASE_URL", "RW_SITE_DIR", "RW_DB_PATH"} 54 if !dev { 55 required = append(required, "RW_MAIL_FROM", "SMTP_HOST") 56 } 57 var missing []string 58 for _, k := range required { 59 if getenv(k) == "" { 60 missing = append(missing, k) 61 } 62 } 63 if len(missing) > 0 { 64 return c, fmt.Errorf("set these environment variables: %s", strings.Join(missing, ", ")) 65 } 66 if host := getenv("SMTP_HOST"); host != "" { 67 port := getenv("SMTP_PORT") 68 if port == "" { 69 port = "587" 70 } 71 c.SMTPAddr = net.JoinHostPort(host, port) 72 } 73 if from := getenv("RW_MAIL_FROM"); from != "" { 74 addr, err := mail.ParseAddress(from) 75 if err != nil { 76 return c, fmt.Errorf("RW_MAIL_FROM: %w", err) 77 } 78 c.MailFrom = addr 79 } 80 if s := getenv("RW_NOTIFY_INTERVAL"); s != "" { 81 d, err := time.ParseDuration(s) 82 if err != nil || d <= 0 { 83 return c, fmt.Errorf("RW_NOTIFY_INTERVAL: not a positive duration such as 15m") 84 } 85 c.Interval = d 86 } 87 return c, nil 88 } 89 90 func main() { 91 dev := flag.Bool("dev", false, "write email to standard error, not to SMTP (development only)") 92 flag.Parse() 93 logger := log.New(os.Stderr, "", log.LstdFlags) 94 95 cfg, err := loadConfig(os.Getenv, *dev) 96 if err != nil { 97 logger.Fatal(err) 98 } 99 db, err := openDB(cfg.DBPath) 100 if err != nil { 101 logger.Fatal(err) 102 } 103 defer db.Close() 104 105 var m mailer = logMailer{W: os.Stderr} 106 if !*dev { 107 m = smtpMailer{Addr: cfg.SMTPAddr, Username: cfg.SMTPUsername, Password: cfg.SMTPPassword, From: cfg.MailFrom} 108 } 109 a := &app{cfg: cfg, db: db, mail: m, now: time.Now, log: logger} 110 111 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 112 defer stop() 113 go a.notifyLoop(cfg.Interval, ctx.Done()) 114 115 srv := &http.Server{ 116 Addr: cfg.Listen, 117 Handler: a.routes(), 118 ReadHeaderTimeout: 10 * time.Second, 119 ReadTimeout: 30 * time.Second, 120 WriteTimeout: 60 * time.Second, 121 } 122 go func() { 123 <-ctx.Done() 124 shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second) 125 defer cancel() 126 srv.Shutdown(shutdown) 127 }() 128 logger.Printf("listening on %s", cfg.Listen) 129 if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 130 logger.Fatal(err) 131 } 132 }