feat: show user creation on dashboard

This commit is contained in:
Ruidy 2025-09-20 01:28:57 +02:00
parent f02d41901d
commit 3c7092236f
No known key found for this signature in database
GPG key ID: 705C24D202990805
7 changed files with 75 additions and 16 deletions

View file

@ -42,3 +42,12 @@ func (s *Service) Authenticate(ctx context.Context, email UserEmail, password st
return account, nil return account, nil
} }
// LookupByEmail fetches a user by canonical email.
func (s *Service) LookupByEmail(ctx context.Context, email UserEmail) (*User, error) {
if email.IsZero() {
return nil, ErrInvalidInput
}
return s.store.FindByEmail(ctx, email)
}

View file

@ -1,6 +1,14 @@
package server package server
import "net/http" import (
"log"
"net/http"
"time"
"github.com/rjnemo/auth/internal/auth"
)
const dashboardTimeDisplayLayout = "02 Jan 2006 15:04 MST"
func (s *Server) dashboardHandler() http.HandlerFunc { func (s *Server) dashboardHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@ -12,6 +20,23 @@ func (s *Server) dashboardHandler() http.HandlerFunc {
return return
} }
s.render(w, "in.html", PageData{Email: state.Email, CSRFToken: state.CSRFToken}) email, err := auth.NewUserEmail(state.Email)
if err != nil {
log.Printf("dashboard: invalid session email: %v", err)
http.Error(w, "session invalid", http.StatusUnauthorized)
return
}
account, err := s.authService.LookupByEmail(r.Context(), email)
if err != nil {
log.Printf("dashboard: lookup failed: %v", err)
http.Error(w, "unable to load account", http.StatusInternalServerError)
return
}
createdAtISO := account.CreatedAt.Format(time.RFC3339)
createdAtDisplay := account.CreatedAt.Format(dashboardTimeDisplayLayout)
s.render(w, "in.html", newDashboardData(state.Email, state.CSRFToken, createdAtDisplay, createdAtISO))
} }
} }

View file

@ -3,14 +3,13 @@ package server
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors" "fmt"
"net/http" "net/http"
"time" "time"
) )
const ( const (
sessionCookieName = "auth_session" sessionCookieName = "auth_session"
csrfSessionKey = "csrf_token"
sessionLifetime = 12 * time.Hour sessionLifetime = 12 * time.Hour
sessionSecretMinLength = 32 sessionSecretMinLength = 32
csrfTokenByteLength int = 32 csrfTokenByteLength int = 32
@ -24,7 +23,7 @@ type SessionStore struct {
// NewSessionStore creates a cookie-backed session store. // NewSessionStore creates a cookie-backed session store.
func NewSessionStore(secret []byte) (*SessionStore, error) { func NewSessionStore(secret []byte) (*SessionStore, error) {
if len(secret) < sessionSecretMinLength { if len(secret) < sessionSecretMinLength {
return nil, errors.New("session secret must be at least 32 bytes") return nil, fmt.Errorf("session secret must be at least %d bytes", sessionSecretMinLength)
} }
// copy secret to avoid external mutation // copy secret to avoid external mutation
buf := make([]byte, len(secret)) buf := make([]byte, len(secret))

View file

@ -2,9 +2,11 @@ package server
// PageData contains fields shared by the templates for now. // PageData contains fields shared by the templates for now.
type PageData struct { type PageData struct {
Email string Email string
Error string Error string
CSRFToken string CSRFToken string
CreatedAt string
CreatedAtISO string
} }
func newIndexData(email, errMsg, token string) PageData { func newIndexData(email, errMsg, token string) PageData {
@ -14,3 +16,7 @@ func newIndexData(email, errMsg, token string) PageData {
func newUnauthorizedData(errMsg, token string) PageData { func newUnauthorizedData(errMsg, token string) PageData {
return PageData{Error: errMsg, CSRFToken: token} return PageData{Error: errMsg, CSRFToken: token}
} }
func newDashboardData(email, token, createdAt, createdAtISO string) PageData {
return PageData{Email: email, CSRFToken: token, CreatedAt: createdAt, CreatedAtISO: createdAtISO}
}

View file

@ -12,12 +12,24 @@
<body> <body>
<main class="container"> <main class="container">
<h1>Welcome</h1> <h1>Welcome</h1>
<p>You are signed in as <strong>{{.Email}}</strong>.</p> <article>
<p>This placeholder dashboard will evolve as we flesh out the auth flow.</p> <p>You are signed in as <strong>{{.Email}}</strong>.</p>
<form method="post" action="/logout"> {{if .CreatedAt}}
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" /> <p>
<button type="submit" class="secondary">Sign out</button> Member since
</form> <time datetime="{{.CreatedAtISO}}">{{.CreatedAt}}</time>.
</p>
{{end}}
<p>
This placeholder dashboard will evolve as we flesh out the auth flow.
</p>
<footer>
<form method="post" action="/logout">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}" />
<button type="submit" class="secondary">Sign out</button>
</form>
</footer>
</article>
</main> </main>
</body> </body>
</html> </html>

View file

@ -15,7 +15,12 @@
<p>Authenticate with the demo credentials below to view the dashboard.</p> <p>Authenticate with the demo credentials below to view the dashboard.</p>
<article> <article>
<header>Demo account</header> <header>Demo account</header>
<p><strong>Email:</strong> user@example.com<br /><strong>Password:</strong> password123</p> <p>
<strong>Email:</strong> user@example.com<br /><strong>
Password:
</strong>
password123
</p>
</article> </article>
{{if .Error}} {{if .Error}}
<article class="secondary"> <article class="secondary">

View file

@ -12,7 +12,10 @@
<body> <body>
<main class="container"> <main class="container">
<h1>Unauthorized</h1> <h1>Unauthorized</h1>
<p>{{if .Error}}{{.Error}}{{else}}You do not have permission to view that page.{{end}}</p> <p>
{{if .Error}}{{.Error}}{{else}}You do not have permission to view that
page.{{end}}
</p>
<a href="/" role="button">Back to safety</a> <a href="/" role="button">Back to safety</a>
</main> </main>
</body> </body>