mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
575 lines
15 KiB
Go
575 lines
15 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go-backend/internal/auth"
|
|
"go-backend/internal/http/middleware"
|
|
"go-backend/internal/http/response"
|
|
"go-backend/internal/security"
|
|
"go-backend/internal/store/sqlite"
|
|
"go-backend/internal/ws"
|
|
)
|
|
|
|
type Handler struct {
|
|
repo *sqlite.Repository
|
|
jwtSecret string
|
|
wsServer *ws.Server
|
|
}
|
|
|
|
type loginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
CaptchaID string `json:"captchaId"`
|
|
}
|
|
|
|
type nameRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type configSingleRequest struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type changePasswordRequest struct {
|
|
NewUsername string `json:"newUsername"`
|
|
CurrentPassword string `json:"currentPassword"`
|
|
NewPassword string `json:"newPassword"`
|
|
ConfirmPassword string `json:"confirmPassword"`
|
|
}
|
|
|
|
type flowItem struct {
|
|
N string `json:"n"`
|
|
U int64 `json:"u"`
|
|
D int64 `json:"d"`
|
|
}
|
|
|
|
func New(repo *sqlite.Repository, jwtSecret string) *Handler {
|
|
return &Handler{repo: repo, jwtSecret: jwtSecret, wsServer: ws.NewServer(repo, jwtSecret)}
|
|
}
|
|
|
|
func (h *Handler) WebSocketHandler() http.Handler {
|
|
return h.wsServer
|
|
}
|
|
|
|
func (h *Handler) Register(mux *http.ServeMux) {
|
|
mux.HandleFunc("/api/v1/user/login", h.login)
|
|
mux.HandleFunc("/api/v1/config/get", h.getConfigByName)
|
|
mux.HandleFunc("/api/v1/config/list", h.getConfigs)
|
|
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
|
mux.HandleFunc("/api/v1/config/update-single", h.updateSingleConfig)
|
|
mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha)
|
|
mux.HandleFunc("/api/v1/user/package", h.userPackage)
|
|
mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword)
|
|
|
|
mux.HandleFunc("/flow/test", h.flowTest)
|
|
mux.HandleFunc("/flow/config", h.flowConfig)
|
|
mux.HandleFunc("/flow/upload", h.flowUpload)
|
|
}
|
|
|
|
func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
var req loginRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil {
|
|
response.WriteJSON(w, response.Err(500, "请求参数错误"))
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Username) == "" {
|
|
response.WriteJSON(w, response.Err(500, "用户名不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Password) == "" {
|
|
response.WriteJSON(w, response.Err(500, "密码不能为空"))
|
|
return
|
|
}
|
|
|
|
captchaEnabled, err := h.captchaEnabled()
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if captchaEnabled && strings.TrimSpace(req.CaptchaID) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
|
return
|
|
}
|
|
|
|
user, err := h.repo.GetUserByUsername(req.Username)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if user == nil {
|
|
response.WriteJSON(w, response.ErrDefault("账号或密码错误"))
|
|
return
|
|
}
|
|
if user.Pwd != security.MD5(req.Password) {
|
|
response.WriteJSON(w, response.ErrDefault("账号或密码错误"))
|
|
return
|
|
}
|
|
if user.Status == 0 {
|
|
response.WriteJSON(w, response.ErrDefault("账号被停用"))
|
|
return
|
|
}
|
|
|
|
token, err := auth.GenerateToken(user.ID, user.User, user.RoleID, h.jwtSecret)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
requirePasswordChange := req.Username == "admin_user" || req.Password == "admin_user"
|
|
response.WriteJSON(w, response.OK(map[string]interface{}{
|
|
"token": token,
|
|
"name": user.User,
|
|
"role_id": user.RoleID,
|
|
"requirePasswordChange": requirePasswordChange,
|
|
}))
|
|
}
|
|
|
|
func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
var req nameRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil {
|
|
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
|
return
|
|
}
|
|
|
|
cfg, err := h.repo.GetConfigByName(req.Name)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if cfg == nil {
|
|
response.WriteJSON(w, response.ErrDefault("配置不存在"))
|
|
return
|
|
}
|
|
|
|
response.WriteJSON(w, response.OK(cfg))
|
|
}
|
|
|
|
func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
cfgMap, err := h.repo.ListConfigs()
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
response.WriteJSON(w, response.OK(cfgMap))
|
|
}
|
|
|
|
func (h *Handler) checkCaptcha(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
enabled, err := h.captchaEnabled()
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if enabled {
|
|
response.WriteJSON(w, response.OK(1))
|
|
return
|
|
}
|
|
response.WriteJSON(w, response.OK(0))
|
|
}
|
|
|
|
func (h *Handler) flowTest(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
_, _ = w.Write([]byte("test"))
|
|
}
|
|
|
|
func (h *Handler) flowConfig(w http.ResponseWriter, r *http.Request) {
|
|
secret := r.URL.Query().Get("secret")
|
|
if ok, _ := h.repo.NodeExistsBySecret(secret); !ok {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
_, _ = w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
|
|
_, _ = readAndDecryptFlowBody(r.Body, secret)
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
_, _ = w.Write([]byte("ok"))
|
|
}
|
|
|
|
func (h *Handler) flowUpload(w http.ResponseWriter, r *http.Request) {
|
|
secret := r.URL.Query().Get("secret")
|
|
if ok, _ := h.repo.NodeExistsBySecret(secret); !ok {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
_, _ = w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
|
|
raw, err := readAndDecryptFlowBody(r.Body, secret)
|
|
if err == nil && strings.TrimSpace(raw) != "" {
|
|
var items []flowItem
|
|
if json.Unmarshal([]byte(raw), &items) == nil {
|
|
for _, item := range items {
|
|
parts := strings.Split(item.N, "_")
|
|
if len(parts) < 3 || item.N == "web_api" {
|
|
continue
|
|
}
|
|
forwardID, err1 := strconv.ParseInt(parts[0], 10, 64)
|
|
userID, err2 := strconv.ParseInt(parts[1], 10, 64)
|
|
userTunnelID, err3 := strconv.ParseInt(parts[2], 10, 64)
|
|
if err1 != nil || err2 != nil || err3 != nil {
|
|
continue
|
|
}
|
|
_ = h.repo.AddFlow(forwardID, userID, userTunnelID, item.D, item.U)
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
_, _ = w.Write([]byte("ok"))
|
|
}
|
|
|
|
func (h *Handler) updateConfigs(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
var payload map[string]string
|
|
if err := decodeJSON(r.Body, &payload); err != nil {
|
|
response.WriteJSON(w, response.ErrDefault("配置数据不能为空"))
|
|
return
|
|
}
|
|
if len(payload) == 0 {
|
|
response.WriteJSON(w, response.ErrDefault("配置数据不能为空"))
|
|
return
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
for k, v := range payload {
|
|
key := strings.TrimSpace(k)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if err := h.repo.UpsertConfig(key, v, now); err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
}
|
|
|
|
response.WriteJSON(w, response.OKEmpty())
|
|
}
|
|
|
|
func (h *Handler) updateSingleConfig(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
var req configSingleRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil {
|
|
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Value) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("配置值不能为空"))
|
|
return
|
|
}
|
|
|
|
if err := h.repo.UpsertConfig(strings.TrimSpace(req.Name), req.Value, time.Now().UnixMilli()); err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
response.WriteJSON(w, response.OKEmpty())
|
|
}
|
|
|
|
func (h *Handler) userPackage(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
|
if !ok {
|
|
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
|
return
|
|
}
|
|
|
|
userID, err := parseUserID(claims.Sub)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
|
return
|
|
}
|
|
|
|
user, err := h.repo.GetUserByID(userID)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if user == nil {
|
|
response.WriteJSON(w, response.ErrDefault("用户不存在"))
|
|
return
|
|
}
|
|
|
|
tunnels, err := h.repo.GetUserPackageTunnels(userID)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
forwards, err := h.repo.GetUserPackageForwards(userID)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
stats, err := h.repo.GetStatisticsFlows(userID, 24)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
sort.Slice(stats, func(i, j int) bool { return stats[i].ID < stats[j].ID })
|
|
|
|
tunnelOut := make([]map[string]interface{}, 0, len(tunnels))
|
|
for _, t := range tunnels {
|
|
item := map[string]interface{}{
|
|
"id": t.ID,
|
|
"userId": t.UserID,
|
|
"tunnelId": t.TunnelID,
|
|
"tunnelName": t.TunnelName,
|
|
"tunnelFlow": t.TunnelFlow,
|
|
"flow": t.Flow,
|
|
"inFlow": t.InFlow,
|
|
"outFlow": t.OutFlow,
|
|
"num": t.Num,
|
|
"flowResetTime": t.FlowResetTime,
|
|
"expTime": t.ExpTime,
|
|
"speedId": nil,
|
|
"speedLimitName": nil,
|
|
"speed": nil,
|
|
}
|
|
if t.SpeedID.Valid {
|
|
item["speedId"] = t.SpeedID.Int64
|
|
}
|
|
if t.SpeedLimit.Valid {
|
|
item["speedLimitName"] = t.SpeedLimit.String
|
|
}
|
|
if t.Speed.Valid {
|
|
item["speed"] = t.Speed.Int64
|
|
}
|
|
tunnelOut = append(tunnelOut, item)
|
|
}
|
|
|
|
forwardOut := make([]map[string]interface{}, 0, len(forwards))
|
|
for _, f := range forwards {
|
|
item := map[string]interface{}{
|
|
"id": f.ID,
|
|
"name": f.Name,
|
|
"tunnelId": f.TunnelID,
|
|
"tunnelName": f.TunnelName,
|
|
"inIp": f.InIP,
|
|
"inPort": nil,
|
|
"remoteAddr": f.RemoteAddr,
|
|
"inFlow": f.InFlow,
|
|
"outFlow": f.OutFlow,
|
|
"status": f.Status,
|
|
"createdTime": f.CreatedAt,
|
|
}
|
|
if f.InPort.Valid {
|
|
item["inPort"] = f.InPort.Int64
|
|
}
|
|
forwardOut = append(forwardOut, item)
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"userInfo": map[string]interface{}{
|
|
"id": user.ID,
|
|
"name": user.User,
|
|
"user": user.User,
|
|
"status": user.Status,
|
|
"flow": user.Flow,
|
|
"inFlow": user.InFlow,
|
|
"outFlow": user.OutFlow,
|
|
"num": user.Num,
|
|
"expTime": user.ExpTime,
|
|
"flowResetTime": user.FlowResetTime,
|
|
"createdTime": user.CreatedTime,
|
|
"updatedTime": nullableNullInt64(user.UpdatedTime),
|
|
},
|
|
"tunnelPermissions": tunnelOut,
|
|
"forwards": forwardOut,
|
|
"statisticsFlows": stats,
|
|
}
|
|
|
|
response.WriteJSON(w, response.OK(payload))
|
|
}
|
|
|
|
func (h *Handler) updatePassword(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
|
return
|
|
}
|
|
|
|
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
|
if !ok {
|
|
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
|
return
|
|
}
|
|
|
|
userID, err := parseUserID(claims.Sub)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
|
return
|
|
}
|
|
|
|
var req changePasswordRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil {
|
|
response.WriteJSON(w, response.ErrDefault("修改账号密码时发生错误"))
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.NewUsername) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("新用户名不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.CurrentPassword) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("当前密码不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.NewPassword) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("新密码不能为空"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.ConfirmPassword) == "" {
|
|
response.WriteJSON(w, response.ErrDefault("确认密码不能为空"))
|
|
return
|
|
}
|
|
if req.NewPassword != req.ConfirmPassword {
|
|
response.WriteJSON(w, response.ErrDefault("新密码和确认密码不匹配"))
|
|
return
|
|
}
|
|
|
|
user, err := h.repo.GetUserByID(userID)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if user == nil {
|
|
response.WriteJSON(w, response.ErrDefault("用户不存在"))
|
|
return
|
|
}
|
|
|
|
if user.Pwd != security.MD5(req.CurrentPassword) {
|
|
response.WriteJSON(w, response.ErrDefault("当前密码错误"))
|
|
return
|
|
}
|
|
|
|
exists, err := h.repo.UsernameExistsExceptID(req.NewUsername, userID)
|
|
if err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
if exists {
|
|
response.WriteJSON(w, response.ErrDefault("用户名已存在"))
|
|
return
|
|
}
|
|
|
|
if err := h.repo.UpdateUserNameAndPassword(userID, req.NewUsername, security.MD5(req.NewPassword), time.Now().UnixMilli()); err != nil {
|
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
return
|
|
}
|
|
|
|
response.WriteJSON(w, response.OKEmpty())
|
|
}
|
|
|
|
func (h *Handler) captchaEnabled() (bool, error) {
|
|
cfg, err := h.repo.GetConfigByName("captcha_enabled")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if cfg == nil {
|
|
return false, nil
|
|
}
|
|
return strings.EqualFold(cfg.Value, "true"), nil
|
|
}
|
|
|
|
func decodeJSON(body io.ReadCloser, out interface{}) error {
|
|
defer body.Close()
|
|
decoder := json.NewDecoder(body)
|
|
decoder.DisallowUnknownFields()
|
|
return decoder.Decode(out)
|
|
}
|
|
|
|
func parseUserID(sub string) (int64, error) {
|
|
id, err := strconv.ParseInt(sub, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return 0, strconv.ErrSyntax
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func nullableNullInt64(v sql.NullInt64) interface{} {
|
|
if v.Valid {
|
|
return v.Int64
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readAndDecryptFlowBody(body io.ReadCloser, secret string) (string, error) {
|
|
defer body.Close()
|
|
raw, err := io.ReadAll(body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
text := strings.TrimSpace(string(raw))
|
|
if text == "" {
|
|
return "", nil
|
|
}
|
|
|
|
var wrap struct {
|
|
Encrypted bool `json:"encrypted"`
|
|
Data string `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
if err := json.Unmarshal(raw, &wrap); err != nil || !wrap.Encrypted || strings.TrimSpace(wrap.Data) == "" {
|
|
return text, nil
|
|
}
|
|
|
|
crypto, err := security.NewAESCrypto(secret)
|
|
if err != nil {
|
|
return text, nil
|
|
}
|
|
plain, err := crypto.Decrypt(wrap.Data)
|
|
if err != nil {
|
|
return text, nil
|
|
}
|
|
return string(plain), nil
|
|
}
|