task/1.13
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.logger.error.Println(err.Error())
78 http.Error(w, "Internal Server Error", 500)
79 return
80 }
81
82 // Prepare template data
83 data := PageData{
84 Title: th.title,
85 ServiceName: th.app.serviceName,
86 Content: nil,
87 }
88
89 // Execute template
90 err = ts.ExecuteTemplate(w, "base", data)
91 if err != nil {
92 th.app.logger.error.Println(err.Error())
93 http.Error(w, "Internal Server Error", 500)
94 }
95}
96
97// home displays the buylater.email landing page
98// This demonstrates using http.HandlerFunc to convert a regular function
99// into something that satisfies the http.Handler interface
100func (app *application) home(w http.ResponseWriter, r *http.Request) {
101 w.Header().Add("Server", app.serviceName)
102
103 // Parse templates with inheritance using configurable paths
104 ts, err := template.ParseFiles(
105 app.config.htmlDir+"/layouts/base.tmpl",
106 app.config.htmlDir+"/pages/home.tmpl",
107 )
108 if err != nil {
109 app.logger.error.Println(err.Error())
110 http.Error(w, "Internal Server Error", 500)
111 return
112 }
113
114 // Prepare template data
115 data := PageData{
116 Title: "Home",
117 ServiceName: app.serviceName,
118 Content: nil,
119 }
120
121 // Execute template
122 err = ts.ExecuteTemplate(w, "base", data)
123 if err != nil {
124 app.logger.error.Println(err.Error())
125 http.Error(w, "Internal Server Error", 500)
126 }
127}
128
129// submitForm displays the email submission form for scheduling purchase reminders
130// This will be handled by the TemplateHandler type to demonstrate custom handler types
131
132// processSubmit handles the form submission and schedules the email reminder
133// This demonstrates a method handler with business logic
134func (app *application) processSubmit(w http.ResponseWriter, r *http.Request) {
135 w.Header().Add("Server", app.serviceName)
136
137 // Parse templates with inheritance using configurable paths
138 ts, err := template.ParseFiles(
139 app.config.htmlDir+"/layouts/base.tmpl",
140 app.config.htmlDir+"/pages/complete.tmpl",
141 )
142 if err != nil {
143 app.logger.error.Println(err.Error())
144 http.Error(w, "Internal Server Error", 500)
145 return
146 }
147
148 // Prepare template data
149 data := PageData{
150 Title: "Submission Complete",
151 ServiceName: app.serviceName,
152 Content: nil,
153 }
154
155 // Execute template
156 err = ts.ExecuteTemplate(w, "base", data)
157 if err != nil {
158 app.logger.error.Println(err.Error())
159 http.Error(w, "Internal Server Error", 500)
160 }
161}
162
163// confirmWithToken handles magic link confirmation with token validation
164// This demonstrates dependency injection by making it a method on application
165func (app *application) confirmWithToken(w http.ResponseWriter, r *http.Request) {
166 token := r.PathValue("token")
167
168 if !isValidToken(token) {
169 w.Header().Add("Server", app.serviceName)
170 app.logger.error.Printf("Invalid token attempted: %s", token)
171 http.NotFound(w, r)
172 return
173 }
174
175 w.Header().Add("Server", app.serviceName)
176 app.logger.info.Printf("Token confirmed successfully: %s", token[:8]+"...")
177 w.WriteHeader(http.StatusOK)
178 w.Write([]byte("Email Confirmed! Your purchase reminder has been activated via magic link."))
179}
180
181// isValidToken validates that the token is alphanumeric and at least 32 characters
182func isValidToken(token string) bool {
183 if len(token) < 32 {
184 return false
185 }
186
187 // Check if token contains only alphanumeric characters
188 matched, _ := regexp.MatchString("^[a-zA-Z0-9]+$", token)
189 return matched
190}
191
192// initializeApplication creates and configures the application with all dependencies
193// This centralizes dependency injection and makes testing easier
194func initializeApplication(cfg config) *application {
195 // Create structured loggers
196 infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
197 errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
198
199 // Log startup configuration
200 infoLog.Printf("Configuration loaded: port=%d, env=%s, staticDir=%s, htmlDir=%s",
201 cfg.port, cfg.env, cfg.staticDir, cfg.htmlDir)
202
203 // Create application with injected dependencies
204 app := &application{
205 config: cfg,
206 logger: struct {
207 info *log.Logger
208 error *log.Logger
209 }{
210 info: infoLog,
211 error: errorLog,
212 },
213 serviceName: "buylater.email",
214 // Future service dependencies will be initialized here:
215 // database: initializeDatabase(cfg),
216 // mailer: initializeMailer(cfg),
217 }
218
219 // Validate all required dependencies are properly initialized
220 if err := app.validateDependencies(); err != nil {
221 log.Fatalf("Dependency validation failed: %v", err)
222 }
223
224 app.logger.info.Println("Application dependencies initialized successfully")
225 return app
226}
227
228// validateDependencies ensures all required dependencies are properly configured
229func (app *application) validateDependencies() error {
230 // Validate configuration
231 if app.config.port <= 0 || app.config.port > 65535 {
232 return fmt.Errorf("invalid port: %d", app.config.port)
233 }
234 if app.config.env == "" {
235 return fmt.Errorf("environment not specified")
236 }
237 if app.config.staticDir == "" {
238 return fmt.Errorf("static directory not specified")
239 }
240 if app.config.htmlDir == "" {
241 return fmt.Errorf("HTML directory not specified")
242 }
243
244 // Validate loggers
245 if app.logger.info == nil {
246 return fmt.Errorf("info logger not initialized")
247 }
248 if app.logger.error == nil {
249 return fmt.Errorf("error logger not initialized")
250 }
251
252 // Validate service name
253 if app.serviceName == "" {
254 return fmt.Errorf("service name not specified")
255 }
256
257 // Future dependency validation will be added here:
258 // if app.database == nil { return fmt.Errorf("database not initialized") }
259 // if app.mailer == nil { return fmt.Errorf("mailer not initialized") }
260
261 return nil
262}
263
264// about displays information about the buylater.email service
265// This will be handled by the TemplateHandler type to demonstrate reusable custom handlers