rebase
This commit is contained in:
479
user.go
Executable file
479
user.go
Executable 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
|
||||
}
|
||||
Reference in New Issue
Block a user