package web import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "log" "net/http" "nfeeder/internal/db" "strconv" "time" "github.com/golang-jwt/jwt/v5" "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" ) const ISSUER = "nfeeder-app" type AuthResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } func (s *Server) handleRegister() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") password := r.FormValue("password") // hash password hashpw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { http.Error(w, "Internal Error", http.StatusInternalServerError) return } // create the user user, err := s.store.CreateUser(r.Context(), db.CreateUserParams{ Email: email, Password: string(hashpw), }) if err != nil { // Log the actual error for yourself, send a generic one to the user log.Printf("failed to create user: %v", err) http.Error(w, "Could not create user", http.StatusBadRequest) return } // Auto-login after reg s.issueToken(w, r, user.ID) } } func (s *Server) handleLogin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") password := r.FormValue("password") user, err := s.store.GetUserByEmail(r.Context(), email) if err != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } // compare passwords err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } s.issueToken(w, r, user.ID) } } func (s *Server) handleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // To logout, we simply instruct the browser to delete the cookie http.SetCookie(w, &http.Cookie{ Name: "nfeeder_token", Value: "", Path: "/", Expires: time.Unix(0, 0), // Expire immediately HttpOnly: true, }) http.Redirect(w, r, "/login", http.StatusSeeOther) } } func (s *Server) handleRefresh() http.HandlerFunc { // TODO: refresh logic /* Read the refresh token cookie Hash it with sha256 Look up the hash in the DB Check it's not expired Delete the old DB record Call issueTokenAndRedirect — which creates a new JWT and a new refresh token in one go */ return func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusOK) } } func (s *Server) issueToken(w http.ResponseWriter, r *http.Request, userID int64) { nowTime := time.Now() jwtExpireTime := nowTime.Add(24 * time.Hour) refreshExpireTime := nowTime.Add((24 * time.Hour) * 3) claims := jwt.RegisteredClaims{ Issuer: ISSUER, Subject: strconv.FormatInt(userID, 10), ExpiresAt: jwt.NewNumericDate(jwtExpireTime), IssuedAt: jwt.NewNumericDate(nowTime), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(s.jwtSecret) if err != nil { http.Error(w, "Internal Error", http.StatusInternalServerError) return } // Generate refresh token rawToken := rand.Text() hash := sha256.Sum256([]byte(rawToken)) hex_token := hex.EncodeToString(hash[:]) _, err = s.store.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{ UserID: userID, TokenHash: hex_token, ExpiresAt: pgtype.Timestamptz{Time: refreshExpireTime, Valid: true}, }) if err != nil { // Log the actual error for yourself, send a generic one to the user log.Printf("failed to create refresh token: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } resp := AuthResponse{ AccessToken: tokenString, RefreshToken: rawToken, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("json encoding failed: %v", err) return } }