Files
MeowMusicServer/user.go
2025-12-09 16:33:44 +08:00

480 lines
12 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
"golang.org/x/crypto/bcrypt"
)
// UserStore manages user data
type UserStore struct {
mu sync.RWMutex
Users map[string]*User // key: user ID
Usernames map[string]string // key: username, value: user ID
Emails map[string]string // key: email, value: user ID
Sessions map[string]string // key: session token, value: user ID
filePath string
}
var userStore *UserStore
// InitUserStore initializes the user store
func InitUserStore() {
userStore = &UserStore{
Users: make(map[string]*User),
Usernames: make(map[string]string),
Emails: make(map[string]string),
Sessions: make(map[string]string),
filePath: "./files/users.json",
}
// Create files directory if it doesn't exist
os.MkdirAll("./files", 0755)
// Load existing users
userStore.loadFromFile()
}
// loadFromFile loads users from JSON file
func (us *UserStore) loadFromFile() error {
us.mu.Lock()
defer us.mu.Unlock()
data, err := os.ReadFile(us.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil // File doesn't exist yet, that's ok
}
return err
}
var storageUsers []*userStorage
if err := json.Unmarshal(data, &storageUsers); err != nil {
return err
}
// Convert from storage format to User struct and rebuild indexes
for _, su := range storageUsers {
user := &User{
ID: su.ID,
Username: su.Username,
Email: su.Email,
Password: su.Password, // Restore password
CreatedAt: su.CreatedAt,
UpdatedAt: su.UpdatedAt,
}
us.Users[user.ID] = user
us.Usernames[strings.ToLower(user.Username)] = user.ID
us.Emails[strings.ToLower(user.Email)] = user.ID
}
return nil
}
// userStorage is used for saving/loading users with passwords
type userStorage struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"` // Include password for storage
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// saveToFile saves users to JSON file
// NOTE: Caller must hold the lock!
func (us *UserStore) saveToFile() error {
// Convert map to slice of storage struct (with passwords)
users := make([]*userStorage, 0, len(us.Users))
for _, user := range us.Users {
users = append(users, &userStorage{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Password: user.Password,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
})
}
data, err := json.MarshalIndent(users, "", " ")
if err != nil {
return err
}
return os.WriteFile(us.filePath, data, 0644)
}
// generateID generates a random ID
func generateID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// generateToken generates a session token
func generateToken() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// CreateUser creates a new user
func (us *UserStore) CreateUser(username, email, password string) (*User, error) {
// Hash password BEFORE acquiring lock (this is slow!)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
us.mu.Lock()
defer us.mu.Unlock()
// Check if username already exists
if _, exists := us.Usernames[strings.ToLower(username)]; exists {
return nil, fmt.Errorf("username already exists")
}
// Check if email already exists
if _, exists := us.Emails[strings.ToLower(email)]; exists {
return nil, fmt.Errorf("email already exists")
}
// Create user
user := &User{
ID: generateID(),
Username: username,
Email: email,
Password: string(hashedPassword),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Store user
us.Users[user.ID] = user
us.Usernames[strings.ToLower(username)] = user.ID
us.Emails[strings.ToLower(email)] = user.ID
// Save to file
if err := us.saveToFile(); err != nil {
return nil, err
}
return user, nil
}
// AuthenticateUser authenticates a user by username and password
func (us *UserStore) AuthenticateUser(username, password string) (*User, string, error) {
us.mu.RLock()
userID, exists := us.Usernames[strings.ToLower(username)]
us.mu.RUnlock()
if !exists {
return nil, "", fmt.Errorf("invalid username or password")
}
us.mu.RLock()
user := us.Users[userID]
us.mu.RUnlock()
// Compare password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return nil, "", fmt.Errorf("invalid username or password")
}
// Generate session token
token := generateToken()
us.mu.Lock()
us.Sessions[token] = user.ID
us.mu.Unlock()
return user, token, nil
}
// GetUserByToken retrieves a user by session token
func (us *UserStore) GetUserByToken(token string) (*User, error) {
us.mu.RLock()
defer us.mu.RUnlock()
userID, exists := us.Sessions[token]
if !exists {
return nil, fmt.Errorf("invalid session token")
}
user, exists := us.Users[userID]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
// Logout removes a session token
func (us *UserStore) Logout(token string) {
us.mu.Lock()
defer us.mu.Unlock()
delete(us.Sessions, token)
}
// HTTP Handlers
// HandleRegister handles user registration
func HandleRegister(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[API] Register request received from %s\n", r.RemoteAddr)
if r.Method != http.MethodPost {
fmt.Printf("[API] Register: Method not allowed: %s\n", r.Method)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
fmt.Printf("[API] Reading request body...\n")
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Printf("[API] Error reading body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
fmt.Printf("[API] Body received: %s\n", string(body))
var req RegisterRequest
if err := json.Unmarshal(body, &req); err != nil {
fmt.Printf("[API] Error parsing JSON: %v\n", err)
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Printf("[API] Parsed: username=%s, email=%s\n", req.Username, req.Email)
// Validate input
if req.Username == "" || req.Email == "" || req.Password == "" {
fmt.Printf("[API] Validation failed: missing fields\n")
http.Error(w, "All fields are required", http.StatusBadRequest)
return
}
if len(req.Password) < 6 {
fmt.Printf("[API] Validation failed: password too short\n")
http.Error(w, "Password must be at least 6 characters", http.StatusBadRequest)
return
}
// Create user
fmt.Printf("[API] Creating user...\n")
user, err := userStore.CreateUser(req.Username, req.Email, req.Password)
if err != nil {
fmt.Printf("[API] Error creating user: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("[API] User created successfully! ID=%s\n", user.ID)
// Generate session token
token := generateToken()
userStore.mu.Lock()
userStore.Sessions[token] = user.ID
userStore.mu.Unlock()
// Initialize user's favorite playlist
if playlistManager != nil {
playlistManager.InitializeUserPlaylists(user.ID)
}
fmt.Printf("[Info] User registered: %s\n", user.Username)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(AuthResponse{
Token: token,
User: *user,
})
}
// HandleLogin handles user login
func HandleLogin(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[API] Login request received from %s\n", r.RemoteAddr)
if r.Method != http.MethodPost {
fmt.Printf("[API] Login: Method not allowed: %s\n", r.Method)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
fmt.Printf("[API] Reading login body...\n")
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Printf("[API] Error reading body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
fmt.Printf("[API] Login body: %s\n", string(body))
var req LoginRequest
if err := json.Unmarshal(body, &req); err != nil {
fmt.Printf("[API] Error parsing JSON: %v\n", err)
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Printf("[API] Login username: %s\n", req.Username)
// Authenticate user
fmt.Printf("[API] Authenticating...\n")
user, token, err := userStore.AuthenticateUser(req.Username, req.Password)
fmt.Printf("[API] Authentication result: %v\n", err)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
fmt.Printf("[Info] User logged in: %s\n", user.Username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(AuthResponse{
Token: token,
User: *user,
})
}
// HandleLogout handles user logout
func HandleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Missing authorization token", http.StatusUnauthorized)
return
}
// Remove "Bearer " prefix if present
token = strings.TrimPrefix(token, "Bearer ")
userStore.Logout(token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"message": "Logged out successfully",
})
}
// HandleGetCurrentUser returns current user info
func HandleGetCurrentUser(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Missing authorization token", http.StatusUnauthorized)
return
}
// Remove "Bearer " prefix if present
token = strings.TrimPrefix(token, "Bearer ")
user, err := userStore.GetUserByToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// AuthMiddleware authenticates requests
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 1. 尝试从 Authorization Header 获取
token := r.Header.Get("Authorization")
if token != "" {
token = strings.TrimPrefix(token, "Bearer ")
user, err := userStore.GetUserByToken(token)
if err == nil {
r.Header.Set("X-User-ID", user.ID)
next(w, r)
return
}
}
// 2. 尝试从 X-Device-Token 获取 (ESP32设备)
deviceToken := r.Header.Get("X-Device-Token")
if deviceToken != "" {
// 这里需要调用DeviceManager但为了避免循环依赖如果user包独立我们使用全局函数或接口
// 假设GetDeviceManager()是全局可用的
dm := GetDeviceManager()
device, err := dm.VerifyToken(deviceToken)
if err == nil {
// 找到了设备,现在需要找到对应的用户
// Device结构体中有Username我们需要根据Username找到User对象
userID := userStore.GetUserIDByUsername(device.Username)
if userID != "" {
r.Header.Set("X-User-ID", userID)
// 更新设备最后在线时间
dm.UpdateLastSeen(device.MAC)
next(w, r)
return
}
}
}
// 3. 尝试从 Cookie 获取 (Web Session)
cookie, err := r.Cookie("session_token")
if err == nil {
username := userStore.GetUsernameByToken(cookie.Value)
if username != "" {
userID := userStore.GetUserIDByUsername(username)
if userID != "" {
r.Header.Set("X-User-ID", userID)
next(w, r)
return
}
}
}
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
}
}
// GetUserStore 返回UserStore单例
func GetUserStore() *UserStore {
return userStore
}
// GetUserIDByUsername returns user ID by username
func (us *UserStore) GetUserIDByUsername(username string) string {
us.mu.RLock()
defer us.mu.RUnlock()
return us.Usernames[username]
}
// GetUsernameByToken 通过session token获取用户名
func (us *UserStore) GetUsernameByToken(token string) string {
us.mu.RLock()
defer us.mu.RUnlock()
userID, exists := us.Sessions[token]
if !exists {
return ""
}
user, exists := us.Users[userID]
if !exists {
return ""
}
return user.Username
}