All checks were successful
Tests / unit-tests (push) Successful in 25s
- 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>
222 lines
6.2 KiB
Go
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,
|
|
})
|
|
}
|