This commit is contained in:
2025-12-09 16:33:44 +08:00
parent 345af5e2a3
commit 1d42f5ea50
49 changed files with 12015 additions and 1 deletions

479
user.go Executable file
View File

@@ -0,0 +1,479 @@
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
}