Recently Written · git

recentlywritten

my writings (done recently)

git clone https://github.com/equwal/recentlywritten

Log | Files | Refs


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 }