package website import ( "Portifolio/internal/middleware" "Portifolio/internal/model" "Portifolio/internal/service" "crypto/rand" _ "embed" "encoding/base64" "fmt" "net/http" "os" "strconv" "time" "github.com/FuLygon/go-totp/v2" _ "github.com/mattn/go-sqlite3" ) //go:embed static/index.html var indexHTML []byte func Getsite() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(indexHTML) } } //go:embed static/login.html var loginxHTML []byte func GetAdminLogin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(loginxHTML) } } type contextKey string const ContextKeyRequestTime contextKey = "requestTime" func LoginHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // checked once when the handler is registered envUsername := os.Getenv("username") envPassword := os.Getenv("password") envTOTPSecret := os.Getenv("AUTH_TOTP_SECRET") if envUsername == "" || envPassword == "" || envTOTPSecret == "" { http.Error(w, "env var missing", http.StatusInternalServerError) } ts, ok := r.Context().Value(ContextKeyRequestTime).(time.Time) if !ok { ts = time.Now() } if err := r.ParseForm(); err != nil { http.Error(w, "invalid form data", http.StatusBadRequest) return } password := r.FormValue("password") username := r.FormValue("username") code := r.FormValue("auth_code") fmt.Println(username, password, code) if password == "" || username == "" || code == "" { http.Error(w, "form value is empty", http.StatusBadRequest) return } if password != envPassword || username != envUsername { http.Error(w, "username or password is not valid", http.StatusUnauthorized) return } v := totp.Validator{ Algorithm: totp.AlgorithmSHA1, Digits: 6, Period: 30, Secret: envTOTPSecret, } valid, err := v.ValidateWithTimestamp(code, ts.Unix()) if err != nil { http.Error(w, fmt.Sprintf("error validating TOTP code: %s", err), http.StatusUnauthorized) return } if !valid { http.Error(w, "invalid auth code", http.StatusUnauthorized) return } // In your login POST handler, after setting the cookie: token := generateSessionToken() middleware.RegisterSession(token) // <-- register before writing cookie http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: 86400, }) w.Header().Set("HX-Redirect", "/admin") w.WriteHeader(http.StatusOK) } } // Simple session token generator func generateSessionToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic(err) } return base64.URLEncoding.EncodeToString(b) } //go:embed static/admin.html var adminHTML []byte func GetAdmin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(adminHTML) } } //go:embed static/styles.css var styleCSSmain []byte func GetstylesheetMain() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Write(styleCSSmain) } } //go:embed static/login.css var styleCSSlogin []byte func GetstylesheetLogin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Write(styleCSSlogin) } } //go:embed static/admin.css var styleCSSadmin []byte func GetstylesheetAdmin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Write(styleCSSadmin) } } func HealthFragment(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h := svc.CheckHealth() status := h["status"].(string) uptime := h["uptime"].(string) dotClass := "ok" statusText := "backend ok" if status != "ok" { dotClass = "error" statusText = "backend " + status w.WriteHeader(http.StatusServiceUnavailable) // ← triggers htmx:responseError } dbInfo := h["database"].(map[string]any) dbStatus := dbInfo["status"].(string) dbLatency := dbInfo["latency"].(string) dbText := "db " + dbStatus if dbLatency != "" { dbText += " · " + dbLatency } fmt.Fprintf(w, ` %s · up %s · %s `, dotClass, statusText, uptime, dbText) } } // GET /positions/fragment — returns rows for positions tbody func PositionsFragment(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { positions, err := svc.GetPositions() if err != nil { fmt.Fprintf(w, `load failed: %s`, err) return } if len(positions) == 0 { fmt.Fprint(w, `no positions`) return } var total float64 for _, p := range positions { total += p.CostBasis } for _, p := range positions { weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter if total > 0 { weight = p.CostBasis / total * 100 } fmt.Fprintf(w, ` %s %s %s %s
%.1f%%
`, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight) } } } // GET /positions/fragment — returns rows for positions tbody func CompanyFragment(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { companies, err := svc.GetAllCompanies() if err != nil { fmt.Fprintf(w, `load failed: %s`, err) return } if len(companies) == 0 { fmt.Fprint(w, `no positions`) return } for _, c := range companies { fmt.Fprintf(w, ` %s %s %.2f %d %.2f `, c.Symbol, c.CurrencyCode, c.Price, c.SharesOutstanding, c.Price*float64(c.SharesOutstanding)) } } } // GET /positions/fragment — returns rows for positions tbody func SummaryFragment(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { positions, err := svc.GetPositions() if err != nil { fmt.Fprintf(w, `load failed: %s`, err) return } if len(positions) == 0 { fmt.Fprint(w, `no positions`) return } var total float64 for _, p := range positions { total += p.CostBasis } for _, p := range positions { weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter if total > 0 { weight = p.CostBasis / total * 100 } fmt.Fprintf(w, ` %s %s %s %s
%.1f%%
`, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight) } } } func fmtNum(v float64) string { if v == 0 { return "—" } return strconv.FormatFloat(v, 'f', 2, 64) } func fmtInt(v int) string { return strconv.Itoa(v) } var productLabels = map[model.TradeProduct]string{ model.StockTrade: "Stock", model.OptionCallTrade: "Call Option", model.OptionPutTrade: "Put Option", model.CurrencyTrade: "Currency", model.BondTrade: "Bond", } func TradeFragment(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { trades, err := svc.GetTrades() if err != nil { fmt.Fprintf(w, `load failed: %s`, err) return } if len(trades) == 0 { fmt.Fprint(w, `no trades`) return } for _, t := range trades { dir, cls := "SELL", "dir-sell" if t.Type == model.BuyType { dir, cls = "BUY", "dir-buy" } label := productLabels[t.Product] fmt.Fprintf(w, ` %s %s %s %s %s %s %s `, t.Symbol, label, t.CurrencyCode, t.Date.Format("2006-01-02"), cls, dir, fmtNum(float64(t.Shares)), fmtNum(t.Price)) } } } func TradeCount(svc *service.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { trades, err := svc.GetTrades() if err != nil { fmt.Fprint(w, "—") return } count := len(trades) if count == 1 { fmt.Fprint(w, "1 trade") } else { fmt.Fprintf(w, "%d trades", count) } } }