claude-web-chat/main.go
djuka 6c0ca3a96f
All checks were successful
Tests / unit-tests (push) Successful in 25s
Dodato kreiranje projekta kroz UI i change-password template
- CreateProject() sa validacijom imena (regex, duplikati, prazno)
- POST /projects/create ruta sa AuthMiddleware
- Modal forma na projects stranici za unos imena
- 10 unit testova za CreateProject
- change-password.html template i testovi
- Ažuriran TESTING.md sa novom sekcijom

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:27:10 +00:00

222 lines
6.2 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("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,
})
}