task/1.14
1package main
2
3import (
4 "fmt"
5 "html/template"
6 "log"
7 "net/http"
8 "os"
9 "regexp"
10)
11
12// Interfaces for future service dependencies
13// These will be implemented when we add database and email functionality
14
15// Database represents the database service interface
16type Database interface {
17 // Future database methods will be defined here
18 // Example: GetReminder(id string) (*Reminder, error)
19 // Example: SaveReminder(reminder *Reminder) error
20}
21
22// Mailer represents the email service interface
23type Mailer interface {
24 // Future email methods will be defined here
25 // Example: SendReminder(email, productURL string) error
26 // Example: SendConfirmation(email, token string) error
27}
28
29// PageData holds common data passed to templates
30type PageData struct {
31 Title string
32 ServiceName string
33 Content interface{}
34}
35
36// config holds all the configuration settings for the application
37type config struct {
38 port int
39 env string
40 staticDir string
41 htmlDir string
42}
43
44// application holds the application-wide dependencies and configuration
45// This serves as the central dependency injection container
46type application struct {
47 config config
48 logger struct {
49 info *log.Logger
50 error *log.Logger
51 }
52 serviceName string
53 // Future service dependencies (interfaces for clean injection)
54 // database Database
55 // mailer Mailer
56}
57
58// TemplateHandler represents handlers that render templates
59// This is a custom handler type that implements http.Handler interface
60type TemplateHandler struct {
61 app *application
62 templateName string
63 pageName string
64 title string
65}
66
67// ServeHTTP makes TemplateHandler satisfy the http.Handler interface
68func (th *TemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
69 w.Header().Add("Server", th.app.serviceName)
70
71 // Parse templates with inheritance using configurable paths
72 ts, err := template.ParseFiles(
73 th.app.config.htmlDir+"/layouts/base.tmpl",
74 th.app.config.htmlDir+"/pages/"+th.templateName,
75 )
76 if err != nil {
77 th.app.serverError(w, err)
78 return
79 }
80
81 // Prepare template data
82 data := PageData{
83 Title: th.title,
84 ServiceName: th.app.serviceName,
85 Content: nil,
86 }
87
88 // Execute template
89 err = ts.ExecuteTemplate(w, "base", data)
90 if err != nil {
91 th.app.serverError(w, err)
92 }
93}
94
95// home displays the buylater.email landing page
96// This demonstrates using http.HandlerFunc to convert a regular function
97// into something that satisfies the http.Handler interface
98func (app *application) home(w http.ResponseWriter, r *http.Request) {
99 w.Header().Add("Server", app.serviceName)
100
101 // Parse templates with inheritance using configurable paths
102 ts, err := template.ParseFiles(
103 app.config.htmlDir+"/layouts/base.tmpl",
104 app.config.htmlDir+"/pages/home.tmpl",
105 )
106 if err != nil {
107 app.serverError(w, err)
108 return
109 }
110
111 // Prepare template data
112 data := PageData{
113 Title: "Home",
114 ServiceName: app.serviceName,
115 Content: nil,
116 }
117
118 // Execute template
119 err = ts.ExecuteTemplate(w, "base", data)
120 if err != nil {
121 app.serverError(w, err)
122 }
123}
124
125// submitForm displays the email submission form for scheduling purchase reminders
126// This will be handled by the TemplateHandler type to demonstrate custom handler types
127
128// processSubmit handles the form submission and schedules the email reminder
129// This demonstrates a method handler with business logic
130func (app *application) processSubmit(w http.ResponseWriter, r *http.Request) {
131 w.Header().Add("Server", app.serviceName)
132
133 // Parse templates with inheritance using configurable paths
134 ts, err := template.ParseFiles(
135 app.config.htmlDir+"/layouts/base.tmpl",
136 app.config.htmlDir+"/pages/complete.tmpl",
137 )
138 if err != nil {
139 app.serverError(w, err)
140 return
141 }
142
143 // Prepare template data
144 data := PageData{
145 Title: "Submission Complete",
146 ServiceName: app.serviceName,
147 Content: nil,
148 }
149
150 // Execute template
151 err = ts.ExecuteTemplate(w, "base", data)
152 if err != nil {
153 app.serverError(w, err)
154 }
155}
156
157// confirmWithToken handles magic link confirmation with token validation
158// This demonstrates dependency injection by making it a method on application
159func (app *application) confirmWithToken(w http.ResponseWriter, r *http.Request) {
160 token := r.PathValue("token")
161
162 if !isValidToken(token) {
163 app.logger.error.Printf("Invalid token attempted: %s", token)
164 app.notFound(w)
165 return
166 }
167
168 w.Header().Add("Server", app.serviceName)
169 app.logger.info.Printf("Token confirmed successfully: %s", token[:8]+"...")
170 w.WriteHeader(http.StatusOK)
171 w.Write([]byte("Email Confirmed! Your purchase reminder has been activated via magic link."))
172}
173
174// isValidToken validates that the token is alphanumeric and at least 32 characters
175func isValidToken(token string) bool {
176 if len(token) < 32 {
177 return false
178 }
179
180 // Check if token contains only alphanumeric characters
181 matched, _ := regexp.MatchString("^[a-zA-Z0-9]+$", token)
182 return matched
183}
184
185// initializeApplication creates and configures the application with all dependencies
186// This centralizes dependency injection and makes testing easier
187func initializeApplication(cfg config) *application {
188 // Create structured loggers
189 infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
190 errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
191
192 // Log startup configuration
193 infoLog.Printf("Configuration loaded: port=%d, env=%s, staticDir=%s, htmlDir=%s",
194 cfg.port, cfg.env, cfg.staticDir, cfg.htmlDir)
195
196 // Create application with injected dependencies
197 app := &application{
198 config: cfg,
199 logger: struct {
200 info *log.Logger
201 error *log.Logger
202 }{
203 info: infoLog,
204 error: errorLog,
205 },
206 serviceName: "buylater.email",
207 // Future service dependencies will be initialized here:
208 // database: initializeDatabase(cfg),
209 // mailer: initializeMailer(cfg),
210 }
211
212 // Validate all required dependencies are properly initialized
213 if err := app.validateDependencies(); err != nil {
214 log.Fatalf("Dependency validation failed: %v", err)
215 }
216
217 app.logger.info.Println("Application dependencies initialized successfully")
218 return app
219}
220
221// validateDependencies ensures all required dependencies are properly configured
222func (app *application) validateDependencies() error {
223 // Validate configuration
224 if app.config.port <= 0 || app.config.port > 65535 {
225 return fmt.Errorf("invalid port: %d", app.config.port)
226 }
227 if app.config.env == "" {
228 return fmt.Errorf("environment not specified")
229 }
230 if app.config.staticDir == "" {
231 return fmt.Errorf("static directory not specified")
232 }
233 if app.config.htmlDir == "" {
234 return fmt.Errorf("HTML directory not specified")
235 }
236
237 // Validate loggers
238 if app.logger.info == nil {
239 return fmt.Errorf("info logger not initialized")
240 }
241 if app.logger.error == nil {
242 return fmt.Errorf("error logger not initialized")
243 }
244
245 // Validate service name
246 if app.serviceName == "" {
247 return fmt.Errorf("service name not specified")
248 }
249
250 // Future dependency validation will be added here:
251 // if app.database == nil { return fmt.Errorf("database not initialized") }
252 // if app.mailer == nil { return fmt.Errorf("mailer not initialized") }
253
254 return nil
255}
256
257// about displays information about the buylater.email service
258// This will be handled by the TemplateHandler type to demonstrate reusable custom handlers