├── records
└── .gitkeep
├── .gitignore
├── frontend
├── .eslintignore
├── .env.development
├── .env.production
├── embedfs.go
├── public
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── site.webmanifest
│ └── index.html
├── src
│ ├── assets
│ │ ├── logo.png
│ │ └── 404_images
│ │ │ ├── 404.png
│ │ │ └── 404_cloud.png
│ ├── views
│ │ ├── AboutView.vue
│ │ ├── HomeView.vue
│ │ ├── dashboard
│ │ │ └── index.vue
│ │ ├── banips
│ │ │ └── index.vue
│ │ ├── terminal
│ │ │ └── index.vue
│ │ ├── history
│ │ │ └── index.vue
│ │ ├── record
│ │ │ └── index.vue
│ │ ├── 404.vue
│ │ ├── login
│ │ │ └── index.vue
│ │ ├── script
│ │ │ └── index.vue
│ │ ├── credential
│ │ │ └── index.vue
│ │ └── cron
│ │ │ └── index.vue
│ ├── layout
│ │ ├── components
│ │ │ ├── index.js
│ │ │ ├── Sidebar
│ │ │ │ ├── FixiOSBug.js
│ │ │ │ ├── Item.vue
│ │ │ │ ├── Link.vue
│ │ │ │ ├── index.vue
│ │ │ │ ├── Logo.vue
│ │ │ │ └── SidebarItem.vue
│ │ │ ├── AppMain.vue
│ │ │ └── Navbar.vue
│ │ ├── mixin
│ │ │ └── ResizeHandler.js
│ │ └── index.vue
│ ├── App.vue
│ ├── icons
│ │ ├── svg
│ │ │ ├── link.svg
│ │ │ ├── user.svg
│ │ │ ├── example.svg
│ │ │ ├── table.svg
│ │ │ ├── password.svg
│ │ │ ├── nested.svg
│ │ │ ├── eye.svg
│ │ │ ├── eye-open.svg
│ │ │ ├── tree.svg
│ │ │ ├── dashboard.svg
│ │ │ └── form.svg
│ │ ├── index.js
│ │ └── svgo.yml
│ ├── utils
│ │ ├── get-page-title.js
│ │ ├── auth.js
│ │ ├── validate.js
│ │ ├── scroll-to.js
│ │ ├── request.js
│ │ └── index.js
│ ├── settings.js
│ ├── store
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── settings.js
│ │ │ ├── app.js
│ │ │ ├── permission.js
│ │ │ └── user.js
│ ├── styles
│ │ ├── mixin.scss
│ │ ├── variables.scss
│ │ ├── element-ui.scss
│ │ ├── transition.scss
│ │ ├── index.scss
│ │ └── sidebar.scss
│ ├── api
│ │ ├── cron.js
│ │ ├── script.js
│ │ ├── credential.js
│ │ ├── server.js
│ │ ├── task.js
│ │ └── user.js
│ ├── main.js
│ ├── components
│ │ ├── Hamburger
│ │ │ └── index.vue
│ │ ├── SvgIcon
│ │ │ └── index.vue
│ │ ├── Breadcrumb
│ │ │ └── index.vue
│ │ └── Pagination
│ │ │ └── index.vue
│ ├── permission.js
│ └── router
│ │ └── index.js
├── babel.config.js
├── .gitignore
├── jsconfig.json
├── README.md
├── .eslintrc.js
├── package.json
└── vue.config.js
├── main.go
├── misc
├── logger.go
└── config.go
├── config.yml.example
├── cmd
├── socks5.go
├── root.go
├── app.go
├── sshd.go
└── seed.go
├── middlewares
├── banip.go
├── enforce.go
└── token.go
├── models
├── permission.go
├── record.go
├── cron.go
├── script.go
├── task.go
├── credential.go
├── db.go
├── scopes.go
├── server.go
└── user.go
├── README.md
├── LICENSE
├── policy
└── enforcer.go
├── cache
└── cache.go
├── actions
├── script.go
├── credential.go
├── server.go
├── task.go
├── app.go
├── cron.go
└── terminal.go
└── go.mod
/records/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | records/*.cast
3 | config.yml
4 |
--------------------------------------------------------------------------------
/frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | src/assets
2 | public
3 | dist
4 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "diamond/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/.env.development:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'development'
3 |
4 | # base api
5 | VUE_APP_BASE_API = ''
6 |
--------------------------------------------------------------------------------
/frontend/.env.production:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'production'
3 |
4 | # base api
5 | VUE_APP_BASE_API = ''
6 |
--------------------------------------------------------------------------------
/frontend/embedfs.go:
--------------------------------------------------------------------------------
1 | package frontend
2 |
3 | import "embed"
4 |
5 | //go:embed dist
6 | var Index embed.FS
7 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/favicon-32x32.png
--------------------------------------------------------------------------------
/frontend/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/frontend/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/frontend/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/frontend/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stevenhwang/diamond/HEAD/frontend/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/frontend/src/views/AboutView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is an about page
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/misc/logger.go:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/rs/zerolog"
7 | )
8 |
9 | var Logger zerolog.Logger
10 |
11 | func init() {
12 | Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
13 | }
14 |
--------------------------------------------------------------------------------
/config.yml.example:
--------------------------------------------------------------------------------
1 | mysql:
2 | host: 192.168.1.188
3 | port: 3306
4 | user: root
5 | password: test123456
6 | dbName: diamond
7 | redis:
8 | host: 192.168.1.188
9 | port: 6379
10 | password: 12345
11 | dbName: 8
12 | poolSize: 10
13 | jwt:
14 | secret: secret
15 |
--------------------------------------------------------------------------------
/frontend/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/frontend/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'Vue Element'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/misc/config.go:
--------------------------------------------------------------------------------
1 | package misc
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/spf13/viper"
7 | )
8 |
9 | var Config *viper.Viper
10 |
11 | func init() {
12 | Config = viper.New()
13 | Config.SetConfigFile("./config.yml")
14 | err := Config.ReadInConfig()
15 | if err != nil {
16 | log.Fatalf("read config failed: %v", err)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/frontend/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: 'Vue Element',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: false,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether show the logo in sidebar
14 | */
15 | sidebarLogo: false
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "baseUrl": "./",
6 | "moduleResolution": "node",
7 | "paths": {
8 | "@/*": [
9 | "src/*"
10 | ]
11 | },
12 | "lib": [
13 | "esnext",
14 | "dom",
15 | "dom.iterable",
16 | "scripthost"
17 | ]
18 | }
19 | }
--------------------------------------------------------------------------------
/frontend/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | token: state => state.user.token,
5 | avatar: state => state.user.avatar,
6 | name: state => state.user.name,
7 | menus: state => state.user.menus,
8 | permission_routes: state => state.permission.routes
9 | }
10 | export default getters
11 |
--------------------------------------------------------------------------------
/frontend/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'vue_element_token'
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken(token) {
10 | return Cookies.set(TokenKey, token, { expires: 7 })
11 | }
12 |
13 | export function removeToken() {
14 | return Cookies.remove(TokenKey)
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # vue-element
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/frontend/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

5 |
6 |
7 |
8 |
9 |
20 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername(str) {
18 | const valid_map = ['admin', 'editor']
19 | return valid_map.indexOf(str.trim()) >= 0
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 | import app from './modules/app'
5 | import permission from './modules/permission'
6 | import settings from './modules/settings'
7 | import user from './modules/user'
8 |
9 | Vue.use(Vuex)
10 |
11 | const store = new Vuex.Store({
12 | modules: {
13 | app,
14 | permission,
15 | settings,
16 | user
17 | },
18 | getters
19 | })
20 |
21 | export default store
22 |
--------------------------------------------------------------------------------
/cmd/socks5.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "diamond/socks5"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var socks5Cmd = &cobra.Command{
10 | Use: "socks5",
11 | Short: "start socks5 server[开启socks5服务]",
12 |
13 | Run: func(cmd *cobra.Command, args []string) {
14 | addr, _ := cmd.Flags().GetString("addr")
15 | socks5.Start(addr)
16 | },
17 | }
18 |
19 | func init() {
20 | socks5Cmd.Flags().StringP("addr", "a", ":8538", "socks5 listen address")
21 | RootCmd.AddCommand(socks5Cmd)
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var RootCmd = &cobra.Command{
11 | Use: "help",
12 | Short: "Diamond devops",
13 | Run: func(cmd *cobra.Command, args []string) {
14 | fmt.Println("===================")
15 | fmt.Println("Welcome to Diamond!")
16 | fmt.Println("===================")
17 | },
18 | }
19 |
20 | func Execute() {
21 | if err := RootCmd.Execute(); err != nil {
22 | fmt.Fprintln(os.Stderr, err)
23 | os.Exit(1)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | parser: 'babel-eslint',
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true
11 | },
12 | extends: ['plugin:vue/recommended', 'eslint:recommended', 'prettier'],
13 |
14 | rules: {
15 | 'space-before-function-paren': 0,
16 | 'vue/multi-word-component-names': 0,
17 | "vue/first-attribute-linebreak": 0,
18 | "vue/component-definition-name-casing": 0
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/app.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "diamond/actions"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var appCmd = &cobra.Command{
10 | Use: "app",
11 | Short: "start app server[开启app服务]",
12 |
13 | Run: func(cmd *cobra.Command, args []string) {
14 | actions.CronStart()
15 | addr, _ := cmd.Flags().GetString("addr")
16 | app := actions.App
17 | app.Logger.Fatal(app.Start(addr))
18 | },
19 | }
20 |
21 | func init() {
22 | appCmd.Flags().StringP("addr", "a", ":8000", "app listen address")
23 | RootCmd.AddCommand(appCmd)
24 | }
25 |
--------------------------------------------------------------------------------
/middlewares/banip.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "diamond/cache"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // BanIP 中间件检查IP是否在黑名单
11 | func BanIP(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | c.Response().Header().Set(echo.HeaderServer, "Echo/Golang")
14 | b, err := cache.GetBan(c.RealIP())
15 | if err != nil {
16 | return echo.NewHTTPError(400, err.Error())
17 | }
18 | if b {
19 | return echo.NewHTTPError(http.StatusForbidden, "ip forbiden")
20 | }
21 | return next(c)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/models/permission.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Permission struct {
8 | ID uint `json:"id"`
9 | Name string `gorm:"size:256;unique" json:"name" filter:"name" validate:"required"`
10 | URL string `gorm:"size:128;index:idx_route,unique" json:"url" filter:"url" validate:"required"`
11 | Method string `gorm:"size:32;index:idx_route,unique" json:"method" filter:"method" validate:"required"` // GET POST PUT DELETE
12 | CreatedAt time.Time `json:"created_at"`
13 | UpdatedAt time.Time `json:"updated_at"`
14 | }
15 |
16 | type Permissions []Permission
17 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cmd/sshd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "diamond/sshd"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var sshdCmd = &cobra.Command{
10 | Use: "sshd",
11 | Short: "start sshd server[开启sshd服务]",
12 |
13 | Run: func(cmd *cobra.Command, args []string) {
14 | addr, _ := cmd.Flags().GetString("addr")
15 | keyPath, _ := cmd.Flags().GetString("keyPath")
16 | sshd.Start(addr, keyPath)
17 | },
18 | }
19 |
20 | func init() {
21 | sshdCmd.Flags().StringP("addr", "a", ":2222", "sshd listen address")
22 | sshdCmd.Flags().StringP("keyPath", "k", "C:/Users/90hua/.ssh/id_rsa", "sshd private key path")
23 | RootCmd.AddCommand(sshdCmd)
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/api/cron.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getCrons(params) {
4 | return request({
5 | url: '/api/crons',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
11 | export function createCron(data) {
12 | return request({
13 | url: '/api/crons',
14 | method: 'post',
15 | data
16 | })
17 | }
18 |
19 | export function updateCron(id, data) {
20 | return request({
21 | url: `/api/crons/${id}`,
22 | method: 'put',
23 | data
24 | })
25 | }
26 |
27 | export function deleteCron(id) {
28 | return request({
29 | url: `/api/crons/${id}`,
30 | method: 'delete'
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/middlewares/enforce.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "diamond/policy"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // Enforce 中间件使用casbin进行权限检查
11 | func Enforce(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | username := c.Get("username").(string)
14 | pass, err := policy.Enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method)
15 | if err != nil {
16 | return echo.NewHTTPError(http.StatusForbidden, err.Error())
17 | }
18 | if !pass {
19 | return echo.NewHTTPError(http.StatusForbidden, "This action is forbidden")
20 | }
21 | return next(c)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/api/script.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getScripts(params) {
4 | return request({
5 | url: '/api/scripts',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
11 | export function createScript(data) {
12 | return request({
13 | url: '/api/scripts',
14 | method: 'post',
15 | data
16 | })
17 | }
18 |
19 | export function updateScript(id, data) {
20 | return request({
21 | url: `/api/scripts/${id}`,
22 | method: 'put',
23 | data
24 | })
25 | }
26 |
27 | export function deleteScript(id) {
28 | return request({
29 | url: `/api/scripts/${id}`,
30 | method: 'delete'
31 | })
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/api/credential.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getCredentials(params) {
4 | return request({
5 | url: '/api/credentials',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
11 | export function createCredential(data) {
12 | return request({
13 | url: '/api/credentials',
14 | method: 'post',
15 | data
16 | })
17 | }
18 |
19 | export function updateCredential(id, data) {
20 | return request({
21 | url: `/api/credentials/${id}`,
22 | method: 'put',
23 | data
24 | })
25 | }
26 |
27 | export function deleteCredential(id) {
28 | return request({
29 | url: `/api/credentials/${id}`,
30 | method: 'delete'
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
4 |
5 | const state = {
6 | showSettings: showSettings,
7 | fixedHeader: fixedHeader,
8 | sidebarLogo: sidebarLogo
9 | }
10 |
11 | const mutations = {
12 | CHANGE_SETTING: (state, { key, value }) => {
13 | // eslint-disable-next-line no-prototype-builtins
14 | if (state.hasOwnProperty(key)) {
15 | state[key] = value
16 | }
17 | }
18 | }
19 |
20 | const actions = {
21 | changeSetting({ commit }, data) {
22 | commit('CHANGE_SETTING', data)
23 | }
24 | }
25 |
26 | export default {
27 | namespaced: true,
28 | state,
29 | mutations,
30 | actions
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/frontend/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 180px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets
4 |
5 | import ElementUI from 'element-ui'
6 | import 'element-ui/lib/theme-chalk/index.css'
7 | // import locale from 'element-ui/lib/locale/lang/en' // lang i18n
8 |
9 | import '@/styles/index.scss' // global css
10 |
11 | import App from './App'
12 | import store from './store'
13 | import router from './router'
14 |
15 | import '@/icons' // icon
16 | import '@/permission' // permission control
17 |
18 | // set ElementUI lang to EN
19 | // Vue.use(ElementUI, { locale })
20 | // 中文版 element-ui,按如下方式声明
21 | Vue.use(ElementUI)
22 |
23 | Vue.config.productionTip = false
24 |
25 | new Vue({
26 | router,
27 | store,
28 | render: h => h(App)
29 | }).$mount('#app')
30 |
--------------------------------------------------------------------------------
/models/record.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "diamond/misc"
5 | "os"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | type Record struct {
12 | ID uint `json:"id"`
13 | User string `gorm:"size:128" json:"user"` // 用户
14 | IP string `gorm:"size:128" json:"ip"` // 服务器IP
15 | FromIP string `gorm:"size:128" json:"from_ip"` // from IP
16 | File string `gorm:"size:128" json:"file"` // 记录文件名
17 | CreatedAt time.Time `json:"created_at"`
18 | }
19 |
20 | type Records []Record
21 |
22 | func (r *Record) AfterDelete(tx *gorm.DB) (err error) {
23 | if err := os.Remove(r.File); err != nil {
24 | misc.Logger.Info().Str("from", "db").Msg("file not exists")
25 | }
26 | misc.Logger.Info().Str("from", "db").Msg("file removed")
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/api/server.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getServers(params) {
4 | return request({
5 | url: '/api/servers',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
11 | export function createServer(data) {
12 | return request({
13 | url: '/api/servers',
14 | method: 'post',
15 | data
16 | })
17 | }
18 |
19 | export function updateServer(id, data) {
20 | return request({
21 | url: `/api/servers/${id}`,
22 | method: 'put',
23 | data
24 | })
25 | }
26 |
27 | export function deleteServer(id) {
28 | return request({
29 | url: `/api/servers/${id}`,
30 | method: 'delete'
31 | })
32 | }
33 |
34 | export function getRecords(params) {
35 | return request({
36 | url: '/api/records',
37 | method: 'get',
38 | params
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
20 |
21 |
33 |
34 |
42 |
--------------------------------------------------------------------------------
/models/cron.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | type Cron struct {
6 | ID uint `json:"id"`
7 | EntryID int `json:"entryID"` // 定时任务EntryID(自动生成)
8 | Name string `gorm:"size:128" json:"name" validate:"required" filter:"name"` // 定时任务名称
9 | Target string `gorm:"size:128" json:"target" validate:"required" filter:"target"` // ansible目标(服务器ip或服务器分组)
10 | ScriptID uint `json:"script_id" validate:"required"` // 关联执行的脚本
11 | Args string `gorm:"size:256" json:"args"` // 执行script时传入的参数(空格分开)
12 | Spec string `gorm:"size:128" json:"spec" validate:"required"` // 定时任务时间
13 | CreatedAt time.Time `json:"created_at"`
14 | UpdatedAt time.Time `json:"updated_at"`
15 | }
16 |
17 | type Crons []Cron
18 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
45 |
--------------------------------------------------------------------------------
/cmd/seed.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "diamond/models"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var seedCmd = &cobra.Command{
11 | Use: "seed",
12 | Short: "seed user account[创建用户账户]",
13 |
14 | Run: func(cmd *cobra.Command, args []string) {
15 | username, _ := cmd.Flags().GetString("username")
16 | password, _ := cmd.Flags().GetString("password")
17 | user := models.User{Username: username, Password: password, IsActive: true}
18 | res := models.DB.Create(&user)
19 | if res.Error != nil {
20 | fmt.Printf("seed user error: %s\n", res.Error.Error())
21 | } else {
22 | fmt.Printf("seed user success: user=> %s password=> %s\n", username, password)
23 | }
24 | },
25 | }
26 |
27 | func init() {
28 | seedCmd.Flags().StringP("username", "u", "admin", "seed username")
29 | seedCmd.Flags().StringP("password", "p", "12345678", "seed password")
30 | RootCmd.AddCommand(seedCmd)
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
46 | // to fix el-date-picker css style
47 | .el-range-separator {
48 | box-sizing: content-box;
49 | }
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## About
2 |
3 | diamond 是一个 golang 开发的完全开源的 devops 自动化运维平台
4 |
5 | - [x] 堡垒机(支持 web 终端和 ssh 客户端,支持密码和公钥连接)
6 | - [x] 权限控制(前端菜单,后端接口,服务器分配,ACL 权限)
7 | - [x] 任务平台(基于 ansible script 模块,配备脚本管理)
8 | - [x] 定时任务(支持秒级的 crontab 表达式)
9 | - [x] 支持 ssh 隧道,便于 navicate 连接使用
10 | - [x] 内置 socks5 代理,便于跟服务器内网通讯(比如使用 ftp 客户端等)
11 |
12 | 持续开发中。。。
13 |
14 | ## Require
15 |
16 | ```bash
17 | linux
18 | ansible
19 | golang 1.19+
20 | mysql 5.8+
21 | redis 3.2+
22 | ```
23 |
24 | ## Installation
25 |
26 | ```bash
27 | 前端:cd frontend && npm install && npm run build
28 | 后端: go build
29 | 编译出的二进制使用embed将前端打包的dist嵌入,所以可以单独部署
30 | ```
31 |
32 | ## Settings
33 |
34 | ```bash
35 | config.yml
36 | ```
37 |
38 | ## Usage
39 |
40 | ```bash
41 | Available Commands:
42 | app start app server[开启app服务]
43 | seed seed user account[创建用户账户]
44 | socks5 start socks5 server[开启socks5服务]
45 | sshd start sshd server[开启sshd服务]
46 | ```
47 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/models/script.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | type Script struct {
11 | ID uint `json:"id"`
12 | Name string `gorm:"size:256;unique" json:"name" validate:"required" filter:"name"` //脚本名称
13 | Content string `gorm:"type:text" json:"content" validate:"required"` //脚本内容
14 | CreatedAt time.Time `json:"created_at"`
15 | UpdatedAt time.Time `json:"updated_at"`
16 | }
17 |
18 | type Scripts []Script
19 |
20 | func (s *Script) BeforeDelete(tx *gorm.DB) (err error) {
21 | // 删除之前要检查是否有任务或定时任务在依赖这个脚本
22 | var countTask int64
23 | DB.Model(&Task{}).Where("script_id = ?", s.ID).Count(&countTask)
24 | if countTask > 0 {
25 | return errors.New("can not delete because an associated task exists")
26 | }
27 | var countCron int64
28 | DB.Model(&Cron{}).Where("script_id = ?", s.ID).Count(&countCron)
29 | if countCron > 0 {
30 | return errors.New("can not delete because an associated cron exists")
31 | }
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/api/task.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getTasks(params) {
4 | return request({
5 | url: '/api/tasks',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
11 | export function createTask(data) {
12 | return request({
13 | url: '/api/tasks',
14 | method: 'post',
15 | data
16 | })
17 | }
18 |
19 | export function updateTask(id, data) {
20 | return request({
21 | url: `/api/tasks/${id}`,
22 | method: 'put',
23 | data
24 | })
25 | }
26 |
27 | export function deleteTask(id) {
28 | return request({
29 | url: `/api/tasks/${id}`,
30 | method: 'delete'
31 | })
32 | }
33 |
34 | export function invokeTask(id) {
35 | return request({
36 | url: `/api/tasks/${id}`,
37 | method: 'post'
38 | })
39 | }
40 |
41 | export function getTaskHist(params) {
42 | return request({
43 | url: '/api/taskhist',
44 | method: 'get',
45 | params
46 | })
47 | }
48 |
49 | export function getTaskHistDetail(id) {
50 | return request({
51 | url: `/api/taskhist/${id}`,
52 | method: 'get'
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/middlewares/token.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "diamond/models"
5 |
6 | "github.com/golang-jwt/jwt/v4"
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // Token 中间件用来解析 jwt token
11 | func Token(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | token, ok := c.Get("user").(*jwt.Token)
14 | if !ok {
15 | return echo.NewHTTPError(400, "JWT token missing or invalid")
16 | }
17 | claims, ok := token.Claims.(jwt.MapClaims)
18 | if !ok {
19 | return echo.NewHTTPError(400, "failed to cast claims as jwt.MapClaims")
20 | }
21 | uid := uint(claims["uid"].(float64))
22 | username := claims["username"].(string)
23 | // 查看用户是否被禁用,有可能是登录之后被禁用,所以需要查询实时数据
24 | user := models.User{}
25 | if res := models.DB.First(&user, uid); res.Error != nil {
26 | return echo.NewHTTPError(400, res.Error.Error())
27 | }
28 | if !user.IsActive {
29 | return echo.NewHTTPError(401, "账号禁用")
30 | }
31 | // set context
32 | c.Set("uid", uid)
33 | c.Set("username", username)
34 | c.Set("menus", user.Menus)
35 | return next(c)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 steven
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
18 |
24 |
25 | <%= htmlWebpackPlugin.options.title %>
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/models/task.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Task struct {
8 | ID uint `json:"id"`
9 | Name string `gorm:"size:256;unique" json:"name" validate:"required" filter:"name"` // 名称
10 | Target string `gorm:"size:128" json:"target" validate:"required" filter:"target"` // ansible目标(服务器ip或服务器分组)
11 | ScriptID uint `json:"script_id" validate:"required"` // 关联执行的脚本
12 | Args string `gorm:"size:256" json:"args"` // 执行script时传入的参数(空格分开)
13 | CreatedAt time.Time `json:"created_at"`
14 | UpdatedAt time.Time `json:"updated_at"`
15 | }
16 |
17 | type Tasks []Task
18 |
19 | type TaskHistory struct {
20 | ID uint `json:"id"`
21 | TaskName string `gorm:"size:256" json:"task_name"`
22 | User string `gorm:"size:128" json:"user"` // 执行者
23 | FromIP string `gorm:"size:128" json:"from_ip"` // from IP
24 | Success bool `json:"success"` // 执行成功、失败
25 | Content string `gorm:"type:mediumtext" json:"content"` // 执行结果
26 | CreatedAt time.Time `json:"created_at"`
27 | }
28 |
29 | type TaskHistorys []TaskHistory
30 |
--------------------------------------------------------------------------------
/frontend/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 | @import './mixin.scss';
3 | @import './transition.scss';
4 | @import './element-ui.scss';
5 | @import './sidebar.scss';
6 |
7 | body {
8 | height: 100%;
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 | }
14 |
15 | label {
16 | font-weight: 700;
17 | }
18 |
19 | html {
20 | height: 100%;
21 | box-sizing: border-box;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
28 | *,
29 | *:before,
30 | *:after {
31 | box-sizing: inherit;
32 | }
33 |
34 | a:focus,
35 | a:active {
36 | outline: none;
37 | }
38 |
39 | a,
40 | a:focus,
41 | a:hover {
42 | cursor: pointer;
43 | color: inherit;
44 | text-decoration: none;
45 | }
46 |
47 | div:focus {
48 | outline: none;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | visibility: hidden;
54 | display: block;
55 | font-size: 0;
56 | content: " ";
57 | clear: both;
58 | height: 0;
59 | }
60 | }
61 |
62 | // main-container global css
63 | .app-container {
64 | padding: 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop'
9 | }
10 |
11 | const mutations = {
12 | TOGGLE_SIDEBAR: state => {
13 | state.sidebar.opened = !state.sidebar.opened
14 | state.sidebar.withoutAnimation = false
15 | if (state.sidebar.opened) {
16 | Cookies.set('sidebarStatus', 1)
17 | } else {
18 | Cookies.set('sidebarStatus', 0)
19 | }
20 | },
21 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
22 | Cookies.set('sidebarStatus', 0)
23 | state.sidebar.opened = false
24 | state.sidebar.withoutAnimation = withoutAnimation
25 | },
26 | TOGGLE_DEVICE: (state, device) => {
27 | state.device = device
28 | }
29 | }
30 |
31 | const actions = {
32 | toggleSideBar({ commit }) {
33 | commit('TOGGLE_SIDEBAR')
34 | },
35 | closeSideBar({ commit }, { withoutAnimation }) {
36 | commit('CLOSE_SIDEBAR', withoutAnimation)
37 | },
38 | toggleDevice({ commit }, device) {
39 | commit('TOGGLE_DEVICE', device)
40 | }
41 | }
42 |
43 | export default {
44 | namespaced: true,
45 | state,
46 | mutations,
47 | actions
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
31 |
32 |
44 |
--------------------------------------------------------------------------------
/policy/enforcer.go:
--------------------------------------------------------------------------------
1 | package policy
2 |
3 | import (
4 | "diamond/misc"
5 | "diamond/models"
6 |
7 | "github.com/casbin/casbin/v2"
8 | "github.com/casbin/casbin/v2/model"
9 | gormadapter "github.com/casbin/gorm-adapter/v3"
10 | )
11 |
12 | var Enforcer *casbin.Enforcer
13 |
14 | func init() {
15 | // 具有超级用户的ACL
16 | text :=
17 | `
18 | [request_definition]
19 | r = sub, obj, act
20 |
21 | [policy_definition]
22 | p = sub, obj, act
23 |
24 | [policy_effect]
25 | e = some(where (p.eft == allow))
26 |
27 | [matchers]
28 | m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act || r.sub == "admin"
29 | `
30 | m, err := model.NewModelFromString(text)
31 | if err != nil {
32 | misc.Logger.Fatal().Err(err).Str("from", "policy").Msg("load policy model failed")
33 | }
34 |
35 | a, err := gormadapter.NewAdapterByDB(models.DB)
36 | if err != nil {
37 | misc.Logger.Fatal().Err(err).Str("from", "policy").Msg("load policy adapter failed")
38 | }
39 |
40 | // 创建执行者
41 | e, err := casbin.NewEnforcer(m, a)
42 | if err != nil {
43 | misc.Logger.Fatal().Err(err).Str("from", "policy").Msg("init policy enforcer failed")
44 | }
45 | // add match func
46 | // e.AddNamedMatchingFunc("g2", "KeyMatch2", util.KeyMatch2)
47 | // Load the policy from DB.
48 | e.LoadPolicy()
49 |
50 | Enforcer = e
51 | }
52 |
53 | /* policy
54 | // route policy
55 | p, alice, /api/users, GET
56 | p, bob, /api/users/:id, PUT
57 | */
58 |
--------------------------------------------------------------------------------
/frontend/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | const { body } = document
4 | const WIDTH = 992 // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route() {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler)
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler)
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile()
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile')
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect()
32 | return rect.width - 1 < WIDTH
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile()
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/models/credential.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/Stevenhwang/gommon/tools"
8 | "gorm.io/gorm"
9 | )
10 |
11 | type Credential struct {
12 | ID uint `json:"id"`
13 | Name string `gorm:"size:128" json:"name" validate:"required"` // 名称
14 | AuthType uint `json:"auth_type" validate:"required"` // 认证类型 1.密码 2.密钥
15 | AuthUser string `gorm:"size:128" json:"auth_user" validate:"required"` // 认证用户
16 | AuthContent string `gorm:"type:text" json:"auth_content"` // 认证内容,密码或者私钥(没必要用加密的私钥)
17 | CreatedAt time.Time `json:"created_at"`
18 | UpdatedAt time.Time `json:"updated_at"`
19 | }
20 |
21 | type Credentials []Credential
22 |
23 | func (s *Credential) BeforeCreate(tx *gorm.DB) (err error) {
24 | if len(s.AuthContent) > 0 {
25 | pass := tools.AesEncrypt(s.AuthContent, "0123456789012345")
26 | s.AuthContent = pass
27 | }
28 | return
29 | }
30 |
31 | func (s *Credential) BeforeUpdate(tx *gorm.DB) (err error) {
32 | if len(s.AuthContent) > 0 {
33 | pass := tools.AesEncrypt(s.AuthContent, "0123456789012345")
34 | s.AuthContent = pass
35 | }
36 | return
37 | }
38 |
39 | func (u *Credential) BeforeDelete(tx *gorm.DB) (err error) {
40 | var count int64
41 | DB.Model(&Server{}).Where("credential_id = ?", u.ID).Count(&count)
42 | if count > 0 {
43 | return errors.New("can not delete because an associated server exists")
44 | }
45 | return
46 | }
47 |
--------------------------------------------------------------------------------
/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 | "diamond/misc"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/go-redis/redis/v8"
10 | )
11 |
12 | var ctx = context.Background()
13 | var Cache *redis.Client
14 |
15 | func init() {
16 | host := misc.Config.Get("redis.host")
17 | port := misc.Config.Get("redis.port")
18 | password := misc.Config.GetString("redis.password")
19 | dbName := misc.Config.GetInt("redis.dbName")
20 | poolSize := misc.Config.GetInt("redis.poolSize")
21 |
22 | addr := fmt.Sprintf("%v:%v", host, port)
23 | Cache = redis.NewClient(&redis.Options{
24 | Addr: addr,
25 | Password: password,
26 | DB: dbName,
27 | PoolSize: poolSize,
28 | })
29 | if res := Cache.Ping(ctx); res.Err() != nil {
30 | panic(res.Err())
31 | }
32 | }
33 |
34 | // ban ip for 7 days
35 | func Ban(ip string) error {
36 | res := Cache.Set(ctx, ip, 1, 168*time.Hour)
37 | return res.Err()
38 | }
39 |
40 | // get baned ip, true means banned ip exists
41 | func GetBan(ip string) (bool, error) {
42 | _, err := Cache.Get(ctx, ip).Result()
43 | if err == redis.Nil {
44 | return false, nil
45 | }
46 | if err != nil {
47 | return false, err
48 | }
49 | return true, nil
50 | }
51 |
52 | func FilterBanIPs(keyword string) ([]string, error) {
53 | var key string
54 | if len(keyword) > 0 {
55 | key = fmt.Sprintf("*%s*", keyword)
56 | } else {
57 | key = "*"
58 | }
59 | return Cache.Keys(ctx, key).Result()
60 | }
61 |
62 | func DelBanIP(ip string) error {
63 | _, err := Cache.Del(ctx, ip).Result()
64 | return err
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | import { asyncRoutes, constantRoutes } from '@/router'
2 |
3 | /**
4 | * Use meta.role to determine if the current user has permission
5 | * @param menus
6 | * @param route
7 | */
8 | function hasPermission(menus, route) {
9 | if (menus.indexOf(route.name) >= 0 || route.path === '*') {
10 | return true
11 | } else {
12 | return false
13 | }
14 | }
15 |
16 | /**
17 | * Filter asynchronous routing tables by recursion
18 | * @param routes asyncRoutes
19 | * @param menus
20 | */
21 | export function filterAsyncRoutes(routes, menus) {
22 | const res = []
23 |
24 | routes.forEach(route => {
25 | const tmp = { ...route }
26 | if (hasPermission(menus, tmp)) {
27 | res.push(tmp)
28 | }
29 | })
30 |
31 | return res
32 | }
33 |
34 | const state = {
35 | routes: [],
36 | addRoutes: []
37 | }
38 |
39 | const mutations = {
40 | SET_ROUTES: (state, routes) => {
41 | state.addRoutes = routes
42 | state.routes = constantRoutes.concat(routes)
43 | }
44 | }
45 |
46 | const actions = {
47 | generateRoutes({ commit }, data) {
48 | return new Promise(resolve => {
49 | let accessedRoutes
50 | if (data.name === 'admin') {
51 | accessedRoutes = asyncRoutes
52 | } else {
53 | accessedRoutes = filterAsyncRoutes(asyncRoutes, data.menus)
54 | }
55 | commit('SET_ROUTES', accessedRoutes)
56 | resolve(accessedRoutes)
57 | })
58 | }
59 | }
60 |
61 | export default {
62 | namespaced: true,
63 | state,
64 | mutations,
65 | actions
66 | }
67 |
--------------------------------------------------------------------------------
/models/db.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "diamond/misc"
5 | "fmt"
6 | "time"
7 |
8 | "gorm.io/driver/mysql"
9 | "gorm.io/gorm"
10 | "gorm.io/gorm/logger"
11 | )
12 |
13 | var DB *gorm.DB
14 |
15 | func init() {
16 | host := misc.Config.GetString("mysql.host")
17 | port := misc.Config.GetInt("mysql.port")
18 | user := misc.Config.GetString("mysql.user")
19 | password := misc.Config.GetString("mysql.password")
20 | dbName := misc.Config.GetString("mysql.dbName")
21 |
22 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", user, password, host, port, dbName)
23 | // db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
24 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)})
25 | if err != nil {
26 | misc.Logger.Fatal().Err(err).Str("from", "db").Msg("connect mysql error")
27 | }
28 | sqlDB, err := db.DB()
29 | if err != nil {
30 | panic(err)
31 | }
32 | // SetMaxIdleConns 设置空闲连接池中连接的最大数量
33 | sqlDB.SetMaxIdleConns(5)
34 | // SetMaxOpenConns 设置打开数据库连接的最大数量
35 | sqlDB.SetMaxOpenConns(20)
36 | // SetConnMaxLifetime 设置了连接可复用的最大时间
37 | sqlDB.SetConnMaxLifetime(time.Hour)
38 | // 迁移 schema
39 | db.AutoMigrate(&User{}, &Server{}, &Credential{}, &Permission{}, &Record{}, &Script{}, &Task{}, &TaskHistory{}, &Cron{})
40 | // seed admin user
41 | // var count int64
42 | // db.Model(&User{}).Where("username = ?", "admin").Count(&count)
43 | // if count == 0 {
44 | // admin := User{Username: "admin", Password: "12345678", IsActive: true}
45 | // db.Create(&admin)
46 | // }
47 | DB = db
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
12 |
13 |
14 |
53 |
54 |
69 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
57 |
--------------------------------------------------------------------------------
/models/scopes.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strconv"
7 |
8 | "github.com/labstack/echo/v4"
9 | "gorm.io/gorm"
10 | )
11 |
12 | // 分页
13 | func Paginate(c echo.Context) func(db *gorm.DB) *gorm.DB {
14 | return func(db *gorm.DB) *gorm.DB {
15 | page, _ := strconv.Atoi(c.QueryParam("page"))
16 | if page <= 0 {
17 | page = 1
18 | }
19 | pageSize, _ := strconv.Atoi(c.QueryParam("limit"))
20 | if pageSize <= 0 {
21 | pageSize = 15
22 | }
23 | offset := (page - 1) * pageSize
24 | return db.Offset(offset).Limit(pageSize)
25 | }
26 | }
27 |
28 | // and,like查询,带filter tag的string字段,需要查询端带相应字段来查询
29 | func Filter(model interface{}, c echo.Context) func(db *gorm.DB) *gorm.DB {
30 | reflectType := reflect.ValueOf(model).Type()
31 | return func(db *gorm.DB) *gorm.DB {
32 | for i := 0; i < reflectType.NumField(); i++ {
33 | field := reflectType.Field(i).Tag.Get("filter")
34 | if f := c.QueryParam(field); len(f) > 0 {
35 | qString := fmt.Sprintf("%s like ?", field)
36 | db.Where(qString, "%"+f+"%")
37 | }
38 | }
39 | return db
40 | }
41 | }
42 |
43 | // or,like查询,带filter tag的string字段,不需要携带字段,只要带查询关键字query
44 | func AnyFilter(model interface{}, c echo.Context) func(db *gorm.DB) *gorm.DB {
45 | query := c.QueryParam("query")
46 | if len(query) > 0 {
47 | reflectType := reflect.ValueOf(model).Type()
48 | return func(db *gorm.DB) *gorm.DB {
49 | for i := 0; i < reflectType.NumField(); i++ {
50 | if field := reflectType.Field(i).Tag.Get("filter"); len(field) > 0 {
51 | qString := fmt.Sprintf("%s like ?", field)
52 | db.Or(qString, "%"+query+"%")
53 | }
54 | }
55 | return db
56 | }
57 | } else {
58 | return func(db *gorm.DB) *gorm.DB {
59 | return db
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-element",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "asciinema-player": "^3.0.1",
12 | "axios": "^1.2.1",
13 | "core-js": "^3.8.3",
14 | "element-ui": "^2.15.12",
15 | "js-cookie": "^3.0.1",
16 | "normalize.css": "^8.0.1",
17 | "nprogress": "^0.2.0",
18 | "path-browserify": "^1.0.1",
19 | "path-to-regexp": "^6.2.1",
20 | "vue": "^2.6.14",
21 | "vue-codemirror": "^4.0.6",
22 | "vue-router": "^3.5.1",
23 | "vuex": "^3.6.2",
24 | "xterm": "^4.9.0",
25 | "xterm-addon-attach": "^0.6.0",
26 | "xterm-addon-fit": "^0.4.0"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.12.16",
30 | "@babel/eslint-parser": "^7.12.16",
31 | "@vue/cli-plugin-babel": "~5.0.0",
32 | "@vue/cli-plugin-eslint": "~5.0.0",
33 | "@vue/cli-plugin-router": "~5.0.0",
34 | "@vue/cli-service": "~5.0.0",
35 | "babel-eslint": "^10.1.0",
36 | "eslint": "^7.32.0",
37 | "eslint-config-prettier": "^8.5.0",
38 | "eslint-plugin-vue": "^8.0.3",
39 | "sass": "^1.56.2",
40 | "sass-loader": "^13.2.0",
41 | "svg-sprite-loader": "^6.0.11",
42 | "vue-template-compiler": "^2.6.14"
43 | },
44 | "eslintConfig": {
45 | "root": true,
46 | "env": {
47 | "node": true
48 | },
49 | "extends": [
50 | "plugin:vue/essential",
51 | "eslint:recommended"
52 | ],
53 | "parserOptions": {
54 | "parser": "@babel/eslint-parser"
55 | },
56 | "rules": {}
57 | },
58 | "browserslist": [
59 | "> 1%",
60 | "last 2 versions",
61 | "not dead"
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hello, {{ name }}
4 |
5 |
8 |
9 |
10 |
13 |
14 |
15 |
18 |
19 |
20 |
23 |
24 |
25 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
50 |
51 |
62 |
--------------------------------------------------------------------------------
/frontend/src/utils/scroll-to.js:
--------------------------------------------------------------------------------
1 | Math.easeInOutQuad = function(t, b, c, d) {
2 | t /= d / 2
3 | if (t < 1) {
4 | return c / 2 * t * t + b
5 | }
6 | t--
7 | return -c / 2 * (t * (t - 2) - 1) + b
8 | }
9 |
10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
11 | var requestAnimFrame = (function() {
12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
13 | })()
14 |
15 | /**
16 | * Because it's so fucking difficult to detect the scrolling element, just move them all
17 | * @param {number} amount
18 | */
19 | function move(amount) {
20 | document.documentElement.scrollTop = amount
21 | document.body.parentNode.scrollTop = amount
22 | document.body.scrollTop = amount
23 | }
24 |
25 | function position() {
26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
27 | }
28 |
29 | /**
30 | * @param {number} to
31 | * @param {number} duration
32 | * @param {Function} callback
33 | */
34 | export function scrollTo(to, duration, callback) {
35 | const start = position()
36 | const change = to - start
37 | const increment = 20
38 | let currentTime = 0
39 | duration = (typeof (duration) === 'undefined') ? 500 : duration
40 | var animateScroll = function() {
41 | // increment the time
42 | currentTime += increment
43 | // find the value with the quadratic in-out easing function
44 | var val = Math.easeInOutQuad(currentTime, start, change, duration)
45 | // move the document.body
46 | move(val)
47 | // do the animation unless its over
48 | if (currentTime < duration) {
49 | requestAnimFrame(animateScroll)
50 | } else {
51 | if (callback && typeof (callback) === 'function') {
52 | // the animation is done so lets callback
53 | callback()
54 | }
55 | }
56 | }
57 | animateScroll()
58 | }
59 |
--------------------------------------------------------------------------------
/models/server.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | var InstanceTypes = map[string]string{
10 | "t2.nano": "1核0.5G",
11 | "t2.micro": "1核1G",
12 | "t2.small": "1核2G",
13 | "t2.medium": "2核4G",
14 | "t2.large": "2核8G",
15 | "t2.xlarge": "4核16G",
16 | "t2.2xlarge": "8核32G",
17 | "t3.nano": "2核0.5G",
18 | "t3.micro": "2核1G",
19 | "t3.small": "2核2G",
20 | "t3.medium": "2核4G",
21 | "t3.large": "2核8G",
22 | "t3.xlarge": "4核16G",
23 | "t3.2xlarge": "8核32G",
24 | "c4.large": "2核3.75G",
25 | "c4.xlarge": "4核7.5G",
26 | "c4.2xlarge": "8核15G",
27 | "c4.4xlarge": "16核30G",
28 | "c4.8xlarge": "36核60G",
29 | "c5.large": "2核4G",
30 | "c5.xlarge": "4核8G",
31 | "c5.2xlarge": "8核16G",
32 | "c5.4xlarge": "16核32G",
33 | }
34 |
35 | type Server struct {
36 | ID uint `json:"id"`
37 | Name string `gorm:"size:128" json:"name" validate:"required" filter:"name"` // 机器名称或主机名
38 | IP string `gorm:"size:128;unique" json:"ip" validate:"required,ip" filter:"ip"`
39 | Port uint `gorm:"default:22" json:"port" validate:"required,gte=0,lte=65535"`
40 | CredentialID uint `json:"credential_id" validate:"required"` // 关联认证
41 | Remark string `gorm:"type:text" json:"remark" filter:"remark"` // 记录机器用途
42 | InstanceType string `gorm:"size:128" json:"instance_type"` // 实例类型
43 | Specifications string `gorm:"size:128" json:"specifications"` // 实例配置
44 | CreatedAt time.Time `json:"created_at"`
45 | UpdatedAt time.Time `json:"updated_at"`
46 | }
47 |
48 | type Servers []Server
49 |
50 | func (s *Server) BeforeCreate(tx *gorm.DB) (err error) {
51 | if len(s.InstanceType) > 0 {
52 | s.Specifications = InstanceTypes[s.InstanceType]
53 | }
54 | return nil
55 | }
56 |
57 | func (s *Server) BeforeUpdate(tx *gorm.DB) (err error) {
58 | if len(s.InstanceType) > 0 {
59 | s.Specifications = InstanceTypes[s.InstanceType]
60 | }
61 | return nil
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/views/banips/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
12 |
13 |
14 |
15 |
18 |
20 | {{tag}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
79 |
--------------------------------------------------------------------------------
/frontend/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { Message } from 'element-ui'
3 | import store from '@/store'
4 | import { getToken } from '@/utils/auth'
5 |
6 | // create an axios instance
7 | const service = axios.create({
8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
9 | // withCredentials: true, // send cookies when cross-domain requests
10 | timeout: 5000 // request timeout
11 | })
12 |
13 | // request interceptor
14 | service.interceptors.request.use(
15 | config => {
16 | // do something before request is sent
17 |
18 | if (store.getters.token) {
19 | // let each request carry token
20 | // ['X-Token'] is a custom headers key
21 | // please modify it according to the actual situation
22 | // config.headers['X-Token'] = getToken()
23 | const token = getToken()
24 | config.headers['Authorization'] = `Bearer ${token}`
25 | }
26 | return config
27 | },
28 | error => {
29 | // do something with request error
30 | console.log(error) // for debug
31 | return Promise.reject(error)
32 | }
33 | )
34 |
35 | // response interceptor
36 | service.interceptors.response.use(
37 | /**
38 | * If you want to get http information such as headers or status
39 | * Please return response => response
40 | */
41 |
42 | /**
43 | * Determine the request status by custom code
44 | * Here is just an example
45 | * You can also judge the status by HTTP Status Code
46 | */
47 | response => {
48 | const res = response.data
49 |
50 | // 返回success为true代表返回值正确
51 | if (!res.success) {
52 | Message({
53 | message: res.reason || 'Error',
54 | type: 'error',
55 | duration: 5 * 1000
56 | })
57 | return Promise.reject(new Error(res.reason || 'Error'))
58 | } else {
59 | return res
60 | }
61 | },
62 | error => {
63 | console.log('err' + error) // for debug
64 | Message({
65 | message: error.message,
66 | type: 'error',
67 | duration: 5 * 1000
68 | })
69 | return Promise.reject(error)
70 | }
71 | )
72 |
73 | export default service
74 |
--------------------------------------------------------------------------------
/actions/script.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "diamond/models"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | func getScripts(c echo.Context) error {
10 | var total int64
11 | scripts := models.Scripts{}
12 | if res := models.DB.Scopes(models.AnyFilter(models.Script{}, c)).Model(&models.Script{}).Count(&total); res.Error != nil {
13 | return echo.NewHTTPError(400, res.Error.Error())
14 | }
15 | if res := models.DB.Scopes(models.Paginate(c), models.AnyFilter(models.Script{}, c)).Find(&scripts); res.Error != nil {
16 | return echo.NewHTTPError(400, res.Error.Error())
17 | }
18 | return c.JSON(200, echo.Map{"success": true, "data": scripts, "total": total})
19 | }
20 |
21 | func createScript(c echo.Context) error {
22 | script := models.Script{}
23 | if err := c.Bind(&script); err != nil {
24 | return echo.NewHTTPError(400, err.Error())
25 | }
26 | if err := c.Validate(&script); err != nil {
27 | return echo.NewHTTPError(400, err.Error())
28 | }
29 | if result := models.DB.Create(&script); result.Error != nil {
30 | return echo.NewHTTPError(400, result.Error.Error())
31 | }
32 | return c.JSON(200, echo.Map{"success": true})
33 | }
34 |
35 | func updateScript(c echo.Context) error {
36 | script := models.Script{}
37 | if result := models.DB.First(&script, c.Param("id")); result.Error != nil {
38 | return echo.NewHTTPError(400, result.Error.Error())
39 | }
40 | if err := c.Bind(&script); err != nil {
41 | return echo.NewHTTPError(400, err.Error())
42 | }
43 | if err := c.Validate(&script); err != nil {
44 | return echo.NewHTTPError(400, err.Error())
45 | }
46 | if result := models.DB.Save(&script); result.Error != nil {
47 | return echo.NewHTTPError(400, result.Error.Error())
48 | }
49 | return c.JSON(200, echo.Map{"success": true})
50 | }
51 |
52 | func deleteScript(c echo.Context) error {
53 | script := models.Script{}
54 | if result := models.DB.First(&script, c.Param("id")); result.Error != nil {
55 | return echo.NewHTTPError(400, result.Error.Error())
56 | }
57 | // ensure delete hook run
58 | if res := models.DB.Delete(&script); res.Error != nil {
59 | return echo.NewHTTPError(400, res.Error.Error())
60 | }
61 | return c.JSON(200, echo.Map{"success": true})
62 | }
63 |
--------------------------------------------------------------------------------
/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/Stevenhwang/gommon/nulls"
8 | "github.com/Stevenhwang/gommon/tools"
9 | passwordvalidator "github.com/wagslane/go-password-validator"
10 | "gorm.io/datatypes"
11 | "gorm.io/gorm"
12 | )
13 |
14 | type User struct {
15 | ID uint `json:"id"`
16 | Username string `gorm:"size:128;unique" json:"username" validate:"required"`
17 | Password string `gorm:"size:128" json:"password"`
18 | Publickey string `gorm:"type:text" json:"publickey"` // 公钥,用于免密登录ssh服务器
19 | Menus datatypes.JSON `gorm:"type:json" json:"menus"` // 给用户分配的菜单
20 | IsActive bool `json:"is_active"` // 账号是否激活
21 | LastLoginIP nulls.String `gorm:"size:128" json:"last_login_ip"`
22 | LastLoginTime nulls.Time `json:"last_login_time"`
23 | Servers Servers `gorm:"many2many:user_servers;"`
24 | CreatedAt time.Time `json:"created_at"`
25 | UpdatedAt time.Time `json:"updated_at"`
26 | }
27 |
28 | type Users []User
29 |
30 | var entropy = passwordvalidator.GetEntropy("a very safe password")
31 |
32 | func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
33 | if len(u.Password) == 0 {
34 | return errors.New("password can not be empty")
35 | }
36 | // 密码强度检测
37 | errs := passwordvalidator.Validate(u.Password, entropy)
38 | if errs != nil {
39 | return errs
40 | }
41 | pass, err := tools.GeneratePassword(u.Password)
42 | if err != nil {
43 | return err
44 | }
45 | u.Password = pass
46 | // generate otp key
47 | return nil
48 | }
49 |
50 | func (u *User) AfterCreate(tx *gorm.DB) (err error) {
51 | return nil
52 | }
53 |
54 | func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
55 | if len(u.Password) > 0 {
56 | // 密码强度检测
57 | errs := passwordvalidator.Validate(u.Password, entropy)
58 | if errs != nil {
59 | return errs
60 | }
61 | pass, err := tools.GeneratePassword(u.Password)
62 | if err != nil {
63 | return err
64 | }
65 | u.Password = pass
66 | }
67 | return nil
68 | }
69 |
70 | func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
71 | return
72 | }
73 |
74 | func (u *User) AfterDelete(tx *gorm.DB) (err error) {
75 | return
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function login(data) {
4 | return request({
5 | url: '/api/login',
6 | method: 'post',
7 | data
8 | })
9 | }
10 |
11 | export function getInfo() {
12 | return request({
13 | url: '/api/user_info',
14 | method: 'get'
15 | })
16 | }
17 |
18 | export function resetPass(data) {
19 | return request({
20 | url: '/api/reset_pw',
21 | method: 'post',
22 | data
23 | })
24 | }
25 |
26 | export function getUsers(params) {
27 | return request({
28 | url: '/api/users',
29 | method: 'get',
30 | params
31 | })
32 | }
33 |
34 | export function createUser(data) {
35 | return request({
36 | url: '/api/users',
37 | method: 'post',
38 | data
39 | })
40 | }
41 |
42 | export function updateUser(id, data) {
43 | return request({
44 | url: `/api/users/${id}`,
45 | method: 'put',
46 | data
47 | })
48 | }
49 |
50 | export function deleteUser(id) {
51 | return request({
52 | url: `/api/users/${id}`,
53 | method: 'delete'
54 | })
55 | }
56 |
57 | export function getUserPerms(params) {
58 | return request({
59 | url: '/api/userPerms',
60 | method: 'get',
61 | params
62 | })
63 | }
64 |
65 | export function assignUserPerm(data) {
66 | return request({
67 | url: '/api/userPerms',
68 | method: 'post',
69 | data
70 | })
71 | }
72 |
73 | export function getUserServers(params) {
74 | return request({
75 | url: '/api/userServers',
76 | method: 'get',
77 | params
78 | })
79 | }
80 |
81 | export function assignUserServer(data) {
82 | return request({
83 | url: '/api/userServers',
84 | method: 'post',
85 | data
86 | })
87 | }
88 |
89 | export function syncPerms() {
90 | return request({
91 | url: '/api/syncPerms',
92 | method: 'post',
93 | })
94 | }
95 |
96 | export function getBanIPs(params) {
97 | return request({
98 | url: '/api/banips',
99 | method: 'get',
100 | params: params
101 | })
102 | }
103 |
104 | export function delBanIP(data) {
105 | return request({
106 | url: '/api/delbanip',
107 | method: 'post',
108 | data: data
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/frontend/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
55 |
56 |
97 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
45 |
46 |
95 |
--------------------------------------------------------------------------------
/frontend/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/actions/credential.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "diamond/models"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | func getCredentials(c echo.Context) error {
10 | var total int64
11 | credentials := models.Credentials{}
12 | if res := models.DB.Model(&models.Credential{}).Count(&total); res.Error != nil {
13 | return echo.NewHTTPError(400, res.Error.Error())
14 | }
15 | if res := models.DB.Scopes(models.Paginate(c)).Omit("auth_content").Find(&credentials); res.Error != nil {
16 | return echo.NewHTTPError(400, res.Error.Error())
17 | }
18 | return c.JSON(200, echo.Map{"success": true, "data": credentials, "total": total})
19 | }
20 |
21 | func createCredential(c echo.Context) error {
22 | credential := models.Credential{}
23 | if err := c.Bind(&credential); err != nil {
24 | return echo.NewHTTPError(400, err.Error())
25 | }
26 | if err := c.Validate(&credential); err != nil {
27 | return echo.NewHTTPError(400, err.Error())
28 | }
29 | if result := models.DB.Create(&credential); result.Error != nil {
30 | return echo.NewHTTPError(400, result.Error.Error())
31 | }
32 | return c.JSON(200, echo.Map{"success": true})
33 | }
34 |
35 | func updateCredential(c echo.Context) error {
36 | credential := models.Credential{}
37 | if res := models.DB.First(&credential, c.Param("id")); res.Error != nil {
38 | return echo.NewHTTPError(400, res.Error.Error())
39 | }
40 | if err := c.Bind(&credential); err != nil {
41 | return echo.NewHTTPError(400, err.Error())
42 | }
43 | if err := c.Validate(&credential); err != nil {
44 | return echo.NewHTTPError(400, err.Error())
45 | }
46 | // 处理auth_content更新
47 | excludeColumns := []string{}
48 | if len(credential.AuthContent) == 0 {
49 | excludeColumns = append(excludeColumns, "auth_content")
50 | }
51 | if result := models.DB.Select("*").Omit(excludeColumns...).Updates(&credential); result.Error != nil {
52 | return echo.NewHTTPError(400, result.Error.Error())
53 | }
54 | return c.JSON(200, echo.Map{"success": true})
55 | }
56 |
57 | func deleteCredential(c echo.Context) error {
58 | credential := models.Credential{}
59 | if result := models.DB.First(&credential, c.Param("id")); result.Error != nil {
60 | return echo.NewHTTPError(400, result.Error.Error())
61 | }
62 | // ensure delete hook run
63 | if res := models.DB.Delete(&credential); res.Error != nil {
64 | return echo.NewHTTPError(400, res.Error.Error())
65 | }
66 | return c.JSON(200, echo.Map{"success": true})
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 | {{ item.meta.title }}
9 | {{ item.meta.title }}
11 |
12 |
13 |
14 |
15 |
16 |
69 |
70 |
83 |
--------------------------------------------------------------------------------
/frontend/src/views/terminal/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
78 |
89 |
--------------------------------------------------------------------------------
/frontend/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
91 |
92 |
101 |
--------------------------------------------------------------------------------
/frontend/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // progress bar
5 | import 'nprogress/nprogress.css' // progress bar style
6 | import { getToken } from '@/utils/auth' // get token from cookie
7 | import getPageTitle from '@/utils/get-page-title'
8 |
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 |
11 | const whiteList = ['/login'] // no redirect whitelist
12 |
13 | router.beforeEach(async (to, from, next) => {
14 | // start progress bar
15 | NProgress.start()
16 |
17 | // set page title
18 | document.title = getPageTitle(to.meta.title)
19 |
20 | // determine whether the user has logged in
21 | const hasToken = getToken()
22 |
23 | if (hasToken) {
24 | if (to.path === '/login') {
25 | // if is logged in, redirect to the home page
26 | next({ path: '/' })
27 | NProgress.done()
28 | } else {
29 | // determine whether the user has obtained his permission roles through getInfo
30 | const hasName = store.getters.name
31 | if (hasName) {
32 | next()
33 | } else {
34 | try {
35 | // get user info
36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
37 | const data = await store.dispatch('user/getInfo')
38 |
39 | // generate accessible routes map based on roles
40 | const accessRoutes = await store.dispatch('permission/generateRoutes', data)
41 |
42 | // dynamically add accessible routes
43 | // router.addRoutes(accessRoutes)
44 | for (let x of accessRoutes) {
45 | router.addRoute(x)
46 | }
47 |
48 | // hack method to ensure that addRoutes is complete
49 | // set the replace: true, so the navigation will not leave a history record
50 | next({ ...to, replace: true })
51 | } catch (error) {
52 | // remove token and go to login page to re-login
53 | await store.dispatch('user/resetToken')
54 | Message.error(error || 'Has Error')
55 | next(`/login?redirect=${to.path}`)
56 | NProgress.done()
57 | }
58 | }
59 | }
60 | } else {
61 | /* has no token*/
62 |
63 | if (whiteList.indexOf(to.path) !== -1) {
64 | // in the free login whitelist, go directly
65 | next()
66 | } else {
67 | // other pages that do not have permission to access are redirected to the login page.
68 | next(`/login?redirect=${to.path}`)
69 | NProgress.done()
70 | }
71 | }
72 | })
73 |
74 | router.afterEach(() => {
75 | // finish progress bar
76 | NProgress.done()
77 | })
78 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, getInfo } from '@/api/user'
2 | import { getToken, setToken, removeToken } from '@/utils/auth'
3 | import { resetRouter } from '@/router'
4 |
5 | const getDefaultState = () => {
6 | return {
7 | token: getToken(),
8 | name: '',
9 | avatar: '',
10 | menus: []
11 | }
12 | }
13 |
14 | const state = getDefaultState()
15 |
16 | const mutations = {
17 | RESET_STATE: (state) => {
18 | Object.assign(state, getDefaultState())
19 | },
20 | SET_TOKEN: (state, token) => {
21 | state.token = token
22 | },
23 | SET_NAME: (state, name) => {
24 | state.name = name
25 | },
26 | SET_AVATAR: (state, avatar) => {
27 | state.avatar = avatar
28 | },
29 | SET_MENUS: (state, menus) => {
30 | state.menus = menus
31 | }
32 | }
33 |
34 | const actions = {
35 | // user login
36 | login({ commit }, userInfo) {
37 | const { username, password } = userInfo
38 | return new Promise((resolve, reject) => {
39 | login({ username: username.trim(), password: password }).then(resp => {
40 | commit('SET_TOKEN', resp.token)
41 | setToken(resp.token)
42 | resolve()
43 | }).catch(error => {
44 | reject(error)
45 | })
46 | })
47 | },
48 |
49 | // get user info
50 | getInfo({ commit, state }) {
51 | return new Promise((resolve, reject) => {
52 | getInfo(state.token).then(resp => {
53 |
54 | if (!resp) {
55 | reject('Verification failed, please Login again.')
56 | }
57 |
58 | const { menus, name } = resp
59 |
60 | // // roles must be a non-empty array
61 | // if (!roles || roles.length <= 0) {
62 | // reject('getInfo: roles must be a non-null array!')
63 | // }
64 |
65 | commit('SET_MENUS', menus)
66 | commit('SET_NAME', name)
67 | commit('SET_AVATAR', 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif')
68 | resolve(resp)
69 | }).catch(error => {
70 | reject(error)
71 | })
72 | })
73 | },
74 |
75 | // user logout
76 | logout({ commit }) {
77 | return new Promise(resolve => {
78 | removeToken() // must remove token first
79 | resetRouter()
80 | commit('RESET_STATE')
81 | resolve()
82 | })
83 | },
84 |
85 | // remove token
86 | resetToken({ commit }) {
87 | return new Promise(resolve => {
88 | removeToken() // must remove token first
89 | commit('RESET_STATE')
90 | resolve()
91 | })
92 | }
93 | }
94 |
95 | export default {
96 | namespaced: true,
97 | state,
98 | mutations,
99 | actions
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/frontend/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * Parse the time to string
7 | * @param {(Object|string|number)} time
8 | * @param {string} cFormat
9 | * @returns {string | null}
10 | */
11 | export function parseTime(time, cFormat) {
12 | if (arguments.length === 0 || !time) {
13 | return null
14 | }
15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16 | let date
17 | if (typeof time === 'object') {
18 | date = time
19 | } else {
20 | if ((typeof time === 'string')) {
21 | if ((/^[0-9]+$/.test(time))) {
22 | // support "1548221490638"
23 | time = parseInt(time)
24 | } else {
25 | // support safari
26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
27 | time = time.replace(new RegExp(/-/gm), '/')
28 | }
29 | }
30 |
31 | if ((typeof time === 'number') && (time.toString().length === 10)) {
32 | time = time * 1000
33 | }
34 | date = new Date(time)
35 | }
36 | const formatObj = {
37 | y: date.getFullYear(),
38 | m: date.getMonth() + 1,
39 | d: date.getDate(),
40 | h: date.getHours(),
41 | i: date.getMinutes(),
42 | s: date.getSeconds(),
43 | a: date.getDay()
44 | }
45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
46 | const value = formatObj[key]
47 | // Note: getDay() returns 0 on Sunday
48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
49 | return value.toString().padStart(2, '0')
50 | })
51 | return time_str
52 | }
53 |
54 | /**
55 | * @param {number} time
56 | * @param {string} option
57 | * @returns {string}
58 | */
59 | export function formatTime(time, option) {
60 | if (('' + time).length === 10) {
61 | time = parseInt(time) * 1000
62 | } else {
63 | time = +time
64 | }
65 | const d = new Date(time)
66 | const now = Date.now()
67 |
68 | const diff = (now - d) / 1000
69 |
70 | if (diff < 30) {
71 | return '刚刚'
72 | } else if (diff < 3600) {
73 | // less 1 hour
74 | return Math.ceil(diff / 60) + '分钟前'
75 | } else if (diff < 3600 * 24) {
76 | return Math.ceil(diff / 3600) + '小时前'
77 | } else if (diff < 3600 * 24 * 2) {
78 | return '1天前'
79 | }
80 | if (option) {
81 | return parseTime(time, option)
82 | } else {
83 | return (
84 | d.getMonth() +
85 | 1 +
86 | '月' +
87 | d.getDate() +
88 | '日' +
89 | d.getHours() +
90 | '时' +
91 | d.getMinutes() +
92 | '分'
93 | )
94 | }
95 | }
96 |
97 | /**
98 | * @param {string} url
99 | * @returns {Object}
100 | */
101 | export function param2Obj(url) {
102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
103 | if (!search) {
104 | return {}
105 | }
106 | const obj = {}
107 | const searchArr = search.split('&')
108 | searchArr.forEach(v => {
109 | const index = v.indexOf('=')
110 | if (index !== -1) {
111 | const name = v.substring(0, index)
112 | const val = v.substring(index + 1, v.length)
113 | obj[name] = val
114 | }
115 | })
116 | return obj
117 | }
118 |
--------------------------------------------------------------------------------
/actions/server.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "diamond/models"
5 |
6 | "github.com/labstack/echo/v4"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // 获取servers, 根据用户分配的服务器获取
11 | func getServers(c echo.Context) error {
12 | var total int64
13 | servers := models.Servers{}
14 | baseQuery := models.DB.Order("created_at desc")
15 | username := c.Get("username")
16 | if username.(string) != "admin" {
17 | user := models.User{}
18 | uid := c.Get("uid").(uint)
19 | models.DB.Preload("Servers", func(db *gorm.DB) *gorm.DB {
20 | return db.Where(db.Order("created_at desc")).Where(db.Scopes(models.AnyFilter(models.Server{}, c)))
21 | }).First(&user, uid)
22 | total = int64(len(user.Servers))
23 | var sids []uint
24 | for _, s := range user.Servers {
25 | sids = append(sids, s.ID)
26 | }
27 | if res := baseQuery.Scopes(models.Paginate(c)).Where("id IN ?", sids).Find(&servers); res.Error != nil {
28 | return echo.NewHTTPError(400, res.Error.Error())
29 | }
30 | } else {
31 | if res := baseQuery.Scopes(models.AnyFilter(models.Server{}, c)).Model(&models.Server{}).Count(&total); res.Error != nil {
32 | return echo.NewHTTPError(400, res.Error.Error())
33 | }
34 | if res := baseQuery.Scopes(models.Paginate(c), models.AnyFilter(models.Server{}, c)).Find(&servers); res.Error != nil {
35 | return echo.NewHTTPError(400, res.Error.Error())
36 | }
37 | }
38 | return c.JSON(200, echo.Map{"success": true, "data": servers, "total": total})
39 | }
40 |
41 | func createServer(c echo.Context) error {
42 | server := models.Server{}
43 | if err := c.Bind(&server); err != nil {
44 | return echo.NewHTTPError(400, err.Error())
45 | }
46 | if err := c.Validate(&server); err != nil {
47 | return echo.NewHTTPError(400, err.Error())
48 | }
49 | if result := models.DB.Create(&server); result.Error != nil {
50 | return echo.NewHTTPError(400, result.Error.Error())
51 | }
52 | return c.JSON(200, echo.Map{"success": true})
53 | }
54 |
55 | func updateServer(c echo.Context) error {
56 | server := models.Server{}
57 | if result := models.DB.First(&server, c.Param("id")); result.Error != nil {
58 | return echo.NewHTTPError(400, result.Error.Error())
59 | }
60 | if err := c.Bind(&server); err != nil {
61 | return echo.NewHTTPError(400, err.Error())
62 | }
63 | if err := c.Validate(&server); err != nil {
64 | return echo.NewHTTPError(400, err.Error())
65 | }
66 | if result := models.DB.Save(&server); result.Error != nil {
67 | return echo.NewHTTPError(400, result.Error.Error())
68 | }
69 | return c.JSON(200, echo.Map{"success": true})
70 | }
71 |
72 | func deleteServer(c echo.Context) error {
73 | server := models.Server{}
74 | if res := models.DB.Delete(&server, c.Param("id")); res.Error != nil {
75 | return echo.NewHTTPError(400, res.Error.Error())
76 | }
77 | return c.JSON(200, echo.Map{"success": true})
78 | }
79 |
80 | func getRecords(c echo.Context) error {
81 | var total int64
82 | records := models.Records{}
83 | if res := models.DB.Model(&models.Record{}).Count(&total); res.Error != nil {
84 | return echo.NewHTTPError(400, res.Error.Error())
85 | }
86 | if res := models.DB.Scopes(models.Paginate(c)).Order("created_at desc").Find(&records); res.Error != nil {
87 | return echo.NewHTTPError(400, res.Error.Error())
88 | }
89 | return c.JSON(200, echo.Map{"success": true, "data": records, "total": total})
90 | }
91 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
8 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
102 |
--------------------------------------------------------------------------------
/frontend/src/views/history/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | {{ content }}
7 |
8 |
9 |
14 |
18 |
20 |
22 |
24 |
25 | 成功
27 | 失败
29 |
30 |
31 |
34 |
35 |
38 | 查看
39 |
40 |
41 |
42 |
45 |
46 | {{ parseTime(new Date(scope.row.created_at)) }}
47 |
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
106 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module diamond
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/Stevenhwang/gommon v0.1.0
7 | github.com/casbin/casbin/v2 v2.60.0
8 | github.com/casbin/gorm-adapter/v3 v3.14.0
9 | github.com/gliderlabs/ssh v0.3.5
10 | github.com/go-playground/validator/v10 v10.11.2
11 | github.com/go-redis/redis/v8 v8.11.5
12 | github.com/golang-jwt/jwt/v4 v4.4.3
13 | github.com/gorilla/websocket v1.5.0
14 | github.com/labstack/echo-jwt/v4 v4.1.0
15 | github.com/labstack/echo/v4 v4.10.0
16 | github.com/olekukonko/tablewriter v0.0.5
17 | github.com/robfig/cron/v3 v3.0.1
18 | github.com/rs/zerolog v1.29.0
19 | github.com/spf13/cobra v1.6.1
20 | github.com/spf13/viper v1.15.0
21 | github.com/wagslane/go-password-validator v0.3.0
22 | golang.org/x/crypto v0.5.0
23 | golang.org/x/term v0.4.0
24 | gorm.io/datatypes v1.1.0
25 | gorm.io/driver/mysql v1.4.5
26 | gorm.io/gorm v1.24.3
27 | )
28 |
29 | require (
30 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
31 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
32 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
33 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
34 | github.com/dustin/go-humanize v1.0.1 // indirect
35 | github.com/fsnotify/fsnotify v1.6.0 // indirect
36 | github.com/glebarez/go-sqlite v1.20.3 // indirect
37 | github.com/glebarez/sqlite v1.6.0 // indirect
38 | github.com/go-playground/locales v0.14.1 // indirect
39 | github.com/go-playground/universal-translator v0.18.1 // indirect
40 | github.com/go-sql-driver/mysql v1.7.0 // indirect
41 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
42 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
43 | github.com/golang-sql/sqlexp v0.1.0 // indirect
44 | github.com/google/uuid v1.3.0 // indirect
45 | github.com/hashicorp/hcl v1.0.0 // indirect
46 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
47 | github.com/jackc/pgpassfile v1.0.0 // indirect
48 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
49 | github.com/jackc/pgx/v5 v5.2.0 // indirect
50 | github.com/jinzhu/inflection v1.0.0 // indirect
51 | github.com/jinzhu/now v1.1.5 // indirect
52 | github.com/labstack/gommon v0.4.0 // indirect
53 | github.com/leodido/go-urn v1.2.1 // indirect
54 | github.com/magiconair/properties v1.8.7 // indirect
55 | github.com/mattn/go-colorable v0.1.13 // indirect
56 | github.com/mattn/go-isatty v0.0.17 // indirect
57 | github.com/mattn/go-runewidth v0.0.14 // indirect
58 | github.com/microsoft/go-mssqldb v0.20.0 // indirect
59 | github.com/mitchellh/mapstructure v1.5.0 // indirect
60 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect
61 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
62 | github.com/rivo/uniseg v0.4.3 // indirect
63 | github.com/spf13/afero v1.9.3 // indirect
64 | github.com/spf13/cast v1.5.0 // indirect
65 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
66 | github.com/spf13/pflag v1.0.5 // indirect
67 | github.com/subosito/gotenv v1.4.2 // indirect
68 | github.com/valyala/bytebufferpool v1.0.0 // indirect
69 | github.com/valyala/fasttemplate v1.2.2 // indirect
70 | golang.org/x/net v0.5.0 // indirect
71 | golang.org/x/sys v0.4.0 // indirect
72 | golang.org/x/text v0.6.0 // indirect
73 | golang.org/x/time v0.3.0 // indirect
74 | gopkg.in/ini.v1 v1.67.0 // indirect
75 | gopkg.in/yaml.v3 v3.0.1 // indirect
76 | gorm.io/driver/postgres v1.4.6 // indirect
77 | gorm.io/driver/sqlserver v1.4.2 // indirect
78 | gorm.io/plugin/dbresolver v1.4.1 // indirect
79 | modernc.org/libc v1.22.2 // indirect
80 | modernc.org/mathutil v1.5.0 // indirect
81 | modernc.org/memory v1.5.0 // indirect
82 | modernc.org/sqlite v1.20.3 // indirect
83 | )
84 |
--------------------------------------------------------------------------------
/actions/task.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "diamond/models"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 |
9 | "github.com/labstack/echo/v4"
10 | )
11 |
12 | func getTasks(c echo.Context) error {
13 | var total int64
14 | tasks := models.Tasks{}
15 | if res := models.DB.Scopes(models.AnyFilter(models.Task{}, c)).Model(&models.Task{}).Count(&total); res.Error != nil {
16 | return echo.NewHTTPError(400, res.Error.Error())
17 | }
18 | if res := models.DB.Scopes(models.Paginate(c), models.AnyFilter(models.Task{}, c)).Find(&tasks); res.Error != nil {
19 | return echo.NewHTTPError(400, res.Error.Error())
20 | }
21 | return c.JSON(200, echo.Map{"success": true, "data": tasks, "total": total})
22 | }
23 |
24 | func createTask(c echo.Context) error {
25 | task := models.Task{}
26 | if err := c.Bind(&task); err != nil {
27 | return echo.NewHTTPError(400, err.Error())
28 | }
29 | if err := c.Validate(&task); err != nil {
30 | return echo.NewHTTPError(400, err.Error())
31 | }
32 | if result := models.DB.Create(&task); result.Error != nil {
33 | return echo.NewHTTPError(400, result.Error.Error())
34 | }
35 | return c.JSON(200, echo.Map{"success": true})
36 | }
37 |
38 | func updateTask(c echo.Context) error {
39 | task := models.Task{}
40 | if result := models.DB.First(&task, c.Param("id")); result.Error != nil {
41 | return echo.NewHTTPError(400, result.Error.Error())
42 | }
43 | if err := c.Bind(&task); err != nil {
44 | return echo.NewHTTPError(400, err.Error())
45 | }
46 | if err := c.Validate(&task); err != nil {
47 | return echo.NewHTTPError(400, err.Error())
48 | }
49 | if result := models.DB.Save(&task); result.Error != nil {
50 | return echo.NewHTTPError(400, result.Error.Error())
51 | }
52 | return c.JSON(200, echo.Map{"success": true})
53 | }
54 |
55 | func deleteTask(c echo.Context) error {
56 | task := models.Task{}
57 | if res := models.DB.Delete(&task, c.Param("id")); res.Error != nil {
58 | return echo.NewHTTPError(400, res.Error.Error())
59 | }
60 | return c.JSON(200, echo.Map{"success": true})
61 | }
62 |
63 | func getTasksHist(c echo.Context) error {
64 | var total int64
65 | taskhists := models.TaskHistorys{}
66 | if res := models.DB.Model(&models.TaskHistory{}).Count(&total); res.Error != nil {
67 | return echo.NewHTTPError(400, res.Error.Error())
68 | }
69 | if res := models.DB.Scopes(models.Paginate(c)).Order("created_at desc").Omit("content").Find(&taskhists); res.Error != nil {
70 | return echo.NewHTTPError(400, res.Error.Error())
71 | }
72 | return c.JSON(200, echo.Map{"success": true, "data": taskhists, "total": total})
73 | }
74 |
75 | func getTasksHistDetail(c echo.Context) error {
76 | taskHist := models.TaskHistory{}
77 | if result := models.DB.First(&taskHist, c.Param("id")); result.Error != nil {
78 | return echo.NewHTTPError(400, result.Error.Error())
79 | }
80 | return c.JSON(200, echo.Map{"success": true, "data": taskHist})
81 | }
82 |
83 | func invokeTask(c echo.Context) error {
84 | task := models.Task{}
85 | if res := models.DB.First(&task, c.Param("id")); res.Error != nil {
86 | return echo.NewHTTPError(400, res.Error.Error())
87 | }
88 | script := models.Script{}
89 | if res := models.DB.First(&script, task.ScriptID); res.Error != nil {
90 | return echo.NewHTTPError(400, res.Error.Error())
91 | }
92 | // create temp script file
93 | f, err := os.CreateTemp("", "tempscript")
94 | if err != nil {
95 | return echo.NewHTTPError(400, err.Error())
96 | }
97 | if _, err := f.WriteString(script.Content); err != nil {
98 | return echo.NewHTTPError(400, err.Error())
99 | }
100 | // ansible localhost -m script -a "/tmp/test.sh arg1 arg2"
101 | scriptArgs := fmt.Sprintf("%s %s", f.Name(), task.Args)
102 | cmdArgs := []string{task.Target, "-m", "script", "-a", scriptArgs}
103 | cmd := exec.Command("ansible", cmdArgs...)
104 | username := c.Get("username").(string)
105 | fromip := c.RealIP()
106 | go func() {
107 | defer os.Remove(f.Name()) // ensure temp script file is deleted
108 | output, err := cmd.CombinedOutput()
109 | var (
110 | success bool
111 | content string
112 | )
113 | if err != nil {
114 | success = false
115 | content = string(output) + "\n" + err.Error()
116 | } else {
117 | success = true
118 | content = string(output)
119 | }
120 | hist := models.TaskHistory{TaskName: task.Name, User: username, FromIP: fromip, Success: success, Content: content}
121 | models.DB.Create(&hist)
122 | }()
123 | return c.JSON(200, echo.Map{"success": true})
124 | }
125 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const defaultSettings = require('./src/settings.js')
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, dir)
7 | }
8 |
9 | const name = defaultSettings.title || 'Vue Element' // page title
10 |
11 | // If your port is set to 80,
12 | // use administrator privileges to execute the command line.
13 | // For example, Mac: sudo npm run
14 | // You can change the port by the following methods:
15 | // port = 9528 npm run dev OR npm run dev --port = 9528
16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port
17 |
18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/
19 | module.exports = {
20 | /**
21 | * You will need to set publicPath if you plan to deploy your site under a sub path,
22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
23 | * then publicPath should be set to "/bar/".
24 | * In most cases please use '/' !!!
25 | * Detail: https://cli.vuejs.org/config/#publicpath
26 | */
27 | publicPath: '/',
28 | outputDir: 'dist',
29 | assetsDir: 'static',
30 | lintOnSave: process.env.NODE_ENV === 'development',
31 | productionSourceMap: false,
32 | devServer: {
33 | // host: "localhost",
34 | port: port,
35 | // open: true,
36 | proxy: {
37 | '/api': {
38 | target: 'http://127.0.0.1:8000',
39 | changeOrigin: true,
40 | ws: true,
41 | },
42 | '/records': {
43 | target: 'http://127.0.0.1:8000',
44 | changeOrigin: true,
45 | }
46 | }
47 | },
48 | configureWebpack: {
49 | // provide the app's title in webpack's name field, so that
50 | // it can be accessed in index.html to inject the correct title.
51 | name: name,
52 | resolve: {
53 | alias: {
54 | '@': resolve('src')
55 | },
56 | fallback: {
57 | "path": require.resolve("path-browserify")
58 | }
59 | }
60 | },
61 | chainWebpack(config) {
62 | // it can improve the speed of the first screen, it is recommended to turn on preload
63 | // config.plugin('preload').tap(() => [
64 | // {
65 | // rel: 'preload',
66 | // // to ignore runtime.js
67 | // // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
68 | // fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
69 | // include: 'initial'
70 | // }
71 | // ])
72 |
73 | // when there are many pages, it will cause too many meaningless requests
74 | config.plugins.delete('prefetch')
75 |
76 | // set svg-sprite-loader
77 | config.module
78 | .rule('svg')
79 | .exclude.add(resolve('src/icons'))
80 | .end()
81 | config.module
82 | .rule('icons')
83 | .test(/\.svg$/)
84 | .include.add(resolve('src/icons'))
85 | .end()
86 | .use('svg-sprite-loader')
87 | .loader('svg-sprite-loader')
88 | .options({
89 | symbolId: 'icon-[name]'
90 | })
91 | .end()
92 |
93 | config
94 | .when(process.env.NODE_ENV !== 'development',
95 | config => {
96 | // config
97 | // .plugin('ScriptExtHtmlWebpackPlugin')
98 | // .after('html')
99 | // .use('script-ext-html-webpack-plugin', [{
100 | // // `runtime` must same as runtimeChunk name. default is `runtime`
101 | // inline: /runtime\..*\.js$/
102 | // }])
103 | // .end()
104 | config
105 | .optimization.splitChunks({
106 | chunks: 'all',
107 | cacheGroups: {
108 | libs: {
109 | name: 'chunk-libs',
110 | test: /[\\/]node_modules[\\/]/,
111 | priority: 10,
112 | chunks: 'initial' // only package third parties that are initially dependent
113 | },
114 | elementUI: {
115 | name: 'chunk-elementUI', // split elementUI into a single package
116 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
117 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
118 | },
119 | commons: {
120 | name: 'chunk-commons',
121 | test: resolve('src/components'), // can customize your rules
122 | minChunks: 3, // minimum common number
123 | priority: 5,
124 | reuseExistingChunk: true
125 | }
126 | }
127 | })
128 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
129 | config.optimization.runtimeChunk('single')
130 | }
131 | )
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/frontend/src/views/record/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
43 |
44 |
49 |
52 |
55 |
58 |
60 |
61 | {{ scope.row.file.replace('./records/', '') }}
62 |
63 |
64 |
67 |
68 | {{ parseTime(new Date(scope.row.created_at)) }}
69 |
70 |
71 |
73 |
74 |
79 | 播放
80 |
81 |
82 |
83 |
84 |
85 |
86 |
91 |
92 |
93 |
94 |
95 |
146 |
--------------------------------------------------------------------------------
/frontend/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .sub-el-icon {
61 | margin-right: 12px;
62 | margin-left: -2px;
63 | }
64 |
65 | .el-menu {
66 | border: none;
67 | height: 100%;
68 | width: 100% !important;
69 | }
70 |
71 | // menu hover
72 | .submenu-title-noDropdown,
73 | .el-submenu__title {
74 | &:hover {
75 | background-color: $menuHover !important;
76 | }
77 | }
78 |
79 | .is-active>.el-submenu__title {
80 | color: $subMenuActiveText !important;
81 | }
82 |
83 | & .nest-menu .el-submenu>.el-submenu__title,
84 | & .el-submenu .el-menu-item {
85 | min-width: $sideBarWidth !important;
86 | background-color: $subMenuBg !important;
87 |
88 | &:hover {
89 | background-color: $subMenuHover !important;
90 | }
91 | }
92 | }
93 |
94 | .hideSidebar {
95 | .sidebar-container {
96 | width: 54px !important;
97 | }
98 |
99 | .main-container {
100 | margin-left: 54px;
101 | }
102 |
103 | .submenu-title-noDropdown {
104 | padding: 0 !important;
105 | position: relative;
106 |
107 | .el-tooltip {
108 | padding: 0 !important;
109 |
110 | .svg-icon {
111 | margin-left: 20px;
112 | }
113 |
114 | .sub-el-icon {
115 | margin-left: 19px;
116 | }
117 | }
118 | }
119 |
120 | .el-submenu {
121 | overflow: hidden;
122 |
123 | &>.el-submenu__title {
124 | padding: 0 !important;
125 |
126 | .svg-icon {
127 | margin-left: 20px;
128 | }
129 |
130 | .sub-el-icon {
131 | margin-left: 19px;
132 | }
133 |
134 | .el-submenu__icon-arrow {
135 | display: none;
136 | }
137 | }
138 | }
139 |
140 | .el-menu--collapse {
141 | .el-submenu {
142 | &>.el-submenu__title {
143 | &>span {
144 | height: 0;
145 | width: 0;
146 | overflow: hidden;
147 | visibility: hidden;
148 | display: inline-block;
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
155 | .el-menu--collapse .el-menu .el-submenu {
156 | min-width: $sideBarWidth !important;
157 | }
158 |
159 | // mobile responsive
160 | .mobile {
161 | .main-container {
162 | margin-left: 0px;
163 | }
164 |
165 | .sidebar-container {
166 | transition: transform .28s;
167 | width: $sideBarWidth !important;
168 | }
169 |
170 | &.hideSidebar {
171 | .sidebar-container {
172 | pointer-events: none;
173 | transition-duration: 0.3s;
174 | transform: translate3d(-$sideBarWidth, 0, 0);
175 | }
176 | }
177 | }
178 |
179 | .withoutAnimation {
180 |
181 | .main-container,
182 | .sidebar-container {
183 | transition: none;
184 | }
185 | }
186 | }
187 |
188 | // when menu collapsed
189 | .el-menu--vertical {
190 | &>.el-menu {
191 | .svg-icon {
192 | margin-right: 16px;
193 | }
194 | .sub-el-icon {
195 | margin-right: 12px;
196 | margin-left: -2px;
197 | }
198 | }
199 |
200 | .nest-menu .el-submenu>.el-submenu__title,
201 | .el-menu-item {
202 | &:hover {
203 | // you can use $subMenuHover
204 | background-color: $menuHover !important;
205 | }
206 | }
207 |
208 | // the scroll bar appears when the subMenu is too long
209 | >.el-menu--popup {
210 | max-height: 100vh;
211 | overflow-y: auto;
212 |
213 | &::-webkit-scrollbar-track-piece {
214 | background: #d3dce6;
215 | }
216 |
217 | &::-webkit-scrollbar {
218 | width: 6px;
219 | }
220 |
221 | &::-webkit-scrollbar-thumb {
222 | background: #99a9bf;
223 | border-radius: 20px;
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/actions/app.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "database/sql/driver"
5 | "diamond/middlewares"
6 | "diamond/misc"
7 | "fmt"
8 | "io/fs"
9 | "net/http"
10 | "reflect"
11 |
12 | "diamond/frontend"
13 |
14 | "github.com/Stevenhwang/gommon/nulls"
15 |
16 | "github.com/go-playground/validator/v10"
17 | echojwt "github.com/labstack/echo-jwt/v4"
18 | "github.com/labstack/echo/v4"
19 | "github.com/labstack/echo/v4/middleware"
20 | )
21 |
22 | func getFileSystem() http.FileSystem {
23 | fsys, err := fs.Sub(frontend.Index, "dist")
24 | if err != nil {
25 | panic(err)
26 | }
27 |
28 | return http.FS(fsys)
29 | }
30 |
31 | var App *echo.Echo
32 |
33 | // 自定义错误处理函数
34 | func customHTTPErrorHandler(err error, c echo.Context) {
35 | code := 500
36 | message := err.Error()
37 | if he, ok := err.(*echo.HTTPError); ok {
38 | code = he.Code
39 | message = fmt.Sprintf("%v", he.Message)
40 | }
41 | // c.Logger().Error(err)
42 | c.JSON(200, echo.Map{"success": false, "code": code, "reason": message})
43 | }
44 |
45 | // 自定义validator
46 | type customValidator struct {
47 | validator *validator.Validate
48 | }
49 |
50 | func (cv *customValidator) Validate(i interface{}) error {
51 | if err := cv.validator.Struct(i); err != nil {
52 | return err
53 | }
54 | return nil
55 | }
56 |
57 | func init() {
58 | // Echo instance
59 | e := echo.New()
60 |
61 | // custom error handler
62 | e.HTTPErrorHandler = customHTTPErrorHandler
63 |
64 | // custom validator
65 | validate := validator.New()
66 | validate.RegisterCustomTypeFunc(func(field reflect.Value) interface{} {
67 | if valuer, ok := field.Interface().(driver.Valuer); ok {
68 | val, err := valuer.Value()
69 | if err == nil { // nulls 包里这个err一直是nil, 所以不用继续处理
70 | return val
71 | }
72 | // handle the error how you want
73 | }
74 | return nil
75 | }, nulls.String{}, nulls.Time{}, nulls.UInt{})
76 | e.Validator = &customValidator{validator: validate}
77 |
78 | // Middleware
79 | // e.Use(middleware.Logger())
80 | e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
81 | LogURI: true,
82 | LogLatency: true,
83 | LogRemoteIP: true,
84 | LogHost: true,
85 | LogMethod: true,
86 | LogStatus: true,
87 | LogUserAgent: true,
88 | LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
89 | username := c.Get("username")
90 | var un string
91 | if username == nil {
92 | un = ""
93 | } else {
94 | un = username.(string)
95 | }
96 | misc.Logger.Info().
97 | Str("from", "app").
98 | Str("user", un).
99 | Str("remote_ip", v.RemoteIP).
100 | Str("host", v.Host).
101 | Str("method", v.Method).
102 | Int("status", v.Status).
103 | Str("uri", v.URI).
104 | Str("latency", v.Latency.String()).
105 | Str("user_agent", v.UserAgent).
106 | Msg("")
107 | return nil
108 | },
109 | }))
110 | e.Use(middleware.Recover())
111 |
112 | // With proxies using X-Forwarded-For header to get real client ip
113 | // e.IPExtractor = echo.ExtractIPFromXFFHeader()
114 | e.IPExtractor = echo.ExtractIPDirect()
115 |
116 | // home route
117 | // e.GET("/", func(c echo.Context) error {
118 | // return c.String(200, "Hello from diamond!")
119 | // })
120 |
121 | // banip middleware
122 | e.Use(middlewares.BanIP)
123 |
124 | assetHandler := http.FileServer(getFileSystem())
125 | e.GET("/", echo.WrapHandler(assetHandler))
126 | e.GET("/static/*", echo.WrapHandler(assetHandler))
127 | e.GET("*.png", echo.WrapHandler(assetHandler))
128 | // e.GET("/favicon.ico", echo.WrapHandler(assetHandler))
129 |
130 | // ssh records 目录
131 | e.Static("/records", "./records")
132 |
133 | // api group route
134 | api := e.Group("/api")
135 | api.POST("/login", login)
136 | api.GET("/terminal", terminal)
137 | // jwt middleware
138 | api.Use(echojwt.WithConfig(echojwt.Config{
139 | SigningKey: []byte(misc.Config.GetString("jwt.secret")),
140 | }))
141 | // token middleware
142 | api.Use(middlewares.Token)
143 | api.GET("/user_info", userInfo)
144 | api.POST("/reset_pw", resetPW)
145 | // enforce middleware
146 | api.Use(middlewares.Enforce)
147 |
148 | api.GET("/users", getUsers)
149 | api.POST("/users", createUser)
150 | api.PUT("/users/:id", updateUser)
151 | api.DELETE("/users/:id", deleteUser)
152 |
153 | api.GET("/banips", getBanIPs)
154 | api.POST("/delbanip", delBanIP)
155 |
156 | api.POST("/syncPerms", syncPerms)
157 |
158 | api.GET("/userPerms", getUserPerms)
159 | api.POST("/userPerms", assignUserPerm)
160 |
161 | api.GET("/userServers", getUserServers)
162 | api.POST("/userServers", assignUserServer)
163 |
164 | api.GET("/servers", getServers)
165 | api.POST("/servers", createServer)
166 | api.PUT("/servers/:id", updateServer)
167 | api.DELETE("/servers/:id", deleteServer)
168 |
169 | api.GET("/records", getRecords)
170 |
171 | api.GET("/credentials", getCredentials)
172 | api.POST("/credentials", createCredential)
173 | api.PUT("/credentials/:id", updateCredential)
174 | api.DELETE("/credentials/:id", deleteCredential)
175 |
176 | api.GET("/scripts", getScripts)
177 | api.POST("/scripts", createScript)
178 | api.PUT("/scripts/:id", updateScript)
179 | api.DELETE("/scripts/:id", deleteScript)
180 |
181 | api.GET("/tasks", getTasks)
182 | api.POST("/tasks", createTask)
183 | api.PUT("/tasks/:id", updateTask)
184 | api.DELETE("/tasks/:id", deleteTask)
185 | api.POST("/tasks/:id", invokeTask)
186 | api.GET("/taskhist", getTasksHist)
187 | api.GET("/taskhist/:id", getTasksHistDetail)
188 |
189 | api.GET("/crons", getCrons)
190 | api.POST("/crons", createCron)
191 | api.PUT("/crons/:id", updateCron)
192 | api.DELETE("/crons/:id", deleteCron)
193 |
194 | App = e
195 | }
196 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
15 |
17 |
21 |
22 |
24 |
28 |
29 |
31 |
35 |
36 |
37 |
43 |
44 |
45 |
46 |
72 |
73 |
74 |
75 |
137 |
138 |
216 |
--------------------------------------------------------------------------------
/frontend/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | // vue router 版本升级 https://github.com/vuejs/vue-router/issues/2881#issuecomment-520554378
5 | const originalPush = Router.prototype.push
6 | Router.prototype.push = function push(location, onResolve, onReject) {
7 | if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
8 | return originalPush.call(this, location).catch(err => err)
9 | }
10 |
11 | Vue.use(Router)
12 |
13 | /* Layout */
14 | import Layout from '@/layout'
15 |
16 | /**
17 | * Note: sub-menu only appear when route children.length >= 1
18 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
19 | *
20 | * hidden: true if set true, item will not show in the sidebar(default is false)
21 | * alwaysShow: true if set true, will always show the root menu
22 | * if not set alwaysShow, when item has more than one children route,
23 | * it will becomes nested mode, otherwise not show the root menu
24 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
25 | * name:'router-name' the name is used by (must set!!!)
26 | * meta : {
27 | roles: ['admin','editor'] control the page roles (you can set multiple roles)
28 | title: 'title' the name show in sidebar and breadcrumb (recommend set)
29 | icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
30 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
31 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
32 | }
33 | */
34 |
35 | /**
36 | * constantRoutes
37 | * a base page that does not have permission requirements
38 | * all roles can be accessed
39 | */
40 | export const constantRoutes = [
41 | {
42 | path: '/login',
43 | component: () => import('@/views/login/index'),
44 | hidden: true
45 | },
46 |
47 | {
48 | path: '/404',
49 | component: () => import('@/views/404'),
50 | hidden: true
51 | },
52 |
53 | {
54 | path: '/',
55 | component: Layout,
56 | redirect: '/dashboard',
57 | children: [{
58 | path: 'dashboard',
59 | name: 'Dashboard',
60 | component: () => import('@/views/dashboard/index'),
61 | meta: { title: '首页', icon: 'dashboard' }
62 | }]
63 | },
64 |
65 | {
66 | path: '/terminal',
67 | component: () => import('@/views/terminal/index'),
68 | hidden: true
69 | },
70 |
71 | // {
72 | // path: '/grafana',
73 | // component: Layout,
74 | // children: [
75 | // {
76 | // path: 'https://grafana.com/',
77 | // meta: { title: 'Grafana', icon: 'link' }
78 | // }
79 | // ]
80 | // },
81 |
82 | ]
83 |
84 | /**
85 | * asyncRoutes
86 | * the routes that need to be dynamically loaded based on user roles
87 | */
88 | export const asyncRoutes = [
89 | {
90 | path: '/server',
91 | name: 'server',
92 | component: Layout,
93 | children: [
94 | {
95 | path: 'index',
96 | // name: 'server',
97 | component: () => import('@/views/server/index'),
98 | meta: { title: '服务器', icon: 'el-icon-cpu' }
99 | }
100 | ]
101 | },
102 | {
103 | path: '/script',
104 | name: 'script',
105 | component: Layout,
106 | children: [
107 | {
108 | path: 'index',
109 | component: () => import('@/views/script/index'),
110 | meta: { title: '脚本', icon: 'el-icon-document' }
111 | }
112 | ]
113 | },
114 | {
115 | path: '/cron',
116 | name: 'cron',
117 | component: Layout,
118 | children: [
119 | {
120 | path: 'index',
121 | component: () => import('@/views/cron/index'),
122 | meta: { title: '定时任务', icon: 'el-icon-set-up' }
123 | }
124 | ]
125 | },
126 | {
127 | path: '/task',
128 | name: 'task',
129 | component: Layout,
130 | children: [
131 | {
132 | path: 'index',
133 | component: () => import('@/views/task/index'),
134 | meta: { title: '任务', icon: 'el-icon-bangzhu' }
135 | }
136 | ]
137 | },
138 | {
139 | path: '/history',
140 | name: 'history',
141 | component: Layout,
142 | children: [
143 | {
144 | path: 'index',
145 | component: () => import('@/views/history/index'),
146 | meta: { title: '任务历史', icon: 'el-icon-data-board' }
147 | }
148 | ]
149 | },
150 | {
151 | path: '/credential',
152 | name: 'credential',
153 | component: Layout,
154 | children: [
155 | {
156 | path: 'index',
157 | // name: 'credential',
158 | component: () => import('@/views/credential/index'),
159 | meta: { title: '认证', icon: 'el-icon-key' }
160 | }
161 | ]
162 | },
163 | {
164 | path: '/record',
165 | name: 'record',
166 | component: Layout,
167 | children: [
168 | {
169 | path: 'index',
170 | component: () => import('@/views/record/index'),
171 | meta: { title: 'SSH记录', icon: 'el-icon-reading' }
172 | }
173 | ]
174 | },
175 | {
176 | path: '/user',
177 | name: 'user',
178 | component: Layout,
179 | children: [
180 | {
181 | path: 'index',
182 | // name: 'user',
183 | component: () => import('@/views/user/index'),
184 | meta: { title: '用户', icon: 'user' }
185 | }
186 | ]
187 | },
188 | {
189 | path: '/banips',
190 | name: 'banips',
191 | component: Layout,
192 | children: [
193 | {
194 | path: 'index',
195 | component: () => import('@/views/banips/index'),
196 | meta: { title: 'IP黑名单', icon: 'el-icon-document-delete' }
197 | }
198 | ]
199 | },
200 |
201 | // 404 page must be placed at the end !!!
202 | { path: '*', redirect: '/404', hidden: true }
203 | ]
204 |
205 | const createRouter = () => new Router({
206 | // mode: 'history', // require service support
207 | scrollBehavior: () => ({ y: 0 }),
208 | routes: constantRoutes
209 | })
210 |
211 | const router = createRouter()
212 |
213 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
214 | export function resetRouter() {
215 | const newRouter = createRouter()
216 | router.matcher = newRouter.matcher // reset router
217 | }
218 |
219 | export default router
220 |
--------------------------------------------------------------------------------
/frontend/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
OOPS!
20 |
25 |
{{ message }}
26 |
Please check that the URL you entered is correct, or click the button below to return to the homepage.
27 |
Back to home
29 |
30 |
31 |
32 |
33 |
34 |
45 |
46 |
240 |
--------------------------------------------------------------------------------
/frontend/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
Diamond
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
40 |
42 |
43 |
44 |
45 |
46 | Login
50 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
132 |
133 |
179 |
180 |
243 |
--------------------------------------------------------------------------------
/frontend/src/views/script/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
13 |
15 |
16 |
19 |
25 |
26 |
27 |
37 |
38 |
39 |
41 |
48 | 新增
52 |
53 |
54 |
55 |
60 |
64 |
67 |
69 |
70 |
74 | 编辑
75 |
76 |
80 | 删除
81 |
82 |
83 |
84 |
85 |
86 |
87 |
92 |
93 |
94 |
95 |
96 |
256 |
--------------------------------------------------------------------------------
/actions/cron.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "diamond/misc"
5 | "diamond/models"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 |
10 | "github.com/labstack/echo/v4"
11 | ocron "github.com/robfig/cron/v3"
12 | )
13 |
14 | var mycron *ocron.Cron
15 |
16 | func init() {
17 | mycron = ocron.New(ocron.WithSeconds(),
18 | ocron.WithChain(ocron.SkipIfStillRunning(ocron.DefaultLogger),
19 | ocron.Recover(ocron.DefaultLogger)))
20 | }
21 |
22 | func CronStart() {
23 | // 初始化的时候如果数据库有定时任务,要添加进去,同时要更新entryid
24 | crons := models.Crons{}
25 | if result := models.DB.Find(&crons); result.Error != nil {
26 | misc.Logger.Error().Err(result.Error).Str("from", "cron").Msg("")
27 | }
28 | for _, c := range crons {
29 | // 添加定时任务
30 | entryid, _ := mycron.AddFunc(c.Spec, execCron(c.Name, c.Target, c.ScriptID, c.Args))
31 | // 更新entryid
32 | c.EntryID = int(entryid)
33 | models.DB.Save(&c)
34 | }
35 | mycron.Start()
36 | }
37 |
38 | // cron 执行函数
39 | func execCron(name string, target string, scriptID uint, args string) func() {
40 | return func() {
41 | script := models.Script{}
42 | if res := models.DB.First(&script, scriptID); res.Error != nil {
43 | misc.Logger.Error().Err(res.Error).Str("from", "cron").Msg("find script error")
44 | return
45 | }
46 | // create temp script file
47 | f, err := os.CreateTemp("", "crontempscript")
48 | if err != nil {
49 | misc.Logger.Error().Err(err).Str("from", "cron").Msg("create temp file error")
50 | return
51 | }
52 | defer os.Remove(f.Name()) // ensure temp script file is deleted
53 | if _, err := f.WriteString(script.Content); err != nil {
54 | misc.Logger.Error().Err(err).Str("from", "cron").Msg("write temp file error")
55 | return
56 | }
57 | scriptArgs := fmt.Sprintf("%s %s", f.Name(), args)
58 | cmdArgs := []string{target, "-m", "script", "-a", scriptArgs}
59 | cmd := exec.Command("ansible", cmdArgs...)
60 | output, err := cmd.CombinedOutput()
61 | if err != nil {
62 | msg := fmt.Sprintf("%s任务执行失败", name)
63 | misc.Logger.Error().Err(err).Str("from", "cron").Msg(msg)
64 | } else {
65 | msg := fmt.Sprintf("%s任务执行成功", name)
66 | misc.Logger.Info().Str("from", "cron").Msg(msg)
67 | }
68 | misc.Logger.Info().Str("from", "cron").Msg(string(output))
69 | }
70 | }
71 |
72 | /*
73 | ******************
74 | 定时任务actions
75 | ******************
76 | */
77 | func createCron(c echo.Context) error {
78 | cron := models.Cron{}
79 | if err := c.Bind(&cron); err != nil {
80 | return echo.NewHTTPError(400, err.Error())
81 | }
82 | if err := c.Validate(&cron); err != nil {
83 | return echo.NewHTTPError(400, err.Error())
84 | }
85 | entryid, err := mycron.AddFunc(cron.Spec, execCron(cron.Name, cron.Target, cron.ScriptID, cron.Args))
86 | if err != nil {
87 | return echo.NewHTTPError(400, err.Error())
88 | }
89 | cron.EntryID = int(entryid)
90 | if res := models.DB.Create(&cron); res.Error != nil {
91 | return echo.NewHTTPError(400, res.Error.Error())
92 | }
93 | return c.JSON(200, echo.Map{"success": true})
94 | }
95 |
96 | func updateCron(c echo.Context) error {
97 | cron := models.Cron{}
98 | if result := models.DB.First(&cron, c.Param("id")); result.Error != nil {
99 | return echo.NewHTTPError(400, result.Error.Error())
100 | }
101 | if err := c.Bind(&cron); err != nil {
102 | return echo.NewHTTPError(400, err.Error())
103 | }
104 | if err := c.Validate(&cron); err != nil {
105 | return echo.NewHTTPError(400, err.Error())
106 | }
107 | // 先删除cron
108 | mycron.Remove(ocron.EntryID(cron.EntryID))
109 | // 再添加cron
110 | entryid, err := mycron.AddFunc(cron.Spec, execCron(cron.Name, cron.Target, cron.ScriptID, cron.Args))
111 | if err != nil {
112 | return echo.NewHTTPError(400, err.Error())
113 | }
114 | // 更新entryid和其他数据到数据库
115 | cron.EntryID = int(entryid)
116 | if result := models.DB.Save(&cron); result.Error != nil {
117 | return echo.NewHTTPError(400, result.Error.Error())
118 | }
119 | return c.JSON(200, echo.Map{"success": true})
120 | }
121 |
122 | func deleteCron(c echo.Context) error {
123 | cron := models.Cron{}
124 | if result := models.DB.First(&cron, c.Param("id")); result.Error != nil {
125 | return echo.NewHTTPError(400, result.Error.Error())
126 | }
127 | mycron.Remove(ocron.EntryID(cron.EntryID))
128 | if res := models.DB.Delete(&cron); res.Error != nil {
129 | return echo.NewHTTPError(400, res.Error.Error())
130 | }
131 | return c.JSON(200, echo.Map{"success": true})
132 | }
133 |
134 | func getCrons(c echo.Context) error {
135 | var total int64
136 | crons := models.Crons{}
137 | if res := models.DB.Scopes(models.AnyFilter(models.Cron{}, c)).Model(&models.Cron{}).Count(&total); res.Error != nil {
138 | return echo.NewHTTPError(400, res.Error.Error())
139 | }
140 | if res := models.DB.Scopes(models.Paginate(c), models.AnyFilter(models.Cron{}, c)).Find(&crons); res.Error != nil {
141 | return echo.NewHTTPError(400, res.Error.Error())
142 | }
143 | return c.JSON(200, echo.Map{"success": true, "data": crons, "total": total})
144 | }
145 |
146 | // type remoteRes struct {
147 | // // Cpu int
148 | // // Memory int
149 | // // Disk int
150 | // InstanceType string
151 | // Err error
152 | // }
153 |
154 | // func remoteSSH(server models.Server) remoteRes {
155 | // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // 5秒超时
156 | // defer cancel()
157 | // resChan := make(chan remoteRes)
158 | // go func(ctx context.Context, ch chan remoteRes) {
159 | // credential := models.Credential{}
160 | // if res := models.DB.First(&credential, server.CredentialID); res.Error != nil {
161 | // ch <- remoteRes{Err: res.Error}
162 | // return
163 | // }
164 | // var client *gossh.Client
165 | // spass := tools.AesDecrypt(credential.AuthContent, "0123456789012345")
166 | // if credential.AuthType == 1 {
167 | // cli, err := ssh.GetSSHClientByPassword(server.IP, spass, ssh.SSHOptions{Port: server.Port, User: credential.AuthUser})
168 | // if err != nil {
169 | // ch <- remoteRes{Err: err}
170 | // return
171 | // }
172 | // client = cli
173 | // }
174 | // if credential.AuthType == 2 {
175 | // cli, err := ssh.GetSSHClientByKey(server.IP, []byte(spass), ssh.SSHOptions{Port: server.Port, User: credential.AuthUser})
176 | // if err != nil {
177 | // ch <- remoteRes{Err: err}
178 | // return
179 | // }
180 | // client = cli
181 | // }
182 | // defer client.Close()
183 | // // cpustr, err := ssh.SSHExec(`echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]`, client)
184 | // // if err != nil {
185 | // // ch <- remoteRes{Err: err}
186 | // // return
187 | // // }
188 | // // memstr, err := ssh.SSHExec(`echo $(free | grep Mem | awk '{printf "%d", $3/$2 * 100.0}')`, client)
189 | // // if err != nil {
190 | // // ch <- remoteRes{Err: err}
191 | // // return
192 | // // }
193 | // // diskstr, err := ssh.SSHExec(`df -Th / | tail -1 | awk '{print $6}' | sed s/%//g`, client)
194 | // // if err != nil {
195 | // // ch <- remoteRes{Err: err}
196 | // // return
197 | // // }
198 | // instance_type, err := ssh.SSHExec("curl -s http://169.254.169.254/latest/meta-data/instance-type", client)
199 | // if err != nil {
200 | // ch <- remoteRes{Err: err}
201 | // return
202 | // }
203 | // // cpu, _ := strconv.Atoi(strings.TrimRight(cpustr, "\n"))
204 | // // mem, _ := strconv.Atoi(strings.TrimRight(memstr, "\n"))
205 | // // disk, _ := strconv.Atoi(strings.TrimRight(diskstr, "\n"))
206 | // res := remoteRes{InstanceType: instance_type, Err: nil}
207 | // ch <- res
208 | // }(ctx, resChan)
209 | // // 检测channel
210 | // select {
211 | // case x := <-resChan:
212 | // return x
213 | // case <-ctx.Done():
214 | // return remoteRes{Err: ctx.Err()}
215 | // }
216 | // }
217 |
218 | // func gatherTask() {
219 | // servers := models.Servers{}
220 | // if res := models.DB.Find(&servers); res.Error != nil {
221 | // misc.Logger.Error().Err(res.Error).Str("from", "crons").Msg("获取服务器列表失败")
222 | // return
223 | // }
224 | // var wg sync.WaitGroup
225 | // for _, server := range servers {
226 | // wg.Add(1)
227 | // x := server
228 | // go func() {
229 | // defer wg.Done()
230 | // res := remoteSSH(x)
231 | // if res.Err != nil {
232 | // x.InstanceType = res.Err.Error()
233 | // } else {
234 | // x.InstanceType = res.InstanceType
235 | // }
236 | // models.DB.Save(&x)
237 | // }()
238 | // }
239 | // wg.Wait()
240 | // }
241 |
--------------------------------------------------------------------------------
/frontend/src/views/credential/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
9 |
12 |
14 |
15 |
18 |
20 |
24 |
25 |
26 |
27 |
30 |
32 |
33 |
36 |
39 |
40 |
41 |
51 |
52 |
53 |
55 |
79 | 新增
83 |
84 |
85 |
86 |
91 |
93 |
95 |
96 | {{ scope.row.auth_type === 1? "密码认证": "私钥认证" }}
97 |
98 |
99 |
101 |
104 |
105 | {{ parseTime(new Date(scope.row.created_at)) }}
106 |
107 |
108 |
111 |
112 | {{ parseTime(new Date(scope.row.updated_at)) }}
113 |
114 |
115 |
117 |
118 |
122 | 编辑
123 |
124 |
128 | 删除
129 |
130 |
131 |
132 |
133 |
134 |
135 |
140 |
141 |
142 |
143 |
144 |
274 |
--------------------------------------------------------------------------------
/frontend/src/views/cron/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
9 |
12 |
14 |
15 |
18 |
21 |
22 |
25 |
30 |
34 |
35 |
36 |
37 |
40 |
44 |
48 |
49 |
50 |
51 |
54 |
57 |
58 |
59 |
69 |
70 |
71 |
73 |
80 | 新增
84 |
85 |
86 |
87 |
92 |
96 |
100 |
103 |
105 |
106 |
110 | 编辑
111 |
112 |
116 | 删除
117 |
118 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
130 |
131 |
132 |
279 |
--------------------------------------------------------------------------------
/actions/terminal.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "context"
5 | "diamond/misc"
6 | "diamond/models"
7 | "encoding/json"
8 | "fmt"
9 | "net/http"
10 | "os"
11 | "strconv"
12 | "time"
13 |
14 | "github.com/Stevenhwang/gommon/slice"
15 | "github.com/Stevenhwang/gommon/ssh"
16 | "github.com/Stevenhwang/gommon/times"
17 | "github.com/Stevenhwang/gommon/tools"
18 | "gorm.io/gorm"
19 |
20 | gossh "golang.org/x/crypto/ssh"
21 |
22 | "github.com/golang-jwt/jwt/v4"
23 | "github.com/gorilla/websocket"
24 | "github.com/labstack/echo/v4"
25 | )
26 |
27 | var upgrader = websocket.Upgrader{
28 | ReadBufferSize: 1024,
29 | WriteBufferSize: 1024,
30 | CheckOrigin: func(r *http.Request) bool {
31 | return true
32 | },
33 | }
34 |
35 | // 超时控制,分钟
36 | var maxTimeout = 30 * time.Minute
37 | var idleTimeout = 5 * time.Minute
38 |
39 | func terminal(c echo.Context) error {
40 | ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
41 | if err != nil {
42 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("")
43 | }
44 | defer ws.Close()
45 | // 检查server id
46 | id := c.QueryParam("id")
47 | if len(id) == 0 {
48 | ws.WriteMessage(websocket.TextMessage, []byte("请指定服务器id"))
49 | return nil
50 | }
51 | // 检查token
52 | token := c.QueryParam("token")
53 | if len(token) == 0 {
54 | ws.WriteMessage(websocket.TextMessage, []byte("请携带token请求"))
55 | return nil
56 | }
57 | // parse token
58 | t, _ := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
59 | return []byte(misc.Config.GetString("jwt.secret")), nil
60 | })
61 | // 解析token
62 | if t != nil {
63 | claims, ok := t.Claims.(jwt.MapClaims)
64 | if !ok {
65 | ws.WriteMessage(websocket.TextMessage, []byte("failed to cast claims as jwt.MapClaims"))
66 | return nil
67 | }
68 | uid := uint(claims["uid"].(float64))
69 | username := claims["username"].(string)
70 | // 还要判断用户是否有权限连接此服务器
71 | if username != "admin" {
72 | user := models.User{}
73 | models.DB.Preload("Servers", func(db *gorm.DB) *gorm.DB {
74 | return db.Order("created_at desc")
75 | }).First(&user, uid)
76 | // 如果用户被禁用
77 | if !user.IsActive {
78 | ws.WriteMessage(websocket.TextMessage, []byte("账号禁用"))
79 | return nil
80 | }
81 | var sids []int
82 | for _, s := range user.Servers {
83 | sids = append(sids, int(s.ID))
84 | }
85 | intid, err := strconv.Atoi(id)
86 | if err != nil {
87 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
88 | return nil
89 | }
90 | if !slice.FindValInIntSlice(sids, intid) {
91 | ws.WriteMessage(websocket.TextMessage, []byte("你没有权限连接此服务器"))
92 | return nil
93 | }
94 | }
95 | // 显示
96 | ws.WriteMessage(websocket.TextMessage, []byte("================================\r\n"))
97 | ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("连接最长%s, 空闲超时%s\r\n", maxTimeout, idleTimeout)))
98 | ws.WriteMessage(websocket.TextMessage, []byte("================================\r\n"))
99 | // 开始连接服务器
100 | server := models.Server{}
101 | if res := models.DB.First(&server, id); res.Error != nil {
102 | ws.WriteMessage(websocket.TextMessage, []byte("服务器不存在"))
103 | return nil
104 | }
105 | // 开始连接流程
106 | credential := models.Credential{}
107 | if res := models.DB.First(&credential, server.CredentialID); res.Error != nil {
108 | ws.WriteMessage(websocket.TextMessage, []byte("认证信息不存在"))
109 | return nil
110 | }
111 | var client *gossh.Client
112 | spass := tools.AesDecrypt(credential.AuthContent, "0123456789012345")
113 | if credential.AuthType == 1 {
114 | cli, err := ssh.GetSSHClientByPassword(server.IP, spass, ssh.SSHOptions{Port: server.Port, User: credential.AuthUser})
115 | if err != nil {
116 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
117 | return nil
118 | }
119 | client = cli
120 | }
121 | if credential.AuthType == 2 {
122 | cli, err := ssh.GetSSHClientByKey(server.IP, []byte(spass), ssh.SSHOptions{Port: server.Port, User: credential.AuthUser})
123 | if err != nil {
124 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
125 | return nil
126 | }
127 | client = cli
128 | }
129 | defer client.Close()
130 | session, err := client.NewSession()
131 | if err != nil {
132 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
133 | return nil
134 | }
135 | defer session.Close()
136 | // Set up terminal modes
137 | modes := gossh.TerminalModes{
138 | gossh.ECHO: 1, // disable echoing
139 | gossh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
140 | gossh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
141 | }
142 | // Request pseudo terminal
143 | cols, _ := strconv.Atoi(c.QueryParam("cols"))
144 | rows, _ := strconv.Atoi(c.QueryParam("rows"))
145 | if err := session.RequestPty("xterm", rows, cols, modes); err != nil {
146 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
147 | return nil
148 | }
149 | // stdin, stdout
150 | stdin, err := session.StdinPipe()
151 | if err != nil {
152 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
153 | return nil
154 | }
155 | stdout, err := session.StdoutPipe()
156 | if err != nil {
157 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
158 | return nil
159 | }
160 | // Start remote shell
161 | if err := session.Shell(); err != nil {
162 | ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
163 | return nil
164 | }
165 | // 连接超时控制,默认30分钟
166 | ctx, cancel := context.WithTimeout(context.Background(), maxTimeout)
167 | defer cancel()
168 | ch := make(chan []byte)
169 | exitCh := make(chan bool)
170 | idletimer := time.NewTimer(idleTimeout) // 如果用户空闲5分钟没输入,也退出
171 | // 读取用户输入,并传递给远程主机
172 | go func() {
173 | for {
174 | // misc.Logger.Info().Str("from", "terminal").Msg("开始读取用户输入")
175 | _, msg, err := ws.ReadMessage()
176 | ch <- msg
177 | // 收到用户输入,重置timer
178 | if !idletimer.Stop() {
179 | <-idletimer.C
180 | }
181 | idletimer.Reset(idleTimeout)
182 | if err != nil {
183 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
184 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("")
185 | }
186 | exitCh <- true
187 | return
188 | }
189 | // log.Printf("%s\n", msg)
190 | // log.Println("开始发送消息到远程主机: ", string(msg))
191 | // 过滤出 window change 事件
192 | var resize [2]int
193 | if err := json.Unmarshal(msg, &resize); err != nil {
194 | if _, err := stdin.Write(msg); err != nil {
195 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("session stdin write error")
196 | exitCh <- true
197 | return
198 | }
199 | } else {
200 | misc.Logger.Info().Str("from", "terminal").Msg("更改窗口大小")
201 | session.WindowChange(resize[1], resize[0])
202 | }
203 | // misc.Logger.Info().Str("from", "terminal").Msg("结束读取用户输入")
204 | }
205 | }()
206 | // 读取远程返回
207 | go func() {
208 | // 开始记录终端录屏
209 | now := time.Now()
210 | time_str, _ := times.GetTimeString(0, times.TimeOptions{Layout: "20060102150405"})
211 | record_dir := "./records/"
212 | record_file := fmt.Sprintf("%s%s-%s-%s-web.cast", record_dir, username, server.IP, time_str)
213 | record := models.Record{User: username, IP: server.IP, FromIP: c.RealIP(), File: record_file}
214 | models.DB.Create(&record)
215 | var f *os.File
216 | f, _ = os.Create(record_file)
217 | defer f.Close()
218 | // 记录文件头
219 | timestamp, _ := strconv.ParseFloat(fmt.Sprintf("%.9f", float64(now.UnixNano())/float64(1e9)), 64)
220 | env := map[string]string{
221 | "SHELL": "/bin/bash",
222 | "TERM": "xterm",
223 | }
224 | header := map[string]interface{}{
225 | "version": 2,
226 | "width": 80,
227 | "height": 24,
228 | "timestamp": timestamp,
229 | "env": env,
230 | }
231 | headerbyte, _ := json.Marshal(header)
232 | f.WriteString(string(headerbyte) + "\n")
233 | // 开始读取远程
234 | for {
235 | buf := make([]byte, 1024)
236 | n, err := stdout.Read(buf[:])
237 | if err != nil {
238 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("session stdout read error")
239 | exitCh <- true
240 | return
241 | }
242 | // 记录终端内容
243 | nt, _ := strconv.ParseFloat(fmt.Sprintf("%.9f", float64(time.Now().UnixNano())/float64(1e9)), 64)
244 | iodata := []string{fmt.Sprintf("%.9f", nt-timestamp), "o", string(buf[:n])} // 指定读出多少,不然都是补0的多余数据
245 | iodatabyte, _ := json.Marshal(iodata)
246 | f.WriteString(string(iodatabyte) + "\n")
247 | // 输出到 websocket
248 | err = ws.WriteMessage(websocket.TextMessage, buf[:n])
249 | if err != nil {
250 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("write ws msg error")
251 | exitCh <- true
252 | return
253 | }
254 | // misc.Logger.Info().Str("from", "terminal").Msg("结束读取远程")
255 | }
256 | }()
257 | // 监控连接超时,空闲超时和退出信号
258 | for {
259 | select {
260 | case <-ctx.Done():
261 | misc.Logger.Error().Err(ctx.Err()).Str("from", "terminal").Msg("超时")
262 | if err := ws.WriteMessage(websocket.TextMessage, []byte("连接超时断开连接")); err != nil {
263 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("write ws msg error")
264 | }
265 | // 超时的话退出连接
266 | return nil
267 | case <-ch:
268 | // misc.Logger.Info().Str("from", "terminal").Msg("没有超时")
269 | continue
270 | case <-exitCh:
271 | misc.Logger.Info().Str("from", "terminal").Msg("收到退出信号")
272 | return nil
273 | case <-idletimer.C:
274 | misc.Logger.Info().Str("from", "terminal").Msg("收到空闲超时信号")
275 | if err := ws.WriteMessage(websocket.TextMessage, []byte("空闲超时断开连接")); err != nil {
276 | misc.Logger.Error().Err(err).Str("from", "terminal").Msg("write ws msg error")
277 | }
278 | return nil
279 | }
280 | }
281 | // session.Wait()
282 | } else {
283 | ws.WriteMessage(websocket.TextMessage, []byte("token nil"))
284 | }
285 | return nil
286 | }
287 |
--------------------------------------------------------------------------------