diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cd9504 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module Parley + +go 1.25.0 + +require modernc.org/sqlite v1.53.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.44.0 // indirect + modernc.org/gc/v3 v3.1.3 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/strutil v1.2.1 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..1e3a1cb --- /dev/null +++ b/main.go @@ -0,0 +1,384 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + _ "modernc.org/sqlite" +) + +var ( + db *sql.DB + basePath string +) + +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Active bool `json:"active"` + IsAdmin bool `json:"is_admin"` +} + +type Message struct { + ID int `json:"id"` + FromID int `json:"from_id"` + ToID int `json:"to_id"` + Text string `json:"text"` + Username string `json:"username"` + Timestamp int64 `json:"timestamp"` +} + +func main() { + // Получаем базовый путь из переменной окружения + basePath = os.Getenv("BASE_PATH") + if basePath == "" { + basePath = "/" + } + // Убираем слеш в конце если есть + basePath = strings.TrimSuffix(basePath, "/") + if basePath == "" { + basePath = "/" + } + + var err error + db, err = sql.Open("sqlite", "./chat.db") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + initDB() + + // Роуты с учетом базового пути + http.HandleFunc(basePath+"/", serveIndex) + http.HandleFunc(basePath+"/api/register", handleRegister) + http.HandleFunc(basePath+"/api/login", handleLogin) + http.HandleFunc(basePath+"/api/users", handleGetUsers) + http.HandleFunc(basePath+"/api/messages", handleMessages) + http.HandleFunc(basePath+"/api/admin/users", handleAdminGetUsers) + http.HandleFunc(basePath+"/api/admin/toggle", handleAdminToggle) + + // Статические файлы + fs := http.FileServer(http.Dir("static")) + http.Handle(basePath+"/static/", http.StripPrefix(basePath+"/static/", fs)) + + fmt.Printf("Чат запущен на :3100 (базовый путь: %s)\n", basePath) + log.Fatal(http.ListenAndServe(":3100", nil)) +} + +func initDB() { + queries := []string{ + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + active INTEGER DEFAULT 0, + is_admin INTEGER DEFAULT 0 + )`, + `CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_id INTEGER NOT NULL, + to_id INTEGER NOT NULL, + text TEXT NOT NULL, + timestamp INTEGER NOT NULL + )`, + `INSERT OR IGNORE INTO users (username, password, active, is_admin) +-- // ПОМЕНЯТЬ НА СВОИ + VALUES ('admin', 'admin123', 1, 1)`, + } + + for _, q := range queries { + if _, err := db.Exec(q); err != nil { + log.Printf("Error executing query: %v", err) + } + } +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + // Перенаправляем /parley на /parley/ + if r.URL.Path == basePath && basePath != "/" { + http.Redirect(w, r, basePath+"/", http.StatusMovedPermanently) + return + } + + tmpl, err := template.ParseFiles("static/index.html") + if err != nil { + http.Error(w, "Template error", 500) + return + } + + data := struct { + BasePath string + }{ + BasePath: basePath, + } + + tmpl.Execute(w, data) +} + +func handleRegister(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", 405) + return + } + + var user User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + jsonError(w, "Invalid request") + return + } + + if user.Username == "" || user.Password == "" { + jsonError(w, "Username and password required") + return + } + + _, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", + user.Username, user.Password) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + jsonError(w, "Username already exists") + } else { + jsonError(w, "Registration failed") + } + return + } + + jsonResponse(w, map[string]interface{}{"message": "Registration successful. Wait for admin activation."}) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", 405) + return + } + + var user User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + jsonError(w, "Invalid request") + return + } + + var dbUser User + var activeInt, adminInt int + err := db.QueryRow("SELECT id, username, active, is_admin FROM users WHERE username = ? AND password = ?", + user.Username, user.Password).Scan(&dbUser.ID, &dbUser.Username, &activeInt, &adminInt) + + if err == sql.ErrNoRows { + jsonError(w, "Invalid credentials") + return + } + if err != nil { + jsonError(w, "Database error") + return + } + + dbUser.Active = activeInt == 1 + dbUser.IsAdmin = adminInt == 1 + + if !dbUser.Active { + jsonError(w, "Account not activated. Contact admin.") + return + } + + jsonResponse(w, map[string]interface{}{ + "user_id": dbUser.ID, + "username": dbUser.Username, + "is_admin": dbUser.IsAdmin, + }) +} + +func handleGetUsers(w http.ResponseWriter, r *http.Request) { + userID, err := strconv.Atoi(r.URL.Query().Get("user_id")) + if err != nil { + jsonError(w, "Invalid user_id") + return + } + + // Проверяем, активен ли пользователь + var activeInt int + err = db.QueryRow("SELECT active FROM users WHERE id = ?", userID).Scan(&activeInt) + if err != nil || activeInt != 1 { + jsonError(w, "User not active") + return + } + + rows, err := db.Query("SELECT id, username FROM users WHERE active = 1 AND id != ?", userID) + if err != nil { + jsonError(w, "Database error") + return + } + defer rows.Close() + + var users []map[string]interface{} + for rows.Next() { + var id int + var username string + rows.Scan(&id, &username) + users = append(users, map[string]interface{}{ + "id": id, + "username": username, + }) + } + + jsonResponse(w, users) +} + +func handleMessages(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + getMessages(w, r) + case "POST": + sendMessage(w, r) + default: + http.Error(w, "Method not allowed", 405) + } +} + +func getMessages(w http.ResponseWriter, r *http.Request) { + userID, _ := strconv.Atoi(r.URL.Query().Get("user_id")) + partnerID, _ := strconv.Atoi(r.URL.Query().Get("partner_id")) + + rows, err := db.Query(` + SELECT m.id, m.from_id, m.to_id, m.text, m.timestamp, u.username + FROM messages m + JOIN users u ON m.from_id = u.id + WHERE (m.from_id = ? AND m.to_id = ?) OR (m.from_id = ? AND m.to_id = ?) + ORDER BY m.timestamp ASC + LIMIT 100`, userID, partnerID, partnerID, userID) + + if err != nil { + jsonError(w, "Database error") + return + } + defer rows.Close() + + var messages []Message + for rows.Next() { + var msg Message + rows.Scan(&msg.ID, &msg.FromID, &msg.ToID, &msg.Text, &msg.Timestamp, &msg.Username) + messages = append(messages, msg) + } + + if messages == nil { + messages = []Message{} + } + + jsonResponse(w, messages) +} + +func sendMessage(w http.ResponseWriter, r *http.Request) { + var msg Message + if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { + jsonError(w, "Invalid request") + return + } + + msg.Timestamp = time.Now().Unix() + + result, err := db.Exec("INSERT INTO messages (from_id, to_id, text, timestamp) VALUES (?, ?, ?, ?)", + msg.FromID, msg.ToID, msg.Text, msg.Timestamp) + if err != nil { + jsonError(w, "Failed to send message") + return + } + + id, _ := result.LastInsertId() + msg.ID = int(id) + + // Получаем username отправителя + db.QueryRow("SELECT username FROM users WHERE id = ?", msg.FromID).Scan(&msg.Username) + + jsonResponse(w, msg) +} + +func handleAdminGetUsers(w http.ResponseWriter, r *http.Request) { + adminID, err := strconv.Atoi(r.URL.Query().Get("admin_id")) + if err != nil { + jsonError(w, "Invalid admin_id") + return + } + + // Проверяем, что запрос от админа + var isAdmin int + db.QueryRow("SELECT is_admin FROM users WHERE id = ?", adminID).Scan(&isAdmin) + if isAdmin != 1 { + jsonError(w, "Not admin") + return + } + + rows, err := db.Query("SELECT id, username, active FROM users") + if err != nil { + jsonError(w, "Database error") + return + } + defer rows.Close() + + var users []map[string]interface{} + for rows.Next() { + var id, active int + var username string + rows.Scan(&id, &username, &active) + users = append(users, map[string]interface{}{ + "id": id, + "username": username, + "active": active == 1, + }) + } + + jsonResponse(w, users) +} + +func handleAdminToggle(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", 405) + return + } + + var req struct { + AdminID int `json:"admin_id"` + UserID int `json:"user_id"` + Active bool `json:"active"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, "Invalid request") + return + } + + // Проверяем, что запрос от админа + var isAdmin int + db.QueryRow("SELECT is_admin FROM users WHERE id = ?", req.AdminID).Scan(&isAdmin) + if isAdmin != 1 { + jsonError(w, "Not admin") + return + } + + activeInt := 0 + if req.Active { + activeInt = 1 + } + + db.Exec("UPDATE users SET active = ? WHERE id = ?", activeInt, req.UserID) + jsonResponse(w, map[string]string{"message": "User status updated"}) +} + +func jsonResponse(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +} + +func jsonError(w http.ResponseWriter, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": message}) +}