feat: add login example demonstrating authentication flow#5
Conversation
This example demonstrates the authentication v0.5 features: 1. HTTP login with SetCookie() for session management 2. HTTP redirect with Redirect() to dashboard after login 3. BroadcastAware interface (OnConnect, OnDisconnect) for WebSocket lifecycle 4. Server-initiated push message via Broadcaster.Send() Flow: - User submits HTTP form → server sets HttpOnly cookie → redirects to dashboard - Dashboard page loads → WebSocket connects (authenticated via cookie) - Server detects login via OnConnect() → pushes welcome message - Client UI updates to show the server-pushed message Includes: - main.go: Server with AuthState implementing Change and BroadcastAware - templates/auth.html: Login form and dashboard views - login_test.go: E2E browser tests and HTTP cookie tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive login example demonstrating authentication features from livetemplate v0.5, including HTTP cookie-based session management, redirects, and WebSocket lifecycle integration with server-initiated push messages.
Key Changes:
- HTTP form authentication with session cookies and redirects
- WebSocket lifecycle hooks (
BroadcastAwareinterface) for server-push messaging - End-to-end browser tests using chromedp and HTTP cookie validation tests
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
login/go.mod |
Defines module dependencies with local replacements for livetemplate auth features |
login/go.sum |
Checksums for all transitive dependencies |
login/templates/auth.html |
HTML template with login form and dashboard views, includes styling and client-side library |
login/main.go |
Main server implementation with AuthState struct handling login/logout and WebSocket lifecycle |
login/login_test.go |
Comprehensive E2E tests using chromedp and HTTP cookie validation tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <form method="POST"> | ||
| <input type="hidden" name="lvt-action" value="login"> | ||
| <div class="form-group"> | ||
| <label for="username">Username</label> | ||
| <input type="text" id="username" name="username" placeholder="Enter username" required> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label for="password">Password</label> | ||
| <input type="password" id="password" name="password" placeholder="Enter password" required> | ||
| </div> | ||
| <button type="submit" class="btn-primary">Login</button> | ||
| </form> |
There was a problem hiding this comment.
Missing form accessibility: The login form lacks a descriptive label or aria-label for screen readers. Consider adding an aria-label to the form element: <form method="POST" aria-label="Login form">.
| time.Sleep(2 * time.Second) | ||
| for i := 0; i < 30; i++ { | ||
| if resp, err := http.Get(serverURL); err == nil { | ||
| resp.Body.Close() | ||
| break | ||
| } | ||
| time.Sleep(100 * time.Millisecond) | ||
| } |
There was a problem hiding this comment.
Magic number: The 2 second initial sleep and 30 iteration polling loop have hardcoded values without explanation. Consider defining these as named constants (e.g., const initialWaitTime = 2 * time.Second and const maxRetries = 30) to improve maintainability.
| // Set HttpOnly session cookie | ||
| err := ctx.SetCookie(&http.Cookie{ | ||
| Name: "session_token", | ||
| Value: fmt.Sprintf("session_%s_%d", username, time.Now().Unix()), |
There was a problem hiding this comment.
The session token generation is predictable as it uses only the username and Unix timestamp. This could allow session hijacking or fixation attacks. Consider using a cryptographically secure random value instead: crypto/rand with uuid.New() or base64.URLEncoding.EncodeToString() of random bytes.
| func (s *AuthState) handleLogin(ctx *livetemplate.ActionContext) error { | ||
| username := ctx.GetString("username") | ||
| password := ctx.GetString("password") | ||
|
|
||
| // Simple validation | ||
| if username == "" || password == "" { | ||
| s.Error = "Username and password are required" | ||
| return nil | ||
| } | ||
|
|
||
| // Demo: accept any username with password "secret" | ||
| if password != "secret" { | ||
| s.Error = "Invalid credentials" | ||
| return nil | ||
| } | ||
|
|
||
| // Clear error and set logged in state | ||
| s.Error = "" | ||
| s.Username = username | ||
| s.IsLoggedIn = true | ||
| s.LoginTime = time.Now() | ||
| s.ServerMessage = "" // Will be set when WebSocket connects | ||
|
|
||
| // Set HttpOnly session cookie | ||
| err := ctx.SetCookie(&http.Cookie{ | ||
| Name: "session_token", | ||
| Value: fmt.Sprintf("session_%s_%d", username, time.Now().Unix()), | ||
| Path: "/", | ||
| HttpOnly: true, | ||
| Secure: false, // Set to true in production with HTTPS | ||
| SameSite: http.SameSiteStrictMode, | ||
| MaxAge: 3600, // 1 hour | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to set cookie: %w", err) | ||
| } | ||
|
|
||
| // Redirect to dashboard (page will load, then WebSocket connects) | ||
| return ctx.Redirect("/", http.StatusSeeOther) |
There was a problem hiding this comment.
Missing mutex protection: The handleLogin function reads and writes shared state fields (s.Error, s.Username, s.IsLoggedIn, s.LoginTime, s.ServerMessage) without holding the mutex. This creates race conditions when concurrent requests access the same AuthState instance. All field access should be protected with s.mu.Lock()/Unlock().
| {{end}} | ||
| </div> | ||
| <!-- HTTP logout (deletes session cookie) --> | ||
| <form method="POST"> |
There was a problem hiding this comment.
Missing form accessibility: The logout form lacks a descriptive label or aria-label for screen readers. The hidden input and button alone may not provide enough context. Consider adding an aria-label to the form element: <form method="POST" aria-label="Logout form">.
| <form method="POST"> | |
| <form method="POST" aria-label="Logout form"> |
| jar, _ := http.NewRequest("GET", serverURL, nil) | ||
| _ = jar // Client will handle cookies automatically |
There was a problem hiding this comment.
Incorrect variable usage: A *http.Request is created and assigned to jar but never used for its intended purpose. The variable name suggests it should be a cookie jar, but http.NewRequest doesn't create a jar. This line appears to be dead code and should be removed, or replaced with jar, _ := cookiejar.New(nil) if a cookie jar is actually needed.
| for _, c := range cookies { | ||
| if c.Name == "session_token" { | ||
| t.Logf("Deleted cookie: %s (MaxAge=%d)", c.Name, c.MaxAge) | ||
| if c.MaxAge >= 0 { |
There was a problem hiding this comment.
Incorrect cookie deletion check: The condition c.MaxAge >= 0 will incorrectly flag cookies with MaxAge=0 as errors. According to HTTP cookie spec, MaxAge=0 means "delete immediately" (same as negative values). The condition should be c.MaxAge > 0 to correctly identify cookies that are NOT being deleted.
| if c.MaxAge >= 0 { | |
| if c.MaxAge > 0 { |
| s.mu.Lock() | ||
| s.broadcaster = b | ||
| isLoggedIn := s.IsLoggedIn | ||
| username := s.Username | ||
| s.mu.Unlock() |
There was a problem hiding this comment.
Race condition: Reading s.IsLoggedIn and s.Username without holding the mutex. These fields should be read while the lock is held at lines 104-108. If another goroutine modifies these fields (e.g., during logout), inconsistent state could be observed.
| // This demonstrates pushing updates from server to client without user action. | ||
| func (s *AuthState) sendWelcomeMessage() { | ||
| // Small delay so the page fully renders first | ||
| time.Sleep(500 * time.Millisecond) |
There was a problem hiding this comment.
Magic number: The 500ms delay is hardcoded without explanation of why this specific duration is needed. Consider defining this as a named constant (e.g., const welcomeMessageDelay = 500 * time.Millisecond) with a comment explaining its purpose (e.g., "delay to ensure page renders before pushing update").
Revert to the old testing API that is compatible with the pinned lvt version (v0.0.0-20251103195948-fbcd6dfae2d0): - StartDockerChrome returns *exec.Cmd - StopDockerChrome takes (t, cmd, debugPort) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
e13e68d to
86a8359
Compare
Summary
This example demonstrates the authentication v0.5 features from livetemplate/livetemplate#60:
SetCookie()for session managementRedirect()to dashboard after loginBroadcastAwareinterface (OnConnect,OnDisconnect) for WebSocket lifecycleBroadcaster.Send()Flow
OnConnect()→ pushes welcome messageFiles
login/main.go: Server with AuthState implementing Change and BroadcastAwarelogin/templates/auth.html: Login form and dashboard viewslogin/login_test.go: E2E browser tests and HTTP cookie testsTest plan
🤖 Generated with Claude Code