task/1.13
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.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