From 2b6644e0a4d7b70acfdfae129b4253b564c0b0d5 Mon Sep 17 00:00:00 2001 From: Cody Couperus Date: Tue, 24 Feb 2026 20:07:20 -0500 Subject: [PATCH] add login form --- frontend/src/App.tsx | 40 +++++++++++------ frontend/src/components/login-form.tsx | 52 +++++++++++++++++++++++ frontend/src/components/register-form.tsx | 4 +- frontend/src/components/user-menu.tsx | 15 +++++++ frontend/src/contexts/auth-context.tsx | 7 ++- go.mod | 7 ++- handlers/user.go | 17 +++++--- main.go | 10 +++-- services/auth.go | 2 +- 9 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/login-form.tsx create mode 100644 frontend/src/components/user-menu.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 193adc3..3537393 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,26 +5,42 @@ import TemperatureHistory from "@/components/temperature-history" import TemperaturePlot from "@/components/temperature-plot" import RegisterForm from './components/register-form' import { AuthProvider } from './contexts/auth-context' +import { useAuth } from './hooks/use-auth' +import UserMenu from './components/user-menu' +import LoginForm from './components/login-form' +function AppContent() { + const { isAuthenticated, isLoading } = useAuth() + if (isLoading) return
Loading...
+ + return ( +
+ {isAuthenticated ? : +
+ + +
+ } +
+ +
+ + {isAuthenticated && } +
+
+
+ ) + +} function App() { + return ( -
-
- -
-
- -
- - -
-
-
+
) diff --git a/frontend/src/components/login-form.tsx b/frontend/src/components/login-form.tsx new file mode 100644 index 0000000..0a0d2cd --- /dev/null +++ b/frontend/src/components/login-form.tsx @@ -0,0 +1,52 @@ +import { useState } from "react" +import { useAuth } from "@/hooks/use-auth" +import { Button } from "./ui/button" + +export default function LoginForm() { + const { login, isLoading } = useAuth() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [formError, setFormError] = useState("") + + async function handleSubmit(e: React.SubmitEvent) { + e.preventDefault() + setFormError(null) + try { + await login(email, password) + } + catch (err) { + setFormError(err instanceof Error ? err.message : "Login failed") + } + } + + return ( +
+ +

Login

+ {formError &&

{formError}

} + setEmail(e.target.value)} + required + className="p-2 border rounded bg-background" + /> + setPassword(e.target.value)} + required + className="p-2 border rounded bg-background" + /> + + +
+ ) + +} + + diff --git a/frontend/src/components/register-form.tsx b/frontend/src/components/register-form.tsx index 657c7b5..fb55c2b 100644 --- a/frontend/src/components/register-form.tsx +++ b/frontend/src/components/register-form.tsx @@ -9,7 +9,7 @@ export default function RegisterForm() { const [success, setSuccess] = useState(false) const [loading, setLoading] = useState(false) - async function handleSubmit(e: React.FormEvent) { + async function handleSubmit(e: React.SubmitEvent) { e.preventDefault() setError(null) setSuccess(false) @@ -52,7 +52,7 @@ export default function RegisterForm() { } return ( -
+

Register

{error &&

{error}

} {success &&

Account created

} diff --git a/frontend/src/components/user-menu.tsx b/frontend/src/components/user-menu.tsx new file mode 100644 index 0000000..fb32d38 --- /dev/null +++ b/frontend/src/components/user-menu.tsx @@ -0,0 +1,15 @@ +import { useAuth } from "@/hooks/use-auth"; +import { Button } from "./ui/button"; + +export default function UserMenu() { + const { user, logout } = useAuth() + + return ( +
+ {user?.email} + +
+ ) +} diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 9f4d013..285507a 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -35,10 +35,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { const res = await fetch("/api/me", { credentials: "include", }) - if (res.ok) { - const data = await res.json() - setUser(data.user) - } + const data = await res.json() + // api response will be null if user is not authenticated + setUser(data.user) } catch { setUser(null) diff --git a/go.mod b/go.mod index 52aa002..8891e4a 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,8 @@ module go-weather go 1.25.6 -require github.com/lib/pq v1.11.1 - require ( - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - golang.org/x/crypto v0.48.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/lib/pq v1.11.1 + golang.org/x/crypto v0.48.0 ) diff --git a/handlers/user.go b/handlers/user.go index 9257b0a..998dfc9 100644 --- a/handlers/user.go +++ b/handlers/user.go @@ -5,6 +5,7 @@ import ( "go-weather/middleware" "go-weather/models" "go-weather/services" + "log/slog" "net/http" "strings" ) @@ -20,17 +21,20 @@ func NewUserHandler(us *services.UserService, as *services.AuthService) *UserHan func (h *UserHandler) Me(w http.ResponseWriter, r *http.Request) { claims := middleware.GetClaims(r) + w.Header().Set("Content-Type", "application/json") + if claims == nil { - http.Error(w, "unauthorized", http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]any{"user": nil}) + return } user, err := h.userService.GetUserByID(claims.UserID) if err != nil { - http.Error(w, "user not found", http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]any{"user": nil}) + return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{"user": user}) + json.NewEncoder(w).Encode(map[string]any{"user": user}) } func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { @@ -51,8 +55,8 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { return } - if len(req.Password) < 8 { - http.Error(w, "password must be at least 8 characters", http.StatusBadRequest) + if len(req.Password) < 4 { + http.Error(w, "password must be at least 4 characters", http.StatusBadRequest) return } @@ -105,6 +109,7 @@ func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) { token, err := h.authService.GenerateToken(user.ID, user.Email) if err != nil { + slog.Error("failed to generate token", "user_id", user.ID, "error", err) http.Error(w, "failed to generate token", http.StatusInternalServerError) return } diff --git a/main.go b/main.go index dc57a09..79ab366 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "go-weather/middleware" "go-weather/services" "log" + "log/slog" "net/http" "os" "os/signal" @@ -49,6 +50,11 @@ func startPoller(db *sql.DB, ws *services.WeatherService, defaultCity string, in } func main() { config := config.Load() + // Setup logger + var handler slog.Handler + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + slog.SetDefault(slog.New(handler)) + // Connect to DB connStr := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", @@ -82,9 +88,7 @@ func main() { http.HandlerFunc(weatherHandler.FetchWeather)), ) - http.Handle("/weather/me", middleware.AuthMiddleware(authService)( - http.HandlerFunc(userHandler.Me)), - ) + http.Handle("/me", http.HandlerFunc(userHandler.Me)) http.HandleFunc("/weather/history", weatherHandler.GetHistory) http.HandleFunc("/health", handlers.Health) diff --git a/services/auth.go b/services/auth.go index d767cd8..2a1ba68 100644 --- a/services/auth.go +++ b/services/auth.go @@ -31,7 +31,7 @@ func (s *AuthService) GenerateToken(userID int, email string) (string, error) { }, } - token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.secretKey) }