Skip to content

feat: add login example demonstrating authentication flow#5

Merged
adnaan merged 2 commits intomainfrom
feature/login-example
Nov 29, 2025
Merged

feat: add login example demonstrating authentication flow#5
adnaan merged 2 commits intomainfrom
feature/login-example

Conversation

@adnaan
Copy link
Copy Markdown
Contributor

@adnaan adnaan commented Nov 29, 2025

Summary

This example demonstrates the authentication v0.5 features from livetemplate/livetemplate#60:

  • HTTP login with SetCookie() for session management
  • HTTP redirect with Redirect() to dashboard after login
  • BroadcastAware interface (OnConnect, OnDisconnect) for WebSocket lifecycle
  • Server-initiated push message via Broadcaster.Send()

Flow

  1. User submits HTTP form → server sets HttpOnly cookie → redirects to dashboard
  2. Dashboard page loads → WebSocket connects (authenticated via cookie)
  3. Server detects login via OnConnect() → pushes welcome message
  4. Client UI updates to show the server-pushed message

Files

  • login/main.go: Server with AuthState implementing Change and BroadcastAware
  • login/templates/auth.html: Login form and dashboard views
  • login/login_test.go: E2E browser tests and HTTP cookie tests

Test plan

  • E2E browser tests pass (chromedp)
  • HTTP cookie tests pass

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings November 29, 2025 23:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (BroadcastAware interface) 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.

Comment on lines +152 to +163
<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>
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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">.

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +234
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)
}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// Set HttpOnly session cookie
err := ctx.SetCookie(&http.Cookie{
Name: "session_token",
Value: fmt.Sprintf("session_%s_%d", username, time.Now().Unix()),
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +80
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)
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copilot uses AI. Check for mistakes.
{{end}}
</div>
<!-- HTTP logout (deletes session cookie) -->
<form method="POST">
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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">.

Suggested change
<form method="POST">
<form method="POST" aria-label="Logout form">

Copilot uses AI. Check for mistakes.
Comment on lines +239 to +240
jar, _ := http.NewRequest("GET", serverURL, nil)
_ = jar // Client will handle cookies automatically
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
for _, c := range cookies {
if c.Name == "session_token" {
t.Logf("Deleted cookie: %s (MaxAge=%d)", c.Name, c.MaxAge)
if c.MaxAge >= 0 {
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if c.MaxAge >= 0 {
if c.MaxAge > 0 {

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +108
s.mu.Lock()
s.broadcaster = b
isLoggedIn := s.IsLoggedIn
username := s.Username
s.mu.Unlock()
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// 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)
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Copilot uses AI. Check for mistakes.
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>
@adnaan adnaan force-pushed the feature/login-example branch from e13e68d to 86a8359 Compare November 29, 2025 23:19
@adnaan adnaan merged commit 299d781 into main Nov 29, 2025
9 checks passed
@adnaan adnaan deleted the feature/login-example branch November 29, 2025 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants