Merge pull request #41 from Sagit-chu/opencode/proud-cactus
chore: remove obsolete root-level legacy Go files
@@ -1,42 +0,0 @@
|
|||||||
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
|
|
||||||
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BaseController provides common functionality for all controllers, including authentication checks.
|
|
||||||
type BaseController struct{}
|
|
||||||
|
|
||||||
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
|
||||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
|
||||||
if !session.IsLogin(c) {
|
|
||||||
if isAjax(c) {
|
|
||||||
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
|
|
||||||
} else {
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
|
||||||
}
|
|
||||||
c.Abort()
|
|
||||||
} else {
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
|
||||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
|
||||||
anyfunc, funcExists := c.Get("I18n")
|
|
||||||
if !funcExists {
|
|
||||||
logger.Warning("I18n function not exists in gin context!")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
|
|
||||||
msg := i18nFunc(locale.Web, name, params...)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/websocket"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InboundController handles HTTP requests related to Xray inbounds management.
|
|
||||||
type InboundController struct {
|
|
||||||
inboundService service.InboundService
|
|
||||||
xrayService service.XrayService
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInboundController creates a new InboundController and sets up its routes.
|
|
||||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
|
||||||
a := &InboundController{}
|
|
||||||
a.initRouter(g)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// initRouter initializes the routes for inbound-related operations.
|
|
||||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|
||||||
|
|
||||||
g.GET("/list", a.getInbounds)
|
|
||||||
g.GET("/get/:id", a.getInbound)
|
|
||||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
|
||||||
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
|
|
||||||
|
|
||||||
g.POST("/add", a.addInbound)
|
|
||||||
g.POST("/del/:id", a.delInbound)
|
|
||||||
g.POST("/update/:id", a.updateInbound)
|
|
||||||
g.POST("/clientIps/:email", a.getClientIps)
|
|
||||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
|
||||||
g.POST("/addClient", a.addInboundClient)
|
|
||||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
|
||||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
|
||||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
|
||||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
|
||||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
|
||||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
|
||||||
g.POST("/import", a.importInbound)
|
|
||||||
g.POST("/onlines", a.onlines)
|
|
||||||
g.POST("/lastOnline", a.lastOnline)
|
|
||||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
|
||||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInbounds retrieves the list of inbounds for the logged-in user.
|
|
||||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
|
||||||
user := session.GetLoginUser(c)
|
|
||||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonObj(c, inbounds, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInbound retrieves a specific inbound by its ID.
|
|
||||||
func (a *InboundController) getInbound(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inbound, err := a.inboundService.GetInbound(id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonObj(c, inbound, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClientTraffics retrieves client traffic information by email.
|
|
||||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
|
||||||
email := c.Param("email")
|
|
||||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonObj(c, clientTraffics, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClientTrafficsById retrieves client traffic information by inbound ID.
|
|
||||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonObj(c, clientTraffics, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addInbound creates a new inbound configuration.
|
|
||||||
func (a *InboundController) addInbound(c *gin.Context) {
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user := session.GetLoginUser(c)
|
|
||||||
inbound.UserId = user.Id
|
|
||||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
|
||||||
} else {
|
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
inbound, needRestart, err := a.inboundService.AddInbound(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
// Broadcast inbounds update via WebSocket
|
|
||||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
|
||||||
websocket.BroadcastInbounds(inbounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// delInbound deletes an inbound configuration by its ID.
|
|
||||||
func (a *InboundController) delInbound(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
needRestart, err := a.inboundService.DelInbound(id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
// Broadcast inbounds update via WebSocket
|
|
||||||
user := session.GetLoginUser(c)
|
|
||||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
|
||||||
websocket.BroadcastInbounds(inbounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateInbound updates an existing inbound configuration.
|
|
||||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inbound := &model.Inbound{
|
|
||||||
Id: id,
|
|
||||||
}
|
|
||||||
err = c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
// Broadcast inbounds update via WebSocket
|
|
||||||
user := session.GetLoginUser(c)
|
|
||||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
|
||||||
websocket.BroadcastInbounds(inbounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClientIps retrieves the IP addresses associated with a client by email.
|
|
||||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
|
||||||
email := c.Param("email")
|
|
||||||
|
|
||||||
ips, err := a.inboundService.GetInboundClientIps(email)
|
|
||||||
if err != nil || ips == "" {
|
|
||||||
jsonObj(c, "No IP Record", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonObj(c, ips, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearClientIps clears the IP addresses for a client by email.
|
|
||||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
|
||||||
email := c.Param("email")
|
|
||||||
|
|
||||||
err := a.inboundService.ClearClientIps(email)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.updateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addInboundClient adds a new client to an existing inbound.
|
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
|
||||||
data := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(data)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart, err := a.inboundService.AddInboundClient(data)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clientId := c.Param("clientId")
|
|
||||||
|
|
||||||
needRestart, err := a.inboundService.DelInboundClient(id, clientId)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateInboundClient updates a client's configuration in an inbound.
|
|
||||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|
||||||
clientId := c.Param("clientId")
|
|
||||||
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetClientTraffic resets the traffic counter for a specific client in an inbound.
|
|
||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
email := c.Param("email")
|
|
||||||
|
|
||||||
needRestart, err := a.inboundService.ResetClientTraffic(id, email)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundClientTrafficSuccess"), nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetAllTraffics resets all traffic counters across all inbounds.
|
|
||||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
|
||||||
err := a.inboundService.ResetAllTraffics()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetAllClientTraffics resets traffic counters for all clients in a specific inbound.
|
|
||||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.inboundService.ResetAllClientTraffics(id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// importInbound imports an inbound configuration from provided data.
|
|
||||||
func (a *InboundController) importInbound(c *gin.Context) {
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user := session.GetLoginUser(c)
|
|
||||||
inbound.Id = 0
|
|
||||||
inbound.UserId = user.Id
|
|
||||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
|
||||||
} else {
|
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
for index := range inbound.ClientStats {
|
|
||||||
inbound.ClientStats[index].Id = 0
|
|
||||||
inbound.ClientStats[index].Enable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart := false
|
|
||||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
|
||||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err)
|
|
||||||
if err == nil && needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.
|
|
||||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = a.inboundService.DelDepletedClients(id)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// onlines retrieves the list of currently online clients.
|
|
||||||
func (a *InboundController) onlines(c *gin.Context) {
|
|
||||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// lastOnline retrieves the last online timestamps for clients.
|
|
||||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
|
||||||
data, err := a.inboundService.GetClientsLastOnline()
|
|
||||||
jsonObj(c, data, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateClientTraffic updates the traffic statistics for a client by email.
|
|
||||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
|
||||||
email := c.Param("email")
|
|
||||||
|
|
||||||
// Define the request structure for traffic update
|
|
||||||
type TrafficUpdateRequest struct {
|
|
||||||
Upload int64 `json:"upload"`
|
|
||||||
Download int64 `json:"download"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var request TrafficUpdateRequest
|
|
||||||
err := c.ShouldBindJSON(&request)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// delInboundClientByEmail deletes a client from an inbound by email address.
|
|
||||||
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
|
||||||
inboundId, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "Invalid inbound ID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
email := c.Param("email")
|
|
||||||
needRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "Failed to delete client by email", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMsg(c, "Client deleted successfully", nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
|
|
||||||
type XUIController struct {
|
|
||||||
BaseController
|
|
||||||
|
|
||||||
settingController *SettingController
|
|
||||||
xraySettingController *XraySettingController
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewXUIController creates a new XUIController and initializes its routes.
|
|
||||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
|
||||||
a := &XUIController{}
|
|
||||||
a.initRouter(g)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// initRouter sets up the main panel routes and initializes sub-controllers.
|
|
||||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|
||||||
g = g.Group("/panel")
|
|
||||||
g.Use(a.checkLogin)
|
|
||||||
|
|
||||||
g.GET("/", a.index)
|
|
||||||
g.GET("/inbounds", a.inbounds)
|
|
||||||
g.GET("/settings", a.settings)
|
|
||||||
g.GET("/xray", a.xraySettings)
|
|
||||||
|
|
||||||
a.settingController = NewSettingController(g)
|
|
||||||
a.xraySettingController = NewXraySettingController(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
// index renders the main panel index page.
|
|
||||||
func (a *XUIController) index(c *gin.Context) {
|
|
||||||
html(c, "index.html", "pages.index.title", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// inbounds renders the inbounds management page.
|
|
||||||
func (a *XUIController) inbounds(c *gin.Context) {
|
|
||||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// settings renders the settings management page.
|
|
||||||
func (a *XUIController) settings(c *gin.Context) {
|
|
||||||
html(c, "settings.html", "pages.settings.title", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// xraySettings renders the Xray settings page.
|
|
||||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
|
||||||
html(c, "xray.html", "pages.xray.title", nil)
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea/caches
|
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
local.properties
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'com.flux'
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.flux"
|
|
||||||
minSdk 24
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0.1"
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
|
||||||
implementation 'androidx.webkit:webkit:1.8.0'
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"artifactType": {
|
|
||||||
"type": "APK",
|
|
||||||
"kind": "Directory"
|
|
||||||
},
|
|
||||||
"applicationId": "com.flux",
|
|
||||||
"variantName": "release",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "SINGLE",
|
|
||||||
"filters": [],
|
|
||||||
"attributes": [],
|
|
||||||
"versionCode": 1,
|
|
||||||
"versionName": "1.0.1",
|
|
||||||
"outputFile": "app-release.apk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"elementType": "File"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.flux
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.flux", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.flux" >
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.Flux"
|
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
tools:targetApi="31" >
|
|
||||||
<activity
|
|
||||||
android:name="com.flux.MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:configChanges="uiMode|orientation|screenSize" >
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 406 KiB |
@@ -1,214 +0,0 @@
|
|||||||
package com.flux
|
|
||||||
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowInsetsController
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.webkit.*
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var webView: WebView
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
webView = findViewById(R.id.webView)
|
|
||||||
WebView.setWebContentsDebuggingEnabled(true)
|
|
||||||
setupWebView()
|
|
||||||
loadUrl()
|
|
||||||
|
|
||||||
setupSystemBars()
|
|
||||||
setupRootBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupWebView() {
|
|
||||||
val settings = webView.settings
|
|
||||||
settings.javaScriptEnabled = true
|
|
||||||
settings.domStorageEnabled = true
|
|
||||||
settings.databaseEnabled = true
|
|
||||||
settings.loadWithOverviewMode = true
|
|
||||||
settings.useWideViewPort = true
|
|
||||||
settings.setSupportZoom(true)
|
|
||||||
settings.builtInZoomControls = true
|
|
||||||
settings.displayZoomControls = false
|
|
||||||
|
|
||||||
// 隐藏滚动条但不影响滚动
|
|
||||||
webView.isVerticalScrollBarEnabled = false
|
|
||||||
webView.isHorizontalScrollBarEnabled = false
|
|
||||||
webView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
|
|
||||||
webView.overScrollMode = View.OVER_SCROLL_NEVER
|
|
||||||
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = resources.configuration.uiMode and
|
|
||||||
Configuration.UI_MODE_NIGHT_MASK ==
|
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
|
||||||
|
|
||||||
webView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
|
|
||||||
|
|
||||||
|
|
||||||
// 添加JavaScript接口
|
|
||||||
webView.addJavascriptInterface(object {
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun getPanelAddresses(callback: String) {
|
|
||||||
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
|
|
||||||
val addresses = sharedPrefs.getString("panel_addresses", "[]")
|
|
||||||
webView.post {
|
|
||||||
webView.evaluateJavascript("window.$callback($addresses);", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun savePanelAddress(name: String, address: String) {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("name", name)
|
|
||||||
json.put("address", address)
|
|
||||||
json.put("inx", false)
|
|
||||||
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
|
|
||||||
val addresses = sharedPrefs.getString("panel_addresses", "[]")
|
|
||||||
val jsonArray = JSONArray(addresses)
|
|
||||||
jsonArray.put(json)
|
|
||||||
sharedPrefs.edit {
|
|
||||||
putString("panel_addresses", jsonArray.toString())
|
|
||||||
}
|
|
||||||
webView.post {
|
|
||||||
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun setCurrentPanelAddress(name: String) {
|
|
||||||
|
|
||||||
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
|
|
||||||
val addresses = sharedPrefs.getString("panel_addresses", "[]")
|
|
||||||
val jsonArray = JSONArray(addresses)
|
|
||||||
for (i in 0 until jsonArray.length()) {
|
|
||||||
val obj = jsonArray.getJSONObject(i)
|
|
||||||
if (name == obj.getString("name")) {
|
|
||||||
obj.put("inx", true)
|
|
||||||
}else{
|
|
||||||
obj.put("inx", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sharedPrefs.edit {
|
|
||||||
putString("panel_addresses", jsonArray.toString())
|
|
||||||
}
|
|
||||||
webView.post {
|
|
||||||
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun deletePanelAddress(name: String) {
|
|
||||||
val jsonArray = JSONArray()
|
|
||||||
val sharedPrefs = getSharedPreferences("panel_config", MODE_PRIVATE)
|
|
||||||
val addresses = sharedPrefs.getString("panel_addresses", "[]")
|
|
||||||
val jsonArraya = JSONArray(addresses)
|
|
||||||
for (i in 0 until jsonArraya.length()) {
|
|
||||||
val obj = jsonArraya.getJSONObject(i)
|
|
||||||
if (name != obj.getString("name")) {
|
|
||||||
jsonArray.put(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sharedPrefs.edit {
|
|
||||||
putString("panel_addresses", jsonArray.toString())
|
|
||||||
}
|
|
||||||
webView.post {
|
|
||||||
webView.evaluateJavascript("window.setPanelAddresses(${jsonArray.toString()});", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, "JsInterface")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUrl() {
|
|
||||||
//webView.loadUrl("http://192.168.100.9:3000")
|
|
||||||
webView.loadUrl("file:///android_asset/index.html")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupSystemBars() {
|
|
||||||
val isDarkTheme = resources.configuration.uiMode and
|
|
||||||
Configuration.UI_MODE_NIGHT_MASK ==
|
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
|
||||||
|
|
||||||
if (isDarkTheme) {
|
|
||||||
window.statusBarColor = Color.BLACK
|
|
||||||
window.navigationBarColor = Color.BLACK
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
window.insetsController?.setSystemBarsAppearance(
|
|
||||||
0,
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
|
||||||
)
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.decorView.systemUiVisibility =
|
|
||||||
window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.decorView.systemUiVisibility =
|
|
||||||
window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.statusBarColor = Color.WHITE
|
|
||||||
window.navigationBarColor = Color.WHITE
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
window.insetsController?.setSystemBarsAppearance(
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
|
|
||||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
|
||||||
)
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.decorView.systemUiVisibility =
|
|
||||||
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
window.decorView.systemUiVisibility =
|
|
||||||
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
setupSystemBars()
|
|
||||||
setupWebViewBackground()
|
|
||||||
setupRootBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupWebViewBackground() {
|
|
||||||
val isDarkTheme = resources.configuration.uiMode and
|
|
||||||
Configuration.UI_MODE_NIGHT_MASK ==
|
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
|
||||||
webView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupRootBackground() {
|
|
||||||
val isDarkTheme = resources.configuration.uiMode and
|
|
||||||
Configuration.UI_MODE_NIGHT_MASK ==
|
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
|
||||||
val rootView = findViewById<View>(android.R.id.content)
|
|
||||||
rootView.setBackgroundColor(if (isDarkTheme) Color.BLACK else Color.WHITE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/webView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Base.Theme.Flux" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<!-- Customize your dark theme here. -->
|
|
||||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">Flux</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Base.Theme.Flux" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<!-- Customize your light theme here. -->
|
|
||||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Flux" parent="Base.Theme.Flux" />
|
|
||||||
</resources>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample backup rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/guide/topics/data/autobackup
|
|
||||||
for details.
|
|
||||||
Note: This file is ignored for devices older that API 31
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore
|
|
||||||
-->
|
|
||||||
<full-backup-content>
|
|
||||||
<!--
|
|
||||||
<include domain="sharedpref" path="."/>
|
|
||||||
<exclude domain="sharedpref" path="device.xml"/>
|
|
||||||
-->
|
|
||||||
</full-backup-content>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
|
||||||
for details.
|
|
||||||
-->
|
|
||||||
<data-extraction-rules>
|
|
||||||
<cloud-backup>
|
|
||||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
-->
|
|
||||||
</cloud-backup>
|
|
||||||
<!--
|
|
||||||
<device-transfer>
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
</device-transfer>
|
|
||||||
-->
|
|
||||||
</data-extraction-rules>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.flux
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
plugins {
|
|
||||||
id 'com.android.application' version '8.2.2' apply false
|
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Project-wide Gradle settings.
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#Wed Aug 20 09:15:27 CST 2025
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright 2015 the original author or authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
##
|
|
||||||
## Gradle start up script for UN*X
|
|
||||||
##
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
PRG="$0"
|
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=`basename "$0"`
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD="maximum"
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN* )
|
|
||||||
cygwin=true
|
|
||||||
;;
|
|
||||||
Darwin* )
|
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="java"
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
|
||||||
if [ $? -eq 0 ] ; then
|
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
ulimit -n $MAX_FD
|
|
||||||
if [ $? -ne 0 ] ; then
|
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Escape application args
|
|
||||||
save () {
|
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
|
||||||
echo " "
|
|
||||||
}
|
|
||||||
APP_ARGS=`save "$@"`
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "Flux"
|
|
||||||
include ':app'
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const captchaTokenTTL = 5 * time.Minute
|
|
||||||
|
|
||||||
func (h *Handler) storeCaptchaToken(token string) {
|
|
||||||
if h == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token = normalizeCaptchaToken(token)
|
|
||||||
if token == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.captchaMu.Lock()
|
|
||||||
defer h.captchaMu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
h.pruneExpiredCaptchaTokensLocked(now)
|
|
||||||
h.captchaTokens[token] = now + int64(captchaTokenTTL/time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) consumeCaptchaToken(token string) bool {
|
|
||||||
if h == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
token = normalizeCaptchaToken(token)
|
|
||||||
if token == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
h.captchaMu.Lock()
|
|
||||||
defer h.captchaMu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
h.pruneExpiredCaptchaTokensLocked(now)
|
|
||||||
expiresAt, ok := h.captchaTokens[token]
|
|
||||||
if !ok || expiresAt <= now {
|
|
||||||
delete(h.captchaTokens, token)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
delete(h.captchaTokens, token)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) pruneExpiredCaptchaTokensLocked(now int64) {
|
|
||||||
for token, expiresAt := range h.captchaTokens {
|
|
||||||
if expiresAt <= now {
|
|
||||||
delete(h.captchaTokens, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCaptchaToken(token string) string {
|
|
||||||
return trimToken(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimToken(token string) string {
|
|
||||||
for len(token) > 0 && (token[0] == ' ' || token[0] == '\t' || token[0] == '\n' || token[0] == '\r') {
|
|
||||||
token = token[1:]
|
|
||||||
}
|
|
||||||
for len(token) > 0 && (token[len(token)-1] == ' ' || token[len(token)-1] == '\t' || token[len(token)-1] == '\n' || token[len(token)-1] == '\r') {
|
|
||||||
token = token[:len(token)-1]
|
|
||||||
}
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
@@ -238,6 +238,7 @@ func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *i
|
|||||||
FROM user_tunnel ut
|
FROM user_tunnel ut
|
||||||
LEFT JOIN speed_limit sl ON sl.id = ut.speed_id
|
LEFT JOIN speed_limit sl ON sl.id = ut.speed_id
|
||||||
WHERE ut.user_id = ? AND ut.tunnel_id = ?
|
WHERE ut.user_id = ? AND ut.tunnel_id = ?
|
||||||
|
ORDER BY ut.id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, userID, tunnelID)
|
`, userID, tunnelID)
|
||||||
var userTunnelID int64
|
var userTunnelID int64
|
||||||
@@ -256,6 +257,58 @@ func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *i
|
|||||||
return userTunnelID, &v, nil
|
return userTunnelID, &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listUserTunnelIDs(userID, tunnelID int64) ([]int64, error) {
|
||||||
|
rows, err := h.repo.DB().Query(`
|
||||||
|
SELECT id
|
||||||
|
FROM user_tunnel
|
||||||
|
WHERE user_id = ? AND tunnel_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, userID, tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]int64, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listUserTunnelIDsByUser(userID int64) ([]int64, error) {
|
||||||
|
rows, err := h.repo.DB().Query(`
|
||||||
|
SELECT id
|
||||||
|
FROM user_tunnel
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]int64, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) syncForwardServices(forward *forwardRecord, method string, allowFallbackAdd bool) error {
|
func (h *Handler) syncForwardServices(forward *forwardRecord, method string, allowFallbackAdd bool) error {
|
||||||
if h == nil || forward == nil {
|
if h == nil || forward == nil {
|
||||||
return errors.New("invalid forward sync context")
|
return errors.New("invalid forward sync context")
|
||||||
@@ -311,20 +364,66 @@ func (h *Handler) controlForwardServices(forward *forwardRecord, commandType str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
base := buildForwardServiceBase(forward.ID, forward.UserID, userTunnelID)
|
userTunnelIDs, err := h.listUserTunnelIDs(forward.UserID, forward.TunnelID)
|
||||||
payload := map[string]interface{}{
|
if err != nil {
|
||||||
"services": buildForwardControlServiceNames(base, commandType),
|
return err
|
||||||
}
|
}
|
||||||
|
allUserTunnelIDs, err := h.listUserTunnelIDsByUser(forward.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
candidateTunnelIDs := make([]int64, 0, len(userTunnelIDs)+len(allUserTunnelIDs))
|
||||||
|
candidateTunnelIDs = append(candidateTunnelIDs, userTunnelIDs...)
|
||||||
|
candidateTunnelIDs = append(candidateTunnelIDs, allUserTunnelIDs...)
|
||||||
|
bases := buildForwardServiceBaseCandidates(forward.ID, forward.UserID, userTunnelID, candidateTunnelIDs)
|
||||||
seen := map[int64]struct{}{}
|
seen := map[int64]struct{}{}
|
||||||
for _, fp := range ports {
|
for _, fp := range ports {
|
||||||
if _, ok := seen[fp.NodeID]; ok {
|
if _, ok := seen[fp.NodeID]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[fp.NodeID] = struct{}{}
|
seen[fp.NodeID] = struct{}{}
|
||||||
_, err := h.sendNodeCommand(fp.NodeID, commandType, payload, false, tolerateNotFound)
|
|
||||||
if err != nil {
|
var lastNotFoundErr error
|
||||||
return err
|
nodeHandled := false
|
||||||
|
|
||||||
|
for _, base := range bases {
|
||||||
|
variants := []string{base + "_tcp", base + "_udp"}
|
||||||
|
if shouldTryLegacySingleService(commandType) || strings.EqualFold(strings.TrimSpace(commandType), "DeleteService") {
|
||||||
|
variants = append(variants, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateHandled := false
|
||||||
|
for _, name := range variants {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"services": []string{name},
|
||||||
|
}
|
||||||
|
_, err := h.sendNodeCommand(fp.NodeID, commandType, payload, false, false)
|
||||||
|
if err == nil {
|
||||||
|
candidateHandled = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isNotFoundError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lastNotFoundErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if candidateHandled {
|
||||||
|
nodeHandled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nodeHandled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tolerateNotFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lastNotFoundErr != nil {
|
||||||
|
return lastNotFoundErr
|
||||||
|
}
|
||||||
|
return errors.New("service control failed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -852,6 +951,31 @@ func buildForwardServiceBase(forwardID, userID, userTunnelID int64) string {
|
|||||||
return fmt.Sprintf("%d_%d_%d", forwardID, userID, userTunnelID)
|
return fmt.Sprintf("%d_%d_%d", forwardID, userID, userTunnelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildForwardServiceBaseCandidates(forwardID, userID, preferredUserTunnelID int64, userTunnelIDs []int64) []string {
|
||||||
|
orderedIDs := make([]int64, 0, len(userTunnelIDs)+2)
|
||||||
|
seen := make(map[int64]struct{}, len(userTunnelIDs)+2)
|
||||||
|
|
||||||
|
appendID := func(id int64) {
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
orderedIDs = append(orderedIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendID(preferredUserTunnelID)
|
||||||
|
for _, id := range userTunnelIDs {
|
||||||
|
appendID(id)
|
||||||
|
}
|
||||||
|
appendID(0)
|
||||||
|
|
||||||
|
bases := make([]string, 0, len(orderedIDs))
|
||||||
|
for _, id := range orderedIDs {
|
||||||
|
bases = append(bases, buildForwardServiceBase(forwardID, userID, id))
|
||||||
|
}
|
||||||
|
return bases
|
||||||
|
}
|
||||||
|
|
||||||
func buildForwardControlServiceNames(base, commandType string) []string {
|
func buildForwardControlServiceNames(base, commandType string) []string {
|
||||||
names := []string{base + "_tcp", base + "_udp"}
|
names := []string{base + "_tcp", base + "_udp"}
|
||||||
if strings.EqualFold(strings.TrimSpace(commandType), "DeleteService") {
|
if strings.EqualFold(strings.TrimSpace(commandType), "DeleteService") {
|
||||||
@@ -860,6 +984,19 @@ func buildForwardControlServiceNames(base, commandType string) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldTryLegacySingleService(commandType string) bool {
|
||||||
|
cmd := strings.ToLower(strings.TrimSpace(commandType))
|
||||||
|
return cmd == "pauseservice" || cmd == "resumeservice"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotFoundError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||||
|
return strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")
|
||||||
|
}
|
||||||
|
|
||||||
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiter *int) []map[string]interface{} {
|
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiter *int) []map[string]interface{} {
|
||||||
protocols := []string{"tcp", "udp"}
|
protocols := []string{"tcp", "udp"}
|
||||||
services := make([]map[string]interface{}, 0, 2)
|
services := make([]map[string]interface{}, 0, 2)
|
||||||
|
|||||||
@@ -25,3 +25,31 @@ func TestBuildForwardControlServiceNamesDelete(t *testing.T) {
|
|||||||
t.Fatalf("expected %v, got %v", want, got)
|
t.Fatalf("expected %v, got %v", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildForwardServiceBaseCandidates(t *testing.T) {
|
||||||
|
got := buildForwardServiceBaseCandidates(12, 34, 56, []int64{56, 78, 90})
|
||||||
|
want := []string{"12_34_56", "12_34_78", "12_34_90", "12_34_0"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("expected %v, got %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildForwardServiceBaseCandidatesWithZeroPreferred(t *testing.T) {
|
||||||
|
got := buildForwardServiceBaseCandidates(12, 34, 0, []int64{78, 0, 90})
|
||||||
|
want := []string{"12_34_0", "12_34_78", "12_34_90"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("expected %v, got %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldTryLegacySingleService(t *testing.T) {
|
||||||
|
if !shouldTryLegacySingleService("PauseService") {
|
||||||
|
t.Fatalf("PauseService should require legacy fallback")
|
||||||
|
}
|
||||||
|
if !shouldTryLegacySingleService("resumeService") {
|
||||||
|
t.Fatalf("ResumeService should require legacy fallback")
|
||||||
|
}
|
||||||
|
if shouldTryLegacySingleService("DeleteService") {
|
||||||
|
t.Fatalf("DeleteService should not require legacy fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,9 +27,6 @@ type Handler struct {
|
|||||||
jwtSecret string
|
jwtSecret string
|
||||||
wsServer *ws.Server
|
wsServer *ws.Server
|
||||||
|
|
||||||
captchaMu sync.Mutex
|
|
||||||
captchaTokens map[string]int64
|
|
||||||
|
|
||||||
jobsMu sync.Mutex
|
jobsMu sync.Mutex
|
||||||
jobsCancel context.CancelFunc
|
jobsCancel context.CancelFunc
|
||||||
jobsStarted bool
|
jobsStarted bool
|
||||||
@@ -65,10 +63,9 @@ type flowItem struct {
|
|||||||
|
|
||||||
func New(repo *sqlite.Repository, jwtSecret string) *Handler {
|
func New(repo *sqlite.Repository, jwtSecret string) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
jwtSecret: jwtSecret,
|
jwtSecret: jwtSecret,
|
||||||
wsServer: ws.NewServer(repo, jwtSecret),
|
wsServer: ws.NewServer(repo, jwtSecret),
|
||||||
captchaTokens: make(map[string]int64),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +85,6 @@ func (h *Handler) Register(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
||||||
mux.HandleFunc("/api/v1/config/update-single", h.updateSingleConfig)
|
mux.HandleFunc("/api/v1/config/update-single", h.updateSingleConfig)
|
||||||
mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha)
|
mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha)
|
||||||
mux.HandleFunc("/api/v1/captcha/generate", h.captchaGenerate)
|
|
||||||
mux.HandleFunc("/api/v1/captcha/verify", h.captchaVerify)
|
|
||||||
mux.HandleFunc("/api/v1/user/package", h.userPackage)
|
mux.HandleFunc("/api/v1/user/package", h.userPackage)
|
||||||
mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword)
|
mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword)
|
||||||
mux.HandleFunc("/api/v1/node/list", h.nodeList)
|
mux.HandleFunc("/api/v1/node/list", h.nodeList)
|
||||||
@@ -181,13 +176,22 @@ func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if captchaEnabled && strings.TrimSpace(req.CaptchaID) == "" {
|
if captchaEnabled {
|
||||||
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
if strings.TrimSpace(req.CaptchaID) == "" {
|
||||||
return
|
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
||||||
}
|
return
|
||||||
if captchaEnabled && !h.consumeCaptchaToken(req.CaptchaID) {
|
}
|
||||||
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
|
||||||
return
|
secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key")
|
||||||
|
if err != nil || secretCfg == nil || secretCfg.Value == "" {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("验证码配置错误:未配置Secret Key"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.verifyCloudflareTurnstile(req.CaptchaID, secretCfg.Value) {
|
||||||
|
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.repo.GetUserByUsername(req.Username)
|
user, err := h.repo.GetUserByUsername(req.Username)
|
||||||
@@ -961,3 +965,24 @@ func readAndDecryptFlowBody(body io.ReadCloser, secret string) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(plain), nil
|
return string(plain), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) verifyCloudflareTurnstile(token, secretKey string) bool {
|
||||||
|
if token == "" || secretKey == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", url.Values{
|
||||||
|
"secret": {secretKey},
|
||||||
|
"response": {token},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var body struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return body.Success
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -251,58 +252,6 @@ func (h *Handler) userResetFlow(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) captchaGenerate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(`{"success":false,"message":"bad request"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token := randomToken(16)
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"id": token,
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"id": token,
|
|
||||||
},
|
|
||||||
"success": true,
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
_ = json.NewEncoder(w).Encode(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) captchaVerify(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(`{"success":false,"message":"bad request"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req map[string]interface{}
|
|
||||||
if err := decodeJSON(r.Body, &req); err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(`{"success":false,"message":"bad request"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := asString(req["captchaId"])
|
|
||||||
if id == "" {
|
|
||||||
id = asString(req["id"])
|
|
||||||
}
|
|
||||||
trackData := asString(req["data"])
|
|
||||||
if trackData == "" {
|
|
||||||
trackData = asString(req["trackData"])
|
|
||||||
}
|
|
||||||
if id == "" || trackData == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(`{"success":false,"message":"bad request"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.storeCaptchaToken(id)
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"success": true,
|
|
||||||
"data": map[string]interface{}{"validToken": id},
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
_ = json.NewEncoder(w).Encode(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||||
@@ -589,6 +538,31 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) cleanupTunnelRuntime(tunnelID int64) {
|
||||||
|
tunnel, err := h.getTunnelRecord(tunnelID)
|
||||||
|
if err != nil || tunnel.Type != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chainRows, err := h.listChainNodesForTunnel(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := fmt.Sprintf("%d_tls", tunnelID)
|
||||||
|
chainName := fmt.Sprintf("chains_%d", tunnelID)
|
||||||
|
|
||||||
|
for _, row := range chainRows {
|
||||||
|
if row.ChainType == 1 {
|
||||||
|
_, _ = h.sendNodeCommand(row.NodeID, "DeleteChains", map[string]interface{}{"chain": chainName}, false, true)
|
||||||
|
} else if row.ChainType == 2 {
|
||||||
|
_, _ = h.sendNodeCommand(row.NodeID, "DeleteChains", map[string]interface{}{"chain": chainName}, false, true)
|
||||||
|
_, _ = h.sendNodeCommand(row.NodeID, "DeleteService", map[string]interface{}{"services": []string{serviceName}}, false, true)
|
||||||
|
} else if row.ChainType == 3 {
|
||||||
|
_, _ = h.sendNodeCommand(row.NodeID, "DeleteService", map[string]interface{}{"services": []string{serviceName}}, false, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) tunnelGet(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) tunnelGet(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||||
@@ -627,19 +601,34 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.ErrDefault("隧道ID不能为空"))
|
response.WriteJSON(w, response.ErrDefault("隧道ID不能为空"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.cleanupTunnelRuntime(id)
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
_, err := h.repo.DB().Exec(`UPDATE tunnel SET name=?, type=?, flow=?, traffic_ratio=?, status=?, in_ip=?, updated_time=? WHERE id=?`,
|
typeVal := asInt(req["type"], 1)
|
||||||
asString(req["name"]), asInt(req["type"], 1), asInt64(req["flow"], 1), asFloat(req["trafficRatio"], 1.0), asInt(req["status"], 1), nullableText(asString(req["inIp"])), now, id)
|
|
||||||
if err != nil {
|
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tx, err := h.repo.DB().Begin()
|
tx, err := h.repo.DB().Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
runtimeState, err := h.prepareTunnelCreateState(tx, req, typeVal)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtimeState.TunnelID = id
|
||||||
|
applyTunnelPortsToRequest(req, runtimeState)
|
||||||
|
|
||||||
|
_, err = tx.Exec(`UPDATE tunnel SET name=?, type=?, flow=?, traffic_ratio=?, status=?, in_ip=?, updated_time=? WHERE id=?`,
|
||||||
|
asString(req["name"]), typeVal, asInt64(req["flow"], 1), asFloat(req["trafficRatio"], 1.0), asInt(req["status"], 1), nullableText(asString(req["inIp"])), now, id)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM chain_tunnel WHERE tunnel_id = ?`, id); err != nil {
|
if _, err := tx.Exec(`DELETE FROM chain_tunnel WHERE tunnel_id = ?`, id); err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -652,6 +641,16 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if typeVal == 2 {
|
||||||
|
createdChains, createdServices, applyErr := h.applyTunnelRuntime(runtimeState)
|
||||||
|
if applyErr != nil {
|
||||||
|
h.rollbackTunnelRuntime(createdChains, createdServices, id)
|
||||||
|
response.WriteJSON(w, response.ErrDefault(applyErr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.WriteJSON(w, response.OKEmpty())
|
response.WriteJSON(w, response.OKEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,6 +663,7 @@ func (h *Handler) tunnelDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.cleanupTunnelRuntime(id)
|
||||||
if err := h.deleteTunnelByID(id); err != nil {
|
if err := h.deleteTunnelByID(id); err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -721,6 +721,7 @@ func (h *Handler) tunnelBatchDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
success := 0
|
success := 0
|
||||||
fail := 0
|
fail := 0
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
|
h.cleanupTunnelRuntime(id)
|
||||||
if err := h.deleteTunnelByID(id); err != nil {
|
if err := h.deleteTunnelByID(id); err != nil {
|
||||||
fail++
|
fail++
|
||||||
} else {
|
} else {
|
||||||
@@ -948,6 +949,11 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
oldPorts, err := h.listForwardPorts(id)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tunnelID := asInt64(req["tunnelId"], forward.TunnelID)
|
tunnelID := asInt64(req["tunnelId"], forward.TunnelID)
|
||||||
if tunnelID <= 0 {
|
if tunnelID <= 0 {
|
||||||
@@ -1000,13 +1006,19 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = h.replaceForwardPorts(id, tunnelID, port)
|
if err := h.replaceForwardPorts(id, tunnelID, port); err != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
updatedForward, err := h.getForwardRecord(id)
|
updatedForward, err := h.getForwardRecord(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.syncForwardServices(updatedForward, "UpdateService", true); err != nil {
|
if err := h.syncForwardServices(updatedForward, "UpdateService", true); err != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1291,6 +1303,11 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
|||||||
fail++
|
fail++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
oldPorts, listPortsErr := h.listForwardPorts(id)
|
||||||
|
if listPortsErr != nil {
|
||||||
|
fail++
|
||||||
|
continue
|
||||||
|
}
|
||||||
var port sql.NullInt64
|
var port sql.NullInt64
|
||||||
_ = h.repo.DB().QueryRow(`SELECT MIN(port) FROM forward_port WHERE forward_id = ?`, id).Scan(&port)
|
_ = h.repo.DB().QueryRow(`SELECT MIN(port) FROM forward_port WHERE forward_id = ?`, id).Scan(&port)
|
||||||
_, err := h.repo.DB().Exec(`UPDATE forward SET tunnel_id = ?, updated_time = ? WHERE id = ?`, req.TargetTunnelID, time.Now().UnixMilli(), id)
|
_, err := h.repo.DB().Exec(`UPDATE forward SET tunnel_id = ?, updated_time = ? WHERE id = ?`, req.TargetTunnelID, time.Now().UnixMilli(), id)
|
||||||
@@ -1305,13 +1322,19 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
|||||||
if p <= 0 {
|
if p <= 0 {
|
||||||
p = h.pickTunnelPort(req.TargetTunnelID)
|
p = h.pickTunnelPort(req.TargetTunnelID)
|
||||||
}
|
}
|
||||||
_ = h.replaceForwardPorts(id, req.TargetTunnelID, p)
|
if err := h.replaceForwardPorts(id, req.TargetTunnelID, p); err != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
|
fail++
|
||||||
|
continue
|
||||||
|
}
|
||||||
updatedForward, fetchErr := h.getForwardRecord(id)
|
updatedForward, fetchErr := h.getForwardRecord(id)
|
||||||
if fetchErr != nil {
|
if fetchErr != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
fail++
|
fail++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := h.syncForwardServices(updatedForward, "UpdateService", true); err != nil {
|
if err := h.syncForwardServices(updatedForward, "UpdateService", true); err != nil {
|
||||||
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
fail++
|
fail++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2377,44 +2400,187 @@ func (h *Handler) tunnelEntryNodeIDs(tunnelID int64) ([]int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) pickTunnelPort(tunnelID int64) int {
|
func (h *Handler) pickTunnelPort(tunnelID int64) int {
|
||||||
entry, _ := h.tunnelEntryNodeIDs(tunnelID)
|
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||||
if len(entry) == 0 {
|
if err != nil || len(entryNodes) == 0 {
|
||||||
return 10000
|
return 10000
|
||||||
}
|
}
|
||||||
var portRange string
|
|
||||||
_ = h.repo.DB().QueryRow(`SELECT port FROM node WHERE id = ?`, entry[0]).Scan(&portRange)
|
var commonAvailable []int
|
||||||
if portRange == "" {
|
firstNode := true
|
||||||
return 10000
|
|
||||||
}
|
for _, nodeID := range entryNodes {
|
||||||
first := strings.Split(portRange, ",")[0]
|
var portRange string
|
||||||
first = strings.TrimSpace(first)
|
if err := h.repo.DB().QueryRow("SELECT port FROM node WHERE id = ?", nodeID).Scan(&portRange); err != nil {
|
||||||
if strings.Contains(first, "-") {
|
continue
|
||||||
parts := strings.SplitN(first, "-", 2)
|
}
|
||||||
p, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
|
if portRange == "" {
|
||||||
if p > 0 {
|
portRange = "1000-65535"
|
||||||
return p
|
}
|
||||||
|
|
||||||
|
nodePorts, err := parsePorts(portRange)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
used, err := h.getUsedPorts(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var available []int
|
||||||
|
for _, p := range nodePorts {
|
||||||
|
if !used[p] {
|
||||||
|
available = append(available, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstNode {
|
||||||
|
commonAvailable = available
|
||||||
|
firstNode = false
|
||||||
|
} else {
|
||||||
|
set := make(map[int]bool)
|
||||||
|
for _, p := range available {
|
||||||
|
set[p] = true
|
||||||
|
}
|
||||||
|
var newCommon []int
|
||||||
|
for _, p := range commonAvailable {
|
||||||
|
if set[p] {
|
||||||
|
newCommon = append(newCommon, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonAvailable = newCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commonAvailable) == 0 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p, err := strconv.Atoi(first); err == nil && p > 0 {
|
|
||||||
return p
|
if len(commonAvailable) > 0 {
|
||||||
|
idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(commonAvailable))))
|
||||||
|
return commonAvailable[idx.Int64()]
|
||||||
}
|
}
|
||||||
|
|
||||||
return 10000
|
return 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getUsedPorts(nodeID int64) (map[int]bool, error) {
|
||||||
|
used := make(map[int]bool)
|
||||||
|
rows, err := h.repo.DB().Query("SELECT port FROM forward_port WHERE node_id = ?", nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var p int
|
||||||
|
if err := rows.Scan(&p); err == nil {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows2, err := h.repo.DB().Query("SELECT port FROM chain_tunnel WHERE node_id = ? AND port > 0", nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows2.Close()
|
||||||
|
for rows2.Next() {
|
||||||
|
var p int
|
||||||
|
if err := rows2.Scan(&p); err == nil {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePorts(portRange string) ([]int, error) {
|
||||||
|
var ports []int
|
||||||
|
parts := strings.Split(portRange, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(part, "-") {
|
||||||
|
rangeParts := strings.Split(part, "-")
|
||||||
|
if len(rangeParts) != 2 {
|
||||||
|
return nil, errors.New("invalid port range format")
|
||||||
|
}
|
||||||
|
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
|
||||||
|
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
|
||||||
|
if err1 != nil || err2 != nil || start > end {
|
||||||
|
return nil, errors.New("invalid port range values")
|
||||||
|
}
|
||||||
|
for i := start; i <= end; i++ {
|
||||||
|
ports = append(ports, i)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p, err := strconv.Atoi(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid port value")
|
||||||
|
}
|
||||||
|
ports = append(ports, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int) error {
|
func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int) error {
|
||||||
tx, err := h.repo.DB().Begin()
|
tx, err := h.repo.DB().Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
_, _ = tx.Exec(`DELETE FROM forward_port WHERE forward_id = ?`, forwardID)
|
if _, err := tx.Exec(`DELETE FROM forward_port WHERE forward_id = ?`, forwardID); err != nil {
|
||||||
entryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
return err
|
||||||
|
}
|
||||||
|
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, nodeID := range entryNodes {
|
for _, nodeID := range entryNodes {
|
||||||
_, _ = tx.Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeID, port)
|
if _, err := tx.Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeID, port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwardPortRecord) error {
|
||||||
|
tx, err := h.repo.DB().Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(`DELETE FROM forward_port WHERE forward_id = ?`, forwardID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fp := range ports {
|
||||||
|
if _, err := tx.Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, fp.NodeID, fp.Port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts []forwardPortRecord) {
|
||||||
|
if h == nil || oldForward == nil || h.repo == nil || h.repo.DB() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = h.repo.DB().Exec(`
|
||||||
|
UPDATE forward
|
||||||
|
SET user_id = ?, user_name = ?, name = ?, tunnel_id = ?, remote_addr = ?, strategy = ?, status = ?, updated_time = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, oldForward.UserID, oldForward.UserName, oldForward.Name, oldForward.TunnelID, oldForward.RemoteAddr, oldForward.Strategy, oldForward.Status, time.Now().UnixMilli(), oldForward.ID)
|
||||||
|
|
||||||
|
if err := h.replaceForwardPortsWithRecords(oldForward.ID, oldPorts); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = h.syncForwardServices(oldForward, "UpdateService", true)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) upsertUserTunnel(req map[string]interface{}) error {
|
func (h *Handler) upsertUserTunnel(req map[string]interface{}) error {
|
||||||
userID := asInt64(req["userId"], 0)
|
userID := asInt64(req["userId"], 0)
|
||||||
tunnelID := asInt64(req["tunnelId"], 0)
|
tunnelID := asInt64(req["tunnelId"], 0)
|
||||||
@@ -2423,38 +2589,100 @@ func (h *Handler) upsertUserTunnel(req map[string]interface{}) error {
|
|||||||
}
|
}
|
||||||
db := h.repo.DB()
|
db := h.repo.DB()
|
||||||
var existingID int64
|
var existingID int64
|
||||||
err := db.QueryRow(`SELECT id FROM user_tunnel WHERE user_id = ? AND tunnel_id = ? LIMIT 1`, userID, tunnelID).Scan(&existingID)
|
var currentFlow, currentNum, currentExpTime, currentFlowReset int64
|
||||||
flow := asInt64(req["flow"], -1)
|
var currentSpeedID sql.NullInt64
|
||||||
num := asInt(req["num"], -1)
|
var currentStatus int
|
||||||
expTime := asInt64(req["expTime"], -1)
|
|
||||||
flowReset := asInt64(req["flowResetTime"], -1)
|
err := db.QueryRow(`
|
||||||
status := asInt(req["status"], 1)
|
SELECT id, flow, num, exp_time, flow_reset_time, speed_id, status
|
||||||
|
FROM user_tunnel
|
||||||
|
WHERE user_id = ? AND tunnel_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
`, userID, tunnelID).Scan(&existingID, ¤tFlow, ¤tNum, ¤tExpTime, ¤tFlowReset, ¤tSpeedID, ¤tStatus)
|
||||||
|
|
||||||
speedID := asAnyToInt64Ptr(req["speedId"])
|
speedID := asAnyToInt64Ptr(req["speedId"])
|
||||||
|
reqFlow := asInt64(req["flow"], -1)
|
||||||
|
reqNum := asInt(req["num"], -1)
|
||||||
|
reqExpTime := asInt64(req["expTime"], -1)
|
||||||
|
reqFlowReset := asInt64(req["flowResetTime"], -1)
|
||||||
|
reqStatus := asInt(req["status"], -1)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
if flow < 0 || num < 0 || expTime < 0 || flowReset < 0 {
|
if reqFlow < 0 || reqNum < 0 || reqExpTime < 0 || reqFlowReset < 0 {
|
||||||
_ = db.QueryRow(`SELECT flow, num, exp_time, flow_reset_time FROM user WHERE id = ?`, userID).Scan(&flow, &num, &expTime, &flowReset)
|
var uFlow, uNum, uExp, uReset int64
|
||||||
|
if uErr := db.QueryRow(`SELECT flow, num, exp_time, flow_reset_time FROM user WHERE id = ?`, userID).Scan(&uFlow, &uNum, &uExp, &uReset); uErr == nil {
|
||||||
|
if reqFlow < 0 {
|
||||||
|
reqFlow = uFlow
|
||||||
|
}
|
||||||
|
if reqNum < 0 {
|
||||||
|
reqNum = int(uNum)
|
||||||
|
}
|
||||||
|
if reqExpTime < 0 {
|
||||||
|
reqExpTime = uExp
|
||||||
|
}
|
||||||
|
if reqFlowReset < 0 {
|
||||||
|
reqFlowReset = uReset
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if reqFlow < 0 {
|
||||||
|
reqFlow = 0
|
||||||
|
}
|
||||||
|
if reqNum < 0 {
|
||||||
|
reqNum = 0
|
||||||
|
}
|
||||||
|
if reqExpTime < 0 {
|
||||||
|
reqExpTime = time.Now().Add(365 * 24 * time.Hour).UnixMilli()
|
||||||
|
}
|
||||||
|
if reqFlowReset < 0 {
|
||||||
|
reqFlowReset = 1
|
||||||
|
}
|
||||||
|
if reqStatus < 0 {
|
||||||
|
reqStatus = 1
|
||||||
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(?, ?, ?, ?, ?, 0, 0, ?, ?, ?)`,
|
_, err = db.Exec(`INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(?, ?, ?, ?, ?, 0, 0, ?, ?, ?)`,
|
||||||
userID, tunnelID, nullableInt(speedID), num, flow, flowReset, expTime, status)
|
userID, tunnelID, nullableInt(speedID), reqNum, reqFlow, reqFlowReset, reqExpTime, reqStatus)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if flow < 0 {
|
|
||||||
flow = 0
|
newFlow := currentFlow
|
||||||
|
if reqFlow >= 0 {
|
||||||
|
newFlow = reqFlow
|
||||||
}
|
}
|
||||||
if num < 0 {
|
|
||||||
num = 0
|
newNum := int(currentNum)
|
||||||
|
if reqNum >= 0 {
|
||||||
|
newNum = reqNum
|
||||||
}
|
}
|
||||||
if expTime < 0 {
|
|
||||||
expTime = time.Now().Add(365 * 24 * time.Hour).UnixMilli()
|
newExpTime := currentExpTime
|
||||||
|
if reqExpTime >= 0 {
|
||||||
|
newExpTime = reqExpTime
|
||||||
}
|
}
|
||||||
if flowReset < 0 {
|
|
||||||
flowReset = 1
|
newFlowReset := currentFlowReset
|
||||||
|
if reqFlowReset >= 0 {
|
||||||
|
newFlowReset = reqFlowReset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newStatus := currentStatus
|
||||||
|
if reqStatus >= 0 {
|
||||||
|
newStatus = reqStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
newSpeedID := currentSpeedID
|
||||||
|
if speedID != nil {
|
||||||
|
newSpeedID = sql.NullInt64{Int64: *speedID, Valid: true}
|
||||||
|
} else if _, ok := req["speedId"]; ok {
|
||||||
|
newSpeedID = sql.NullInt64{Valid: false}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`UPDATE user_tunnel SET speed_id = ?, flow = ?, num = ?, exp_time = ?, flow_reset_time = ?, status = ? WHERE id = ?`,
|
_, err = db.Exec(`UPDATE user_tunnel SET speed_id = ?, flow = ?, num = ?, exp_time = ?, flow_reset_time = ?, status = ? WHERE id = ?`,
|
||||||
nullableInt(speedID), flow, num, expTime, flowReset, status, existingID)
|
newSpeedID, newFlow, newNum, newExpTime, newFlowReset, newStatus, existingID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_tunnel_unique ON user_tunnel(user_id, tunnel_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS vite_config (
|
CREATE TABLE IF NOT EXISTS vite_config (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
@@ -197,6 +197,339 @@ func TestForwardOwnershipAndScopeContracts(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForwardSwitchTunnelRollbackOnSyncFailure(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, repo := setupContractRouter(t, secret)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||||
|
VALUES(2, 'switch_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||||
|
`, now, now); err != nil {
|
||||||
|
t.Fatalf("insert user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTunnel := func(name string, inx int) int64 {
|
||||||
|
res, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, name, 1.0, 1, "tls", 99999, now, now, 1, nil, inx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert tunnel %s: %v", name, err)
|
||||||
|
}
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get tunnel id %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
insertNode := func(name, ip, portRange string, inx int) int64 {
|
||||||
|
res, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, name, name+"-secret", ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", inx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert node %s: %v", name, err)
|
||||||
|
}
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get node id %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelA := insertTunnel("switch-tunnel-a", 0)
|
||||||
|
tunnelB := insertTunnel("switch-tunnel-b", 1)
|
||||||
|
nodeA := insertNode("switch-node-a", "10.10.0.1", "21000-21010", 0)
|
||||||
|
nodeB := insertNode("switch-node-b", "10.10.0.2", "22000-22010", 1)
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||||
|
VALUES(?, 1, ?, 21001, 'round', 1, 'tls')
|
||||||
|
`, tunnelA, nodeA); err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel tunnelA: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||||
|
VALUES(?, 1, ?, 22001, 'round', 1, 'tls')
|
||||||
|
`, tunnelB, nodeB); err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel tunnelB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||||
|
VALUES(10, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)
|
||||||
|
`, tunnelA); err != nil {
|
||||||
|
t.Fatalf("insert user_tunnel A: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||||
|
VALUES(11, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)
|
||||||
|
`, tunnelB); err != nil {
|
||||||
|
t.Fatalf("insert user_tunnel B: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardRes, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||||
|
VALUES(2, 'switch_user', 'switch-forward', ?, '8.8.8.8:53', 'fifo', 0, 0, ?, ?, 1, 0)
|
||||||
|
`, tunnelA, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert forward: %v", err)
|
||||||
|
}
|
||||||
|
forwardID, err := forwardRes.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get forward id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeA, 21001); err != nil {
|
||||||
|
t.Fatalf("insert forward_port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := `{"id":` + jsonNumber(forwardID) + `,"tunnelId":` + jsonNumber(tunnelB) + `}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/update", bytes.NewBufferString(payload))
|
||||||
|
req.Header.Set("Authorization", adminToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
var out response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code == 0 {
|
||||||
|
t.Fatalf("expected update failure when node is offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunnelAfter int64
|
||||||
|
if err := repo.DB().QueryRow(`SELECT tunnel_id FROM forward WHERE id = ?`, forwardID).Scan(&tunnelAfter); err != nil {
|
||||||
|
t.Fatalf("query forward tunnel_id: %v", err)
|
||||||
|
}
|
||||||
|
if tunnelAfter != tunnelA {
|
||||||
|
t.Fatalf("expected tunnel rollback to %d, got %d", tunnelA, tunnelAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeAfter int64
|
||||||
|
var portAfter int
|
||||||
|
if err := repo.DB().QueryRow(`SELECT node_id, port FROM forward_port WHERE forward_id = ? LIMIT 1`, forwardID).Scan(&nodeAfter, &portAfter); err != nil {
|
||||||
|
t.Fatalf("query forward_port: %v", err)
|
||||||
|
}
|
||||||
|
if nodeAfter != nodeA || portAfter != 21001 {
|
||||||
|
t.Fatalf("expected forward_port rollback to node=%d port=21001, got node=%d port=%d", nodeA, nodeAfter, portAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardBatchChangeTunnelRollbackOnSyncFailure(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, repo := setupContractRouter(t, secret)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||||
|
VALUES(2, 'batch_switch_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||||
|
`, now, now); err != nil {
|
||||||
|
t.Fatalf("insert user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelResA, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES('batch-switch-tunnel-a', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0)
|
||||||
|
`, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert tunnel A: %v", err)
|
||||||
|
}
|
||||||
|
tunnelA, _ := tunnelResA.LastInsertId()
|
||||||
|
|
||||||
|
tunnelResB, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES('batch-switch-tunnel-b', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 1)
|
||||||
|
`, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert tunnel B: %v", err)
|
||||||
|
}
|
||||||
|
tunnelB, _ := tunnelResB.LastInsertId()
|
||||||
|
|
||||||
|
nodeResA, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
|
VALUES('batch-switch-node-a', 'batch-switch-node-a-secret', '10.11.0.1', '10.11.0.1', '', '23000-23010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0)
|
||||||
|
`, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert node A: %v", err)
|
||||||
|
}
|
||||||
|
nodeA, _ := nodeResA.LastInsertId()
|
||||||
|
|
||||||
|
nodeResB, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
|
VALUES('batch-switch-node-b', 'batch-switch-node-b-secret', '10.11.0.2', '10.11.0.2', '', '24000-24010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 1)
|
||||||
|
`, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert node B: %v", err)
|
||||||
|
}
|
||||||
|
nodeB, _ := nodeResB.LastInsertId()
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 1, ?, 23001, 'round', 1, 'tls')`, tunnelA, nodeA); err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel A: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 1, ?, 24001, 'round', 1, 'tls')`, tunnelB, nodeB); err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel B: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(20, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)`, tunnelA); err != nil {
|
||||||
|
t.Fatalf("insert user_tunnel A: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(21, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)`, tunnelB); err != nil {
|
||||||
|
t.Fatalf("insert user_tunnel B: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardRes, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||||
|
VALUES(2, 'batch_switch_user', 'batch-switch-forward', ?, '1.1.1.1:443', 'fifo', 0, 0, ?, ?, 1, 0)
|
||||||
|
`, tunnelA, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert forward: %v", err)
|
||||||
|
}
|
||||||
|
forwardID, _ := forwardRes.LastInsertId()
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeA, 23001); err != nil {
|
||||||
|
t.Fatalf("insert forward_port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := `{"forwardIds":[` + jsonNumber(forwardID) + `],"targetTunnelId":` + jsonNumber(tunnelB) + `}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/batch-change-tunnel", bytes.NewBufferString(payload))
|
||||||
|
req.Header.Set("Authorization", adminToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
var out response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 0 {
|
||||||
|
t.Fatalf("expected API success envelope, got code=%d msg=%q", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := out.Data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected map result, got %T", out.Data)
|
||||||
|
}
|
||||||
|
if int(result["failCount"].(float64)) != 1 {
|
||||||
|
t.Fatalf("expected failCount=1, got %v", result["failCount"])
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunnelAfter int64
|
||||||
|
if err := repo.DB().QueryRow(`SELECT tunnel_id FROM forward WHERE id = ?`, forwardID).Scan(&tunnelAfter); err != nil {
|
||||||
|
t.Fatalf("query forward tunnel_id: %v", err)
|
||||||
|
}
|
||||||
|
if tunnelAfter != tunnelA {
|
||||||
|
t.Fatalf("expected tunnel rollback to %d, got %d", tunnelA, tunnelAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeAfter int64
|
||||||
|
var portAfter int
|
||||||
|
if err := repo.DB().QueryRow(`SELECT node_id, port FROM forward_port WHERE forward_id = ? LIMIT 1`, forwardID).Scan(&nodeAfter, &portAfter); err != nil {
|
||||||
|
t.Fatalf("query forward_port: %v", err)
|
||||||
|
}
|
||||||
|
if nodeAfter != nodeA || portAfter != 23001 {
|
||||||
|
t.Fatalf("expected forward_port rollback to node=%d port=23001, got node=%d port=%d", nodeA, nodeAfter, portAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserTunnelReassignmentKeepsStableID(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, repo := setupContractRouter(t, secret)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||||
|
VALUES(100, 'stable_user', 'pwd', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||||
|
`, now, now); err != nil {
|
||||||
|
t.Fatalf("insert user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelRes, err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES('stable-tunnel', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0)
|
||||||
|
`, now, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert tunnel: %v", err)
|
||||||
|
}
|
||||||
|
tunnelID, _ := tunnelRes.LastInsertId()
|
||||||
|
|
||||||
|
// 1. Assign permission (creates new user_tunnel)
|
||||||
|
// userTunnelBatchAssign expects structure: {userId: 123, tunnels: [{tunnelId: 456, ...}]}
|
||||||
|
assignPayload := `{"userId":100,"tunnels":[{"tunnelId":` + jsonNumber(tunnelID) + `}]}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/batch-assign", bytes.NewBufferString(assignPayload))
|
||||||
|
req.Header.Set("Authorization", adminToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
var out response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 0 {
|
||||||
|
t.Fatalf("expected code 0, got %d msg=%q", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialID int64
|
||||||
|
if err := repo.DB().QueryRow(`SELECT id FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(&initialID); err != nil {
|
||||||
|
t.Fatalf("query initial user_tunnel id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Re-assign permission (should UPDATE, not INSERT)
|
||||||
|
reassignPayload := `{"userId":100,"tunnels":[{"tunnelId":` + jsonNumber(tunnelID) + `}]}`
|
||||||
|
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/batch-assign", bytes.NewBufferString(reassignPayload))
|
||||||
|
req2.Header.Set("Authorization", adminToken)
|
||||||
|
req2.Header.Set("Content-Type", "application/json")
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(res2, req2)
|
||||||
|
|
||||||
|
var out2 response.R
|
||||||
|
if err := json.NewDecoder(res2.Body).Decode(&out2); err != nil {
|
||||||
|
t.Fatalf("decode response 2: %v", err)
|
||||||
|
}
|
||||||
|
if out2.Code != 0 {
|
||||||
|
t.Fatalf("expected code 0, got %d msg=%q", out2.Code, out2.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify stable ID and no duplicates
|
||||||
|
var count int
|
||||||
|
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(&count); err != nil {
|
||||||
|
t.Fatalf("query count: %v", err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatalf("expected exactly 1 user_tunnel record, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentID int64
|
||||||
|
if err := repo.DB().QueryRow(`SELECT id FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(¤tID); err != nil {
|
||||||
|
t.Fatalf("query current user_tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentID != initialID {
|
||||||
|
t.Fatalf("user_tunnel ID changed from %d to %d (unstable ID!)", initialID, currentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func jsonNumber(v int64) string {
|
func jsonNumber(v int64) string {
|
||||||
return strconv.FormatInt(v, 10)
|
return strconv.FormatInt(v, 10)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func JWTAuth() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
|
|
||||||
token := utils.GetToken(c)
|
|
||||||
if token == "" {
|
|
||||||
response.NoAuth("未登录或非法访问,请登录", c)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isBlacklist(token) {
|
|
||||||
response.NoAuth("您的帐户异地登陆或令牌失效", c)
|
|
||||||
utils.ClearToken(c)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
j := utils.NewJWT()
|
|
||||||
// parseToken 解析token包含的信息
|
|
||||||
claims, err := j.ParseToken(token)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, utils.TokenExpired) {
|
|
||||||
response.NoAuth("登录已过期,请重新登录", c)
|
|
||||||
utils.ClearToken(c)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.NoAuth(err.Error(), c)
|
|
||||||
utils.ClearToken(c)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开
|
|
||||||
// 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开
|
|
||||||
|
|
||||||
//if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 {
|
|
||||||
// _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token})
|
|
||||||
// response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
|
|
||||||
// c.Abort()
|
|
||||||
//}
|
|
||||||
c.Set("claims", claims)
|
|
||||||
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
|
|
||||||
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
|
|
||||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
|
|
||||||
newToken, _ := j.CreateTokenByOldToken(token, *claims)
|
|
||||||
newClaims, _ := j.ParseToken(newToken)
|
|
||||||
c.Header("new-token", newToken)
|
|
||||||
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
|
|
||||||
utils.SetToken(c, newToken, int(dr.Seconds()/60))
|
|
||||||
if global.GVA_CONFIG.System.UseMultipoint {
|
|
||||||
// 记录新的活跃jwt
|
|
||||||
_ = utils.SetRedisJWT(newToken, newClaims.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
|
|
||||||
if newToken, exists := c.Get("new-token"); exists {
|
|
||||||
c.Header("new-token", newToken.(string))
|
|
||||||
}
|
|
||||||
if newExpiresAt, exists := c.Get("new-expires-at"); exists {
|
|
||||||
c.Header("new-expires-at", newExpiresAt.(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//@author: [piexlmax](https://github.com/piexlmax)
|
|
||||||
//@function: IsBlacklist
|
|
||||||
//@description: 判断JWT是否在黑名单内部
|
|
||||||
//@param: jwt string
|
|
||||||
//@return: bool
|
|
||||||
|
|
||||||
func isBlacklist(jwt string) bool {
|
|
||||||
_, ok := global.BlackCache.Get(jwt)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ERROR = 7
|
|
||||||
SUCCESS = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func Result(code int, data interface{}, msg string, c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, Response{
|
|
||||||
code,
|
|
||||||
data,
|
|
||||||
msg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ok(c *gin.Context) {
|
|
||||||
Result(SUCCESS, map[string]interface{}{}, "操作成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OkWithMessage(message string, c *gin.Context) {
|
|
||||||
Result(SUCCESS, map[string]interface{}{}, message, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OkWithData(data interface{}, c *gin.Context) {
|
|
||||||
Result(SUCCESS, data, "成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OkWithDetailed(data interface{}, message string, c *gin.Context) {
|
|
||||||
Result(SUCCESS, data, message, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Fail(c *gin.Context) {
|
|
||||||
Result(ERROR, map[string]interface{}{}, "操作失败", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FailWithMessage(message string, c *gin.Context) {
|
|
||||||
Result(ERROR, map[string]interface{}{}, message, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoAuth(message string, c *gin.Context) {
|
|
||||||
c.JSON(http.StatusUnauthorized, Response{
|
|
||||||
7,
|
|
||||||
nil,
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FailWithDetailed(data interface{}, message string, c *gin.Context) {
|
|
||||||
Result(ERROR, data, message, c)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserRouter struct{}
|
|
||||||
|
|
||||||
func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
|
|
||||||
userRouter := Router.Group("user").Use(middleware.OperationRecord())
|
|
||||||
userRouterWithoutRecord := Router.Group("user")
|
|
||||||
{
|
|
||||||
userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号
|
|
||||||
userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码
|
|
||||||
userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限
|
|
||||||
userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户
|
|
||||||
userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息
|
|
||||||
userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息
|
|
||||||
userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组
|
|
||||||
userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码
|
|
||||||
userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置
|
|
||||||
}
|
|
||||||
{
|
|
||||||
userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表
|
|
||||||
userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,563 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
296FEB8A2E56AAF900DC77A3 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 296FEB742E56AAF700DC77A3 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 296FEB7B2E56AAF700DC77A3;
|
|
||||||
remoteInfo = flux;
|
|
||||||
};
|
|
||||||
296FEB942E56AAF900DC77A3 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 296FEB742E56AAF700DC77A3 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 296FEB7B2E56AAF700DC77A3;
|
|
||||||
remoteInfo = flux;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
296FEB7C2E56AAF700DC77A3 /* flux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flux.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
296FEB892E56AAF900DC77A3 /* fluxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fluxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fluxUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
296FEB7E2E56AAF700DC77A3 /* flux */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = flux;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
296FEB8C2E56AAF900DC77A3 /* fluxTests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = fluxTests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
296FEB962E56AAF900DC77A3 /* fluxUITests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = fluxUITests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
296FEB792E56AAF700DC77A3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB862E56AAF900DC77A3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB902E56AAF900DC77A3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
296FEB732E56AAF700DC77A3 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
296FEB7E2E56AAF700DC77A3 /* flux */,
|
|
||||||
296FEB8C2E56AAF900DC77A3 /* fluxTests */,
|
|
||||||
296FEB962E56AAF900DC77A3 /* fluxUITests */,
|
|
||||||
296FEB7D2E56AAF700DC77A3 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
296FEB7D2E56AAF700DC77A3 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
296FEB7C2E56AAF700DC77A3 /* flux.app */,
|
|
||||||
296FEB892E56AAF900DC77A3 /* fluxTests.xctest */,
|
|
||||||
296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
296FEB7B2E56AAF700DC77A3 /* flux */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 296FEB9D2E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "flux" */;
|
|
||||||
buildPhases = (
|
|
||||||
296FEB782E56AAF700DC77A3 /* Sources */,
|
|
||||||
296FEB792E56AAF700DC77A3 /* Frameworks */,
|
|
||||||
296FEB7A2E56AAF700DC77A3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
296FEB7E2E56AAF700DC77A3 /* flux */,
|
|
||||||
);
|
|
||||||
name = flux;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = flux;
|
|
||||||
productReference = 296FEB7C2E56AAF700DC77A3 /* flux.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
296FEB882E56AAF900DC77A3 /* fluxTests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 296FEBA02E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxTests" */;
|
|
||||||
buildPhases = (
|
|
||||||
296FEB852E56AAF900DC77A3 /* Sources */,
|
|
||||||
296FEB862E56AAF900DC77A3 /* Frameworks */,
|
|
||||||
296FEB872E56AAF900DC77A3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
296FEB8B2E56AAF900DC77A3 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
296FEB8C2E56AAF900DC77A3 /* fluxTests */,
|
|
||||||
);
|
|
||||||
name = fluxTests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = fluxTests;
|
|
||||||
productReference = 296FEB892E56AAF900DC77A3 /* fluxTests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
|
||||||
};
|
|
||||||
296FEB922E56AAF900DC77A3 /* fluxUITests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 296FEBA32E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxUITests" */;
|
|
||||||
buildPhases = (
|
|
||||||
296FEB8F2E56AAF900DC77A3 /* Sources */,
|
|
||||||
296FEB902E56AAF900DC77A3 /* Frameworks */,
|
|
||||||
296FEB912E56AAF900DC77A3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
296FEB952E56AAF900DC77A3 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
296FEB962E56AAF900DC77A3 /* fluxUITests */,
|
|
||||||
);
|
|
||||||
name = fluxUITests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = fluxUITests;
|
|
||||||
productReference = 296FEB932E56AAF900DC77A3 /* fluxUITests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
296FEB742E56AAF700DC77A3 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 1640;
|
|
||||||
LastUpgradeCheck = 1640;
|
|
||||||
TargetAttributes = {
|
|
||||||
296FEB7B2E56AAF700DC77A3 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
};
|
|
||||||
296FEB882E56AAF900DC77A3 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
TestTargetID = 296FEB7B2E56AAF700DC77A3;
|
|
||||||
};
|
|
||||||
296FEB922E56AAF900DC77A3 = {
|
|
||||||
CreatedOnToolsVersion = 16.4;
|
|
||||||
TestTargetID = 296FEB7B2E56AAF700DC77A3;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = 296FEB772E56AAF700DC77A3 /* Build configuration list for PBXProject "flux" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = 296FEB732E56AAF700DC77A3;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 296FEB7D2E56AAF700DC77A3 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
296FEB7B2E56AAF700DC77A3 /* flux */,
|
|
||||||
296FEB882E56AAF900DC77A3 /* fluxTests */,
|
|
||||||
296FEB922E56AAF900DC77A3 /* fluxUITests */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
296FEB7A2E56AAF700DC77A3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB872E56AAF900DC77A3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB912E56AAF900DC77A3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
296FEB782E56AAF700DC77A3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB852E56AAF900DC77A3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
296FEB8F2E56AAF900DC77A3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
296FEB8B2E56AAF900DC77A3 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 296FEB7B2E56AAF700DC77A3 /* flux */;
|
|
||||||
targetProxy = 296FEB8A2E56AAF900DC77A3 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
296FEB952E56AAF900DC77A3 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 296FEB7B2E56AAF700DC77A3 /* flux */;
|
|
||||||
targetProxy = 296FEB942E56AAF900DC77A3 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
296FEB9B2E56AAF900DC77A3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
296FEB9C2E56AAF900DC77A3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
296FEB9E2E56AAF900DC77A3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XRXD4YN2Y6;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.flux;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
296FEB9F2E56AAF900DC77A3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XRXD4YN2Y6;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.flux;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
296FEBA12E56AAF900DC77A3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flux";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
296FEBA22E56AAF900DC77A3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flux";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
296FEBA42E56AAF900DC77A3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_TARGET_NAME = flux;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
296FEBA52E56AAF900DC77A3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = bqlpfy.fluxUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_TARGET_NAME = flux;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
296FEB772E56AAF700DC77A3 /* Build configuration list for PBXProject "flux" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
296FEB9B2E56AAF900DC77A3 /* Debug */,
|
|
||||||
296FEB9C2E56AAF900DC77A3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
296FEB9D2E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "flux" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
296FEB9E2E56AAF900DC77A3 /* Debug */,
|
|
||||||
296FEB9F2E56AAF900DC77A3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
296FEBA02E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxTests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
296FEBA12E56AAF900DC77A3 /* Debug */,
|
|
||||||
296FEBA22E56AAF900DC77A3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
296FEBA32E56AAF900DC77A3 /* Build configuration list for PBXNativeTarget "fluxUITests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
296FEBA42E56AAF900DC77A3 /* Debug */,
|
|
||||||
296FEBA52E56AAF900DC77A3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = 296FEB742E56AAF700DC77A3 /* Project object */;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>top-level-items</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>children</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>children</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/bg-Ck8MrDZ5.jpg</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/favicon-DI5FVxIz.ico</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/index-Bf4rZ0gS.css</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/index-ByTRlFFk.js</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/index-Drx943H4.js</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/index-legacy-BYgRAl5I.js</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/index-legacy-DUPUAMxs.js</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/assets/polyfills-legacy-DIE4kOxF.js</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>title</key>
|
|
||||||
<string>assets</string>
|
|
||||||
<key>type</key>
|
|
||||||
<string>group</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/index.html</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>destination</key>
|
|
||||||
<dict>
|
|
||||||
<key>rebasable-url</key>
|
|
||||||
<dict>
|
|
||||||
<key>base</key>
|
|
||||||
<string>workspace</string>
|
|
||||||
<key>payload</key>
|
|
||||||
<dict>
|
|
||||||
<key>relative-path</key>
|
|
||||||
<string>flux/dist/vite.svg</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>DVTDocumentLocation</string>
|
|
||||||
</dict>
|
|
||||||
<key>type</key>
|
|
||||||
<string>bookmark</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>title</key>
|
|
||||||
<string>dist</string>
|
|
||||||
<key>type</key>
|
|
||||||
<string>group</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>flux.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import WebKit
|
|
||||||
|
|
||||||
|
|
||||||
struct WebView: UIViewRepresentable {
|
|
||||||
let fileName: String?
|
|
||||||
let urlString: String?
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
|
||||||
let config = WKWebViewConfiguration()
|
|
||||||
let userContentController = WKUserContentController()
|
|
||||||
|
|
||||||
// 注册 JS -> Native 消息通道
|
|
||||||
userContentController.add(context.coordinator, name: "getPanelAddresses")
|
|
||||||
userContentController.add(context.coordinator, name: "savePanelAddress")
|
|
||||||
userContentController.add(context.coordinator, name: "setCurrentPanelAddress")
|
|
||||||
userContentController.add(context.coordinator, name: "deletePanelAddress")
|
|
||||||
|
|
||||||
// 注入 CSS 隐藏滚动条
|
|
||||||
let css = "body::-webkit-scrollbar { display: none; }"
|
|
||||||
let cssJS = """
|
|
||||||
var style = document.createElement('style');
|
|
||||||
style.innerHTML = '\(css)';
|
|
||||||
document.head.appendChild(style);
|
|
||||||
"""
|
|
||||||
let cssScript = WKUserScript(source: cssJS, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
|
|
||||||
userContentController.addUserScript(cssScript)
|
|
||||||
|
|
||||||
config.userContentController = userContentController
|
|
||||||
|
|
||||||
let webView = WKWebView(frame: .zero, configuration: config)
|
|
||||||
context.coordinator.webView = webView
|
|
||||||
|
|
||||||
// 去掉滚动条和弹性
|
|
||||||
webView.scrollView.showsHorizontalScrollIndicator = false
|
|
||||||
webView.scrollView.showsVerticalScrollIndicator = false
|
|
||||||
webView.scrollView.bounces = false
|
|
||||||
|
|
||||||
// 加载本地 HTML 或远程 URL
|
|
||||||
if let fileName = fileName,
|
|
||||||
let htmlURL = Bundle.main.url(forResource: fileName, withExtension: "html") {
|
|
||||||
webView.loadFileURL(htmlURL, allowingReadAccessTo: htmlURL.deletingLastPathComponent())
|
|
||||||
} else if let urlString = urlString,
|
|
||||||
let url = URL(string: urlString) {
|
|
||||||
webView.load(URLRequest(url: url))
|
|
||||||
}
|
|
||||||
|
|
||||||
return webView
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
|
||||||
|
|
||||||
class Coordinator: NSObject, WKScriptMessageHandler {
|
|
||||||
weak var webView: WKWebView?
|
|
||||||
private let storageKey = "panel_addresses"
|
|
||||||
|
|
||||||
|
|
||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
||||||
switch message.name {
|
|
||||||
case "getPanelAddresses":
|
|
||||||
handleGetPanelAddresses(message: message)
|
|
||||||
case "savePanelAddress":
|
|
||||||
handleSavePanelAddress(message: message)
|
|
||||||
case "setCurrentPanelAddress":
|
|
||||||
handleSetCurrentPanelAddress(message: message)
|
|
||||||
case "deletePanelAddress":
|
|
||||||
handleDeletePanelAddress(message: message)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func handleGetPanelAddresses(message: WKScriptMessage) {
|
|
||||||
let callbackName: String
|
|
||||||
if let cb = message.body as? String, !cb.isEmpty {
|
|
||||||
callbackName = cb
|
|
||||||
} else {
|
|
||||||
callbackName = "setPanelAddresses"
|
|
||||||
}
|
|
||||||
|
|
||||||
let addresses = loadAddressesJSONString()
|
|
||||||
let js = "window.\(callbackName)(\(addresses));"
|
|
||||||
evaluate(js: js)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSavePanelAddress(message: WKScriptMessage) {
|
|
||||||
var name: String?
|
|
||||||
var address: String?
|
|
||||||
|
|
||||||
if let dict = message.body as? [String: Any] {
|
|
||||||
name = dict["name"] as? String
|
|
||||||
address = dict["address"] as? String
|
|
||||||
} else if let arr = message.body as? [Any], arr.count >= 2 {
|
|
||||||
name = arr[0] as? String
|
|
||||||
address = arr[1] as? String
|
|
||||||
} else if let jsonString = message.body as? String,
|
|
||||||
let data = jsonString.data(using: .utf8),
|
|
||||||
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
||||||
name = dict["name"] as? String
|
|
||||||
address = dict["address"] as? String
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let panelName = name, let panelAddress = address, !panelName.isEmpty, !panelAddress.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var array = loadAddressesArray()
|
|
||||||
let newItem: [String: Any] = [
|
|
||||||
"name": panelName,
|
|
||||||
"address": panelAddress,
|
|
||||||
"inx": false
|
|
||||||
]
|
|
||||||
array.append(newItem)
|
|
||||||
saveAddressesArray(array)
|
|
||||||
|
|
||||||
let js = "window.setPanelAddresses(\(toJSONString(array)));"
|
|
||||||
evaluate(js: js)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSetCurrentPanelAddress(message: WKScriptMessage) {
|
|
||||||
var name: String?
|
|
||||||
if let str = message.body as? String {
|
|
||||||
name = str
|
|
||||||
} else if let dict = message.body as? [String: Any] {
|
|
||||||
name = dict["name"] as? String
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let targetName = name else { return }
|
|
||||||
|
|
||||||
var array = loadAddressesArray()
|
|
||||||
for i in 0..<array.count {
|
|
||||||
var obj = array[i]
|
|
||||||
if let n = obj["name"] as? String, n == targetName {
|
|
||||||
obj["inx"] = true
|
|
||||||
} else {
|
|
||||||
obj["inx"] = false
|
|
||||||
}
|
|
||||||
array[i] = obj
|
|
||||||
}
|
|
||||||
saveAddressesArray(array)
|
|
||||||
let js = "window.setPanelAddresses(\(toJSONString(array)));"
|
|
||||||
evaluate(js: js)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleDeletePanelAddress(message: WKScriptMessage) {
|
|
||||||
var name: String?
|
|
||||||
if let str = message.body as? String {
|
|
||||||
name = str
|
|
||||||
} else if let dict = message.body as? [String: Any] {
|
|
||||||
name = dict["name"] as? String
|
|
||||||
}
|
|
||||||
guard let targetName = name else { return }
|
|
||||||
|
|
||||||
let oldArray = loadAddressesArray()
|
|
||||||
let newArray = oldArray.filter { obj in
|
|
||||||
guard let n = obj["name"] as? String else { return true }
|
|
||||||
return n != targetName
|
|
||||||
}
|
|
||||||
saveAddressesArray(newArray)
|
|
||||||
let js = "window.setPanelAddresses(\(toJSONString(newArray)));"
|
|
||||||
evaluate(js: js)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadAddressesJSONString() -> String {
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
if let s = defaults.string(forKey: storageKey), !s.isEmpty {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadAddressesArray() -> [[String: Any]] {
|
|
||||||
let s = loadAddressesJSONString()
|
|
||||||
guard let data = s.data(using: .utf8),
|
|
||||||
let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveAddressesArray(_ array: [[String: Any]]) {
|
|
||||||
let jsonString = toJSONString(array)
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
defaults.set(jsonString, forKey: storageKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toJSONString(_ array: [[String: Any]]) -> String {
|
|
||||||
if let data = try? JSONSerialization.data(withJSONObject: array, options: []),
|
|
||||||
let s = String(data: data, encoding: .utf8) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func evaluate(js: String) {
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.webView?.evaluateJavaScript(js, completionHandler: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
var body: some View {
|
|
||||||
//WebView(fileName: nil, urlString: "http://192.168.100.9:3000")
|
|
||||||
WebView(fileName: "index", urlString: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// fluxApp.swift
|
|
||||||
// flux
|
|
||||||
//
|
|
||||||
// Created by 何磊 on 21/08/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct fluxApp: App {
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// fluxTests.swift
|
|
||||||
// fluxTests
|
|
||||||
//
|
|
||||||
// Created by 何磊 on 21/08/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Testing
|
|
||||||
@testable import flux
|
|
||||||
|
|
||||||
struct fluxTests {
|
|
||||||
|
|
||||||
@Test func example() async throws {
|
|
||||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
//
|
|
||||||
// fluxUITests.swift
|
|
||||||
// fluxUITests
|
|
||||||
//
|
|
||||||
// Created by 何磊 on 21/08/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class fluxUITests: XCTestCase {
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
|
||||||
continueAfterFailure = false
|
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testExample() throws {
|
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testLaunchPerformance() throws {
|
|
||||||
// This measures how long it takes to launch your application.
|
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
||||||
XCUIApplication().launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// fluxUITestsLaunchTests.swift
|
|
||||||
// fluxUITests
|
|
||||||
//
|
|
||||||
// Created by 何磊 on 21/08/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class fluxUITestsLaunchTests: XCTestCase {
|
|
||||||
|
|
||||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
continueAfterFailure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func testLaunch() throws {
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
|
||||||
// such as logging into a test account or navigating somewhere in the app
|
|
||||||
|
|
||||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
|
||||||
attachment.name = "Launch Screen"
|
|
||||||
attachment.lifetime = .keepAlways
|
|
||||||
add(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-is": "^19.2.4",
|
"react-is": "^19.2.4",
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "6.30.3",
|
||||||
|
"react-turnstile": "^1.1.5",
|
||||||
"recharts": "^3.1.1",
|
"recharts": "^3.1.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-variants": "1.0.0",
|
"tailwind-variants": "1.0.0",
|
||||||
|
|||||||
@@ -70,39 +70,22 @@ const CONFIG_ITEMS: ConfigItem[] = [
|
|||||||
type: "switch",
|
type: "switch",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "captcha_type",
|
key: "cloudflare_site_key",
|
||||||
label: "验证码类型",
|
label: "Cloudflare Site Key",
|
||||||
description: "选择验证码的显示类型,不同类型有不同的安全级别",
|
placeholder: "请输入 Cloudflare Site Key",
|
||||||
type: "select",
|
description: "Cloudflare Turnstile 站点密钥",
|
||||||
|
type: "input",
|
||||||
|
dependsOn: "captcha_enabled",
|
||||||
|
dependsValue: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cloudflare_secret_key",
|
||||||
|
label: "Cloudflare Secret Key",
|
||||||
|
placeholder: "请输入 Cloudflare Secret Key",
|
||||||
|
description: "Cloudflare Turnstile 密钥",
|
||||||
|
type: "input",
|
||||||
dependsOn: "captcha_enabled",
|
dependsOn: "captcha_enabled",
|
||||||
dependsValue: "true",
|
dependsValue: "true",
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "随机类型",
|
|
||||||
value: "RANDOM",
|
|
||||||
description: "系统随机选择验证码类型",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "滑块验证码",
|
|
||||||
value: "SLIDER",
|
|
||||||
description: "拖动滑块完成拼图验证",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "文字点选验证码",
|
|
||||||
value: "WORD_IMAGE_CLICK",
|
|
||||||
description: "按顺序点击指定文字",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "旋转验证码",
|
|
||||||
value: "ROTATE",
|
|
||||||
description: "旋转图片到正确角度",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "拼图验证码",
|
|
||||||
value: "CONCAT",
|
|
||||||
description: "拖动滑块完成图片拼接",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -110,7 +93,13 @@ const CONFIG_ITEMS: ConfigItem[] = [
|
|||||||
const getInitialConfigs = (): Record<string, string> => {
|
const getInitialConfigs = (): Record<string, string> => {
|
||||||
if (typeof window === "undefined") return {};
|
if (typeof window === "undefined") return {};
|
||||||
|
|
||||||
const configKeys = ["app_name", "captcha_enabled", "captcha_type", "ip"];
|
const configKeys = [
|
||||||
|
"app_name",
|
||||||
|
"captcha_enabled",
|
||||||
|
"cloudflare_site_key",
|
||||||
|
"cloudflare_secret_key",
|
||||||
|
"ip",
|
||||||
|
];
|
||||||
const initialConfigs: Record<string, string> = {};
|
const initialConfigs: Record<string, string> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,14 +182,7 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
// 处理配置项变更
|
// 处理配置项变更
|
||||||
const handleConfigChange = (key: string, value: string) => {
|
const handleConfigChange = (key: string, value: string) => {
|
||||||
let newConfigs = { ...configs, [key]: value };
|
const newConfigs = { ...configs, [key]: value };
|
||||||
|
|
||||||
// 特殊处理:启用验证码时,如果验证码类型未设置,默认为随机
|
|
||||||
if (key === "captcha_enabled" && value === "true") {
|
|
||||||
if (!newConfigs.captcha_type) {
|
|
||||||
newConfigs.captcha_type = "RANDOM";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfigs(newConfigs);
|
setConfigs(newConfigs);
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import axios from "axios";
|
import Turnstile from "react-turnstile";
|
||||||
|
|
||||||
import { isWebViewFunc } from "@/utils/panel";
|
import { isWebViewFunc } from "@/utils/panel";
|
||||||
import { siteConfig } from "@/config/site";
|
import { siteConfig } from "@/config/site";
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import DefaultLayout from "@/layouts/default";
|
import DefaultLayout from "@/layouts/default";
|
||||||
import { login, LoginData, checkCaptcha } from "@/api";
|
import { login, LoginData, checkCaptcha, getConfigByName } from "@/api";
|
||||||
import "@/utils/tac.css";
|
|
||||||
import "@/utils/tac.min.js";
|
|
||||||
import bgImage from "@/images/bg.jpg";
|
|
||||||
|
|
||||||
interface LoginForm {
|
interface LoginForm {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -21,37 +18,6 @@ interface LoginForm {
|
|||||||
captchaId: string;
|
captchaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CaptchaConfig {
|
|
||||||
requestCaptchaDataUrl: string;
|
|
||||||
validCaptchaUrl: string;
|
|
||||||
bindEl: string;
|
|
||||||
validSuccess: (res: any, captcha: any, tac: any) => void;
|
|
||||||
validFail?: (res: any, captcha: any, tac: any) => void;
|
|
||||||
btnCloseFun?: (event: any, tac: any) => void;
|
|
||||||
btnRefreshFun?: (event: any, tac: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CaptchaStyle {
|
|
||||||
btnUrl?: string;
|
|
||||||
bgUrl?: string;
|
|
||||||
logoUrl?: string | null;
|
|
||||||
moveTrackMaskBgColor?: string;
|
|
||||||
moveTrackMaskBorderColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CaptchaGeneratePayload {
|
|
||||||
id?: string;
|
|
||||||
captcha?: {
|
|
||||||
type?: string;
|
|
||||||
backgroundImage?: string;
|
|
||||||
templateImage?: string;
|
|
||||||
};
|
|
||||||
data?: {
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
success?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const [form, setForm] = useState<LoginForm>({
|
const [form, setForm] = useState<LoginForm>({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -61,93 +27,15 @@ export default function IndexPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errors, setErrors] = useState<Partial<LoginForm>>({});
|
const [errors, setErrors] = useState<Partial<LoginForm>>({});
|
||||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||||
|
const [siteKey, setSiteKey] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const tacInstanceRef = useRef<any>(null);
|
|
||||||
const captchaContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isWebView, setIsWebView] = useState(false);
|
const [isWebView, setIsWebView] = useState(false);
|
||||||
|
|
||||||
// 清理验证码实例
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (tacInstanceRef.current) {
|
|
||||||
tacInstanceRef.current.destroyWindow();
|
|
||||||
tacInstanceRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
// 检测是否在WebView中运行
|
// 检测是否在WebView中运行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsWebView(isWebViewFunc());
|
setIsWebView(isWebViewFunc());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resolveCaptchaBaseURL = () =>
|
|
||||||
axios.defaults.baseURL ||
|
|
||||||
(import.meta.env.VITE_API_BASE
|
|
||||||
? `${import.meta.env.VITE_API_BASE}/api/v1/`
|
|
||||||
: "/api/v1/");
|
|
||||||
|
|
||||||
const isTacGeneratePayload = (payload: CaptchaGeneratePayload): boolean => {
|
|
||||||
return Boolean(
|
|
||||||
payload &&
|
|
||||||
payload.id &&
|
|
||||||
payload.captcha &&
|
|
||||||
typeof payload.captcha.type === "string" &&
|
|
||||||
payload.captcha.type.length > 0,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractCaptchaId = (payload: CaptchaGeneratePayload): string => {
|
|
||||||
if (typeof payload?.id === "string" && payload.id.trim()) {
|
|
||||||
return payload.id;
|
|
||||||
}
|
|
||||||
if (typeof payload?.data?.id === "string" && payload.data.id.trim()) {
|
|
||||||
return payload.data.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyInCompatibilityMode = async (
|
|
||||||
baseURL: string,
|
|
||||||
payload: CaptchaGeneratePayload,
|
|
||||||
): Promise<string> => {
|
|
||||||
const captchaId = extractCaptchaId(payload);
|
|
||||||
|
|
||||||
if (!captchaId) {
|
|
||||||
throw new Error("验证码初始化失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyResp = await axios.post(
|
|
||||||
`${baseURL}captcha/verify`,
|
|
||||||
{
|
|
||||||
captchaId,
|
|
||||||
trackData: JSON.stringify({ mode: "compat", ts: Date.now() }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: 30000,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const verifyData = verifyResp?.data || {};
|
|
||||||
const success =
|
|
||||||
verifyData.success === true ||
|
|
||||||
verifyData.code === 0 ||
|
|
||||||
verifyData.code === 200;
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(verifyData.msg || verifyData.message || "验证码校验失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
const validToken =
|
|
||||||
verifyData?.data?.validToken &&
|
|
||||||
typeof verifyData.data.validToken === "string"
|
|
||||||
? verifyData.data.validToken
|
|
||||||
: captchaId;
|
|
||||||
|
|
||||||
return validToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 验证表单
|
// 验证表单
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Partial<LoginForm> = {};
|
const newErrors: Partial<LoginForm> = {};
|
||||||
@@ -176,86 +64,6 @@ export default function IndexPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化验证码
|
|
||||||
const initCaptcha = async () => {
|
|
||||||
try {
|
|
||||||
// 清理之前的验证码实例
|
|
||||||
if (tacInstanceRef.current) {
|
|
||||||
tacInstanceRef.current.destroyWindow();
|
|
||||||
tacInstanceRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseURL = resolveCaptchaBaseURL();
|
|
||||||
const hasTacRenderer = Boolean(window.TAC && captchaContainerRef.current);
|
|
||||||
|
|
||||||
const generateResp = await axios.post<CaptchaGeneratePayload>(
|
|
||||||
`${baseURL}captcha/generate`,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
timeout: 30000,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const generatePayload = generateResp?.data || {};
|
|
||||||
|
|
||||||
if (!hasTacRenderer || !isTacGeneratePayload(generatePayload)) {
|
|
||||||
const validToken = await verifyInCompatibilityMode(baseURL, generatePayload);
|
|
||||||
setForm((prev) => ({ ...prev, captchaId: validToken }));
|
|
||||||
setShowCaptcha(false);
|
|
||||||
await performLogin(validToken);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: CaptchaConfig = {
|
|
||||||
requestCaptchaDataUrl: `${baseURL}captcha/generate`,
|
|
||||||
validCaptchaUrl: `${baseURL}captcha/verify`,
|
|
||||||
bindEl: "#captcha-container",
|
|
||||||
validSuccess: (res: any, _: any, tac: any) => {
|
|
||||||
const validToken = res?.data?.validToken || "";
|
|
||||||
setForm((prev) => ({ ...prev, captchaId: validToken }));
|
|
||||||
setShowCaptcha(false);
|
|
||||||
tac.destroyWindow();
|
|
||||||
void performLogin(validToken);
|
|
||||||
},
|
|
||||||
validFail: (_: any, _captcha: any, tac: any) => {
|
|
||||||
tac.reloadCaptcha();
|
|
||||||
},
|
|
||||||
btnCloseFun: (_event: any, tac: any) => {
|
|
||||||
setShowCaptcha(false);
|
|
||||||
tac.destroyWindow();
|
|
||||||
setLoading(false);
|
|
||||||
},
|
|
||||||
btnRefreshFun: (_event: any, tac: any) => {
|
|
||||||
tac.reloadCaptcha();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检测暗黑模式
|
|
||||||
const isDarkMode =
|
|
||||||
document.documentElement.classList.contains("dark") ||
|
|
||||||
document.documentElement.getAttribute("data-theme") === "dark" ||
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
||||||
|
|
||||||
// 根据主题调整颜色
|
|
||||||
const trackColor = isDarkMode ? "#4a5568" : "#7db0be"; // 暗黑模式使用更深的灰蓝色
|
|
||||||
|
|
||||||
const style: CaptchaStyle = {
|
|
||||||
bgUrl: bgImage,
|
|
||||||
logoUrl: null,
|
|
||||||
moveTrackMaskBgColor: trackColor,
|
|
||||||
moveTrackMaskBorderColor: trackColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
tacInstanceRef.current = new window.TAC(config, style);
|
|
||||||
tacInstanceRef.current.init();
|
|
||||||
} catch {
|
|
||||||
toast.error("验证码初始化失败,请刷新页面重试");
|
|
||||||
setShowCaptcha(false);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行登录请求
|
// 执行登录请求
|
||||||
const performLogin = async (captchaToken?: string) => {
|
const performLogin = async (captchaToken?: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -274,7 +82,9 @@ export default function IndexPage() {
|
|||||||
|
|
||||||
if (response.code !== 0) {
|
if (response.code !== 0) {
|
||||||
toast.error(response.msg || "登录失败");
|
toast.error(response.msg || "登录失败");
|
||||||
|
if (showCaptcha) {
|
||||||
|
setForm((prev) => ({ ...prev, captchaId: "" }));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,15 +134,21 @@ export default function IndexPage() {
|
|||||||
|
|
||||||
// 根据返回值决定是否显示验证码
|
// 根据返回值决定是否显示验证码
|
||||||
if (checkResponse.data === 0) {
|
if (checkResponse.data === 0) {
|
||||||
// 不需要验证码,直接登录
|
|
||||||
await performLogin();
|
await performLogin();
|
||||||
} else {
|
} else {
|
||||||
// 需要验证码,显示验证码弹层
|
const configResp = await getConfigByName("cloudflare_site_key");
|
||||||
setShowCaptcha(true);
|
|
||||||
// 延时初始化验证码,确保DOM已渲染
|
if (
|
||||||
setTimeout(() => {
|
configResp.code === 0 &&
|
||||||
initCaptcha();
|
configResp.data &&
|
||||||
}, 100);
|
configResp.data.value
|
||||||
|
) {
|
||||||
|
setSiteKey(configResp.data.value);
|
||||||
|
setShowCaptcha(true);
|
||||||
|
} else {
|
||||||
|
toast.error("未配置Cloudflare Site Key,请联系管理员");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("网络错误,请稍后重试" + error);
|
toast.error("网络错误,请稍后重试" + error);
|
||||||
@@ -422,26 +238,50 @@ export default function IndexPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 验证码弹层 */}
|
{/* 验证码弹层 */}
|
||||||
{showCaptcha && (
|
{showCaptcha && siteKey && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
|
{/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
|
||||||
<div className="absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm captcha-backdrop-enter" />
|
<div
|
||||||
|
className="absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm captcha-backdrop-enter"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
setShowCaptcha(false);
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
setShowCaptcha(false);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/* 验证码容器 */}
|
{/* 验证码容器 */}
|
||||||
<div className="mb-4">
|
<div className="mb-4 relative z-50 bg-white dark:bg-zinc-900 p-6 rounded-lg shadow-xl">
|
||||||
<div
|
<div className="mb-4 text-center text-sm font-medium text-gray-700 dark:text-gray-200">请完成安全验证</div>
|
||||||
ref={captchaContainerRef}
|
<div className="flex justify-center">
|
||||||
className="w-full flex justify-center"
|
<Turnstile
|
||||||
id="captcha-container"
|
sitekey={siteKey}
|
||||||
style={{
|
onVerify={(token) => {
|
||||||
filter:
|
setForm((prev) => ({ ...prev, captchaId: token }));
|
||||||
|
void performLogin(token);
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
toast.error("验证失败,请刷新重试");
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
onExpire={() => {
|
||||||
|
setForm((prev) => ({ ...prev, captchaId: "" }));
|
||||||
|
}}
|
||||||
|
theme={
|
||||||
document.documentElement.classList.contains("dark") ||
|
document.documentElement.classList.contains("dark") ||
|
||||||
document.documentElement.getAttribute("data-theme") ===
|
document.documentElement.getAttribute("data-theme") === "dark" ||
|
||||||
"dark" ||
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
? "brightness(0.8) contrast(0.9)"
|
? "dark"
|
||||||
: "none",
|
: "light"
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:318px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:50px;height:50px;text-align:center;display:block;z-index:998;position:absolute;top:105px;left:126px;-moz-user-select:none;-webkit-user-select:none;user-select:none;background-image:url(../images/loading.gif);background-size:cover}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-bottom .close-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-repeat:no-repeat;background-position:0 -14px;float:right;margin-right:2px;cursor:pointer}#tianai-captcha-parent .slider-bottom .refresh-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-position:0 -167px;background-repeat:no-repeat;float:right;margin-right:10px;cursor:pointer}#tianai-captcha-parent .slider-bottom .logo{height:30px;float:left}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px}
|
|
||||||
#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}@keyframes myanimation{from{left:0}to{left:289px}}
|
|
||||||
#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0;user-select:none}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer}
|
|
||||||
#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block}
|
|
||||||
#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px}
|
|
||||||
#tianai-captcha.tianai-captcha-word-click{position:relative;box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{width:130px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box}
|
|
||||||