package main import ( "encoding/json" "fmt" "log" "net/http" "path/filepath" ) var ( cfg *Config templates *TemplateRenderer sessionMgr *SessionManager ptyMgr *PTYSessionManager ) func main() { var err error cfg, err = LoadConfig("config.json") if err != nil { log.Fatalf("Config: %v", err) } templates, err = NewTemplateRenderer("templates") if err != nil { log.Fatalf("Templates: %v", err) } sessionMgr = NewSessionManager(cfg.SessionSecret) ptyMgr = NewPTYSessionManager() defer ptyMgr.Stop() wsHandler := NewTerminalHandler(ptyMgr) mux := http.NewServeMux() // Static files mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) // Public routes mux.HandleFunc("GET /login", handleLoginPage) mux.HandleFunc("POST /login", handleLogin) mux.HandleFunc("GET /logout", handleLogout) // Protected routes mux.Handle("GET /projects", AuthMiddleware(sessionMgr, http.HandlerFunc(handleProjects))) mux.Handle("POST /projects/create", AuthMiddleware(sessionMgr, http.HandlerFunc(handleCreateProject))) mux.Handle("GET /chat/{project}", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChat))) mux.Handle("GET /ws", AuthMiddleware(sessionMgr, wsHandler)) mux.Handle("GET /api/file", AuthMiddleware(sessionMgr, http.HandlerFunc(handleFileAPI))) mux.Handle("GET /change-password", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChangePasswordPage))) mux.Handle("POST /change-password", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChangePassword))) // Root redirect (exact match only) mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/projects", http.StatusSeeOther) }) addr := fmt.Sprintf(":%d", cfg.Port) log.Printf("Claude Web Chat pokrenut na http://0.0.0.0%s (mod: %s)", addr, cfg.Mode) log.Fatal(http.ListenAndServe(addr, mux)) } func handleLoginPage(w http.ResponseWriter, r *http.Request) { // If already logged in, redirect if cookie, err := r.Cookie(sessionCookieName); err == nil { if sessionMgr.Get(cookie.Value) != nil { http.Redirect(w, r, "/projects", http.StatusSeeOther) return } } templates.Render(w, "login.html", map[string]string{"Error": ""}) } func handleLogin(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") if username != cfg.Username || !cfg.CheckPassword(password) { w.WriteHeader(http.StatusUnauthorized) templates.Render(w, "login.html", map[string]string{"Error": "Pogrešno korisničko ime ili lozinka"}) return } sess := sessionMgr.Create(username) SetSessionCookie(w, sess) http.Redirect(w, r, "/projects", http.StatusSeeOther) } func handleLogout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(sessionCookieName); err == nil { sessionMgr.Delete(cookie.Value) } ClearSessionCookie(w) http.Redirect(w, r, "/login", http.StatusSeeOther) } func handleProjects(w http.ResponseWriter, r *http.Request) { projects, err := ListProjects(cfg.ProjectsPath) if err != nil { log.Printf("ListProjects error: %v", err) http.Error(w, "Greška pri čitanju projekata", http.StatusInternalServerError) return } data := map[string]any{ "Projects": projects, "ProjectsPath": cfg.ProjectsPath, } templates.Render(w, "projects.html", data) } func handleCreateProject(w http.ResponseWriter, r *http.Request) { name := r.FormValue("name") if err := CreateProject(cfg.ProjectsPath, name); err != nil { projects, _ := ListProjects(cfg.ProjectsPath) data := map[string]any{ "Projects": projects, "ProjectsPath": cfg.ProjectsPath, "Error": err.Error(), } w.WriteHeader(http.StatusBadRequest) templates.Render(w, "projects.html", data) return } http.Redirect(w, r, "/projects", http.StatusSeeOther) } func handleChat(w http.ResponseWriter, r *http.Request) { project := r.PathValue("project") if project == "" { http.Redirect(w, r, "/projects", http.StatusSeeOther) return } projectDir := filepath.Join(cfg.ProjectsPath, project) files, err := ListMarkdownFiles(projectDir) if err != nil { log.Printf("ListMarkdownFiles error: %v", err) files = nil } data := map[string]any{ "Project": project, "ProjectDir": projectDir, "Files": files, } templates.Render(w, "chat.html", data) } func handleChangePasswordPage(w http.ResponseWriter, r *http.Request) { templates.Render(w, "change-password.html", map[string]string{"Error": "", "Success": ""}) } func handleChangePassword(w http.ResponseWriter, r *http.Request) { currentPassword := r.FormValue("current_password") newPassword := r.FormValue("new_password") confirmPassword := r.FormValue("confirm_password") data := map[string]string{"Error": "", "Success": ""} if !cfg.CheckPassword(currentPassword) { data["Error"] = "Pogrešna trenutna lozinka" w.WriteHeader(http.StatusBadRequest) templates.Render(w, "change-password.html", data) return } if len(newPassword) < 6 { data["Error"] = "Nova lozinka mora imati najmanje 6 karaktera" w.WriteHeader(http.StatusBadRequest) templates.Render(w, "change-password.html", data) return } if newPassword != confirmPassword { data["Error"] = "Nova lozinka i potvrda se ne poklapaju" w.WriteHeader(http.StatusBadRequest) templates.Render(w, "change-password.html", data) return } if err := cfg.SetPassword(newPassword); err != nil { log.Printf("SetPassword error: %v", err) data["Error"] = "Greška pri čuvanju lozinke" w.WriteHeader(http.StatusInternalServerError) templates.Render(w, "change-password.html", data) return } data["Success"] = "Lozinka je uspešno promenjena" templates.Render(w, "change-password.html", data) } func handleFileAPI(w http.ResponseWriter, r *http.Request) { project := r.URL.Query().Get("project") relPath := r.URL.Query().Get("path") if project == "" || relPath == "" { http.Error(w, "missing params", http.StatusBadRequest) return } projectDir := filepath.Join(cfg.ProjectsPath, project) htmlContent, err := RenderMarkdownFile(projectDir, relPath) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "name": relPath, "html": htmlContent, }) }