Some checks failed
Tests / unit-tests (push) Failing after 43s
- Dodat creack/pty za pseudo-terminal podršku - Claude CLI se pokreće u pravom PTY-ju (puni TUI, boje, Shift+Tab) - xterm.js u browseru renderuje terminal identično konzoli - WebSocket bridge: tastatura → PTY stdin, PTY stdout → terminal - Ring buffer (128KB) za replay pri reconnect-u - Automatski reconnect nakon 2 sekunde - PTY sesije žive nezavisno od browsera (60min idle timeout) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
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("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 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)
|
|
|
|
data := map[string]any{
|
|
"Project": project,
|
|
"ProjectDir": projectDir,
|
|
}
|
|
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,
|
|
})
|
|
}
|