105 lines
2.7 KiB
Go
105 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func jsonOK(w http.ResponseWriter, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func jsonErr(w http.ResponseWriter, code int, msg string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
|
}
|
|
|
|
func (s *Server) requireAPIKey(w http.ResponseWriter, r *http.Request) bool {
|
|
key := r.Header.Get("Authorization")
|
|
key = strings.TrimPrefix(key, "Bearer ")
|
|
if key == "" {
|
|
key = r.URL.Query().Get("api_key")
|
|
}
|
|
if key == "" {
|
|
jsonErr(w, 401, "API key required")
|
|
return false
|
|
}
|
|
// First try device token (hashed).
|
|
hash := sha256Hex(key)
|
|
var deviceID, userID, revokedAt sql.NullString
|
|
err := s.db.QueryRow("SELECT id, user_id, revoked_at FROM server_devices WHERE token_hash=?", hash).Scan(&deviceID, &userID, &revokedAt)
|
|
if err == nil {
|
|
if revokedAt.Valid && revokedAt.String != "" {
|
|
jsonErr(w, 401, "device revoked")
|
|
return false
|
|
}
|
|
// Check user not blocked.
|
|
var blocked int
|
|
if userID.Valid && userID.String != "" {
|
|
s.db.QueryRow("SELECT blocked FROM server_users WHERE id=?", userID.String).Scan(&blocked)
|
|
if blocked != 0 {
|
|
jsonErr(w, 403, "user blocked")
|
|
return false
|
|
}
|
|
}
|
|
r.Header.Set("X-Device-ID", deviceID.String)
|
|
r.Header.Set("X-User-ID", userID.String)
|
|
s.db.Exec("UPDATE server_devices SET last_seen=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), deviceID.String)
|
|
return true
|
|
}
|
|
// Fallback to plain api_key (legacy).
|
|
var count int
|
|
err = s.db.QueryRow("SELECT COUNT(*) FROM server_devices WHERE api_key=?", key).Scan(&count)
|
|
if err != nil || count == 0 {
|
|
jsonErr(w, 401, "invalid API key")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Server) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|
cookie, err := r.Cookie("session")
|
|
if err != nil || !s.tokens.Check(cookie.Value) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type PasswordError string
|
|
|
|
const (
|
|
ErrPasswordTooShort PasswordError = "PASSWORD_TOO_SHORT"
|
|
ErrPasswordTooLong PasswordError = "PASSWORD_TOO_LONG"
|
|
)
|
|
|
|
func validatePassword(password string) string {
|
|
if len(password) < 8 {
|
|
return string(ErrPasswordTooShort)
|
|
}
|
|
if len(password) > 256 {
|
|
return string(ErrPasswordTooLong)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) requireUser(w http.ResponseWriter, r *http.Request) (string, bool) {
|
|
key := r.Header.Get("Authorization")
|
|
key = strings.TrimPrefix(key, "Bearer ")
|
|
if key == "" {
|
|
jsonErr(w, 401, "authorization required")
|
|
return "", false
|
|
}
|
|
userID, ok := s.userTokens.Check(key)
|
|
if !ok {
|
|
jsonErr(w, 401, "invalid or expired token")
|
|
return "", false
|
|
}
|
|
return userID, true
|
|
}
|