374 lines
11 KiB
Go
374 lines
11 KiB
Go
package srv
|
|
|
|
import (
|
|
tu "chat/tu"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
)
|
|
|
|
func generateName() string {
|
|
adjectives := []string{"Unrelenting", "Mystical", "Radiant", "Curious", "Peaceful", "Ancient", "Wandering", "Silent", "Celestial", "Dancing", "Eternal", "Resolute", "Whispering", "Serene", "Wild"}
|
|
colors := []string{"Purple", "Azure", "Crimson", "Golden", "Emerald", "Sapphire", "Obsidian", "Silver", "Amber", "Jade", "Indigo", "Violet", "Cerulean", "Copper", "Pearl"}
|
|
nouns := []string{"Elephant", "Phoenix", "Dragon", "Warrior", "Spirit", "Tiger", "Raven", "Mountain", "River", "Storm", "Falcon", "Wolf", "Ocean", "Star", "Moon"}
|
|
|
|
return fmt.Sprintf("%s-%s-%s",
|
|
adjectives[rand.Intn(len(adjectives))],
|
|
colors[rand.Intn(len(colors))],
|
|
nouns[rand.Intn(len(nouns))])
|
|
}
|
|
|
|
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
|
|
clientIP := getClientIP(r)
|
|
s.updateActivity(clientIP)
|
|
s.cleanupActivity()
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut {
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
clientIP := getClientIP(r)
|
|
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req.Username) > s.Config.Options.NameMaxLength {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), http.StatusRequestEntityTooLarge)
|
|
return
|
|
}
|
|
|
|
if !validUsername(req.Username) {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if s.Database.UserNameExists(req.Username) {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if username, ok := s.LoggedIn[clientIP]; ok {
|
|
s.LogUserOut(username)
|
|
s.Database.UserNameChange(username, req.Username)
|
|
s.LogUserIn(clientIP, req.Username)
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "Username changed"})
|
|
} else {
|
|
http.Error(w, `{"error": "Failure to change username"}`, http.StatusUnauthorized)
|
|
}
|
|
|
|
}
|
|
|
|
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodPatch:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var req struct {
|
|
MessageId string `json:"messageId"`
|
|
MessageContent string `json:"messageContent"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
clientIP := getClientIP(r)
|
|
if affected, err := s.Database.MessageEditIfOwner(req.MessageId, req.MessageContent, clientIP); err != nil {
|
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
|
return
|
|
} else if affected == 0 {
|
|
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "Message edited successfully",
|
|
})
|
|
|
|
case http.MethodGet:
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
var body string
|
|
messages := s.Database.MessagesGet()
|
|
for _, msg := range messages {
|
|
clientIP := getClientIP(r)
|
|
username, ok := s.LoggedIn[clientIP]
|
|
timeZone := "UTC"
|
|
if ok {
|
|
timeZone = s.Database.UserGetTimezone(username)
|
|
}
|
|
timeLocal := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone)
|
|
edited := ""
|
|
if msg.Edited {
|
|
edited = "(edited)"
|
|
}
|
|
body += fmt.Sprintf(`<p>%s<br><span class="username">%s</span><br><span class="timestamp">%s %s</span><br><span class="message">%s</span><br></p>`,
|
|
msg.Id, msg.SenderUsername, timeLocal, edited, msg.Content)
|
|
}
|
|
|
|
w.Write([]byte(getMessageTemplate(body)))
|
|
case http.MethodPut:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var msg struct {
|
|
Username string `json:"username"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
clientIP := getClientIP(r)
|
|
if username, ok := s.LoggedIn[clientIP]; ok {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.Database.MessageAdd(username, msg.Message)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "Message received",
|
|
"from": username,
|
|
})
|
|
} else {
|
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
|
|
}
|
|
|
|
case http.MethodDelete:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var req struct {
|
|
MessageId string `json:"messageId"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
clientIP := getClientIP(r)
|
|
if affected, err := s.Database.MessageDeleteIfOwner(req.MessageId, clientIP); err != nil {
|
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
|
return
|
|
} else if affected == 0 {
|
|
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "Message deleted",
|
|
})
|
|
default:
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
|
|
// if r.Method != http.MethodGet {
|
|
// http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
// return
|
|
// }
|
|
clientIP := getClientIP(r)
|
|
username, ok := s.LoggedIn[clientIP]
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"hasUsername": ok,
|
|
"username": username,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
clientIP := getClientIP(r)
|
|
s.updateActivity(clientIP)
|
|
s.cleanupActivity()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var users []string
|
|
for ip := range s.Connected {
|
|
// for all connected, get their usernames
|
|
if username, ok := s.LoggedIn[ip]; ok {
|
|
users = append(users, username)
|
|
}
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"users": users,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
clientIP := getClientIP(r)
|
|
|
|
var req struct {
|
|
Timezone string `json:"timezone"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if username, ok := s.LoggedIn[clientIP]; ok {
|
|
s.Database.UserTimezoneSet(username, req.Timezone)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "success",
|
|
})
|
|
} else {
|
|
http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
file := readFile(s.Config.Paths.IndexHtmlPath)
|
|
w.Write(file)
|
|
}
|
|
|
|
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
|
|
_ = r
|
|
w.Header().Set("Content-Type", "application/javascript")
|
|
file := readFile(s.Config.Paths.IndexJsPath)
|
|
w.Write(file)
|
|
}
|
|
|
|
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
|
|
_ = r
|
|
w.Header().Set("Content-Type", "text/css")
|
|
file := readFile(s.Config.Paths.IndexCssPath)
|
|
w.Write(file)
|
|
}
|
|
|
|
func (s *Server) handleMessagesLength(w http.ResponseWriter, r *http.Request) {
|
|
// should return the number of messages in the database
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
messages := s.Database.MessagesGet()
|
|
json.NewEncoder(w).Encode(map[string]int{
|
|
"length": len(messages),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
w.Header().Set("Content-Type", "text/html")
|
|
file := readFile(s.Config.Paths.LoginHtmlPath)
|
|
w.Write(file)
|
|
return
|
|
case http.MethodPost:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
validLogin := s.Database.UserPasswordCheck(req.Username, req.Password)
|
|
if !validLogin {
|
|
http.Error(w, `{"error": "Invalid username or password"}`, http.StatusUnauthorized)
|
|
return
|
|
} else {
|
|
clientIP := getClientIP(r)
|
|
s.LogUserIn(clientIP, req.Username)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "Logged in",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
w.Header().Set("Content-Type", "text/html")
|
|
file := readFile(s.Config.Paths.SignupHtmlPath)
|
|
w.Write(file)
|
|
return
|
|
case http.MethodPost:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// validate username length
|
|
if len(req.Username) > s.Config.Options.NameMaxLength {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), http.StatusRequestEntityTooLarge)
|
|
return
|
|
}
|
|
|
|
// validate username
|
|
if !validUsername(req.Username) {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// validate user doesnt already exist
|
|
if s.Database.UserNameExists(req.Username) {
|
|
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
// add user to database with hashedpassword
|
|
err := s.Database.UserAddWithPassword(req.Username, req.Password)
|
|
if err != nil {
|
|
fmt.Println("Database error while signing up a new user")
|
|
http.Error(w, fmt.Sprintf(`{"error": "%v"}`, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// log user in
|
|
clientIP := getClientIP(r)
|
|
s.LogUserIn(clientIP, req.Username)
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "User created",
|
|
})
|
|
}
|
|
}
|