package main import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "flag" "fmt" "log" "net/http" "os" "strconv" "sync" "time" "github.com/joho/godotenv" "github.com/pquerna/otp/totp" ) type Config struct { Job string `json:"job"` Emails []string `json:"emails"` NewsSent int `json:"newletter-sent"` } func loadConfig() (*Config, error) { data, err := os.ReadFile("data.json") if err != nil { return nil, err } var c Config return &c, json.Unmarshal(data, &c) } func saveConfig(c *Config) error { data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile("data.json", data, 0644) } func validateEnv() { // checking if env file is there in the directory: err := godotenv.Load(".env") if err != nil { log.Fatal("Error loading .env file") } // checking secret for rooling 2fa secret := os.Getenv("secret") if secret == "" { fmt.Println("set secret first") os.Exit(1) } } func main() { validateEnv() port := flag.String("port", "8081", "port to listen on") flag.Parse() mux := http.NewServeMux() fs := http.FileServer(http.Dir("static")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/index.html") }) mux.HandleFunc("GET /infra", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/infra.html") }) mux.HandleFunc("GET /finance", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") }) mux.HandleFunc("GET /cinema", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") }) mux.HandleFunc("GET /engine", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") }) mux.HandleFunc("GET /42", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/42.html") }) mux.HandleFunc("GET /secret", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/secret.html") }) mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/icon.png") }) mux.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err == nil { sessionMu.RLock() expiry, ok := sessionStore[cookie.Value] sessionMu.RUnlock() // had session, but invalid if !ok || time.Now().After(expiry) { sessionMu.Lock() delete(sessionStore, cookie.Value) sessionMu.Unlock() } else { // valid session http.Redirect(w, r, "/admin", http.StatusSeeOther) } } http.ServeFile(w, r, "./static/login.html") }) mux.HandleFunc("GET /admin", requireAuth(func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/admin.html") })) mux.HandleFunc("GET /logout", requireAuth(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err == nil { // delete session cookie token sessionMu.Lock() delete(sessionStore, cookie.Value) sessionMu.Unlock() } http.Redirect(w, r, "/", http.StatusSeeOther) })) mux.HandleFunc("POST /newsletter/subscribe", func(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` } err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } double := false conf, err := loadConfig() for _, e := range conf.Emails { if e == req.Email { double = true } } if !double { conf.Emails = append(conf.Emails, req.Email) } saveConfig(conf) }) mux.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) { config, err := loadConfig() if err != nil { http.Error(w, "failed to load config", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html") //samantha42.xyz if config.Job == "" || config.Job == "none" { fmt.Fprint(w, `available for work`) } else { fmt.Fprintf(w, `working at: %s`, config.Job) } }) mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() password := r.Form.Get("password") auth := r.Form.Get("totp") // cheking password h := sha256.New() h.Write([]byte(password)) b := hex.EncodeToString(h.Sum(nil)) if string(b) == os.Getenv("password_sha") && totp.Validate(auth, os.Getenv("secret")) { fmt.Println("logged in") token := generateSession() sessionMu.Lock() sessionStore[token] = time.Now().Add(12 * time.Hour) sessionMu.Unlock() http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(48 * time.Hour), }) // htmx reads this header and does the redirect client-side w.Header().Set("HX-Redirect", "/admin") w.WriteHeader(http.StatusOK) } else { // failure w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `
`) } }) //http.HandleFunc("/portfolio", Portfolio) mux.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound) }) // --- /admin/ sub-mux --- adminMux := http.NewServeMux() adminMux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/admin.html") }) // strip the /admin prefix before handing off mux.Handle("/admin/", http.StripPrefix("/admin", adminMux)) // --- /admin/api/ sub-mux --- apiMux := http.NewServeMux() apiMux.HandleFunc("GET /stats/subscribers", requireAuth(func(w http.ResponseWriter, r *http.Request) { conf, err := loadConfig() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } fmt.Fprintf(w, "%s", strconv.Itoa(len(conf.Emails))) })) apiMux.HandleFunc("GET /stats/sent", requireAuth(func(w http.ResponseWriter, r *http.Request) { conf, err := loadConfig() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } fmt.Fprintf(w, "%s", strconv.Itoa(conf.NewsSent)) })) // strip /admin/api before handing off adminMux.Handle("/api/", http.StripPrefix("/api", apiMux)) fmt.Printf("running on http://localhost:%s/\n", *port) if err := http.ListenAndServe(":"+*port, mux); err != nil { log.Fatal(err) } } var sessionStore = map[string]time.Time{} var sessionMu sync.RWMutex func generateSession() string { b := make([]byte, 32) rand.Read(b) return hex.EncodeToString(b) } func requireAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } sessionMu.RLock() expiry, ok := sessionStore[cookie.Value] sessionMu.RUnlock() if !ok || time.Now().After(expiry) { sessionMu.Lock() delete(sessionStore, cookie.Value) sessionMu.Unlock() http.Redirect(w, r, "/login", http.StatusSeeOther) return } next(w, r) } }