Pizzeria App

A Delicious Web App Journey with Go

Presented by: AlexTLDR

AlexTLDR Website

Why Go?

  • The syntax is minimalistic and clean, inspired by C but much more readable.
    package main
    
    import "fmt"
    
    func main() {
           	message := "Hello, pizza lovers!"
           	fmt.Println(message)
    }
  • Consistently formatted with gofmt - no style debates.
  • Fast compilation and execution times.
  • Rich Standard Library (our example with the website, HTTP servers, file I/O, JSON handling, etc) - you can build full applications without relying heavily on external dependencies

Why Go?

  • Backwards compatible.
  • Static typing (preference-based - I love this feature as it makes the code base less confusing).
    Static Typing

Why Go?

  • Cross-Platform & Deployment-Friendly (run your program as a binary).
  • DevOps oriented:
    • Great for containerized apps and microservices
    • Widely used in cloud-native environments (Docker, Kubernetes, Terraform, ETC.)
    • Great fit for CLI tools and backend infrastructure

Learning resources

The Pizzeria Project: Overview

  • Backend:
    • Language: Go
    • Web Server: Native Go HTTP server (from `net/http` package)
      mux := http.NewServeMux()
    • Database: SQLite3 (file-based database)
      db, err := sql.Open("sqlite3", dbPath)
      if err != nil {
          return err
      }
      defer db.Close()

Authentication: Google OAuth2

  • Google OAuth2 for admin authentication:
    func Initialize() (*OAuthConfig, error) {
    	// Load .env file
    	if err := godotenv.Load(); err != nil {
    		return nil, fmt.Errorf("error loading .env file: %w", err)
    	}
    
    	// Get Google OAuth credentials
    	clientID := os.Getenv("GOOGLE_CLIENT_ID")
    	clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
    	redirectURL := os.Getenv("GOOGLE_REDIRECT_URL")
    
    	if clientID == "" || clientSecret == "" || redirectURL == "" {
    		return nil, errors.New("missing Google OAuth configuration in .env file")
    	}
    
    	// Get allowed emails
    	allowedEmailsStr := os.Getenv("ALLOWED_EMAILS")
    	allowedEmails := strings.Split(allowedEmailsStr, ",")
    	for i := range allowedEmails {
    		allowedEmails[i] = strings.TrimSpace(allowedEmails[i])
    	}
    
    	if len(allowedEmails) == 0 || (len(allowedEmails) == 1 && allowedEmails[0] == "") {
    		return nil, errors.New("no allowed email addresses configured in .env file")
    	}
    
    	// Create OAuth config
    	oauthConfig := &oauth2.Config{
    		ClientID:     clientID,
    		ClientSecret: clientSecret,
    		RedirectURL:  redirectURL,
    		Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
    		Endpoint:     google.Endpoint,
    	}
    
    	return &OAuthConfig{
    		GoogleOAuthConfig: oauthConfig,
    		AllowedEmails:     allowedEmails,
    	}, nil
    }

Session Management

  • Custom implementation using secure, signed cookies for authentication:
    func SetSecureSessionCookie(w http.ResponseWriter, email string) {
    	// Current timestamp for the cookie
    	now := time.Now()
    	expires := now.Add(SessionDuration)
    
    	// Create the cookie payload: email|expiration_timestamp
    	expiresStr := strconv.FormatInt(expires.Unix(), 10)
    	payload := fmt.Sprintf("%s|%s", email, expiresStr)
    
    	// Create HMAC signature
    	h := hmac.New(sha256.New, cookieSecret)
    	h.Write([]byte(payload))
    	signature := h.Sum(nil)
    
    	// Encode the payload and signature for the cookie
    	encodedPayload := base64.URLEncoding.EncodeToString([]byte(payload))
    	encodedSignature := base64.URLEncoding.EncodeToString(signature)
    
    	// Final cookie value: base64(payload).base64(signature)
    	cookieValue := fmt.Sprintf("%s.%s", encodedPayload, encodedSignature)
    
    	// Set the cookie
    	cookie := http.Cookie{
    		Name:     SessionCookieName,
    		Value:    cookieValue,
    		Path:     "/",
    		HttpOnly: true,
    		Secure:   true, // Set to true in production
    		SameSite: http.SameSiteLaxMode,
    		MaxAge:   int(SessionDuration.Seconds()),
    		Expires:  expires,
    	}
    
    	http.SetCookie(w, &cookie)
    	log.Printf("Set secure session cookie for %s, expires: %s", email, expires.Format(time.RFC3339))
    }

File System Operations

  • Utilizing standard `os` and `path/filepath` packages for file manipulation:
    var imageURL string
    file, header, err := r.FormFile("image_upload")
    if err == nil {
    	defer file.Close()
    
    	// Create unique filename based on timestamp
    	timestamp := time.Now().Unix()
    	filename := fmt.Sprintf("%d_%s", timestamp, header.Filename)
    
    	// Ensure filename contains only valid characters
    	filename = strings.ReplaceAll(filename, " ", "-")
    
    	// Save the file
    	filePath := filepath.Join("static", "images", "menu", filename)
    	dst, err := os.Create(filePath)
    	if err != nil {
    		m.adminError(w, r, err, http.StatusInternalServerError, "CreateMenuItem - saving image")
    		return
    	}
    	defer dst.Close()
    
    	// Copy the file content
    	_, err = dst.ReadFrom(file)
    	if err != nil {
    		m.adminError(w, r, err, http.StatusInternalServerError, "CreateMenuItem - saving image")
    		return
    	}
    
    	// Set the image URL
    	imageURL = "/" + filePath // Add leading slash for web URLs
    }

File System Operations

  • Utilizing standard `os` and `path/filepath` packages for file manipulation and guarding against traversal attacks:
      var imageURL string
    
    	file, header, err := r.FormFile("image_upload")
    	if err == nil {
    		defer file.Close()
    
    		// Validate that the file is an image
    		if !m.isValidImageExtension(header.Filename) {
    			m.clientError(w, http.StatusBadRequest, "Invalid file type. Only image files (jpg, jpeg, png, gif, webp, bmp, svg) are allowed.")
    			return
    		}
    
    		// Create a completely random filename with timestamp prefix
    		timestamp := time.Now().Unix()
    		extension := filepath.Ext(header.Filename) // Get the file extension
    		randomName := fmt.Sprintf("%d_%s%s", timestamp, uuid.New().String(), extension)
    
    		// Save the file
    		filePath := filepath.Join("static", "images", "menu", randomName)
    
    		dst, err := os.Create(filePath)
    		if err != nil {
    			m.adminError(w, r, err, http.StatusInternalServerError, "CreateMenuItem - saving image")
    			return
    		}
    
    		defer dst.Close()
    
    		// Copy the file content
    		_, err = dst.ReadFrom(file)
    		if err != nil {
    			m.adminError(w, r, err, http.StatusInternalServerError, "CreateMenuItem - saving image")
    			return
    		}
    
    		// Set the image URL
    		imageURL = "/" + filePath // Add leading slash for web URLs
    	}

Frontend

    • Templating: Go's built-in `html/template` package
      templates := map[string]*template.Template{
      		"index.html":           template.Must(template.New("index.html").Funcs(funcMap).ParseFiles("templates/index.html", "templates/header.html", "templates/footer.html", "templates/category-nav.html")),
      		"login.html":           template.Must(template.New("login.html").Funcs(funcMap).ParseFiles("templates/login.html")),
      		"admin-dashboard.html": template.Must(template.New("admin-dashboard.html").Funcs(funcMap).ParseFiles("templates/admin-dashboard.html")),
      		"menu-form.html":       template.Must(template.New("menu-form.html").Funcs(funcMap).ParseFiles("templates/menu-form.html")),
      }
    • CSS Framework: Tailwind CSS V4
    • JavaScript: Minimal usage via inline scripts in templates for eye candy stuff like highlighting the menu category or jumping directly to a menu category (not relevant for this topic)

Development Tools

    • Migrations: Goose for database migrations
      -- +goose Up
      -- SQL in this section is executed when the migration is applied.
      CREATE TABLE menu_items (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          description TEXT,
          price REAL NOT NULL,
          small_price REAL,
          category TEXT NOT NULL,
          image_url TEXT, -- Stores the relative path like /static/images/menu/image.jpg
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
      
      CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          username TEXT NOT NULL UNIQUE,
          password_hash TEXT NOT NULL,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
      
      -- +goose Down
      -- SQL in this section is executed when the migration is rolled back.
      DROP TABLE users;
      DROP TABLE menu_items;
    • Hot Reload: Air for Go hot reloading during development
    • Containerization: Docker support (via Dockerfile and docker-compose.yml)
    • Environment Management: godotenv for .env file loading

Architecture

  • Project Structure:
    • cmd/server: Main application entry point
    • db: Database initialization and migrations
    • internal: Core application logic separated into modules
      - auth: Google OAuth implementation
      - handlers: HTTP request handlers
      - middleware: Authentication and session middleware
      - models: Data models and database interactions
    • static: Static assets (CSS, JavaScript, images)
    • templates: HTML templates for rendering pages

Database Schema

    • menu_items: Restaurant menu items with pricing and categories
    • flash_messages: Announcements/notifications to display on the website
    • users: (Deprecated) Previously used for authentication, now replaced with Google OAuth

Authentication Flow

    • User navigates to `/login`
    • User is redirected to Google OAuth login
    • Google redirects back with an authorization code
    • App exchanges code for a token and retrieves user info
    • App validates if the user's email is in the allowed list
    • Upon successful validation, a secure signed session cookie is created
    • Admin routes are protected by middleware that verifies this cookie

Key Features

  • Menu Management:
    • CRUD operations for menu items
    • Category-based organization
    • Support for different pricing (regular/small sizes)
    • Image upload support for menu items

Admin Dashboard:

    • Secure admin area accessible only to authorized emails
    • Menu item management
    • Flash message management

Public Website:

    • Display of menu items organized by categories
    • Display of flash messages/announcements
    • Responsive design using Tailwind CSS

Security:

    • Google OAuth for authentication
    • HMAC-SHA256 signed cookies for session management
    • Whitelist of allowed admin emails

Standard Library Packages Used

- database/sql: Core database operations
- net/http: HTTP server and client operations
- html/template: HTML templating
- time: Time handling and formatting
- context: Request context management
- encoding/json: JSON encoding/decoding
- encoding/base64: Base64 encoding/decoding for cookies
- crypto/hmac: HMAC signatures for secure cookies
- crypto/sha256: SHA256 hashing for HMAC
- crypto/rand: Secure random number generation
- path/filepath: File path operations
- strings: String manipulation
- log: Logging
- os: File manipulation and operations

External Dependencies

- github.com/joho/godotenv: For loading environment variables from .env files
- github.com/mattn/go-sqlite3: SQLite3 driver for Go
- github.com/pressly/goose/v3: Database migration tool
- golang.org/x/oauth2: Google OAuth2 integration
- tailwindcss: Frontend CSS framework (not Go related)

Deployment

- Docker support via Dockerfile and docker-compose.yml
- Environment configuration via .env files
- Database migrations for setting up the initial schema and seed data
- Using for hosting a GCP VM via Compute Engine
- Caddy for HTTPS cert and routing

Q&A

Presentation Slides

Slides QR Code

Pizzeria Website

Pizzeria Website QR Code