package server import ( "database/sql" "errors" "net/http" "strings" "time" "github.com/StevanFreeborn/chirpy/internal/api" "github.com/StevanFreeborn/chirpy/internal/auth" "github.com/StevanFreeborn/chirpy/internal/database" "github.com/lib/pq" ) const accessTokenTTL = time.Hour type createUserRequest struct { Email string `json:"email"` Password string `json:"password"` } type userResponse struct { Id string `json:"id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Email string `json:"email"` IsChirpyRed bool `json:"is_chirpy_red"` } // toUserResponse maps a database User to its API response shape. func toUserResponse(u database.User) userResponse { return userResponse{ Id: u.ID.String(), CreatedAt: u.CreatedAt.Format(time.RFC3339), UpdatedAt: u.UpdatedAt.Format(time.RFC3339), Email: u.Email, IsChirpyRed: u.IsChirpyRed, } } // HandleCreateUser registers a new user and returns it. func (s *Server) HandleCreateUser(w http.ResponseWriter, r *http.Request) { createUserRequest, err := api.DecodeJSON[createUserRequest](r) if err != nil { api.WriteError(w, http.StatusBadRequest, err.Error()) return } trimmedEmail := strings.TrimSpace(createUserRequest.Email) if trimmedEmail == "" { api.WriteError(w, http.StatusBadRequest, "email is required. must be valid email address.") return } trimmedPassword := strings.TrimSpace(createUserRequest.Password) if trimmedPassword == "" { api.WriteError(w, http.StatusBadRequest, "password is required. must be non-empty string.") return } hashedPassword, err := auth.HashPassword(trimmedPassword) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Uh oh we were unable to create a new user") return } createUserParams := database.CreateUserParams{ Email: trimmedEmail, HashedPassword: hashedPassword, } createdUser, err := s.db.CreateUser(r.Context(), createUserParams) if err != nil { var pgErr *pq.Error if errors.As(err, &pgErr) && pgErr.Code == "23505" { api.WriteError(w, http.StatusConflict, "A user with that email already exists") return } api.WriteError(w, http.StatusInternalServerError, "Uh oh we were unable to create a new user") return } api.WriteJSON(w, http.StatusCreated, toUserResponse(createdUser)) } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } func (r *loginRequest) Validate() error { if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" { return api.NewError("Email and password must be non-empty string") } return nil } type loginResponse struct { Id string `json:"id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Email string `json:"email"` IsChirpyRed bool `json:"is_chirpy_red"` Token string `json:"token"` RefreshToken string `json:"refresh_token"` } // HandleLogin authenticates a user and returns new access and refresh tokens. func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { loginRequest, err := api.DecodeJSON[loginRequest](r) if err != nil { api.WriteError(w, http.StatusBadRequest, err.Error()) return } if err := loginRequest.Validate(); err != nil { api.WriteError(w, http.StatusBadRequest, err.Error()) return } existingUser, err := s.db.GetUserByEmail(r.Context(), loginRequest.Email) if err != nil { api.WriteError(w, http.StatusUnauthorized, "Invalid login request") return } isCorrectPassword, err := auth.CheckPasswordHash(loginRequest.Password, existingUser.HashedPassword) if err != nil || !isCorrectPassword { api.WriteError(w, http.StatusUnauthorized, "Invalid login request") return } accessToken, err := auth.MakeJWT(existingUser.ID, s.jwtSecret, accessTokenTTL) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Login failed") return } createRefreshTokenParams := database.CreateRefreshTokenParams{ Token: auth.MakeRefreshToken(), ExpiresAt: time.Now().Add(60 * 24 * time.Hour), RevokedAt: sql.NullTime{ Valid: false, }, UserID: existingUser.ID, } createdRefreshToken, err := s.db.CreateRefreshToken(r.Context(), createRefreshTokenParams) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Login failed") return } user := toUserResponse(existingUser) api.WriteJSON(w, http.StatusOK, loginResponse{ Id: user.Id, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, Email: user.Email, IsChirpyRed: user.IsChirpyRed, Token: accessToken, RefreshToken: createdRefreshToken.Token, }) } type refreshTokenResponse struct { Token string `json:"token"` } // HandleRefresh exchanges a valid, unrevoked refresh token for a new access // token. func (s *Server) HandleRefresh(w http.ResponseWriter, r *http.Request) { refreshToken, err := auth.GetBearerToken(r.Header) if err != nil { api.WriteError(w, http.StatusUnauthorized, "Unable to refresh token") return } existingRefreshToken, err := s.db.GetRefreshTokenByToken(r.Context(), refreshToken) if err != nil { api.WriteError(w, http.StatusUnauthorized, "Unable to refresh token") return } if existingRefreshToken.ExpiresAt.Before(time.Now()) || existingRefreshToken.RevokedAt.Valid { api.WriteError(w, http.StatusUnauthorized, "Unable to refresh token") return } accessToken, err := auth.MakeJWT(existingRefreshToken.UserID, s.jwtSecret, accessTokenTTL) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Failed to refresh token") return } // TODO: We should rotate the refresh token api.WriteJSON(w, http.StatusOK, refreshTokenResponse{ Token: accessToken, }) } // HandleRevoke revokes a refresh token so it can no longer be used. func (s *Server) HandleRevoke(w http.ResponseWriter, r *http.Request) { refreshToken, err := auth.GetBearerToken(r.Header) if err != nil { api.WriteError(w, http.StatusBadRequest, "No refresh token present in request") return } existingRefreshToken, err := s.db.GetRefreshTokenByToken(r.Context(), refreshToken) if err != nil { api.WriteError(w, http.StatusNotFound, "Unable to revoke token") return } if err := s.db.RevokeRefreshToken(r.Context(), existingRefreshToken.Token); err != nil { api.WriteError(w, http.StatusInternalServerError, "Unable to revoke token") return } api.WriteJSON(w, http.StatusNoContent, nil) } type updateUserRequest struct { Email string `json:"email"` Password string `json:"password"` } func (r *updateUserRequest) Validate() error { if strings.TrimSpace(r.Email) == "" || strings.TrimSpace(r.Password) == "" { return api.NewError("Email and password must be non-empty string") } return nil } // HandleUpdateUser updates the authenticated user's email and password. func (s *Server) HandleUpdateUser(w http.ResponseWriter, r *http.Request) { accessToken, err := auth.GetBearerToken(r.Header) if err != nil { api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action") return } requestUserId, err := auth.ValidateJWT(accessToken, s.jwtSecret) if err != nil { api.WriteError(w, http.StatusUnauthorized, "You are not authorized to perform this action") return } updateUserRequest, err := api.DecodeJSON[updateUserRequest](r) if err != nil { api.WriteError(w, http.StatusBadRequest, err.Error()) return } if err := updateUserRequest.Validate(); err != nil { api.WriteError(w, http.StatusBadRequest, err.Error()) return } updatedHashedPassword, err := auth.HashPassword(updateUserRequest.Password) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Unable to perform update") return } updateUserParams := database.UpdateUserParams{ ID: requestUserId, Email: updateUserRequest.Email, HashedPassword: updatedHashedPassword, } updatedUser, err := s.db.UpdateUser(r.Context(), updateUserParams) if err != nil { api.WriteError(w, http.StatusInternalServerError, "Unable to perform update") return } api.WriteJSON(w, http.StatusOK, toUserResponse(updatedUser)) }