diff --git a/.gitignore b/.gitignore index 179d574..6acf663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .DS_Store .idea/ -/go-client/test.go +go-client/test.go +go-client/build +interface/node_modules +interface/yarn.lock +interface/dist_electron +interface/dist \ No newline at end of file diff --git a/go-client/cmd/awaken/awaken.go b/go-client/cmd/awaken/awaken.go index 7006e0f..4255aca 100755 --- a/go-client/cmd/awaken/awaken.go +++ b/go-client/cmd/awaken/awaken.go @@ -22,7 +22,7 @@ func main() { infoJson := string(decoded) json.Unmarshal([]byte(infoJson), p) r := awaken.Rouse{ - Info: *p, + Info: *p, } r.Run() } @@ -30,4 +30,4 @@ func main() { //go build -o JumpServerClient awaken.go //CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" -o JumpServerClient.exe awaken.go -//CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o JumpServerClient awaken.go \ No newline at end of file +//CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o JumpServerClient awaken.go diff --git a/go-client/config.json b/go-client/config.json new file mode 100644 index 0000000..b0777c7 --- /dev/null +++ b/go-client/config.json @@ -0,0 +1,331 @@ +{ + "filename": "Jumpserve Clients Config", + "windows": { + "terminal": [ + { + "name": "xshell", + "display_name": "XShell", + "protocol": [ + "ssh", + "telnet" + ], + "comment": "XShell是一个强大的安全终端模拟软件,它支持SSH、TELNET协议。", + "download_url": "https://www.xshell.com/zh/xshell-download/", + "type": "linux", + "path": "", + "arg_format": "-newtab {name} -url {protocol}://{username}:{value}@{host}:{port}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + }, + { + "name": "securecrt", + "display_name": "SecureCRT", + "protocol": [ + "ssh", + "telnet" + ], + "comment": "SecureCRT是VanDyke Software所开发销售的一个SSH、Telnet客户端和虚拟终端软件。", + "download_url": "https://www.vandyke.com/cgi-bin/releases.php?product=securecrt", + "type": "linux", + "path": "", + "arg_format": "/N {name} /T /SSH2 /ACCEPTHOSTKEYS /p {port} /password {value} /L {username} {host}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + }, + { + "name": "putty", + "display_name": "PuTTY", + "protocol": [ + "ssh", + "telnet" + ], + "comment": "PuTTY是一个Telnet、SSH、rlogin纯TCP以及串行阜连线软件。", + "download_url": "内置", + "type": "linux", + "path": "putty.exe", + "arg_format": "-{protocol} {username}@{host} -P {port} -pw {value}", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": true + } + ], + "remotedesktop": [ + { + "name": "mstsc", + "display_name": "Microsoft Remote Desktop", + "protocol": [ + "rdp" + ], + "comment": "Microsoft Remote Desktop是一款强大的微软远程连接工具,可以从几乎任何地方连接到远程PC和您的工作资源。", + "download_url": "内置", + "type": "windows", + "path": "mstsc.exe", + "arg_format": "", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": true + } + ], + "filetransfer": [ + { + "name": "winscp", + "display_name": "WinSCP", + "protocol": [ + "sftp" + ], + "comment": "WinSCP是一个Windows环境下使用SSH的开源图形化SFTP客户端,同时支持SCP协议。", + "download_url": "https://winscp.net/eng/index.php", + "type": "linux", + "path": "", + "arg_format": "{protocol}://{username}:{value}@{host}:{port}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + }, + { + "name": "filezilla", + "display_name": "Filezilla", + "protocol": [ + "sftp" + ], + "comment": "FileZilla Client是一款免费、开源的 FTP 客户端。它支持FTP、SFTP。", + "download_url": "https://filezilla-project.org/download.php?type=client", + "type": "linux", + "path": "", + "arg_format": "{protocol}://{username}:{value}@{host}:{port}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ], + "databases": [ + { + "name": "plsql", + "display_name": "PL/SQL Developer", + "protocol": [ + "oracle" + ], + "comment": "PL/SQL Developer是一个集成开发环境,由Allround Automations公司开发,专门面向Oracle数据库存储的程序单元的开发。", + "download_url": "https://www.allroundautomations.com/registered-plsqldev/", + "type": "databases", + "path": "", + "arg_format": "userid={username}/{value}@{host}:{port}/{dbname}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + }, + { + "name": "dbeaver", + "display_name": "DBeaver Community", + "protocol": [ + "oracle", + "mysql", + "postgresql", + "mariadb" + ], + "comment": "DBeaver Community是一个通用的数据库管理工具和SQL客户端,支持MySQL、PostgreSQL、Oracle以及其他兼容JDBC的数据库。", + "download_url": "https://dbeaver.io/download/", + "type": "databases", + "path": "", + "arg_format": "-con name={name}|driver={protocol}|user={username}|password={value}|database={dbname}|host={host}|port={port}|save=false|connect=true", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + }, + { + "name": "resp", + "display_name": "Redis Desktop Manager", + "protocol": [ + "redis" + ], + "comment": "Redis Desktop Manager 是一款能够跨平台使用的开源 Redis 可视化工具,主要针对 Redis 开发设计。", + "download_url": "https://github.com/FuckDoctors/rdm-builder/releases/download/2022.5.1/resp-2022.5.1.exe", + "type": "databases", + "path": "", + "arg_format": "--settings-dir {config_file}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ] + }, + "macos": { + "terminal": [ + { + "name": "terminal", + "display_name": "Terminal", + "protocol": [ + "ssh" + ], + "comment": "Terminal是MacOS操作系统上的虚拟终端应用软件,位于“实用工具”文件夹内。", + "download_url": "内置", + "type": "linux", + "path": "Terminal", + "arg_format": "-{protocol} {username}@{host} -P {port} -pw {value}", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": true + }, + { + "name": "securecrt", + "display_name": "SecureCRT", + "protocol": [ + "ssh" + ], + "comment": "SecureCRT是VanDyke Software所开发销售的一个SSH、Telnet客户端和虚拟终端软件。\n\n!!!手动下载安装,点击保存启用!!!", + "download_url": "https://www.vandyke.com/cgi-bin/releases.php?product=securecrt", + "type": "linux", + "path": "/Applications/SecureCRT.app/Contents/MacOS/SecureCRT", + "arg_format": "/N {name} /T /SSH2 /ACCEPTHOSTKEYS /p {port} /password {value} /L {username} {host}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ], + "remotedesktop": [ + { + "name": "mstsc", + "display_name": "Microsoft Remote Desktop", + "protocol": [ + "rdp" + ], + "comment": "Microsoft Remote Desktop是一款强大的微软远程连接工具,可以从几乎任何地方连接到远程PC和您的工作资源。\n\n!!!手动下载安装,点击保存启用!!!", + "download_url": "内置", + "type": "windows", + "path": "/Applications/Microsoft Remote Desktop.app", + "arg_format": "", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": false + } + ], + "filetransfer": [ + { + "name": "filezilla", + "display_name": "Filezilla", + "protocol": [ + "sftp" + ], + "comment": "FileZilla Client是一款免费、开源的 FTP 客户端。它支持FTP、SFTP。\n\n!!!手动下载安装,点击保存启用!!!", + "download_url": "https://filezilla-project.org/download.php?type=client", + "type": "linux", + "path": "/Applications/FileZilla.app/Contents/MacOS/filezilla", + "arg_format": "{protocol}://{username}:{value}@{host}:{port}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ], + "databases": [ + { + "name": "dbeaver", + "display_name": "DBeaver Community", + "protocol": [ + "oracle", + "mysql", + "postgresql", + "mariadb" + ], + "comment": "DBeaver Community是一个通用的数据库管理工具和SQL客户端,支持MySQL、PostgreSQL、Oracle以及其他兼容JDBC的数据库。\n\n!!!手动下载安装,点击保存启用!!!", + "download_url": "https://dbeaver.io/download/", + "type": "databases", + "path": "/Applications/DBeaver.app/Contents/MacOS/dbeaver", + "arg_format": "-con name={name}|driver={protocol}|user={username}|password={value}|database={dbname}|host={host}|port={port}|save=false|connect=true", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ] + }, + "linux": { + "terminal": [ + { + "name": "terminal", + "display_name": "Terminal", + "protocol": [ + "ssh" + ], + "comment": "Terminal是MacOS操作系统上的虚拟终端应用软件,位于“实用工具”文件夹内。", + "download_url": "内置", + "type": "linux", + "path": "Terminal", + "arg_format": "-{protocol} {username}@{host} -P {port} -pw {value}", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": true + } + ], + "remotedesktop": [ { + "name": "remmina", + "display_name": "Remmina", + "protocol": [ + "rdp" + ], + "comment": "Remmina 是一个使用 GTK+ 开发的远程桌面客户端,提供了 RDP、VNC、XDMCP、SSH 等远程连接协议的支持。", + "download_url": "https://remmina.org/how-to-install-remmina/-内置", + "type": "windows", + "path": "remmina", + "arg_format": "", + "match_first": [], + "is_internal": true, + "is_default": true, + "is_set": true + }], + "filetransfer": [ + { + "name": "filezilla", + "display_name": "Filezilla", + "protocol": [ + "sftp" + ], + "comment": "FileZilla Client是一款免费、开源的 FTP 客户端。它支持FTP、SFTP。", + "download_url": "https://filezilla-project.org/download.php?type=client", + "type": "linux", + "path": "", + "arg_format": "{protocol}://{username}:{value}@{host}:{port}", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ], + "databases": [ + { + "name": "dbeaver", + "display_name": "DBeaver Community", + "protocol": [ + "oracle", + "mysql", + "postgresql", + "mariadb" + ], + "comment": "DBeaver Community是一个通用的数据库管理工具和SQL客户端,支持MySQL、PostgreSQL、Oracle以及其他兼容JDBC的数据库。", + "download_url": "https://dbeaver.io/download/", + "type": "databases", + "path": "", + "arg_format": "-con name={name}|driver={protocol}|user={username}|password={value}|database={dbname}|host={host}|port={port}|save=false|connect=true", + "match_first": [], + "is_internal": false, + "is_default": false, + "is_set": false + } + ] + } +} diff --git a/go-client/pkg/awaken/awaken.go b/go-client/pkg/awaken/awaken.go index 1044ddb..1b1d947 100755 --- a/go-client/pkg/awaken/awaken.go +++ b/go-client/pkg/awaken/awaken.go @@ -1,13 +1,12 @@ package awaken import ( - "fmt" "go-client/global" + "go-client/pkg/config" "io/ioutil" "os" "path/filepath" "regexp" - "strings" ) /*{ @@ -28,9 +27,14 @@ type File struct { type Info struct { ID string `json:"id"` + Name string `json:"name"` Value string `json:"value"` Protocol string `json:"protocol"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` Command string `json:"command"` + DBName string `json:"dbname"` File `json:"file"` } @@ -57,92 +61,39 @@ func removeCurRdpFile() { } } -func (r *Rouse) HandleRDP() { +func (r *Rouse) HandleRDP(appConfig *config.AppConfig) { removeCurRdpFile() - filePath := filepath.Join(filepath.Dir(os.Args[0]), r.Name+".rdp") + filePath := filepath.Join(filepath.Dir(os.Args[0]), r.File.Name+".rdp") err := ioutil.WriteFile(filePath, []byte(r.Content), os.ModePerm) if err != nil { global.LOG.Error(err.Error()) return } - cmd := handleRDP(filePath) + cmd := handleRDP(r, filePath, appConfig) cmd.Run() } -func (r *Rouse) HandleSSH() { +func (r *Rouse) HandleSSH(appConfig *config.AppConfig) { currentPath := filepath.Dir(os.Args[0]) - cmd := handleSSH(r.Command, r.Value, currentPath) + cmd := handleSSH(r, currentPath, appConfig) cmd.Run() } -func structureMySQLCommand(command string) string { - command = strings.ReplaceAll(command, "mysql ", "") - db := &DBCommand{} - paramSlice := strings.Split(command, " ") - for i, v := range paramSlice { - if strings.HasPrefix(v, "-p") { - db.Password = paramSlice[i][2:] - continue - } - switch v { - case "-u": - db.User = paramSlice[i+1] - case "-h": - db.Host = paramSlice[i+1] - case "-P": - db.Port = paramSlice[i+1] - } - } - db.DBName = paramSlice[len(paramSlice)-1] - command = fmt.Sprintf( - "mysql -u %s -p%s -h %s -P %s %s", - db.User, db.Password, db.Host, db.Port, db.DBName, - ) - return command -} - -func structureRedisCommand(command string) string { - command = strings.ReplaceAll(command, "redis-cli ", "") - db := &DBCommand{} - paramSlice := strings.Split(command, " ") - for i, v := range paramSlice { - switch v { - case "-a": - db.Password = paramSlice[i+1] - case "-h": - db.Host = paramSlice[i+1] - case "-p": - db.Port = paramSlice[i+1] - } - } - cmd := fmt.Sprintf( - "redis-cli -h %s -p %s -a %s", - db.Host, db.Port, db.Password, - ) - return cmd -} - -func (r *Rouse) HandleDB() { - command := r.Command - switch r.Protocol { - case "mysql", "mariadb": - command = structureMySQLCommand(command) - case "postgresql": - command = structurePostgreSQLCommand(command) - case "redis": - command = structureRedisCommand(command) - } - cmd := handleDB(command) +func (r *Rouse) HandleDB(appConfig *config.AppConfig) { + cmd := handleDB(r, appConfig) cmd.Run() } + func (r *Rouse) Run() { protocol := r.Protocol + appConfig := config.GetConf() + switch protocol { case "rdp": - r.HandleRDP() - case "ssh": - r.HandleSSH() + r.HandleRDP(&appConfig) + case "ssh", "sftp": + r.HandleSSH(&appConfig) case "mysql", "mariadb", "postgresql", "redis", "oracle", "sqlserver": - r.HandleDB() + r.HandleDB(&appConfig) } } diff --git a/go-client/pkg/awaken/awaken_darwin.go b/go-client/pkg/awaken/awaken_darwin.go index 3a65209..afab831 100755 --- a/go-client/pkg/awaken/awaken_darwin.go +++ b/go-client/pkg/awaken/awaken_darwin.go @@ -3,19 +3,95 @@ package awaken import ( "fmt" "go-client/global" + "go-client/pkg/config" "os/exec" + "path/filepath" + "strconv" + "strings" ) +func getCommandFromArgs(connectInfo map[string]string, argFormat string) string { + for key, value := range connectInfo { + argFormat = strings.Replace(argFormat, "{"+key+"}", value, 1) + } + return argFormat +} + func awakenRDPCommand(filePath string) *exec.Cmd { global.LOG.Debug(filePath) cmd := exec.Command("open", filePath) return cmd } -func awakenCommand(command string) *exec.Cmd { - cmd := exec.Command( - "osascript", "-s", "h", "-e", - fmt.Sprintf(`tell application "Terminal" to do script "%s"`, command), - ) +func awakenSSHCommand(r *Rouse, currentPath string, cfg *config.AppConfig) *exec.Cmd { + var appItem *config.AppItem + var appLst []config.AppItem + switch r.Protocol { + case "ssh": + appLst = cfg.MacOS.Terminal + case "sftp": + appLst = cfg.MacOS.FileTransfer + } + + for _, app := range appLst { + if app.IsActive() && app.IsSupportProtocol(r.Protocol) { + appItem = &app + break + } + } + if appItem == nil { + return nil + } + var cmd *exec.Cmd + if appItem.IsInternal { + clientPath := filepath.Join(currentPath, "client") + command := fmt.Sprintf("%s %s -P %s", clientPath, r.Command, r.Value) + cmd = exec.Command( + "osascript", "-s", "h", + "-e", fmt.Sprintf(`tell application "%s" to do script "%s"`, appItem.DisplayName, command), + ) + } else { + var appPath string + appPath = appItem.Path + + connectMap := map[string]string{ + "name": r.Name, + "protocol": r.Protocol, + "username": r.Username, + "value": r.Value, + "host": r.Host, + "port": strconv.Itoa(r.Port), + } + commands := getCommandFromArgs(connectMap, appItem.ArgFormat) + appPath = appItem.Path + cmd = exec.Command(appPath, strings.Split(commands, " ")...) + } return cmd } + +func awakenDBCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { + var appItem *config.AppItem + appLst := cfg.MacOS.Databases + for _, app := range appLst { + if app.IsSet && app.IsMatchProtocol(r.Protocol) { + appItem = &app + break + } + } + if appItem == nil { + return nil + } + appPath := appItem.Path + + connectMap := map[string]string{ + "name": r.Name, + "protocol": r.Protocol, + "username": r.Username, + "value": r.Value, + "host": r.Host, + "port": strconv.Itoa(r.Port), + "dbname": r.DBName, + } + commands := getCommandFromArgs(connectMap, appItem.ArgFormat) + return exec.Command(appPath, strings.Split(commands, " ")...) +} diff --git a/go-client/pkg/awaken/awaken_linux.go b/go-client/pkg/awaken/awaken_linux.go index 5cc8497..7e8ad89 100755 --- a/go-client/pkg/awaken/awaken_linux.go +++ b/go-client/pkg/awaken/awaken_linux.go @@ -3,17 +3,29 @@ package awaken import ( "fmt" "go-client/global" + "go-client/pkg/config" "os/exec" + "path/filepath" + "strconv" "strings" ) +func getCommandFromArgs(connectInfo map[string]string, argFormat string) string { + for key, value := range connectInfo { + argFormat = strings.Replace(argFormat, "{"+key+"}", value, 1) + } + return argFormat +} + func awakenRDPCommand(filePath string) *exec.Cmd { global.LOG.Debug(filePath) cmd := exec.Command("remmina", filePath) return cmd } -func awakenCommand(command string) *exec.Cmd { +func awakenSSHCommand(r *Rouse, currentPath string, cfg *config.AppConfig) *exec.Cmd { + clientPath := filepath.Join(currentPath, "client") + command := fmt.Sprintf("%s %s -P %s", clientPath, r.Command, r.Value) cmd := new(exec.Cmd) out, _ := exec.Command("bash", "-c", "echo $XDG_CURRENT_DESKTOP").CombinedOutput() currentDesktop := strings.ToLower(strings.Trim(string(out), "\n")) @@ -32,3 +44,30 @@ func awakenCommand(command string) *exec.Cmd { } return cmd } + +func awakenDBCommand(r *Rouse, cfg *config.AppConfig) *exec.Cmd { + var appItem *config.AppItem + appLst := cfg.Windows.Databases + for _, app := range appLst { + if app.IsSet && app.IsMatchProtocol(r.Protocol) { + appItem = &app + break + } + } + if appItem == nil { + return nil + } + appPath := appItem.Path + + connectMap := map[string]string{ + "name": r.Name, + "protocol": r.Protocol, + "username": r.Username, + "value": r.Value, + "host": r.Host, + "port": strconv.Itoa(r.Port), + "dbname": r.DBName, + } + commands := getCommandFromArgs(connectMap, appItem.ArgFormat) + return exec.Command(appPath, strings.Split(commands, " ")...) +} diff --git a/go-client/pkg/awaken/awaken_unix.go b/go-client/pkg/awaken/awaken_unix.go index 7328c1b..47a894c 100755 --- a/go-client/pkg/awaken/awaken_unix.go +++ b/go-client/pkg/awaken/awaken_unix.go @@ -4,50 +4,21 @@ package awaken import ( - "fmt" + "go-client/pkg/config" "os/exec" - "path/filepath" - "strings" ) -func handleRDP(filePath string) *exec.Cmd { +func handleRDP(r *Rouse, filePath string, cfg *config.AppConfig) *exec.Cmd { cmd := awakenRDPCommand(filePath) return cmd } -func handleSSH(c string, secret string, currentPath string) *exec.Cmd { - clientPath := filepath.Join(currentPath, "client") - command := fmt.Sprintf("%s %s -P %s", clientPath, c, secret) - cmd := awakenCommand(command) +func handleSSH(r *Rouse, currentPath string, cfg *config.AppConfig) *exec.Cmd { + cmd := awakenSSHCommand(r, currentPath, cfg) return cmd } -func structurePostgreSQLCommand(command string) string { - command = strings.Trim(strings.ReplaceAll(command, "psql ", ""), `"`) - db := &DBCommand{} - for _, v := range strings.Split(command, " ") { - tp, val := strings.Split(v, "=")[0], strings.Split(v, "=")[1] - switch tp { - case "user": - db.User = val - case "password": - db.Password = val - case "host": - db.Host = val - case "port": - db.Port = val - case "dbname": - db.DBName = val - } - } - command = fmt.Sprintf( - "PGPASSWORD=%s psql -U %s -h %s -p %s -d %s", - db.Password, db.User, db.Host, db.Port, db.DBName, - ) - return command -} - -func handleDB(command string) *exec.Cmd { - cmd := awakenCommand(command) +func handleDB(r *Rouse, cfg *config.AppConfig) *exec.Cmd { + cmd := awakenDBCommand(r, cfg) return cmd } diff --git a/go-client/pkg/awaken/awaken_windows.go b/go-client/pkg/awaken/awaken_windows.go index ca82a7b..8d5df0d 100755 --- a/go-client/pkg/awaken/awaken_windows.go +++ b/go-client/pkg/awaken/awaken_windows.go @@ -1,62 +1,127 @@ package awaken import ( - "fmt" + "encoding/json" + "go-client/global" + "go-client/pkg/config" + "io/ioutil" + "os" "os/exec" "path/filepath" + "strconv" "strings" - "syscall" ) -func handleRDP(filePath string) *exec.Cmd { +func EnsureDirExist(path string) { + if fi, err := os.Stat(path); err == nil && fi.IsDir() { + return + } + if err := os.MkdirAll(path, os.ModePerm); err != nil { + global.LOG.Error(err.Error()) + } +} + +func getCommandFromArgs(connectInfo map[string]string, argFormat string) string { + for key, value := range connectInfo { + argFormat = strings.Replace(argFormat, "{"+key+"}", value, 1) + } + return argFormat +} + +func handleRDP(r *Rouse, filePath string, cfg *config.AppConfig) *exec.Cmd { cmd := exec.Command("mstsc.exe", filePath) return cmd } -func handleSSH(c string, secret string, currentPath string) *exec.Cmd { - puttyPath := "putty.exe" - if _, err := exec.LookPath("putty.exe"); err != nil { - puttyPath = filepath.Join(currentPath, "putty.exe") +func handleSSH(r *Rouse, currentPath string, cfg *config.AppConfig) *exec.Cmd { + var appItem *config.AppItem + var appLst []config.AppItem + switch r.Protocol { + case "ssh": + appLst = cfg.Windows.Terminal + case "sftp": + appLst = cfg.Windows.FileTransfer } - //TODO core api 有时判断系统会判断错 导致返回无putty - if strings.HasPrefix(c, "putty.exe") { - c = strings.Replace(c, "putty.exe -", "", 1) + for _, app := range appLst { + if app.IsActive() && app.IsSupportProtocol(r.Protocol) { + appItem = &app + break + } + } + if appItem == nil { + return nil + } + var appPath string + if appItem.IsInternal { + appPath = filepath.Join(currentPath, appItem.Path) + } else { + appPath = appItem.Path } - c = strings.Replace(c, " -p ", " -P ", 1) - c = fmt.Sprintf("-%s -pw %s", c, secret) - command := strings.Split(c, " ") - return exec.Command(puttyPath, command...) + connectMap := map[string]string{ + "name": r.Name, + "protocol": r.Protocol, + "username": r.Username, + "value": r.Value, + "host": r.Host, + "port": strconv.Itoa(r.Port), + } + commands := getCommandFromArgs(connectMap, appItem.ArgFormat) + return exec.Command(appPath, strings.Split(commands, " ")...) } -func structurePostgreSQLCommand(command string) string { - command = strings.Trim(strings.ReplaceAll(command, "psql ", ""), `"`) - db := &DBCommand{} - for _, v := range strings.Split(command, " ") { - tp, val := strings.Split(v, "=")[0], strings.Split(v, "=")[1] - switch tp { - case "user": - db.User = val - case "password": - db.Password = val - case "host": - db.Host = val - case "port": - db.Port = val - case "dbname": - db.DBName = val +func handleDB(r *Rouse, cfg *config.AppConfig) *exec.Cmd { + var appItem *config.AppItem + appLst := cfg.Windows.Databases + for _, app := range appLst { + if app.IsSet && app.IsMatchProtocol(r.Protocol) { + appItem = &app + break } } - command = fmt.Sprintf( - `psql "user=%s password=%s host=%s dbname=%s port=%s"`, - db.User, db.Password, db.Host, db.DBName, db.Port, - ) - return command -} + if appItem == nil { + return nil + } + appPath := appItem.Path -func handleDB(command string) *exec.Cmd { - cmd := exec.Command("cmd") - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CmdLine: `/c start cmd /k ` + command} - return cmd + connectMap := map[string]string{ + "name": r.Name, + "protocol": r.Protocol, + "username": r.Username, + "value": r.Value, + "host": r.Host, + "port": strconv.Itoa(r.Port), + "dbname": r.DBName, + } + + if r.Protocol == "redis" { + var conList []map[string]string + ss := make(map[string]string) + ss["host"] = r.Host + ss["port"] = strconv.Itoa(r.Port) + ss["name"] = r.Name + ss["auth"] = r.Username + "@" + r.Value + ss["ssh_agent_path"] = "" + ss["ssh_password"] = "" + ss["ssh_private_key_path"] = "" + ss["timeout_connect"] = "60000" + ss["timeout_execute"] = "60000" + conList = append(conList, ss) + + bjson, _ := json.Marshal(conList) + currentPath := filepath.Dir(os.Args[0]) + rdmPath := filepath.Join(currentPath, ".rdm") + EnsureDirExist(rdmPath) + filePath := filepath.Join(rdmPath, "connections.json") + global.LOG.Error(filePath) + err := ioutil.WriteFile(filePath, bjson, os.ModePerm) + if err != nil { + global.LOG.Error(err.Error()) + return nil + } + connectMap["config_file"] = currentPath + } + commands := getCommandFromArgs(connectMap, appItem.ArgFormat) + return exec.Command(appPath, strings.Split(commands, " ")...) } diff --git a/go-client/pkg/config/config.go b/go-client/pkg/config/config.go new file mode 100644 index 0000000..9ca2c9d --- /dev/null +++ b/go-client/pkg/config/config.go @@ -0,0 +1,92 @@ +package config + +import ( + "encoding/json" + "go-client/global" + "os" + "path/filepath" +) + +type AppConfig struct { + FileName string `json:"filename"` + Windows AppType `json:"windows"` + MacOS AppType `json:"macos"` + Linux AppType `json:"linux"` +} + +type AppType struct { + Terminal []AppItem `json:"terminal"` + FileTransfer []AppItem `json:"filetransfer"` + RemoteDesktop []AppItem `json:"remotedesktop"` + Databases []AppItem `json:"databases"` +} + +type AppItem struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Protocol []string `json:"protocol"` + Comment string `json:"comment"` + Type string `json:"type"` + MatchFirst []string `json:"match_first"` + Path string `json:"path"` + ArgFormat string `json:"arg_format"` + IsInternal bool `json:"is_internal"` + IsDefault bool `json:"is_default"` + IsSet bool `json:"is_set"` +} + +func (a *AppItem) IsActive() bool { + if a.IsDefault && a.IsSet { + return true + } + return false +} + +func (a *AppItem) IsSupportProtocol(protocol string) bool { + for _, p := range a.Protocol { + if p == protocol { + return true + } + } + return false +} + +func (a *AppItem) IsMatchProtocol(protocol string) bool { + for _, p := range a.MatchFirst { + if p == protocol { + return true + } + } + return false +} + +func GetConf() AppConfig { + if GlobalConfig == nil { + return getDefaultConfig() + } + return *GlobalConfig +} + +var GlobalConfig *AppConfig + +func getDefaultConfig() AppConfig { + //filePath := filepath.Join(filepath.Dir(os.Args[0]), "config.json") + filePath := filepath.Join("/Users/halo/golang/clients/interface/bin/config.json") + jsonFile, err := os.Open(filePath) + if err != nil { + global.LOG.Error(err.Error()) + } + defer func(jsonFile *os.File) { + err := jsonFile.Close() + if err != nil { + global.LOG.Error(err.Error()) + } + }(jsonFile) + decoder := json.NewDecoder(jsonFile) + err = decoder.Decode(&GlobalConfig) + if err != nil { + global.LOG.Error(err.Error()) + return AppConfig{} + } + return *GlobalConfig +} diff --git a/interface/.gitignore b/interface/.gitignore new file mode 100644 index 0000000..a50bc28 --- /dev/null +++ b/interface/.gitignore @@ -0,0 +1,27 @@ +.DS_Store +node_modules +/dist + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +#Electron-builder output +/dist_electron +bin/* diff --git a/interface/README.md b/interface/README.md new file mode 100644 index 0000000..49a00b4 --- /dev/null +++ b/interface/README.md @@ -0,0 +1,24 @@ +# interface + +## Project setup +``` +yarn install +``` + +### Compiles and hot-reloads for development +``` +yarn serve +``` + +### Compiles and minifies for production +``` +yarn build +``` + +### Lints and fixes files +``` +yarn lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/interface/appveyor.yml b/interface/appveyor.yml new file mode 100644 index 0000000..3bbc79a --- /dev/null +++ b/interface/appveyor.yml @@ -0,0 +1,32 @@ +# Commented sections below can be used to run tests on the CI server +# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing +version: 0.1.{build} + +branches: + only: + - master + +image: Visual Studio 2017 +platform: + - x64 + +cache: + - '%APPDATA%\npm-cache' + - '%USERPROFILE%\.electron' + - '%USERPROFILE%\AppData\Local\Yarn\cache' + +init: + - git config --global core.autocrlf input + +install: + - ps: Install-Product node 16 x64 + - git reset --hard HEAD + - yarn + - node --version + +build_script: + #- yarn test + - yarn release + - yarn upload-dist + +test: false diff --git a/interface/babel.config.js b/interface/babel.config.js new file mode 100644 index 0000000..296f0e0 --- /dev/null +++ b/interface/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ], + plugins: ['@babel/plugin-proposal-optional-chaining'] +} diff --git a/interface/bin/.gitkeep b/interface/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/interface/build/icons/256x256.png b/interface/build/icons/256x256.png new file mode 100644 index 0000000..d4617cf Binary files /dev/null and b/interface/build/icons/256x256.png differ diff --git a/interface/build/icons/icon.icns b/interface/build/icons/icon.icns new file mode 100644 index 0000000..fd03ed5 Binary files /dev/null and b/interface/build/icons/icon.icns differ diff --git a/interface/build/icons/icon.ico b/interface/build/icons/icon.ico new file mode 100644 index 0000000..f973cb5 Binary files /dev/null and b/interface/build/icons/icon.ico differ diff --git a/interface/jsconfig.json b/interface/jsconfig.json new file mode 100644 index 0000000..5384a70 --- /dev/null +++ b/interface/jsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "paths": { + "@/*": [ + "src/renderer/*" + ], + "~/*": [ + "src/*" + ], + "root/*": [ + "./*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + } +} diff --git a/interface/package.json b/interface/package.json new file mode 100644 index 0000000..5b4f7f0 --- /dev/null +++ b/interface/package.json @@ -0,0 +1,67 @@ +{ + "name": "interface", + "author": "Fit2Cloud Technology Co., Ltd.; ", + "version": "v2.0.0", + "homepage": "https://jumpserver.org", + "private": true, + "scripts": { + "serve": "vue-cli-service electron:serve", + "lint": "vue-cli-service lint", + "electron:build": "vue-cli-service electron:build", + "electron:serve": "vue-cli-service electron:serve", + "postinstall": "electron-builder install-app-deps", + "postuninstall": "electron-builder install-app-deps" + }, + "main": "background.js", + "dependencies": { + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@element-plus/icons-vue": "^2.1.0", + "core-js": "^3.8.3", + "element-plus": "^2.3.0", + "fs-extra": "^11.1.1", + "vue": "^3.2.13", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-eslint": "~5.0.0", + "@vue/cli-service": "~5.0.0", + "electron": "^23.1.3", + "electron-devtools-installer": "^3.1.0", + "eslint": "^8.31.0", + "eslint-config-standard": ">=16.0.0", + "eslint-plugin-import": "^2.24.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-vue": "^9.8.0", + "sass": "^1.63.3", + "sass-loader": "^13.3.2", + "vue-cli-plugin-electron-builder": "~2.1.1" + }, + "overrides": { + "vue-cli-plugin-electron-builder": { + "electron-builder": "^23.1.0" + } + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/vue3-essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "@babel/eslint-parser" + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead", + "not ie 11" + ] +} diff --git a/interface/postcss.config.js b/interface/postcss.config.js new file mode 100644 index 0000000..961986e --- /dev/null +++ b/interface/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/interface/public/favicon.ico b/interface/public/favicon.ico new file mode 100644 index 0000000..d4617cf Binary files /dev/null and b/interface/public/favicon.ico differ diff --git a/interface/public/index.html b/interface/public/index.html new file mode 100644 index 0000000..b9b7fa6 --- /dev/null +++ b/interface/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + + diff --git a/interface/src/App.vue b/interface/src/App.vue new file mode 100644 index 0000000..ff4ac54 --- /dev/null +++ b/interface/src/App.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/interface/src/background.js b/interface/src/background.js new file mode 100644 index 0000000..54306ec --- /dev/null +++ b/interface/src/background.js @@ -0,0 +1,250 @@ +import {app, BrowserWindow, ipcMain, protocol} from "electron"; +import {createProtocol} from "vue-cli-plugin-electron-builder/lib"; +import installExtension, {VUEJS3_DEVTOOLS} from "electron-devtools-installer"; +import path from 'path' +import fse from 'fs-extra' +import {execFile} from "child_process"; + +const isDevelopment = process.env.NODE_ENV !== "production"; +// Scheme must be registered before the app is ready +protocol.registerSchemesAsPrivileged([ + {scheme: "app", privileges: {secure: true, standard: true}}, +]); + +let mainWindow + +async function createWindow() { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 860, + height: 550, + center: true, + fullscreenable: false, + resizable: false, + transparent: true, + titleBarStyle: 'hidden', + titleBarOverlay: { + color: "#1f1f1f", + symbolColor: "#fff", + }, + webPreferences: { + // Use pluginOptions.nodeIntegration, leave this alone + // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info + nodeIntegration: true, + contextIsolation: false, + }, + }); + + if (process.env.WEBPACK_DEV_SERVER_URL) { + // Load the url of the dev server if in development mode + await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL); + // if (!process.env.IS_TEST) mainWindow.webContents.openDevTools() + } else { + createProtocol("app"); + // Load the index.html when not in development + await mainWindow.loadURL("app://./index.html"); + // mainWindow.webContents.openDevTools() + } +} + +// Quit when all windows are closed. +app.on("window-all-closed", () => { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); +}); + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on("ready", async () => { + if (isDevelopment && !process.env.IS_TEST) { + // Install Vue Devtools + try { + await installExtension(VUEJS3_DEVTOOLS); + } catch (e) { + console.error("Vue Devtools failed to install:", e.toString()); + } + } + registerLocalResourceProtocol(); + createWindow(); +}); + +function registerLocalResourceProtocol() { + protocol.registerFileProtocol("local-resource", (request, callback) => { + const url = request.url.replace(/^local-resource:\/\//, ""); + // Decode URL to prevent errors when loading filenames with UTF-8 chars or chars like "#" + const decodedUrl = decodeURI(url); // Needed in case URL contains spaces + try { + return callback(decodedUrl); + } catch (error) { + console.error( + "ERROR: registerLocalResourceProtocol: Could not get file path:", + error + ); + } + }); +} + +// Exit cleanly on request from parent process in development mode. +if (isDevelopment) { + if (process.platform === "win32") { + process.on("message", (data) => { + if (data === "graceful-exit") { + app.quit(); + } + }); + } else { + process.on("SIGTERM", () => { + app.quit(); + }); + } +} + + +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient('jms', process.execPath, [path.resolve(process.argv[1])]) + } +} else { + app.setAsDefaultProtocolClient('jms') +} + +const handleOpenFromUrl = (url) => { + let subPath + if (isDevelopment && !process.env.IS_TEST) { + subPath = "bin" + } else { + subPath = process.resourcesPath + "/bin" + } + if (process.platform === "linux") { + switch (process.arch) { + case 'x32': + case 'x64': + subPath += "/linux-amd64" + break; + case 'arm': + case 'arm64': + subPath += "/linux-arm64" + break; + } + } else if (process.platform === "darwin") { + subPath += "/darwin" + } + console.log(subPath) + let exeFilePath = path.join(subPath, 'JumpServerClient') + const {execFile} = require('child_process') + execFile(exeFilePath, [url], (error, stdout, stderr) => { + if (error) { + console.log(error); + } + }); +} + +app.on('open-url', (event, urlStr) => { + handleOpenFromUrl(urlStr); +}); + +// 隐藏主窗口 +const hideWindow = () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.hide() + } +} + +app.on('second-instance', (event, commandLine, workingDirectory) => { + let commands = commandLine.slice(); + // commandLine 是一个数组, 其中最后一个数组元素为我们唤醒的链接 + let urlStr = commands.pop(); + handleOpenFromUrl(urlStr) +}) + + +let STORE_PATH +let configFilePath +if (process.platform === "win32") { + STORE_PATH = app.getPath('appData') + configFilePath = path.join(STORE_PATH, 'JumpServer', 'Client', 'config.json') +} else { + let subPath + if (isDevelopment && !process.env.IS_TEST) { + subPath = "bin" + } else { + subPath = process.resourcesPath + "/bin" + } + configFilePath = path.join(subPath, 'config.json') +} + +const callBackUrlName = 'config-reply-get'//消息发布-发布名称 +//读取本地文件 +ipcMain.on('config-get', function (event) { + // 传给渲染进程数据 + fse.readFile(configFilePath, "utf8", (err, data) => { + if (err) { + event.sender.send(callBackUrlName, 500, "读取 config.json 文件失败"); + } else { + event.sender.send(callBackUrlName, 200, data); + } + }) +}); + + +//增改本地文件 +ipcMain.on('config-set', function (event, type, value) { + value = JSON.parse(value) + fse.readFile(configFilePath, "utf8", (err, data) => { + if (err) { + console.log("目标文件异常") + } else { + let config = JSON.parse(data); + let platform + if (process.platform === "win32") { + platform = "windows" + } else if (process.platform === "darwin") { + platform = "macos" + } else { + platform = "linux" + } + let lst = [] + switch (type) { + case 'sshPage': + lst = config[platform]['terminal'] + break + case 'remotePage': + lst = config[platform]['remotedesktop'] + break + case 'fileTransferPage': + lst = config[platform]['filetransfer'] + break + case 'databasesPage': + lst = config[platform]['databases'] + break + } + lst.forEach(item => { + if (value.is_default) { + item.is_default = false + } + if (item.match_first.length > 0) { + item.match_first = item.match_first.filter(item => !value.match_first.includes(item)) + } + if (item.name === value.name) { + item.path = value.path + item.is_default = value.is_default + item.is_set = value.is_set + item.match_first = value.match_first + } + }) + const config_str = JSON.stringify(config) + fse.writeFileSync(configFilePath, config_str, "utf8") + event.sender.send(callBackUrlName, 200, config_str); + } + }) +}); diff --git a/interface/src/main.js b/interface/src/main.js new file mode 100644 index 0000000..ba1ee5f --- /dev/null +++ b/interface/src/main.js @@ -0,0 +1,18 @@ +import {createApp} from 'vue' +import App from './App.vue' + +import router from './renderer/router' +import ElementUI from 'element-plus' +import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import './renderer/assets/fonts/font-awesome.min.css'; + +const app = createApp(App) + +app.use(ElementUI) +app.use(router) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.mount('#app') diff --git a/interface/src/renderer/assets/JumpServer.png b/interface/src/renderer/assets/JumpServer.png new file mode 100644 index 0000000..66f7090 Binary files /dev/null and b/interface/src/renderer/assets/JumpServer.png differ diff --git a/interface/src/renderer/assets/dbeaver.png b/interface/src/renderer/assets/dbeaver.png new file mode 100644 index 0000000..1fd995f Binary files /dev/null and b/interface/src/renderer/assets/dbeaver.png differ diff --git a/interface/src/renderer/assets/filezilla.png b/interface/src/renderer/assets/filezilla.png new file mode 100644 index 0000000..52f2686 Binary files /dev/null and b/interface/src/renderer/assets/filezilla.png differ diff --git a/interface/src/renderer/assets/fonts/FontAwesome.otf b/interface/src/renderer/assets/fonts/FontAwesome.otf new file mode 100644 index 0000000..401ec0f Binary files /dev/null and b/interface/src/renderer/assets/fonts/FontAwesome.otf differ diff --git a/interface/src/renderer/assets/fonts/font-awesome.min.css b/interface/src/renderer/assets/fonts/font-awesome.min.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/interface/src/renderer/assets/fonts/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/interface/src/renderer/assets/fonts/fontawesome-webfont.eot b/interface/src/renderer/assets/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/interface/src/renderer/assets/fonts/fontawesome-webfont.eot differ diff --git a/interface/src/renderer/assets/fonts/fontawesome-webfont.svg b/interface/src/renderer/assets/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/interface/src/renderer/assets/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/src/renderer/assets/fonts/fontawesome-webfont.ttf b/interface/src/renderer/assets/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/interface/src/renderer/assets/fonts/fontawesome-webfont.ttf differ diff --git a/interface/src/renderer/assets/fonts/fontawesome-webfont.woff b/interface/src/renderer/assets/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/interface/src/renderer/assets/fonts/fontawesome-webfont.woff differ diff --git a/interface/src/renderer/assets/fonts/fontawesome-webfont.woff2 b/interface/src/renderer/assets/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/interface/src/renderer/assets/fonts/fontawesome-webfont.woff2 differ diff --git a/interface/src/renderer/assets/iterm.png b/interface/src/renderer/assets/iterm.png new file mode 100644 index 0000000..dc8d95d Binary files /dev/null and b/interface/src/renderer/assets/iterm.png differ diff --git a/interface/src/renderer/assets/logo.png b/interface/src/renderer/assets/logo.png new file mode 100644 index 0000000..d4617cf Binary files /dev/null and b/interface/src/renderer/assets/logo.png differ diff --git a/interface/src/renderer/assets/mstsc.png b/interface/src/renderer/assets/mstsc.png new file mode 100644 index 0000000..72a887b Binary files /dev/null and b/interface/src/renderer/assets/mstsc.png differ diff --git a/interface/src/renderer/assets/plsql.png b/interface/src/renderer/assets/plsql.png new file mode 100644 index 0000000..c3011dc Binary files /dev/null and b/interface/src/renderer/assets/plsql.png differ diff --git a/interface/src/renderer/assets/putty.png b/interface/src/renderer/assets/putty.png new file mode 100644 index 0000000..1002d47 Binary files /dev/null and b/interface/src/renderer/assets/putty.png differ diff --git a/interface/src/renderer/assets/remmina.png b/interface/src/renderer/assets/remmina.png new file mode 100644 index 0000000..bf91068 Binary files /dev/null and b/interface/src/renderer/assets/remmina.png differ diff --git a/interface/src/renderer/assets/resp.png b/interface/src/renderer/assets/resp.png new file mode 100644 index 0000000..d5b66cb Binary files /dev/null and b/interface/src/renderer/assets/resp.png differ diff --git a/interface/src/renderer/assets/securecrt.png b/interface/src/renderer/assets/securecrt.png new file mode 100644 index 0000000..4dfa678 Binary files /dev/null and b/interface/src/renderer/assets/securecrt.png differ diff --git a/interface/src/renderer/assets/terminal.png b/interface/src/renderer/assets/terminal.png new file mode 100644 index 0000000..605ff16 Binary files /dev/null and b/interface/src/renderer/assets/terminal.png differ diff --git a/interface/src/renderer/assets/winscp.png b/interface/src/renderer/assets/winscp.png new file mode 100644 index 0000000..04b2551 Binary files /dev/null and b/interface/src/renderer/assets/winscp.png differ diff --git a/interface/src/renderer/assets/xshell.png b/interface/src/renderer/assets/xshell.png new file mode 100644 index 0000000..325e2cd Binary files /dev/null and b/interface/src/renderer/assets/xshell.png differ diff --git a/interface/src/renderer/components/Dialog.vue b/interface/src/renderer/components/Dialog.vue new file mode 100644 index 0000000..f8604b4 --- /dev/null +++ b/interface/src/renderer/components/Dialog.vue @@ -0,0 +1,107 @@ + + + + + + diff --git a/interface/src/renderer/components/ListTable.vue b/interface/src/renderer/components/ListTable.vue new file mode 100644 index 0000000..c324892 --- /dev/null +++ b/interface/src/renderer/components/ListTable.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/interface/src/renderer/layouts/Main.vue b/interface/src/renderer/layouts/Main.vue new file mode 100644 index 0000000..6d0c0f3 --- /dev/null +++ b/interface/src/renderer/layouts/Main.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/interface/src/renderer/pages/About.vue b/interface/src/renderer/pages/About.vue new file mode 100644 index 0000000..51df6fc --- /dev/null +++ b/interface/src/renderer/pages/About.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/interface/src/renderer/pages/Databases.vue b/interface/src/renderer/pages/Databases.vue new file mode 100644 index 0000000..604dea8 --- /dev/null +++ b/interface/src/renderer/pages/Databases.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/interface/src/renderer/pages/FileTransfer.vue b/interface/src/renderer/pages/FileTransfer.vue new file mode 100644 index 0000000..f360c60 --- /dev/null +++ b/interface/src/renderer/pages/FileTransfer.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/interface/src/renderer/pages/RemoteDesktop.vue b/interface/src/renderer/pages/RemoteDesktop.vue new file mode 100644 index 0000000..3e61792 --- /dev/null +++ b/interface/src/renderer/pages/RemoteDesktop.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/interface/src/renderer/pages/Terminal.vue b/interface/src/renderer/pages/Terminal.vue new file mode 100644 index 0000000..629c78d --- /dev/null +++ b/interface/src/renderer/pages/Terminal.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/interface/src/renderer/router/index.js b/interface/src/renderer/router/index.js new file mode 100644 index 0000000..66f2eea --- /dev/null +++ b/interface/src/renderer/router/index.js @@ -0,0 +1,44 @@ +import { createRouter, createWebHashHistory } from 'vue-router' + +export default createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/', + name: 'mainPage', + redirect: '/ssh', + component: () => import('@/layouts/Main.vue'), + children: [ + { + path: '/ssh', + name: 'sshPage', + component: () => import('@/pages/Terminal.vue') + }, + { + path: '/remote', + name: 'remotePage', + component: () => import('@/pages/RemoteDesktop.vue') + }, + { + path: '/files', + name: 'fileTransferPage', + component: () => import('@/pages/FileTransfer.vue') + }, + { + path: '/databases', + name: 'databasesPage', + component: () => import('@/pages/Databases.vue') + }, + { + path: '/about', + name: 'aboutPage', + component: () => import('@/pages/About.vue') + } + ] + }, + { + path: '/:pathMatch(.*)*', + redirect: '/' + } + ] +}) diff --git a/interface/test/.eslintrc b/interface/test/.eslintrc new file mode 100644 index 0000000..3f26d66 --- /dev/null +++ b/interface/test/.eslintrc @@ -0,0 +1,11 @@ +{ + "env": { + "mocha": true + }, + "globals": { + "assert": true, + "expect": true, + "should": true, + "__static": true + } +} diff --git a/interface/test/e2e/index.js b/interface/test/e2e/index.js new file mode 100644 index 0000000..af4b0e7 --- /dev/null +++ b/interface/test/e2e/index.js @@ -0,0 +1,18 @@ +'use strict' + +// Set BABEL_ENV to use proper env config +process.env.BABEL_ENV = 'test' + +// Enable use of ES6+ on required files +require('babel-register')({ + ignore: /node_modules/ +}) + +// Attach Chai APIs to global scope +const { expect, should, assert } = require('chai') +global.expect = expect +global.should = should +global.assert = assert + +// Require all JS files in `./specs` for Mocha to consume +require('require-dir')('./specs') diff --git a/interface/test/e2e/specs/Launch.spec.js b/interface/test/e2e/specs/Launch.spec.js new file mode 100644 index 0000000..431116b --- /dev/null +++ b/interface/test/e2e/specs/Launch.spec.js @@ -0,0 +1,13 @@ +import utils from '../utils' + +describe('Launch', function () { + beforeEach(utils.beforeEach) + afterEach(utils.afterEach) + + it('shows the proper application title', function () { + return this.app.client.getTitle() + .then(title => { + expect(title).to.equal('picgo') + }) + }) +}) diff --git a/interface/test/e2e/utils.js b/interface/test/e2e/utils.js new file mode 100644 index 0000000..7d4e0da --- /dev/null +++ b/interface/test/e2e/utils.js @@ -0,0 +1,23 @@ +import electron from 'electron' +import { Application } from 'spectron' + +export default { + afterEach () { + this.timeout(10000) + + if (this.app && this.app.isRunning()) { + return this.app.stop() + } + }, + beforeEach () { + this.timeout(10000) + this.app = new Application({ + path: electron, + args: ['dist/electron/main.js'], + startTimeout: 10000, + waitTimeout: 10000 + }) + + return this.app.start() + } +} diff --git a/interface/test/unit/index.js b/interface/test/unit/index.js new file mode 100644 index 0000000..f07be98 --- /dev/null +++ b/interface/test/unit/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue' +Vue.config.devtools = false +Vue.config.productionTip = false + +// require all test files (files that ends with .spec.js) +const testsContext = require.context('./specs', true, /\.spec$/) +testsContext.keys().forEach(testsContext) + +// require all src files except main.js for coverage. +// you can also change this to match only the subset of files that +// you want coverage for. +const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) +srcContext.keys().forEach(srcContext) diff --git a/interface/test/unit/karma.conf.js b/interface/test/unit/karma.conf.js new file mode 100644 index 0000000..6204011 --- /dev/null +++ b/interface/test/unit/karma.conf.js @@ -0,0 +1,62 @@ +'use strict' + +const path = require('path') +const merge = require('webpack-merge') +const webpack = require('webpack') + +const baseConfig = require('../../.electron-vue/webpack.renderer.config') +const projectRoot = path.resolve(__dirname, '../../src/renderer') + +// Set BABEL_ENV to use proper preset config +process.env.BABEL_ENV = 'test' + +let webpackConfig = merge(baseConfig, { + devtool: '#inline-source-map', + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"testing"' + }) + ] +}) + +// don't treat dependencies as externals +delete webpackConfig.entry +delete webpackConfig.externals +delete webpackConfig.output.libraryTarget + +// apply vue option to apply isparta-loader on js +webpackConfig.module.rules + .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' + +module.exports = config => { + config.set({ + browsers: ['visibleElectron'], + client: { + useIframe: false + }, + coverageReporter: { + dir: './coverage', + reporters: [ + { type: 'lcov', subdir: '.' }, + { type: 'text-summary' } + ] + }, + customLaunchers: { + 'visibleElectron': { + base: 'Electron', + flags: ['--show'] + } + }, + frameworks: ['mocha', 'chai'], + files: ['./index.js'], + preprocessors: { + './index.js': ['webpack', 'sourcemap'] + }, + reporters: ['spec', 'coverage'], + singleRun: true, + webpack: webpackConfig, + webpackMiddleware: { + noInfo: true + } + }) +} diff --git a/interface/test/unit/specs/LandingPage.spec.js b/interface/test/unit/specs/LandingPage.spec.js new file mode 100644 index 0000000..58e3300 --- /dev/null +++ b/interface/test/unit/specs/LandingPage.spec.js @@ -0,0 +1,13 @@ +import Vue from 'vue' +import LandingPage from '@/layouts/LandingPage' + +describe('LandingPage.vue', () => { + it('should renderer correct contents', () => { + const vm = new Vue({ + el: document.createElement('div'), + render: h => h(LandingPage) + }).$mount() + + expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!') + }) +}) diff --git a/interface/vue.config.js b/interface/vue.config.js new file mode 100644 index 0000000..1b50f8f --- /dev/null +++ b/interface/vue.config.js @@ -0,0 +1,110 @@ +const path = require('path') +function resolve (dir) { + return path.join(__dirname, dir) +} + +module.exports = { + configureWebpack: { + devtool: 'nosources-source-map' + }, + chainWebpack: config => { + config.resolve.alias + .set('@', resolve('src/renderer')) + .set('~', resolve('src')) + .set('root', resolve('./')) + }, + pluginOptions: { + electronBuilder: { + nodeIntegration: true, + customFileProtocol: './', + builderOptions: { + productName: 'JumpServer本地客户端工具', + appId: 'com.jumpserver.client', + asar: false, + extraResources: [ + "bin/**" + ], + dmg: { + contents: [ + { + x: 410, + y: 150, + type: 'link', + path: '/Applications' + }, + { + x: 130, + y: 150, + type: 'file' + } + ] + }, + mac: { + icon: 'build/icons/icon.icns', + extendInfo: { + LSUIElement: 0 + }, + target: [{ + target: 'dmg', + arch: [ + 'x64', + 'arm64' + ] + }], + // eslint-disable-next-line no-template-curly-in-string + artifactName: 'JumpServer-Clients-Installer-${os}-${version}-${arch}.dmg', + protocols: { + name: "Jms", + schemes: ["jms"] + }, + }, + win: { + icon: 'build/icons/icon.ico', + // eslint-disable-next-line no-template-curly-in-string + artifactName: 'JumpServer-Clients-Installer-${os}-${version}-${arch}.exe', + target: [{ + target: 'nsis', + arch: [ + 'x64', + 'ia32' + ] + }] + }, + nsis: { + oneClick: false, + allowToChangeInstallationDirectory: true, + }, + linux: { + icon: 'build/icons/', + // eslint-disable-next-line no-template-curly-in-string + artifactName: 'JumpServer-Clients-Installer-${os}-${version}-${arch}.deb', + protocols: { + name: "Jms", + schemes: ["jms"] + }, + target: [{ + target: 'deb', + arch: [ + 'x64', + 'arm64' + ] + }] + }, + }, + chainWebpackMainProcess: (config) => { + config.resolve.alias + .set('@', resolve('src/renderer')) + .set('~', resolve('src')) + .set('root', resolve('./')) + config.output.filename((file) => { + if (file.chunk.name === 'index') { + return 'background.js'; + } else { + return '[name].js'; + } + }); + } + } + } +} +