first commit

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:10:41 +02:00
commit 9b7bdcbc53
39 changed files with 5109 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/util"
)
func JsonEndpoint(w http.ResponseWriter, r *http.Request) {
stock := r.URL.Query().Get("stock")
// Standardwerte setzen, falls Parameter fehlen
if stock == "" {
stock = "IDIA.SW"
}
data, err := util.FetchYahooFinanceData(stock)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(data))
}
func YahooMaxDividendsHandler(w http.ResponseWriter, r *http.Request) {
stock := r.URL.Query().Get("stock")
if stock == "" {
http.Error(w, "Fehlender stock-Parameter", http.StatusBadRequest)
return
}
data, err := util.FetchYahooFinanceDataMax(stock)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var resp model.YahooDividendChartResponse
err = json.Unmarshal([]byte(data), &resp)
if err != nil {
http.Error(w, "Fehler beim Parsen der Yahoo-Dividenden-Daten: "+err.Error(), http.StatusInternalServerError)
return
}
var result [][]interface{}
if len(resp.Chart.Result) > 0 {
for _, div := range resp.Chart.Result[0].Events.Dividends {
result = append(result, []interface{}{div.Date * 1000, div.Amount})
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func StockSearchEndpoint(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "Fehlender q-Parameter", http.StatusBadRequest)
return
}
data, err := util.StockSearch(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(data))
}
// Helper function to get stock currency from Yahoo Finance
func getStockCurrency(stock string) (string, error) {
// Fetch basic stock data from Yahoo Finance
data, err := util.FetchYahooFinanceData(stock)
if err != nil {
return "", err
}
// Parse the JSON response
var resp model.YahooChartResponse
err = json.Unmarshal([]byte(data), &resp)
if err != nil {
return "", err
}
// Extract currency from the response
if len(resp.Chart.Result) > 0 {
currency := resp.Chart.Result[0].Meta.Currency
if currency != "" {
return currency, nil
}
}
return "", fmt.Errorf("currency not found in response")
}
// ClearCurrencyCacheHandler clears the currency conversion cache
func ClearCurrencyCacheHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
util.ClearCache()
response := map[string]string{
"status": "success",
"message": "Currency cache cleared successfully",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// CurrencyCacheInfoHandler returns information about the currency cache
func CurrencyCacheInfoHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed)
return
}
cacheInfo := util.GetCacheInfo()
response := map[string]interface{}{
"status": "success",
"cache_size": len(cacheInfo),
"entries": cacheInfo,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
+126
View File
@@ -0,0 +1,126 @@
package handler
import (
"fmt"
"net/http"
"portfolio-tracker/internal/model" // Add this import
"portfolio-tracker/internal/session"
"golang.org/x/crypto/bcrypt"
)
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
email := r.FormValue("email")
password := r.FormValue("password")
if username == "" || email == "" || password == "" {
http.Error(w, "Alle Felder sind erforderlich", http.StatusBadRequest)
return
}
// Passwort hashen
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError)
return
}
user := model.User{
Username: username,
Email: email,
Password: string(hash),
}
if err := DB.Create(&user).Error; err != nil {
http.Error(w, "Fehler beim Speichern des Users: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
http.Error(w, "Alle Felder sind erforderlich", http.StatusBadRequest)
return
}
var user model.User
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
http.Error(w, "Benutzer nicht gefunden", http.StatusUnauthorized)
return
}
// Passwort prüfen
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
http.Error(w, "Falsches Passwort", http.StatusUnauthorized)
return
}
// Session erstellen oder abrufen
session, err := session.Store.Get(r, "hnrx_pft_session")
if err != nil {
fmt.Printf("Error getting session: %v\n", err)
http.Error(w, "Session-Fehler", http.StatusInternalServerError)
return
}
// Session-Werte setzen
session.Values["authenticated"] = true
session.Values["username"] = username
// Debug output
fmt.Printf("Setting session values - Auth: %v, Username: %s\n", true, username)
fmt.Printf("Session ID before save: %s\n", session.ID)
// Session speichern
err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session: %v\n", err)
http.Error(w, "Fehler beim Speichern der Session", http.StatusInternalServerError)
return
}
fmt.Printf("Session saved successfully with ID: %s\n", session.ID)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, err := session.Store.Get(r, "hnrx_pft_session")
if err != nil {
fmt.Printf("Error getting session in logout: %v\n", err)
// Continue with logout even if session retrieval fails
}
// Clear session values
session.Values["authenticated"] = false
session.Values["username"] = ""
// Set session options to delete the session
session.Options.MaxAge = -1
// Save the session (this will delete it due to MaxAge = -1)
err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session during logout: %v\n", err)
http.Error(w, "Fehler beim Logout", http.StatusInternalServerError)
return
}
fmt.Printf("Session successfully logged out\n")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
+8
View File
@@ -0,0 +1,8 @@
package handler
import (
"gorm.io/gorm"
)
// Global database instance
var DB *gorm.DB
+98
View File
@@ -0,0 +1,98 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/session"
"portfolio-tracker/internal/util"
"portfolio-tracker/internal/web/templates"
"github.com/a-h/templ"
)
// Helper function to get session info
func getSessionInfo(r *http.Request) (bool, string, error) {
session, err := session.Store.Get(r, "hnrx_pft_session")
if err != nil {
return false, "", err
}
auth, ok := session.Values["authenticated"].(bool)
if !ok {
auth = false
}
username, ok := session.Values["username"].(string)
if !ok {
username = ""
}
return auth, username, nil
}
// Helper function to get portfolios for a user
func getUserPortfolios(username string) []model.Portfolio {
var portfolios []model.Portfolio
if username != "" {
// Get user first
var user model.User
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
fmt.Printf("Error getting user: %v\n", err)
return portfolios
}
// Get user's portfolios
if err := DB.Where("user_id = ?", user.ID).Find(&portfolios).Error; err != nil {
fmt.Printf("Error getting portfolios: %v\n", err)
}
}
return portfolios
}
func Handler(w http.ResponseWriter, r *http.Request) {
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error: %v\n", err)
}
portfolios := getUserPortfolios(username)
// Debug output
fmt.Printf("Session values - Auth: %v, Username: %s\n", auth, username)
component := templates.Result(auth, username, portfolios)
templ.Handler(component).ServeHTTP(w, r)
}
func DetailsHandler(w http.ResponseWriter, r *http.Request) {
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in DetailsHandler: %v\n", err)
}
portfolios := getUserPortfolios(username)
stock := r.URL.Query().Get("stock")
if stock == "" {
http.Error(w, "Fehlender stock-Parameter", http.StatusBadRequest)
return
}
data, err := util.FetchYahooFinanceData(stock)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var resp model.YahooChartResponse
err = json.Unmarshal([]byte(data), &resp)
if err != nil {
http.Error(w, "Fehler beim Parsen der Daten: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.StockDetails(auth, username, stock, resp, portfolios).Render(r.Context(), w)
}
+413
View File
@@ -0,0 +1,413 @@
package handler
import (
"fmt"
"net/http"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/web/templates"
"strconv"
"strings"
"time"
"github.com/a-h/templ"
)
func PortfolioHandler(w http.ResponseWriter, r *http.Request) {
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in PortfolioHandler: %v\n", err)
}
portfolios := getUserPortfolios(username)
component := templates.Portfolio(auth, username, portfolios)
templ.Handler(component).ServeHTTP(w, r)
}
func PortfolioDetailHandler(w http.ResponseWriter, r *http.Request) {
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in PortfolioDetailHandler: %v\n", err)
}
// Extract portfolio ID from URL path
path := r.URL.Path
portfolioIDStr := strings.TrimPrefix(path, "/portfolio/")
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
return
}
// Get user
var user model.User
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
http.Error(w, "Benutzer nicht gefunden", http.StatusUnauthorized)
return
}
// Get portfolio with activities
var portfolio model.Portfolio
if err := DB.Preload("Activities").Where("id = ? AND user_id = ?", portfolioID, user.ID).First(&portfolio).Error; err != nil {
http.Error(w, "Portfolio nicht gefunden oder keine Berechtigung", http.StatusForbidden)
return
}
// Get all user portfolios for navigation
portfolios := getUserPortfolios(username)
component := templates.PortfolioDetail(auth, username, portfolio, portfolios)
templ.Handler(component).ServeHTTP(w, r)
}
func CreatePortfolioHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in CreatePortfolioHandler: %v\n", err)
}
if !auth {
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
return
}
if username == "" {
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
return
}
// Get form values
name := r.FormValue("name")
baseCurrency := r.FormValue("base_currency")
description := r.FormValue("description")
// Basic validation
if name == "" || baseCurrency == "" {
http.Error(w, "Name und Basiswährung sind erforderlich", http.StatusBadRequest)
return
}
// Get user from database
var user model.User
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
return
}
// Create new portfolio
portfolio := model.Portfolio{
UserID: user.ID,
Name: name,
BaseCurrency: baseCurrency,
Description: description,
}
if err := DB.Create(&portfolio).Error; err != nil {
fmt.Printf("Error creating portfolio: %v\n", err)
http.Error(w, "Fehler beim Erstellen des Portfolios", http.StatusInternalServerError)
return
}
fmt.Printf("Portfolio created successfully: ID=%d, Name=%s, User=%s\n", portfolio.ID, portfolio.Name, username)
// Redirect to the new portfolio
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolio.ID), http.StatusSeeOther)
}
func PortfolioTransactionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in PortfolioTransactionHandler: %v\n", err)
}
if !auth {
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
return
}
if username == "" {
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
return
}
// Get form values
portfolioIDStr := r.FormValue("portfolio_id")
transactionType := r.FormValue("type")
stock := r.FormValue("stock")
amountStr := r.FormValue("amount")
priceStr := r.FormValue("price")
dateStr := r.FormValue("date")
note := r.FormValue("note")
// Basic validation
if transactionType == "" || stock == "" || amountStr == "" || priceStr == "" || dateStr == "" {
http.Error(w, "Alle Pflichtfelder müssen ausgefüllt werden", http.StatusBadRequest)
return
}
// Parse form values
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
http.Error(w, "Ungültiger Betrag", http.StatusBadRequest)
return
}
price, err := strconv.ParseFloat(priceStr, 64)
if err != nil {
http.Error(w, "Ungültiger Preis", http.StatusBadRequest)
return
}
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
http.Error(w, "Ungültiges Datum", http.StatusBadRequest)
return
}
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
return
}
// Verify portfolio belongs to user
var portfolio model.Portfolio
if err := DB.Joins("User").Where("portfolios.id = ? AND User.username = ?", portfolioID, username).First(&portfolio).Error; err != nil {
http.Error(w, "Portfolio nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
return
}
// Fetch stock currency from Yahoo Finance
stockCurrency, err := getStockCurrency(stock)
if err != nil {
fmt.Printf("Warning: Could not fetch currency for stock %s: %v. Using portfolio base currency.\n", stock, err)
stockCurrency = portfolio.BaseCurrency
}
fmt.Printf("Stock %s currency: %s\n", stock, stockCurrency)
// Create activity
activity := model.Activity{
PortfolioID: uint(portfolioID),
Stock: stock,
Type: model.ActivityType(transactionType),
Amount: amount,
Price: price,
Currency: stockCurrency, // Use the fetched stock currency
Date: date,
Note: note,
}
if err := DB.Create(&activity).Error; err != nil {
fmt.Printf("Error creating activity: %v\n", err)
http.Error(w, "Fehler beim Speichern der Transaktion", http.StatusInternalServerError)
return
}
fmt.Printf("Activity created successfully: ID=%d, Type=%s, Stock=%s, Currency=%s, Portfolio=%d\n",
activity.ID, activity.Type, activity.Stock, activity.Currency, activity.PortfolioID)
// Redirect to portfolio page
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
}
func EditTransactionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in EditTransactionHandler: %v\n", err)
}
if !auth {
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
return
}
if username == "" {
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
return
}
// Get form values
activityIDStr := r.FormValue("activity_id")
portfolioIDStr := r.FormValue("portfolio_id")
transactionType := r.FormValue("type")
stock := r.FormValue("stock")
amountStr := r.FormValue("amount")
priceStr := r.FormValue("price")
dateStr := r.FormValue("date")
note := r.FormValue("note")
// Basic validation
if activityIDStr == "" || transactionType == "" || stock == "" || amountStr == "" || priceStr == "" || dateStr == "" {
http.Error(w, "Alle Pflichtfelder müssen ausgefüllt werden", http.StatusBadRequest)
return
}
// Parse form values
activityID, err := strconv.ParseUint(activityIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Aktivitäts-ID", http.StatusBadRequest)
return
}
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
return
}
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
http.Error(w, "Ungültiger Betrag", http.StatusBadRequest)
return
}
price, err := strconv.ParseFloat(priceStr, 64)
if err != nil {
http.Error(w, "Ungültiger Preis", http.StatusBadRequest)
return
}
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
http.Error(w, "Ungültiges Datum", http.StatusBadRequest)
return
}
// Get existing activity and verify ownership using subquery
var activity model.Activity
if err := DB.Where("id = ? AND portfolio_id IN (SELECT id FROM portfolios WHERE user_id = (SELECT id FROM users WHERE username = ?))",
activityID, username).First(&activity).Error; err != nil {
http.Error(w, "Aktivität nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
return
}
// Verify the activity belongs to the specified portfolio
if activity.PortfolioID != uint(portfolioID) {
http.Error(w, "Aktivität gehört nicht zu diesem Portfolio", http.StatusBadRequest)
return
}
// Get portfolio to access base currency
var portfolio model.Portfolio
if err := DB.Where("id = ?", portfolioID).First(&portfolio).Error; err != nil {
http.Error(w, "Portfolio nicht gefunden", http.StatusBadRequest)
return
}
// Fetch stock currency from Yahoo Finance (or use portfolio base currency as fallback)
stockCurrency, err := getStockCurrency(stock)
if err != nil {
fmt.Printf("Warning: Could not fetch currency for stock %s: %v. Using portfolio base currency.\n", stock, err)
stockCurrency = portfolio.BaseCurrency
}
// Update activity fields
activity.Type = model.ActivityType(transactionType)
activity.Stock = stock
activity.Amount = amount
activity.Price = price
activity.Currency = stockCurrency
activity.Date = date
activity.Note = note
// Save updated activity
if err := DB.Save(&activity).Error; err != nil {
fmt.Printf("Error updating activity: %v\n", err)
http.Error(w, "Fehler beim Aktualisieren der Transaktion", http.StatusInternalServerError)
return
}
fmt.Printf("Activity updated successfully: ID=%d, Type=%s, Stock=%s, Portfolio=%d\n",
activity.ID, activity.Type, activity.Stock, activity.PortfolioID)
// Redirect to portfolio page
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
}
func DeleteTransactionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
auth, username, err := getSessionInfo(r)
if err != nil {
fmt.Printf("Session error in DeleteTransactionHandler: %v\n", err)
}
if !auth {
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
return
}
if username == "" {
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
return
}
// Get form values
activityIDStr := r.FormValue("activity_id")
portfolioIDStr := r.FormValue("portfolio_id")
// Basic validation
if activityIDStr == "" || portfolioIDStr == "" {
http.Error(w, "Aktivitäts-ID und Portfolio-ID sind erforderlich", http.StatusBadRequest)
return
}
// Parse form values
activityID, err := strconv.ParseUint(activityIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Aktivitäts-ID", http.StatusBadRequest)
return
}
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
if err != nil {
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
return
}
// Get existing activity and verify ownership using subquery
var activity model.Activity
if err := DB.Where("id = ? AND portfolio_id IN (SELECT id FROM portfolios WHERE user_id = (SELECT id FROM users WHERE username = ?))",
activityID, username).First(&activity).Error; err != nil {
http.Error(w, "Aktivität nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
return
}
// Verify the activity belongs to the specified portfolio
if activity.PortfolioID != uint(portfolioID) {
http.Error(w, "Aktivität gehört nicht zu diesem Portfolio", http.StatusBadRequest)
return
}
// Delete the activity
if err := DB.Delete(&activity).Error; err != nil {
fmt.Printf("Error deleting activity: %v\n", err)
http.Error(w, "Fehler beim Löschen der Transaktion", http.StatusInternalServerError)
return
}
fmt.Printf("Activity deleted successfully: ID=%d, Type=%s, Stock=%s, Portfolio=%d\n",
activity.ID, activity.Type, activity.Stock, activity.PortfolioID)
// Redirect to portfolio page
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
}