separate frontend repository

This commit is contained in:
Alireza Ahmadi
2025-01-28 23:57:53 +01:00
parent f4b1b09362
commit 7faa28a89d
194 changed files with 48 additions and 19256 deletions
+63
View File
@@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"s-ui/config"
"s-ui/database"
"s-ui/service"
)
func resetAdmin() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
err = userService.UpdateFirstUser("admin", "admin")
if err != nil {
fmt.Println("reset admin credentials failed:", err)
} else {
fmt.Println("reset admin credentials success")
}
}
func updateAdmin(username string, password string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
if username != "" || password != "" {
userService := service.UserService{}
err := userService.UpdateFirstUser(username, password)
if err != nil {
fmt.Println("reset admin credentials failed:", err)
} else {
fmt.Println("reset admin credentials success")
}
}
}
func showAdmin() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
userModel, err := userService.GetFirstUser()
if err != nil {
fmt.Println("get current user info failed,error info:", err)
}
username := userModel.Username
userpasswd := userModel.Password
if (username == "") || (userpasswd == "") {
fmt.Println("current username or password is empty")
}
fmt.Println("First admin credentials:")
fmt.Println("\tUsername:\t", username)
fmt.Println("\tPassword:\t", userpasswd)
}
+101
View File
@@ -0,0 +1,101 @@
package cmd
import (
"flag"
"fmt"
"os"
"s-ui/cmd/migration"
"s-ui/config"
)
func ParseCmd() {
var showVersion bool
flag.BoolVar(&showVersion, "v", false, "show version")
adminCmd := flag.NewFlagSet("admin", flag.ExitOnError)
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var username string
var password string
var port int
var path string
var subPort int
var subPath string
var reset bool
var show bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&path, "path", "", "set panel path")
settingCmd.IntVar(&subPort, "subPort", 0, "set sub port")
settingCmd.StringVar(&subPath, "subPath", "", "set sub path")
adminCmd.BoolVar(&show, "show", false, "show first admin credentials")
adminCmd.BoolVar(&reset, "reset", false, "reset first admin credentials")
adminCmd.StringVar(&username, "username", "", "set login username")
adminCmd.StringVar(&password, "password", "", "set login password")
oldUsage := flag.Usage
flag.Usage = func() {
oldUsage()
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" admin set/reset/show first admin credentials")
fmt.Println(" uri Show panel URI")
fmt.Println(" migrate migrate form older version")
fmt.Println(" setting set/reset/show settings")
fmt.Println()
adminCmd.Usage()
fmt.Println()
settingCmd.Usage()
}
flag.Parse()
if showVersion {
fmt.Println(config.GetVersion())
return
}
switch os.Args[1] {
case "admin":
err := adminCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
switch {
case show:
showAdmin()
case reset:
resetAdmin()
default:
updateAdmin(username, password)
showAdmin()
}
case "uri":
getPanelURI()
case "migrate":
migration.MigrateDb()
case "setting":
err := settingCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
switch {
case show:
showSetting()
case reset:
resetSetting()
default:
updateSetting(port, path, subPort, subPath)
showSetting()
}
default:
fmt.Println("Invalid subcommands")
flag.Usage()
}
}
+87
View File
@@ -0,0 +1,87 @@
package migration
import (
"encoding/json"
"fmt"
"s-ui/database/model"
"strings"
"gorm.io/gorm"
)
func migrateClientSchema(db *gorm.DB) error {
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
if err != nil {
fmt.Println(err)
return err
}
defer rows.Close()
for rows.Next() {
var (
cid int
cname string
ctype string
notnull int
dfltValue interface{}
pk int
)
rows.Scan(&cid, &cname, &ctype, &notnull, &dfltValue, &pk)
if cname == "config" || cname == "inbounds" || cname == "links" {
if ctype == "text" {
fmt.Printf("Column %s has type TEXT\n", cname)
oldData := make([]struct {
Id uint
Data string
}, 0)
db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData)
for _, data := range oldData {
var newData []byte
switch cname {
case "inbounds":
inbounds := strings.Split(data.Data, ",")
newData, _ = json.MarshalIndent(inbounds, "", " ")
case "config":
jsonData := map[string]interface{}{}
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, "", " ")
case "links":
jsonData := make([]interface{}, 0)
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, "", " ")
}
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
if err != nil {
return err
}
}
}
}
}
return nil
}
func deleteOldWebSecret(db *gorm.DB) error {
return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error
}
func changesObj(db *gorm.DB) error {
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
}
func to1_1(db *gorm.DB) error {
err := migrateClientSchema(db)
if err != nil {
return err
}
err = deleteOldWebSecret(db)
if err != nil {
return err
}
err = changesObj(db)
if err != nil {
return err
}
return nil
}
+316
View File
@@ -0,0 +1,316 @@
package migration
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"s-ui/database/model"
"gorm.io/gorm"
)
type InboundData struct {
Id uint
Tag string
Addrs json.RawMessage
OutJson json.RawMessage
}
func moveJsonToDb(db *gorm.DB) error {
binFolderPath := os.Getenv("SUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return err
}
configPath := dir + "/" + binFolderPath + "/config.json"
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
return nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return err
}
var oldConfig map[string]interface{}
err = json.Unmarshal(data, &oldConfig)
if err != nil {
return err
}
oldInbounds := oldConfig["inbounds"].([]interface{})
db.Migrator().DropTable(&model.Inbound{})
db.AutoMigrate(&model.Inbound{})
for _, inbound := range oldInbounds {
inbObj, _ := inbound.(map[string]interface{})
tag, _ := inbObj["tag"].(string)
if tlsObj, ok := inbObj["tls"]; ok {
var tls_id uint
err = db.Raw("SELECT id FROM tls WHERE inbounds like ?", `%"`+tag+`"%`).Find(&tls_id).Error
if err != nil {
return err
}
// Bind or Create tls_id
if tls_id > 0 {
inbObj["tls_id"] = tls_id
} else {
tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
if len(tls_server) > 5 {
newTls := &model.Tls{
Name: tag,
Server: tls_server,
Client: json.RawMessage("{}"),
}
err = db.Create(newTls).Error
if err != nil {
return err
}
inbObj["tls_id"] = newTls.Id
}
}
}
var inbData InboundData
db.Raw("select id,addrs,out_json from inbound_data where tag = ?", tag).Find(&inbData)
if inbData.Id > 0 {
inbObj["out_json"] = inbData.OutJson
var addrs []map[string]interface{}
json.Unmarshal(inbData.Addrs, &addrs)
for index, addr := range addrs {
if tlsEnable, ok := addr["tls"].(bool); ok {
newTls := map[string]interface{}{
"enabled": tlsEnable,
}
if insecure, ok := addr["insecure"].(bool); ok {
newTls["insecure"] = insecure
delete(addrs[index], "insecure")
}
if sni, ok := addr["server_name"].(string); ok {
newTls["server_name"] = sni
delete(addrs[index], "server_name")
}
addrs[index]["tls"] = newTls
}
}
inbObj["addrs"] = addrs
} else {
inbObj["out_json"] = json.RawMessage("{}")
inbObj["addrs"] = json.RawMessage("[]")
}
// Delete deprecated fields
delete(inbObj, "sniff")
delete(inbObj, "sniff_override_destination")
delete(inbObj, "sniff_timeout")
delete(inbObj, "domain_strategy")
inbJson, _ := json.Marshal(inbObj)
var newInbound model.Inbound
err = newInbound.UnmarshalJSON(inbJson)
if err != nil {
return err
}
err = db.Create(&newInbound).Error
if err != nil {
return err
}
}
delete(oldConfig, "inbounds")
blockOutboundTags := []string{}
dnsOutboundTags := []string{}
oldOutbounds := oldConfig["outbounds"].([]interface{})
db.Migrator().DropTable(&model.Outbound{}, &model.Endpoint{})
db.AutoMigrate(&model.Outbound{}, &model.Endpoint{})
for _, outbound := range oldOutbounds {
outType, _ := outbound.(map[string]interface{})["type"].(string)
outboundRaw, _ := json.MarshalIndent(outbound, "", " ")
if outType == "wireguard" { // Check if it is Entrypoint
var newEntrypoint model.Endpoint
err = newEntrypoint.UnmarshalJSON(outboundRaw)
if err != nil {
return err
}
err = db.Create(&newEntrypoint).Error
if err != nil {
return err
}
} else { // It is Outbound
var newOutbound model.Outbound
err = newOutbound.UnmarshalJSON(outboundRaw)
if err != nil {
return err
}
// Delete deprecated fields
if newOutbound.Type == "direct" {
var options map[string]interface{}
json.Unmarshal(newOutbound.Options, &options)
delete(options, "override_address")
delete(options, "override_port")
newOutbound.Options, _ = json.Marshal(options)
}
switch newOutbound.Type {
case "dns":
dnsOutboundTags = append(dnsOutboundTags, newOutbound.Tag)
case "block":
blockOutboundTags = append(blockOutboundTags, newOutbound.Tag)
default:
err = db.Create(&newOutbound).Error
if err != nil {
return err
}
}
}
}
delete(oldConfig, "outbounds")
// Check routing rules
if routingRules, ok := oldConfig["route"].(map[string]interface{}); ok {
if rules, hasRules := routingRules["rules"].([]interface{}); hasRules {
hasDns := false
for index, rule := range rules {
ruleObj, _ := rule.(map[string]interface{})
isBlock := false
isDns := false
outboundTag, _ := ruleObj["outbound"].(string)
for _, tag := range blockOutboundTags {
if tag == outboundTag {
isBlock = true
delete(ruleObj, "outbound")
ruleObj["action"] = "reject"
break
}
}
for _, tag := range dnsOutboundTags {
if tag == outboundTag {
isDns = true
hasDns = true
delete(ruleObj, "outbound")
ruleObj["action"] = "hijack-dns"
break
}
}
if !isBlock && !isDns {
ruleObj["action"] = "route"
}
rules[index] = ruleObj
}
if hasDns {
rules = append(rules, map[string]interface{}{"action": "sniff"})
}
routingRules["rules"] = rules
}
oldConfig["route"] = routingRules
}
// Remove v2rayapi and clashapi from experimental config
experimental := oldConfig["experimental"].(map[string]interface{})
delete(experimental, "v2ray_api")
delete(experimental, "clash_api")
oldConfig["experimental"] = experimental
// Save the other configs
var otherConfigs json.RawMessage
otherConfigs, err = json.MarshalIndent(oldConfig, "", " ")
if err != nil {
return err
}
return db.Save(&model.Setting{
Key: "config",
Value: string(otherConfigs),
}).Error
}
func migrateTls(db *gorm.DB) error {
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
return nil
}
err := db.Migrator().DropColumn(&model.Tls{}, "inbounds")
if err != nil {
return err
}
var tlsConfig []model.Tls
err = db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return err
}
for index, tls := range tlsConfig {
var tlsClient map[string]interface{}
err = json.Unmarshal(tls.Client, &tlsClient)
if err != nil {
continue
}
for key := range tlsClient {
switch key {
case "insecure", "disable_sni", "utls", "ech", "reality":
continue
default:
delete(tlsClient, key)
}
}
tlsConfig[index].Client, _ = json.MarshalIndent(tlsClient, "", " ")
}
return db.Save(&tlsConfig).Error
}
func dropInboundData(db *gorm.DB) error {
if !db.Migrator().HasTable(&InboundData{}) {
return nil
}
return db.Migrator().DropTable(&InboundData{})
}
func migrateClients(db *gorm.DB) error {
var oldClients []model.Client
err := db.Model(model.Client{}).Scan(&oldClients).Error
if err != nil {
return err
}
for index, oldClient := range oldClients {
var old_inbounds []string
err = json.Unmarshal(oldClient.Inbounds, &old_inbounds)
if err != nil {
return err
}
var inbound_ids []uint
err = db.Raw("SELECT id FROM inbounds WHERE tag in ?", old_inbounds).Find(&inbound_ids).Error
if err != nil {
return err
}
oldClients[index].Inbounds, _ = json.Marshal(inbound_ids)
}
return db.Save(oldClients).Error
}
func migrateChanges(db *gorm.DB) error {
return db.Migrator().DropColumn(&model.Changes{}, "index")
}
func to1_2(db *gorm.DB) error {
err := moveJsonToDb(db)
if err != nil {
return err
}
err = migrateTls(db)
if err != nil {
return err
}
err = dropInboundData(db)
if err != nil {
return err
}
err = migrateClients(db)
if err != nil {
return err
}
return migrateChanges(db)
}
+68
View File
@@ -0,0 +1,68 @@
package migration
import (
"fmt"
"log"
"os"
"s-ui/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func MigrateDb() {
// void running on first install
path := config.GetDBPath()
_, err := os.Stat(path)
if err != nil {
println("Database not found")
return
}
db, err := gorm.Open(sqlite.Open(path))
if err != nil {
log.Fatal(err)
return
}
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
currentVersion := config.GetVersion()
dbVersion := ""
tx.Raw("SELECT value FROM settings WHERE key = ?", "version").Find(&dbVersion)
fmt.Println("Current version:", currentVersion, "\nDatabase version:", dbVersion)
if currentVersion == dbVersion {
fmt.Println("Database is up to date, no need to migrate")
return
}
fmt.Println("Start migrating database...")
// Before 1.2
if dbVersion == "" {
err = to1_1(tx)
if err != nil {
log.Fatal("Migration to 1.1 failed: ", err)
return
}
err = to1_2(tx)
if err != nil {
log.Fatal("Migration to 1.2 failed: ", err)
return
}
}
// Set version
err = tx.Raw("UPDATE settings SET value = ? WHERE key = ?", currentVersion, "version").Error
if err != nil {
log.Fatal("Update version failed: ", err)
return
}
fmt.Println("Migration done!")
}
+171
View File
@@ -0,0 +1,171 @@
package cmd
import (
"fmt"
"io"
"net/http"
"s-ui/config"
"s-ui/database"
"s-ui/service"
"strings"
"github.com/shirou/gopsutil/v4/net"
)
func resetSetting() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
err = settingService.ResetSettings()
if err != nil {
fmt.Println("reset setting failed:", err)
} else {
fmt.Println("reset setting success")
}
}
func updateSetting(port int, path string, subPort int, subPath string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
if port > 0 {
err := settingService.SetPort(port)
if err != nil {
fmt.Println("set port failed:", err)
} else {
fmt.Println("set port success")
}
}
if path != "" {
err := settingService.SetWebPath(path)
if err != nil {
fmt.Println("set path failed:", err)
} else {
fmt.Println("set path success")
}
}
if subPort > 0 {
err := settingService.SetSubPort(subPort)
if err != nil {
fmt.Println("set sub port failed:", err)
} else {
fmt.Println("set sub port success")
}
}
if subPath != "" {
err := settingService.SetSubPath(subPath)
if err != nil {
fmt.Println("set sub path failed:", err)
} else {
fmt.Println("set sub path success")
}
}
}
func showSetting() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
allSetting, err := settingService.GetAllSetting()
if err != nil {
fmt.Println("get current port failed,error info:", err)
}
fmt.Println("Current panel settings:")
fmt.Println("\tPanel port:\t", (*allSetting)["webPort"])
fmt.Println("\tPanel path:\t", (*allSetting)["webPath"])
if (*allSetting)["webListen"] != "" {
fmt.Println("\tPanel IP:\t", (*allSetting)["webListen"])
}
if (*allSetting)["webDomain"] != "" {
fmt.Println("\tPanel Domain:\t", (*allSetting)["webDomain"])
}
if (*allSetting)["webURI"] != "" {
fmt.Println("\tPanel URI:\t", (*allSetting)["webURI"])
}
fmt.Println()
fmt.Println("Current subscription settings:")
fmt.Println("\tSub port:\t", (*allSetting)["subPort"])
fmt.Println("\tSub path:\t", (*allSetting)["subPath"])
if (*allSetting)["subListen"] != "" {
fmt.Println("\tSub IP:\t", (*allSetting)["subListen"])
}
if (*allSetting)["subDomain"] != "" {
fmt.Println("\tSub Domain:\t", (*allSetting)["subDomain"])
}
if (*allSetting)["subURI"] != "" {
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
}
}
func getPanelURI() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
Port, _ := settingService.GetPort()
BasePath, _ := settingService.GetWebPath()
Listen, _ := settingService.GetListen()
Domain, _ := settingService.GetWebDomain()
KeyFile, _ := settingService.GetKeyFile()
CertFile, _ := settingService.GetCertFile()
TLS := false
if KeyFile != "" && CertFile != "" {
TLS = true
}
Proto := ""
if TLS {
Proto = "https://"
} else {
Proto = "http://"
}
PortText := fmt.Sprintf(":%d", Port)
if (Port == 443 && TLS) || (Port == 80 && !TLS) {
PortText = ""
}
if len(Domain) > 0 {
fmt.Println(Proto + Domain + PortText + BasePath)
return
}
if len(Listen) > 0 {
fmt.Println(Proto + Listen + PortText + BasePath)
return
}
fmt.Println("Local address:")
// get ip address
netInterfaces, _ := net.Interfaces()
for i := 0; i < len(netInterfaces); i++ {
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
addrs := netInterfaces[i].Addrs
for _, address := range addrs {
IP := strings.Split(address.Addr, "/")[0]
if strings.Contains(address.Addr, ".") {
fmt.Println(Proto + IP + PortText + BasePath)
} else if address.Addr[0:6] != "fe80::" {
fmt.Println(Proto + "[" + IP + "]" + PortText + BasePath)
}
}
}
}
resp, err := http.Get("https://api.ipify.org?format=text")
if err == nil {
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err == nil {
fmt.Printf("\nGlobal address:\n%s%s%s%s\n", Proto, ip, PortText, BasePath)
}
}
}