task/1.14
Raw Download raw file
  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