Initial commit

This commit is contained in:
dushixiang 2020-12-20 21:19:11 +08:00
commit e7f2773c77
77 changed files with 27866 additions and 0 deletions

19
.dockerignore Normal file
View File

@ -0,0 +1,19 @@
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
web/node_modules/

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM golang:alpine as builder
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.io,direct
WORKDIR /app
COPY . .
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add gcc g++
RUN go env && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -ldflags '-linkmode external -extldflags "-static"' -o next_terminal main.go
FROM alpine:3.12.3
LABEL MAINTAINER="helloworld1024@foxmail.com"
WORKDIR /opt/next_terminal
COPY --from=builder /app/next_terminal ./
COPY --from=builder /app/next-terminal.yml ./
COPY --from=builder /app/web/build ./web/build
RUN touch next-terminal.db & chmod +x next_terminal
EXPOSE 8088
ENTRYPOINT ./next_terminal

339
LICENSE Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# next-terminal
just do go
## 主要功能

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module next-terminal
go 1.13
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/gofrs/uuid v3.3.0+incompatible
github.com/gorilla/websocket v1.4.2
github.com/labstack/echo/v4 v4.1.17
github.com/labstack/gommon v0.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/sftp v1.12.0
github.com/shirou/gopsutil v3.20.10+incompatible
github.com/spf13/viper v1.7.1
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
gorm.io/driver/mysql v1.0.1
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.7
)

420
go.sum Normal file
View File

@ -0,0 +1,420 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v1.4.4 h1:1bEiBNeGSUKxcPDGfZ/7IgdhJJZx8wV/pICJh4W2NJI=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo=
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil v3.20.10+incompatible h1:kQuRhh6h6y4luXvnmtu/lJEGtdJ3q8lbu9NQY99GP+o=
github.com/shirou/gopsutil v3.20.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.0 h1:qfIlyaZvrF7kMWY3jBdEBXkXJ2M5MFYMTppjILxS3fQ=
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.7 h1:rMS4CL3pNmYq1V5/X+nHHjh1Dx6dnf27+Cai5zabo+M=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

83
main.go Normal file
View File

@ -0,0 +1,83 @@
package main
import (
"github.com/labstack/gommon/log"
"github.com/patrickmn/go-cache"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"next-terminal/pkg/api"
"next-terminal/pkg/config"
"next-terminal/pkg/handle"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"time"
)
func main() {
log.Fatal(Run())
}
func Run() error {
config.NextTerminal = config.SetupConfig()
var err error
config.DB, err = gorm.Open(mysql.Open(config.NextTerminal.Dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
//config.DB, err = gorm.Open(sqlite.Open("next-terminal.db"), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库异常", err)
}
if err := config.DB.AutoMigrate(&model.User{}); err != nil {
return err
}
users := model.FindAllUser()
if len(users) == 0 {
var pass []byte
if pass, err = utils.Encoder.Encode([]byte("admin")); err != nil {
return err
}
user := model.User{
ID: utils.UUID(),
Username: "admin",
Password: string(pass),
Nickname: "超级管理员",
Created: utils.NowJsonTime(),
}
if err := model.CreateNewUser(&user); err != nil {
return err
}
}
if err := config.DB.AutoMigrate(&model.Asset{}); err != nil {
return err
}
if err := config.DB.AutoMigrate(&model.Session{}); err != nil {
return err
}
if err := config.DB.AutoMigrate(&model.Command{}); err != nil {
return err
}
if err := config.DB.AutoMigrate(&model.Credential{}); err != nil {
return err
}
if err := config.DB.AutoMigrate(&model.Property{}); err != nil {
return err
}
config.Cache = cache.New(5*time.Minute, 10*time.Minute)
config.Store = config.NewStore()
e := api.SetupRoutes()
// 启动定时任务
//go handle.RunTicker()
go handle.RunDataFix()
go handle.InitProperties()
return e.Start(config.NextTerminal.Addr)
}

4
next-terminal.yml Normal file
View File

@ -0,0 +1,4 @@
next-terminal:
# dsn: root:root@tcp(127.0.0.1:3306)/next-terminal?charset=utf8mb4&parseTime=True&loc=Local
dsn: root:mysql@tcp(172.16.101.32:3306)/next-terminal?charset=utf8mb4&parseTime=True&loc=Local
addr: 0.0.0.0:8088

52
pkg/api/account.go Normal file
View File

@ -0,0 +1,52 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"time"
)
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
}
func LoginEndpoint(c echo.Context) error {
var loginAccount LoginAccount
if err := c.Bind(&loginAccount); err != nil {
return err
}
user, err := model.FindUserByUsername(loginAccount.Username)
if err != nil {
return err
}
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
return err
}
token := utils.UUID()
config.Cache.Set(token, user, time.Minute*time.Duration(30))
model.UpdateUserById(&model.User{Online: true}, user.ID)
return Success(c, token)
}
func LogoutEndpoint(c echo.Context) error {
token := GetToken(c)
config.Cache.Delete(token)
return Success(c, nil)
}
func ChangePasswordEndpoint(c echo.Context) error {
return nil
}
func InfoEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
return Success(c, account)
}

96
pkg/api/asset.go Normal file
View File

@ -0,0 +1,96 @@
package api
import (
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"github.com/labstack/echo/v4"
"strconv"
"strings"
)
func AssetCreateEndpoint(c echo.Context) error {
var item model.Asset
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewAsset(&item); err != nil {
return err
}
return Success(c, item)
}
func AssetPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
protocol := c.QueryParam("protocol")
items, total, _ := model.FindPageAsset(pageIndex, pageSize, name, protocol)
return Success(c, H{
"total": total,
"items": items,
})
}
func AssetAllEndpoint(c echo.Context) error {
protocol := c.QueryParam("protocol")
items, _ := model.FindAssetByConditions(protocol)
return Success(c, items)
}
func AssetUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Asset
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateAssetById(&item, id)
return Success(c, nil)
}
func AssetDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteAssetById(split[i])
}
return Success(c, nil)
}
func AssetGetEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Asset
if item, err = model.FindAssetById(id); err != nil {
return err
}
return Success(c, item)
}
func AssetTcpingEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Asset
if item, err = model.FindAssetById(id); err != nil {
return err
}
active := utils.Tcping(item.IP, item.Port)
asset := model.Asset{
Active: active,
}
model.UpdateAssetById(&asset, item.ID)
return Success(c, active)
}

70
pkg/api/command.go Normal file
View File

@ -0,0 +1,70 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func CommandCreateEndpoint(c echo.Context) error {
var item model.Command
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewCommand(&item); err != nil {
return err
}
return Success(c, item)
}
func CommandPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
content := c.QueryParam("content")
items, total, _ := model.FindPageCommand(pageIndex, pageSize, name, content)
return Success(c, H{
"total": total,
"items": items,
})
}
func CommandUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Command
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateCommandById(&item, id)
return Success(c, nil)
}
func CommandDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteCommandById(split[i])
}
return Success(c, nil)
}
func CommandGetEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Command
if item, err = model.FindCommandById(id); err != nil {
return err
}
return Success(c, item)
}

72
pkg/api/credential.go Normal file
View File

@ -0,0 +1,72 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func CredentialAllEndpoint(c echo.Context) error {
items, _ := model.FindAllCredential()
return Success(c, items)
}
func CredentialCreateEndpoint(c echo.Context) error {
var item model.Credential
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewCredential(&item); err != nil {
return err
}
return Success(c, item)
}
func CredentialPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
items, total, _ := model.FindPageCredential(pageIndex, pageSize, name)
return Success(c, H{
"total": total,
"items": items,
})
}
func CredentialUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Credential
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateCredentialById(&item, id)
return Success(c, nil)
}
func CredentialDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteCredentialById(split[i])
}
return Success(c, nil)
}
func CredentialGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, _ := model.FindCredentialById(id)
return Success(c, item)
}

34
pkg/api/middleware.go Normal file
View File

@ -0,0 +1,34 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/config"
"strings"
"time"
)
func Auth(next echo.HandlerFunc) echo.HandlerFunc {
urls := []string{"download", "login"}
return func(c echo.Context) error {
// 路由拦截 - 登录身份、资源权限判断等
for i := range urls {
if c.Request().RequestURI == "/" || strings.HasPrefix(c.Request().RequestURI, "/#") {
return next(c)
}
if strings.Contains(c.Request().RequestURI, urls[i]) {
return next(c)
}
}
token := GetToken(c)
user, found := config.Cache.Get(token)
if !found {
c.Logger().Error("您的登录信息已失效,请重新登录后再试。")
return Fail(c, 403, "您的登录信息已失效,请重新登录后再试。")
}
config.Cache.Set(token, user, time.Minute*time.Duration(30))
return next(c)
}
}

98
pkg/api/overview.go Normal file
View File

@ -0,0 +1,98 @@
package api
import (
"next-terminal/pkg/model"
"fmt"
"github.com/labstack/echo/v4"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"time"
)
type OverviewStatus struct {
Load Load `json:"load"`
Memory Memory `json:"memory"`
CPU CPU `json:"cpu"`
}
type Load struct {
Load1 float64 `json:"load1"`
Load5 float64 `json:"load5"`
Load15 float64 `json:"load15"`
}
type Memory struct {
Total uint64 `json:"total"`
Available uint64 `json:"available"`
UsedPercent float64 `json:"usedPercent"`
Used uint64 `json:"used"`
}
type CPU struct {
PhysicalCount int `json:"physicalCount"`
LogicalCount int `json:"logicalCount"`
Percent float64 `json:"percent"`
ModelName string `json:"modelName"`
}
type Counter struct {
User int64 `json:"user"`
Asset int64 `json:"asset"`
Credential int64 `json:"credential"`
OnlineSession int64 `json:"onlineSession"`
}
func OverviewStatusEndPoint(c echo.Context) error {
info, _ := load.Avg()
memory, _ := mem.VirtualMemory()
infoStats, _ := cpu.Info()
physicalCount, _ := cpu.Counts(false)
logicalCount, _ := cpu.Counts(true)
cps, _ := cpu.Percent(time.Second, false)
fmt.Printf("%+v\n", info)
fmt.Printf("%+v\n", memory)
fmt.Printf("%+v\n", infoStats)
fmt.Printf("%+v\n", physicalCount)
fmt.Printf("%+v\n", logicalCount)
fmt.Printf("%+v\n", cps)
overviewStatus := OverviewStatus{
Load: Load{
Load1: info.Load1,
Load5: info.Load5,
Load15: info.Load15,
},
Memory: Memory{
Total: memory.Total,
Available: memory.Available,
UsedPercent: memory.UsedPercent,
Used: memory.Used,
},
CPU: CPU{
PhysicalCount: physicalCount,
LogicalCount: logicalCount,
Percent: cps[0],
ModelName: infoStats[0].ModelName,
},
}
return Success(c, overviewStatus)
}
func OverviewCounterEndPoint(c echo.Context) error {
countUser, _ := model.CountUser()
countOnlineSession, _ := model.CountOnlineSession()
credential, _ := model.CountCredential()
asset, _ := model.CountAsset()
counter := Counter{
User: countUser,
OnlineSession: countOnlineSession,
Credential: credential,
Asset: asset,
}
return Success(c, counter)
}

29
pkg/api/property.go Normal file
View File

@ -0,0 +1,29 @@
package api
import (
"next-terminal/pkg/model"
"fmt"
"github.com/labstack/echo/v4"
)
func PropertyGetEndpoint(c echo.Context) error {
properties := model.FindAllProperties()
return Success(c, properties)
}
func PropertyUpdateEndpoint(c echo.Context) error {
var item map[string]interface{}
if err := c.Bind(&item); err != nil {
return err
}
for key := range item {
value := fmt.Sprintf("%v", item[key])
property := model.Property{
Name: key,
Value: value,
}
model.UpdatePropertyByName(&property, key)
}
return Success(c, nil)
}

145
pkg/api/routes.go Normal file
View File

@ -0,0 +1,145 @@
package api
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
)
const Token = "X-Auth-Token"
func SetupRoutes() *echo.Echo {
// Echo instance
e := echo.New()
e.File("/", "web/build/index.html")
e.File("/logo.svg", "web/build/logo.svg")
e.File("/favicon.ico", "web/build/favicon.ico")
e.Static("/static", "web/build/static")
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
e.Use(Auth)
e.POST("/login", LoginEndpoint)
e.GET("/tunnel", TunEndpoint)
e.GET("/ssh", SSHEndpoint)
e.POST("/logout", LogoutEndpoint)
e.POST("/change-password", ChangePasswordEndpoint)
e.GET("/info", InfoEndpoint)
users := e.Group("/users")
{
users.POST("", UserCreateEndpoint)
users.GET("/paging", UserPagingEndpoint)
users.PUT("/:id", UserUpdateEndpoint)
users.DELETE("/:id", UserDeleteEndpoint)
users.GET("/:id", UserGetEndpoint)
}
assets := e.Group("/assets", Auth)
{
assets.GET("", AssetAllEndpoint)
assets.POST("", AssetCreateEndpoint)
assets.GET("/paging", AssetPagingEndpoint)
assets.POST("/:id/tcping", AssetTcpingEndpoint)
assets.PUT("/:id", AssetUpdateEndpoint)
assets.DELETE("/:id", AssetDeleteEndpoint)
assets.GET("/:id", AssetGetEndpoint)
}
commands := e.Group("/commands")
{
commands.GET("/paging", CommandPagingEndpoint)
commands.POST("", CommandCreateEndpoint)
commands.PUT("/:id", CommandUpdateEndpoint)
commands.DELETE("/:id", CommandDeleteEndpoint)
commands.GET("/:id", CommandGetEndpoint)
}
credentials := e.Group("/credentials")
{
credentials.GET("", CredentialAllEndpoint)
credentials.GET("/paging", CredentialPagingEndpoint)
credentials.POST("", CredentialCreateEndpoint)
credentials.PUT("/:id", CredentialUpdateEndpoint)
credentials.DELETE("/:id", CredentialDeleteEndpoint)
credentials.GET("/:id", CredentialGetEndpoint)
}
sessions := e.Group("/sessions")
{
sessions.POST("", SessionCreateEndpoint)
sessions.GET("/paging", SessionPagingEndpoint)
sessions.POST("/:id/content", SessionContentEndpoint)
sessions.POST("/:id/discontent", SessionDiscontentEndpoint)
sessions.POST("/:id/resize", SessionResizeEndpoint)
sessions.POST("/:id/upload", SessionUploadEndpoint)
sessions.GET("/:id/download", SessionDownloadEndpoint)
sessions.GET("/:id/ls", SessionLsEndpoint)
sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
sessions.DELETE("/:id/rmdir", SessionRmDirEndpoint)
sessions.DELETE("/:id/rm", SessionRmEndpoint)
sessions.DELETE("/:id", SessionDeleteEndpoint)
}
e.GET("/properties", PropertyGetEndpoint)
e.PUT("/properties", PropertyUpdateEndpoint)
e.GET("/overview/status", OverviewStatusEndPoint)
e.GET("/overview/counter", OverviewCounterEndPoint)
return e
}
type H map[string]interface{}
func Fail(c echo.Context, code int, message string) error {
return c.JSON(200, H{
"code": code,
"message": message,
})
}
func Success(c echo.Context, data interface{}) error {
return c.JSON(200, H{
"code": 1,
"message": "success",
"data": data,
})
}
func NotFound(c echo.Context, message string) error {
return c.JSON(200, H{
"code": -1,
"message": message,
})
}
func GetToken(c echo.Context) string {
token := c.Request().Header.Get(Token)
if len(token) > 0 {
return token
}
return c.QueryParam(Token)
}
func GetCurrentAccount(c echo.Context) (model.User, bool) {
token := GetToken(c)
get, b := config.Cache.Get(token)
if b {
return get.(model.User), true
}
return model.User{}, false
}

430
pkg/api/session.go Normal file
View File

@ -0,0 +1,430 @@
package api
import (
"bytes"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"io"
"io/ioutil"
"net/http"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"os"
"path"
"strconv"
"strings"
)
func SessionPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
status := c.QueryParam("status")
userId := c.QueryParam("userId")
clientIp := c.QueryParam("clientIp")
assetId := c.QueryParam("assetId")
protocol := c.QueryParam("protocol")
items, total, err := model.FindPageSession(pageIndex, pageSize, status, userId, clientIp, assetId, protocol)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func SessionDeleteEndpoint(c echo.Context) error {
sessionIds := c.Param("id")
split := strings.Split(sessionIds, ",")
for i := range split {
model.DeleteSessionById(split[i])
drivePath, err := model.GetDrivePath()
if err != nil {
continue
}
_ = os.Remove(path.Join(drivePath, split[i]))
}
return Success(c, nil)
}
func SessionContentEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session := model.Session{}
session.ID = sessionId
session.Status = model.Connected
session.ConnectedTime = utils.NowJsonTime()
model.UpdateSessionById(&session, sessionId)
return Success(c, nil)
}
func SessionDiscontentEndpoint(c echo.Context) error {
sessionIds := c.Param("id")
split := strings.Split(sessionIds, ",")
for i := range split {
tun, ok := config.Store.Get(split[i])
if ok {
CloseSession(split[i], tun)
}
}
return Success(c, nil)
}
func CloseSession(sessionId string, tun config.Tun) {
_ = tun.Tun.Close()
config.Store.Del(sessionId)
session := model.Session{}
session.ID = sessionId
session.Status = model.Disconnected
session.DisconnectedTime = utils.NowJsonTime()
model.UpdateSessionById(&session, sessionId)
}
func SessionResizeEndpoint(c echo.Context) error {
width := c.QueryParam("width")
height := c.QueryParam("height")
sessionId := c.Param("id")
if len(width) == 0 || len(height) == 0 {
panic("参数异常")
}
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
session := model.Session{}
session.ID = sessionId
session.Width = intWidth
session.Height = intHeight
model.UpdateSessionById(&session, sessionId)
return Success(c, session)
}
func SessionCreateEndpoint(c echo.Context) error {
assetId := c.QueryParam("assetId")
user, _ := GetCurrentAccount(c)
asset, err := model.FindAssetById(assetId)
if err != nil {
return err
}
session := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: model.NoConnect,
Creator: user.ID,
ClientIP: c.RealIP(),
}
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return err
}
session.Username = credential.Username
session.Password = credential.Password
}
if err := model.CreateNewSession(session); err != nil {
return err
}
return Success(c, session)
}
func SessionUploadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
filename := file.Filename
src, err := file.Open()
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename)
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
dstFile, err := tun.SftpClient.Create(remoteFile)
if err != nil {
return err
}
defer dstFile.Close()
buf := make([]byte, 1024)
for {
n, _ := src.Read(buf)
if n == 0 {
break
}
_, _ = dstFile.Write(buf)
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
// Destination
dst, err := os.Create(path.Join(drivePath, remoteFile))
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
return Success(c, nil)
}
return err
}
func SessionDownloadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
//remoteDir := c.Query("dir")
remoteFile := c.QueryParam("file")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
dstFile, err := tun.SftpClient.Open(remoteFile)
if err != nil {
return err
}
defer dstFile.Close()
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", remoteFile))
var buff bytes.Buffer
if _, err := dstFile.WriteTo(&buff); err != nil {
return err
}
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
return c.File(path.Join(drivePath, remoteFile))
}
return err
}
type File struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Mode string `json:"mode"`
IsLink bool `json:"isLink"`
}
func SessionLsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
fileInfos, err := tun.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
var files = make([]File, 0)
for i := range fileInfos {
file := File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
Mode: fileInfos[i].Mode().String(),
IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink,
}
files = append(files, file)
}
return Success(c, files)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
fileInfos, err := ioutil.ReadDir(path.Join(drivePath, remoteDir))
if err != nil {
return err
}
var files = make([]File, 0)
for i := range fileInfos {
file := File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
Mode: fileInfos[i].Mode().String(),
IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink,
}
files = append(files, file)
}
return Success(c, files)
}
return err
}
func SessionMkDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
if err := tun.SftpClient.Mkdir(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.MkdirAll(path.Join(drivePath, remoteDir), os.ModePerm); err != nil {
return err
}
return Success(c, nil)
}
return nil
}
func SessionRmDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
fileInfos, err := tun.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
for i := range fileInfos {
if err := tun.SftpClient.Remove(path.Join(remoteDir, fileInfos[i].Name())); err != nil {
return err
}
}
if err := tun.SftpClient.RemoveDirectory(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.RemoveAll(path.Join(drivePath, remoteDir)); err != nil {
return err
}
return Success(c, nil)
}
return nil
}
func SessionRmEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteFile := c.QueryParam("file")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
if err := tun.SftpClient.Remove(remoteFile); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.Remove(path.Join(drivePath, remoteFile)); err != nil {
return err
}
return Success(c, nil)
}
return nil
}

175
pkg/api/ssh.go Normal file
View File

@ -0,0 +1,175 @@
package api
import (
"bytes"
"next-terminal/pkg/model"
"fmt"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"log"
"net"
"net/http"
"strconv"
"sync"
"time"
)
var UpGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"guacamole"},
}
type NextWriter struct {
b bytes.Buffer
mu sync.Mutex
}
func (w *NextWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.b.Write(p)
}
func (w *NextWriter) Read() ([]byte, int, error) {
w.mu.Lock()
defer w.mu.Unlock()
p := w.b.Bytes()
buf := make([]byte, len(p))
read, err := w.b.Read(buf)
w.b.Reset()
if err != nil {
return nil, 0, err
}
return buf, read, err
}
func SSHEndpoint(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
return err
}
assetId := c.QueryParam("assetId")
width, _ := strconv.Atoi(c.QueryParam("width"))
height, _ := strconv.Atoi(c.QueryParam("height"))
asset, err := model.FindAssetById(assetId)
if err != nil {
return err
}
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return err
}
asset.Username = credential.Username
asset.Password = credential.Password
}
config := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: asset.Username,
Auth: []ssh.AuthMethod{ssh.Password(asset.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", asset.IP, asset.Port)
sshClient, err := ssh.Dial("tcp", addr, config)
if err != nil {
return err
}
session, err := sshClient.NewSession()
if err != nil {
return err
}
defer session.Close()
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err := session.RequestPty("xterm", height, width, modes); err != nil {
return err
}
var b NextWriter
session.Stdout = &b
session.Stderr = &b
stdinPipe, err := session.StdinPipe()
if err != nil {
return err
}
if err := session.Shell(); err != nil {
return err
}
go func() {
for true {
p, n, err := b.Read()
if err != nil {
continue
}
if n > 0 {
WriteByteMessage(ws, p)
}
time.Sleep(time.Duration(100) * time.Millisecond)
}
}()
for true {
_, message, err := ws.ReadMessage()
if err != nil {
continue
}
_, err = stdinPipe.Write(message)
if err != nil {
log.Println("Tunnel write:", err)
}
}
return err
}
func WriteMessage(ws *websocket.Conn, message string) {
WriteByteMessage(ws, []byte(message))
}
func WriteByteMessage(ws *websocket.Conn, p []byte) {
err := ws.WriteMessage(websocket.TextMessage, p)
if err != nil {
log.Println("write:", err)
}
}
func CreateSftpClient(username, password, ip string, port int) (sftpClient *sftp.Client, err error) {
clientConfig := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", ip, port)
sshClient, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil {
return nil, err
}
return sftp.NewClient(sshClient)
}

33
pkg/api/test/ps.go Normal file
View File

@ -0,0 +1,33 @@
package main
func main() {
//v, _ := mem.VirtualMemory()
//c, _ := cpu.Info()
//cc, _ := cpu.Percent(time.Second, false)
//d, _ := disk.Usage("/")
//n, _ := host.Info()
//nv, _ := net.IOCounters(true)
//boottime, _ := host.BootTime()
//btime := time.Unix(int64(boottime), 0).Format("2006-01-02 15:04:05")
//
//fmt.Printf(" Mem : %v MB Free: %v MB Used:%v Usage:%f%%\n", v.Total/1024/1024, v.Available/1024/1024, v.Used/1024/1024, v.UsedPercent)
//if len(c) > 1 {
// for _, sub_cpu := range c {
// modelname := sub_cpu.ModelName
// cores := sub_cpu.Cores
// fmt.Printf(" CPU : %v %v cores \n", modelname, cores)
// }
//} else {
// sub_cpu := c[0]
// modelname := sub_cpu.ModelName
// cores := sub_cpu.Cores
// fmt.Printf(" CPU : %v %v cores \n", modelname, cores)
//
//}
//fmt.Printf(" Network: %v bytes / %v bytes\n", nv[0].BytesRecv, nv[0].BytesSent)
//fmt.Printf(" SystemBoot:%v\n", btime)
//fmt.Printf(" CPU Used : used %f%% \n", cc[0])
//fmt.Printf(" HD : %v GB Free: %v GB Usage:%f%%\n", d.Total/1024/1024/1024, d.Free/1024/1024/1024, d.UsedPercent)
//fmt.Printf(" OS : %v(%v) %v \n", n.Platform, n.PlatformFamily, n.PlatformVersion)
//fmt.Printf(" Hostname : %v \n", n.Hostname)
}

158
pkg/api/tunnel.go Normal file
View File

@ -0,0 +1,158 @@
package api
import (
"next-terminal/pkg/config"
"next-terminal/pkg/guacd"
"next-terminal/pkg/model"
"fmt"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
"log"
"strconv"
)
func TunEndpoint(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
return err
}
width := c.QueryParam("width")
height := c.QueryParam("height")
sessionId := c.QueryParam("sessionId")
connectionId := c.QueryParam("connectionId")
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
configuration := guacd.NewConfiguration()
configuration.SetParameter("width", width)
configuration.SetParameter("height", height)
propertyMap := model.FindAllPropertiesMap()
for name := range propertyMap {
if name == model.GuacdFontSize {
fontSize, _ := strconv.Atoi(propertyMap[name])
fontSize = fontSize * 2
configuration.SetParameter(name, strconv.Itoa(fontSize))
} else {
configuration.SetParameter(name, propertyMap[name])
}
}
var session model.Session
var sftpClient *sftp.Client
if len(connectionId) > 0 {
session, err = model.FindSessionByConnectionId(connectionId)
if err != nil {
return err
}
configuration.ConnectionID = connectionId
} else {
session, err = model.FindSessionById(sessionId)
if err != nil {
return err
}
configuration.Protocol = session.Protocol
switch configuration.Protocol {
case "rdp":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("dpi", "96")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter("enable-sftp", "")
break
case "ssh":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
sftpClient, err = CreateSftpClient(session.Username, session.Password, session.IP, session.Port)
if err != nil {
return err
}
break
case "vnc":
configuration.SetParameter("password", session.Password)
configuration.SetParameter("enable-sftp", "")
break
case "telnet":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("enable-sftp", "")
break
}
configuration.SetParameter("hostname", session.IP)
configuration.SetParameter("port", strconv.Itoa(session.Port))
}
addr := propertyMap[model.GuacdHost] + ":" + propertyMap[model.GuacdPort]
tunnel, err := guacd.NewTunnel(addr, configuration)
if err != nil {
return err
}
fmt.Printf("=====================================================\n")
fmt.Printf("connect to %v with config: %+v\n", addr, configuration)
fmt.Printf("=====================================================\n")
tun := config.Tun{
Tun: tunnel,
SftpClient: sftpClient,
}
config.Store.Set(sessionId, tun)
if len(session.ConnectionId) == 0 {
session.ConnectionId = tunnel.UUID
session.Width = intWidth
session.Height = intHeight
model.UpdateSessionById(&session, sessionId)
}
go func() {
for true {
instruction, err := tunnel.Read()
if err != nil {
CloseSession(sessionId, tun)
log.Printf("WS读取异常: %v", err)
break
}
//fmt.Printf("<= %v \n", string(instruction))
err = ws.WriteMessage(websocket.TextMessage, instruction)
if err != nil {
CloseSession(sessionId, tun)
log.Printf("WS写入异常: %v", err)
break
}
}
}()
for true {
_, message, err := ws.ReadMessage()
if err != nil {
CloseSession(sessionId, tun)
log.Printf("Tunnel读取异常: %v", err)
break
}
_, err = tunnel.WriteAndFlush(message)
if err != nil {
CloseSession(sessionId, tun)
log.Printf("Tunnel写入异常: %v", err)
break
}
}
return err
}

83
pkg/api/user.go Normal file
View File

@ -0,0 +1,83 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func UserCreateEndpoint(c echo.Context) error {
var item model.User
if err := c.Bind(&item); err != nil {
return err
}
var pass []byte
var err error
if pass, err = utils.Encoder.Encode([]byte("admin")); err != nil {
return err
}
item.Password = string(pass)
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewUser(&item); err != nil {
return err
}
return Success(c, item)
}
func UserPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
username := c.QueryParam("username")
nickname := c.QueryParam("nickname")
items, total, err := model.FindPageUser(pageIndex, pageSize, username, nickname)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func UserUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.User
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateUserById(&item, id)
return Success(c, nil)
}
func UserDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteUserById(split[i])
}
return Success(c, nil)
}
func UserGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, err := model.FindUserById(id)
if err != nil {
return err
}
return Success(c, item)
}

14
pkg/config/config.go Normal file
View File

@ -0,0 +1,14 @@
package config
import (
"github.com/patrickmn/go-cache"
"gorm.io/gorm"
)
var DB *gorm.DB
var Cache *cache.Cache
var NextTerminal *NextTerminalConfig
var Store *TunStore

View File

@ -0,0 +1,31 @@
package config
import (
"github.com/spf13/viper"
"log"
)
type NextTerminalConfig struct {
Dsn string
Addr string
}
func SetupConfig() *NextTerminalConfig {
viper.SetConfigName("next-terminal")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/next-terminal/")
viper.AddConfigPath("$HOME/.next-terminal")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatal(err)
}
var config = &NextTerminalConfig{
Dsn: viper.GetString("next-terminal.dsn"),
Addr: viper.GetString("next-terminal.addr"),
}
return config
}

37
pkg/config/store.go Normal file
View File

@ -0,0 +1,37 @@
package config
import (
"github.com/pkg/sftp"
"next-terminal/pkg/guacd"
"sync"
)
type Tun struct {
Tun guacd.Tunnel
SftpClient *sftp.Client
}
type TunStore struct {
m sync.Map
}
func (s *TunStore) Set(k string, v Tun) {
s.m.Store(k, v)
}
func (s *TunStore) Del(k string) {
s.m.Delete(k)
}
func (s *TunStore) Get(k string) (item Tun, ok bool) {
value, ok := s.m.Load(k)
if ok {
return value.(Tun), true
}
return item, false
}
func NewStore() *TunStore {
store := TunStore{sync.Map{}}
return &store
}

227
pkg/guacd/guacd.go Normal file
View File

@ -0,0 +1,227 @@
package guacd
import (
"bufio"
"errors"
"fmt"
"net"
"strings"
)
const Delimiter = ';'
const Version = "VERSION_1_1_0"
type Configuration struct {
ConnectionID string
Protocol string
Parameters map[string]string
}
func NewConfiguration() (ret Configuration) {
ret.Parameters = make(map[string]string)
return ret
}
func (opt *Configuration) SetParameter(name, value string) {
opt.Parameters[name] = value
}
func (opt *Configuration) UnSetParameter(name string) {
delete(opt.Parameters, name)
}
func (opt *Configuration) GetParameter(name string) string {
return opt.Parameters[name]
}
type Instruction struct {
Opcode string
Args []string
ProtocolForm string
}
func NewInstruction(opcode string, args ...string) (ret Instruction) {
ret.Opcode = opcode
ret.Args = args
return ret
}
func (opt *Instruction) String() string {
if len(opt.ProtocolForm) > 0 {
return opt.ProtocolForm
}
opt.ProtocolForm = fmt.Sprintf("%d.%s", len(opt.Opcode), opt.Opcode)
for _, value := range opt.Args {
opt.ProtocolForm += fmt.Sprintf(",%d.%s", len(value), value)
}
opt.ProtocolForm += string(Delimiter)
return opt.ProtocolForm
}
func (opt *Instruction) Parse(content string) Instruction {
if strings.LastIndex(content, ";") > 0 {
content = strings.TrimRight(content, ";")
}
messages := strings.Split(content, ",")
var args = make([]string, len(messages))
for i := range messages {
lm := strings.Split(messages[i], ".")
args[i] = lm[1]
}
return NewInstruction(args[0], args[1:]...)
}
type Tunnel struct {
rw *bufio.ReadWriter
conn net.Conn
UUID string
Config Configuration
IsOpen bool
}
func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return
}
ret.conn = conn
ret.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
ret.Config = config
selectArg := config.ConnectionID
if selectArg == "" {
selectArg = config.Protocol
}
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
return Tunnel{}, err
}
args, err := ret.expect("args")
if err != nil {
return
}
width := config.GetParameter("width")
height := config.GetParameter("height")
// send size
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, "96")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("audio")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("image")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
return Tunnel{}, err
}
parameters := make([]string, len(args.Args))
for i := range args.Args {
argName := args.Args[i]
if strings.Contains(argName, "VERSION") {
parameters[i] = Version
continue
}
parameters[i] = config.GetParameter(argName)
}
// send connect
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
return Tunnel{}, err
}
ready, err := ret.expect("ready")
if err != nil {
return
}
if len(ready.Args) == 0 {
return ret, errors.New("no connection id received")
}
ret.UUID = ready.Args[0]
ret.IsOpen = true
return ret, nil
}
func (opt *Tunnel) WriteInstructionAndFlush(instruction Instruction) error {
if _, err := opt.WriteAndFlush([]byte(instruction.String())); err != nil {
return err
}
return nil
}
func (opt *Tunnel) WriteInstruction(instruction Instruction) error {
if _, err := opt.Write([]byte(instruction.String())); err != nil {
return err
}
return nil
}
func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) {
//fmt.Printf("-> %v \n", string(p))
nn, err := opt.rw.Write(p)
if err != nil {
return nn, err
}
err = opt.rw.Flush()
if err != nil {
return nn, err
}
return nn, nil
}
func (opt *Tunnel) Write(p []byte) (int, error) {
//fmt.Printf("-> %v \n", string(p))
nn, err := opt.rw.Write(p)
if err != nil {
return nn, err
}
return nn, nil
}
func (opt *Tunnel) Flush() error {
return opt.rw.Flush()
}
func (opt *Tunnel) ReadInstruction() (instruction Instruction, err error) {
msg, err := opt.rw.ReadString(Delimiter)
if err != nil {
return instruction, err
}
fmt.Printf("<- %v \n", msg)
return instruction.Parse(msg), err
}
func (opt *Tunnel) Read() ([]byte, error) {
return opt.rw.ReadBytes(Delimiter)
}
func (opt *Tunnel) expect(opcode string) (instruction Instruction, err error) {
instruction, err = opt.ReadInstruction()
if err != nil {
return instruction, err
}
if opcode != instruction.Opcode {
msg := fmt.Sprintf(`expected "%s" instruction but instead received "%s"`, opcode, instruction.Opcode)
return instruction, errors.New(msg)
}
return instruction, nil
}
func (opt *Tunnel) Close() error {
opt.IsOpen = false
return opt.conn.Close()
}

199
pkg/handle/runner.go Normal file
View File

@ -0,0 +1,199 @@
package handle
import (
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"os"
"time"
)
func RunTicker() {
var ch chan int
//定时任务
ticker := time.NewTicker(time.Minute * 5)
go func() {
for range ticker.C {
items, _ := model.FindAllAsset()
for i := range items {
item := items[i]
active := utils.Tcping(item.IP, item.Port)
asset := model.Asset{
Active: active,
}
model.UpdateAssetById(&asset, item.ID)
}
}
ch <- 1
}()
<-ch
}
func RunDataFix() {
sessions, _ := model.FindSessionByStatus(model.Connected)
if sessions == nil {
return
}
for i := range sessions {
session := model.Session{
Status: model.Disconnected,
DisconnectedTime: utils.NowJsonTime(),
}
model.UpdateSessionById(&session, sessions[i].ID)
}
}
func InitProperties() {
propertyMap := model.FindAllPropertiesMap()
if len(propertyMap[model.GuacdHost]) == 0 {
property := model.Property{
Name: model.GuacdHost,
Value: "127.0.0.1",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdPort]) == 0 {
property := model.Property{
Name: model.GuacdPort,
Value: "4822",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDriveName]) == 0 {
property := model.Property{
Name: model.GuacdDriveName,
Value: "File-System",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDrivePath]) == 0 {
path, _ := os.Getwd()
property := model.Property{
Name: model.GuacdDrivePath,
Value: path + "/drive/",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdFontName]) == 0 {
property := model.Property{
Name: model.GuacdFontName,
Value: "menlo",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdFontSize]) == 0 {
property := model.Property{
Name: model.GuacdFontSize,
Value: "12",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdColorScheme]) == 0 {
property := model.Property{
Name: model.GuacdColorScheme,
Value: "gray-black",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableSftp]) == 0 {
property := model.Property{
Name: model.GuacdEnableSftp,
Value: "true",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableDrive]) == 0 {
property := model.Property{
Name: model.GuacdEnableDrive,
Value: "true",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableWallpaper]) == 0 {
property := model.Property{
Name: model.GuacdEnableWallpaper,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableTheming]) == 0 {
property := model.Property{
Name: model.GuacdEnableTheming,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableFontSmoothing]) == 0 {
property := model.Property{
Name: model.GuacdEnableFontSmoothing,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableFullWindowDrag]) == 0 {
property := model.Property{
Name: model.GuacdEnableFullWindowDrag,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableDesktopComposition]) == 0 {
property := model.Property{
Name: model.GuacdEnableDesktopComposition,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableMenuAnimations]) == 0 {
property := model.Property{
Name: model.GuacdEnableMenuAnimations,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableBitmapCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableBitmapCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableOffscreenCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableOffscreenCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableGlyphCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableGlyphCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
}

86
pkg/model/asset.go Normal file
View File

@ -0,0 +1,86 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Asset struct {
ID string `gorm:"primary_key " json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
Protocol string `json:"protocol"`
Port int `json:"port"`
AccountType string `json:"accountType"`
Username string `json:"username"`
Password string `json:"password"`
CredentialId string `json:"credentialId"`
PrivateKey string `json:"privateKey"`
Passphrase string `json:"passphrase"`
Description string `json:"description"`
Active bool `json:"active"`
Created utils.JsonTime `json:"created"`
}
func (r *Asset) TableName() string {
return "assets"
}
func FindAllAsset() (o []Asset, err error) {
err = config.DB.Find(&o).Error
return
}
func FindAssetByConditions(protocol string) (o []Asset, err error) {
db := config.DB
if len(protocol) > 0 {
db = db.Where("protocol = ?", protocol)
}
err = db.Find(&o).Error
return
}
func FindPageAsset(pageIndex, pageSize int, name, protocol string) (o []Asset, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
if len(protocol) > 0 {
db = db.Where("protocol = ?", protocol)
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Asset, 0)
}
return
}
func CreateNewAsset(o *Asset) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindAssetById(id string) (o Asset, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateAssetById(o *Asset, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteAssetById(id string) {
config.DB.Where("id = ?", id).Delete(&Asset{})
}
func CountAsset() (total int64, err error) {
err = config.DB.Find(&Asset{}).Count(&total).Error
return
}

56
pkg/model/command.go Normal file
View File

@ -0,0 +1,56 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Command struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Created utils.JsonTime `json:"created"`
}
func (r *Command) TableName() string {
return "commands"
}
func FindPageCommand(pageIndex, pageSize int, name, content string) (o []Command, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
if len(content) > 0 {
db = db.Where("content like ?", "%"+content+"%")
}
err = db.Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Command, 0)
}
return
}
func CreateNewCommand(o *Command) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindCommandById(id string) (o Command, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateCommandById(o *Command, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteCommandById(id string) {
config.DB.Where("id = ?", id).Delete(&Command{})
}

63
pkg/model/credential.go Normal file
View File

@ -0,0 +1,63 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Credential struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Created utils.JsonTime `json:"created"`
}
func (r *Credential) TableName() string {
return "credentials"
}
func FindAllCredential() (o []Credential, err error) {
err = config.DB.Find(&o).Error
return
}
func FindPageCredential(pageIndex, pageSize int, name string) (o []Credential, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Credential, 0)
}
return
}
func CreateNewCredential(o *Credential) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindCredentialById(id string) (o Credential, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateCredentialById(o *Credential, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteCredentialById(id string) {
config.DB.Where("id = ?", id).Delete(&Credential{})
}
func CountCredential() (total int64, err error) {
err = config.DB.Find(&Credential{}).Count(&total).Error
return
}

78
pkg/model/property.go Normal file
View File

@ -0,0 +1,78 @@
package model
import (
"next-terminal/pkg/config"
"errors"
)
const (
GuacdHost = "host"
GuacdPort = "port"
GuacdFontName = "font-name"
GuacdFontSize = "font-size"
GuacdColorScheme = "color-scheme"
GuacdEnableSftp = "enable-sftp"
GuacdEnableDrive = "enable-drive"
GuacdDriveName = "drive-name"
GuacdDrivePath = "drive-path"
GuacdEnableWallpaper = "enable-wallpaper"
GuacdEnableTheming = "enable-theming"
GuacdEnableFontSmoothing = "enable-font-smoothing"
GuacdEnableFullWindowDrag = "enable-full-window-drag"
GuacdEnableDesktopComposition = "enable-desktop-composition"
GuacdEnableMenuAnimations = "enable-menu-animations"
GuacdDisableBitmapCaching = "disable-bitmap-caching"
GuacdDisableOffscreenCaching = "disable-offscreen-caching"
GuacdDisableGlyphCaching = "disable-glyph-caching"
)
type Property struct {
Name string `gorm:"primary_key" json:"name"`
Value string `json:"value"`
}
func (r *Property) TableName() string {
return "properties"
}
func FindAllProperties() (o []Property) {
if config.DB.Find(&o).Error != nil {
return nil
}
return
}
func CreateNewProperty(o *Property) (err error) {
err = config.DB.Create(o).Error
return
}
func UpdatePropertyByName(o *Property, name string) {
o.Name = name
config.DB.Updates(o)
}
func FindPropertyByName(name string) (o Property, err error) {
err = config.DB.Where("name = ?", name).First(&o).Error
return
}
func FindAllPropertiesMap() map[string]string {
properties := FindAllProperties()
propertyMap := make(map[string]string)
for i := range properties {
propertyMap[properties[i].Name] = properties[i].Value
}
return propertyMap
}
func GetDrivePath() (string, error) {
propertiesMap := FindAllPropertiesMap()
drivePath := propertiesMap[GuacdDrivePath]
if len(drivePath) == 0 {
return "", errors.New("获取RDP挂载目录失败")
}
return drivePath, nil
}

138
pkg/model/session.go Normal file
View File

@ -0,0 +1,138 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
const (
Connected = "connected"
Disconnected = "disconnected"
NoConnect = "no_connect"
)
type Session struct {
ID string `gorm:"primary_key" json:"id"`
Protocol string `json:"protocol"`
IP string `json:"ip"`
Port int `json:"port"`
ConnectionId string `json:"connectionId"`
AssetId string `json:"assetId"`
Username string `json:"username"`
Password string `json:"password"`
Creator string `json:"creator"`
ClientIP string `json:"clientIp"`
Width int `json:"width"`
Height int `json:"height"`
Status string `json:"status"`
ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
}
func (r *Session) TableName() string {
return "sessions"
}
type SessionVo struct {
ID string `json:"id"`
Protocol string `json:"protocol"`
IP string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
ConnectionId string `json:"connectionId"`
AssetId string `json:"assetId"`
Creator string `json:"creator"`
ClientIP string `json:"clientIp"`
Width int `json:"width"`
Height int `json:"height"`
Status string `json:"status"`
ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
AssetName string `json:"assetName"`
CreatorName string `json:"creatorName"`
}
func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol string) (results []SessionVo, total int64, err error) {
db := config.DB
var params []interface{}
params = append(params, status)
itemSql := "SELECT s.id, s.protocol, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? "
countSql := "select count(*) from sessions as s where s.status = ? "
if len(userId) > 0 {
itemSql += " and s.creator = ?"
countSql += " and s.creator = ?"
params = append(params, userId)
}
if len(clientIp) > 0 {
itemSql += " and s.client_ip like ?"
countSql += " and s.client_ip like ?"
params = append(params, "%"+clientIp+"%")
}
if len(assetId) > 0 {
itemSql += " and s.asset_id = ?"
countSql += " and s.asset_id = ?"
params = append(params, assetId)
}
if len(protocol) > 0 {
itemSql += " and s.protocol = ?"
countSql += " and s.protocol = ?"
params = append(params, protocol)
}
params = append(params, (pageIndex-1)*pageSize, pageSize)
itemSql += " order by s.connected_time desc LIMIT ?, ?"
db.Raw(countSql, params...).Scan(&total)
err = db.Raw(itemSql, params...).Scan(&results).Error
if results == nil {
results = make([]SessionVo, 0)
}
return
}
func FindSessionByStatus(status string) (o []Session, err error) {
err = config.DB.Where("status = ?", status).Find(&o).Error
return
}
func CreateNewSession(o *Session) (err error) {
err = config.DB.Create(o).Error
return
}
func FindSessionById(id string) (o Session, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func FindSessionByConnectionId(connectionId string) (o Session, err error) {
err = config.DB.Where("connection_id = ?", connectionId).First(&o).Error
return
}
func UpdateSessionById(o *Session, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteSessionById(id string) {
config.DB.Where("id = ?", id).Delete(&Session{})
}
func DeleteSessionByStatus(status string) {
config.DB.Where("status = ?", status).Delete(&Session{})
}
func CountOnlineSession() (total int64, err error) {
err = config.DB.Where("status = ?", Connected).Find(&Session{}).Count(&total).Error
return
}

79
pkg/model/user.go Normal file
View File

@ -0,0 +1,79 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
"reflect"
)
type User struct {
ID string `gorm:"primary_key" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`
Created utils.JsonTime `json:"created"`
}
func (r *User) TableName() string {
return "users"
}
func (r *User) IsEmpty() bool {
return reflect.DeepEqual(r, User{})
}
func FindAllUser() (o []User) {
if config.DB.Find(&o).Error != nil {
return nil
}
return
}
func FindPageUser(pageIndex, pageSize int, username, nickname string) (o []User, total int64, err error) {
db := config.DB
if len(username) > 0 {
db = db.Where("username like ?", "%"+username+"%")
}
if len(nickname) > 0 {
db = db.Where("nickname like ?", "%"+nickname+"%")
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]User, 0)
}
return
}
func CreateNewUser(o *User) (err error) {
err = config.DB.Create(o).Error
return
}
func FindUserById(id string) (o User, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func FindUserByUsername(username string) (o User, err error) {
err = config.DB.Where("username = ?", username).First(&o).Error
return
}
func UpdateUserById(o *User, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteUserById(id string) {
config.DB.Where("id = ?", id).Delete(&User{})
}
func CountUser() (total int64, err error) {
err = config.DB.Find(&User{}).Count(&total).Error
return
}

81
pkg/utils/utils.go Normal file
View File

@ -0,0 +1,81 @@
package utils
import (
"database/sql/driver"
"fmt"
"github.com/gofrs/uuid"
"golang.org/x/crypto/bcrypt"
"net"
"strconv"
"time"
)
type JsonTime struct {
time.Time
}
func NewJsonTime(t time.Time) JsonTime {
return JsonTime{
Time: t,
}
}
func NowJsonTime() JsonTime {
return JsonTime{
Time: time.Now(),
}
}
func (t JsonTime) MarshalJSON() ([]byte, error) {
var stamp = fmt.Sprintf("\"%s\"", t.Format("2006-01-02 15:04:05"))
return []byte(stamp), nil
}
func (t JsonTime) Value() (driver.Value, error) {
var zeroTime time.Time
if t.Time.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return t.Time, nil
}
func (t *JsonTime) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = JsonTime{Time: value}
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
type Bcrypt struct {
cost int
}
func (b *Bcrypt) Encode(password []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(password, b.cost)
}
func (b *Bcrypt) Match(hashedPassword, password []byte) error {
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}
var Encoder = Bcrypt{
cost: bcrypt.DefaultCost,
}
func UUID() string {
v4, _ := uuid.NewV4()
return v4.String()
}
func Tcping(ip string, port int) bool {
var conn net.Conn
var err error
if conn, err = net.DialTimeout("tcp", ip+":"+strconv.Itoa(port), 2*time.Second); err != nil {
return false
}
defer conn.Close()
return true
}

2
web/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Project exclude paths
/node_modules/

339
web/LICENSE Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

4
web/README.md Normal file
View File

@ -0,0 +1,4 @@
# Next Terminal dashboard
just do go dashboard
## 主要功能

17686
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
web/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "next-terminal",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.3.0",
"antd": "^4.8.4",
"axios": "^0.19.2",
"guacamole-common-js": "^1.2.0",
"http-proxy-middleware": "^1.0.6",
"qs": "^6.9.4",
"react": "^16.14.0",
"react-contexify": "^4.1.1",
"react-dom": "^16.14.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0",
"typescript": "^3.9.7",
"xterm": "^4.9.0",
"xterm-addon-web-links": "^0.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"homepage": ".",
"devDependencies": {
"umi-request": "^1.3.5",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.4.0"
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

31
web/public/index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<title>Next Terminal</title>
</head>
<body style="background-color: darkgrey">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

1
web/public/logo.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1593355412276" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1259" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M272 122.24h480v157.056h96V0.448h-96L272 0c-52.8 0-96 0.448-96 0.448v278.848h96v-157.12z m403.648 603.392L896 512 675.648 298.368 608 364.48 760.128 512 608 659.52l67.648 66.112zM416 659.52L263.808 512 416 364.48l-67.712-66.112L128 512l220.288 213.632L416 659.52z m336 242.304H272v-157.12h-96V1024h672V744.704h-96v157.12z" fill="#ffffff" p-id="1260"></path></svg>

After

Width:  |  Height:  |  Size: 738 B

15
web/public/manifest.json Normal file
View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

95
web/src/App.css Normal file
View File

@ -0,0 +1,95 @@
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 32px;
margin: 16px;
text-align: center;
}
.logo > h1 {
color: white;
font-weight: bold;
line-height: 32px; /*设置line-height与父级元素的height相等*/
text-align: center; /*设置文本水平居中*/
display: inline-block;
}
.site-layout .site-layout-background {
background: #fff;
}
.site-page-header-ghost-wrapper {
background-color: #FFF;
}
.global-header {
position: relative;
display: flex;
align-items: center;
height: 48px;
padding: 0 16px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
}
.page-herder {
margin: 16px 16px 0 16px;
}
.page-search {
background-color: white;
margin: 16px 16px 0 16px;
padding: 16px;
}
.page-search label {
font-weight: bold;
}
.page-search .ant-form-item {
margin-bottom: 0;
}
.page-content {
margin: 16px;
padding: 24px;
min-height: 280px;
}
.page-card {
margin: 16px;
}
.user-in-menu {
align-items: center;
text-align: center;
margin: 10px auto;
color: white;
}
.user-in-menu>.nickname {
margin-top: 20px;
margin-right: auto;
margin-left: auto;
font-weight: bold;
padding: 2px 5px;
border-style: solid;
border-width: 1px;
border-color: white;
width: fit-content;
border-radius: 5%;
}
.monitor .ant-modal-body{
padding: 0;
}

241
web/src/App.js Normal file
View File

@ -0,0 +1,241 @@
import React, {Component} from 'react';
import 'antd/dist/antd.css';
import './App.css';
import {Divider, Layout} from "antd";
import {Switch, Route, Link} from "react-router-dom";
import {Menu} from 'antd';
import Dashboard from "./components/dashboard/Dashboard";
import Asset from "./components/asset/Asset";
import Access from "./components/access/Access";
import User from "./components/user/User";
import OnlineSession from "./components/session/OnlineSession";
import OfflineSession from "./components/session/OfflineSession";
import Login from "./components/Login";
import DynamicCommand from "./components/command/DynamicCommand";
import Credential from "./components/credential/Credential";
import {
DashboardOutlined,
UserOutlined,
IdcardOutlined,
CloudServerOutlined,
CodeOutlined,
BlockOutlined,
AuditOutlined,
DesktopOutlined,
DisconnectOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
SolutionOutlined,
SettingOutlined, LinkOutlined
} from '@ant-design/icons';
import Info from "./components/user/Info";
import request from "./common/request";
import {message} from "antd/es";
import Setting from "./components/setting/Setting";
import BatchCommand from "./components/command/BatchCommand";
const {Footer, Sider} = Layout;
const {SubMenu} = Menu;
class App extends Component {
state = {
collapsed: false,
current: sessionStorage.getItem('current'),
openKeys: sessionStorage.getItem('openKeys') ? JSON.parse(sessionStorage.getItem('openKeys')) : [],
user: {
'nickname': '未定义'
}
};
toggle = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
componentDidMount() {
this.getInfo().then(r => {
});
}
async getInfo() {
if ('/login' === window.location.pathname) {
return;
}
let result = await request.get('/info');
if (result.code === 1) {
this.setState({
user: result.data
})
} else {
message.error(result.message);
}
}
updateUser = (user) => {
this.setState({
user: user
})
}
setCurrent = (key) => {
this.setState({
current: key
})
sessionStorage.setItem('current', key);
}
subMenuChange = (openKeys) => {
console.debug("current open keys");
console.table(openKeys);
this.setState({
openKeys: openKeys
})
sessionStorage.setItem('openKeys', JSON.stringify(openKeys));
}
render() {
return (
<Switch>
<Route path="/access" component={Access}/>
<Route path="/login"><Login updateUser={this.updateUser}/></Route>
<Route path="/">
<Layout className="layout" style={{minHeight: '100vh'}}>
<Sider trigger={null} collapsible collapsed={this.state.collapsed} style={{width: 256}}>
<div className="logo">
<img src='logo.svg' alt='logo'/>
{
!this.state.collapsed ?
<>&nbsp;<h1>Next Terminal</h1></> :
null
}
</div>
<Divider/>
<Menu
onClick={(e) => this.setCurrent(e.key)}
selectedKeys={[this.state.current]}
onOpenChange={this.subMenuChange}
defaultOpenKeys={this.state.openKeys}
theme="dark" mode="inline" defaultSelectedKeys={['dashboard']}
inlineCollapsed={this.state.collapsed}
style={{lineHeight: '64px'}}>
<Menu.Item key="dashboard" icon={<DashboardOutlined/>}>
<Link to={'/dashboard'}>
控制面板
</Link>
</Menu.Item>
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</Link>
</Menu.Item>
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
<Link to={'/asset'}>
资产列表
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='command-manage' title='指令管理' icon={<CodeOutlined/>}>
<Menu.Item key="dynamic-command" icon={<BlockOutlined/>}>
<Link to={'/dynamic-command'}>
动态指令
</Link>
</Menu.Item>
{/*<Menu.Item key="silent-command" icon={<DeploymentUnitOutlined/>}>
<Link to={'/silent-command'}>
静默指令
</Link>
</Menu.Item>*/}
</SubMenu>
<SubMenu key='audit' title='操作审计' icon={<AuditOutlined/>}>
<Menu.Item key="online-session" icon={<LinkOutlined />}>
<Link to={'/online-session'}>
在线会话
</Link>
</Menu.Item>
<Menu.Item key="offline-session" icon={<DisconnectOutlined/>}>
<Link to={'/offline-session'}>
历史会话
</Link>
</Menu.Item>
</SubMenu>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
</Link>
</Menu.Item>
<Menu.Item key="info" icon={<SolutionOutlined/>}>
<Link to={'/info'}>
个人中心
</Link>
</Menu.Item>
<Menu.Item key="setting" icon={<SettingOutlined/>}>
<Link to={'/setting'}>
系统设置
</Link>
</Menu.Item>
</Menu>
<div>
{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.toggle,
})}
</div>
</Sider>
<Layout className="site-layout">
{/*<Header className="site-layout-background"
style={{padding: 0, height: 48, lineHeight: 48}}>
</Header>*/}
<Route path="/dashboard" component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/asset" component={Asset}/>
<Route path="/credential" component={Credential}/>
<Route path="/dynamic-command" component={DynamicCommand}/>
<Route path="/batch-command" component={BatchCommand}/>
<Route path="/online-session" component={OnlineSession}/>
<Route path="/offline-session" component={OfflineSession}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2020 Created by 杜世翔
</Footer>
</Layout>
</Layout>
</Route>
</Switch>
);
}
}
export default App;

9
web/src/App.test.js Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -0,0 +1,20 @@
// prod
// export const server = '';
// export const wsServer = '';
// export const prefix = '';
// dev
export const server = '//127.0.0.1:8088';
export const wsServer = 'ws://127.0.0.1:8088';
export const prefix = '';
// export const server = '//172.16.101.32:8080';
// export const wsServer = 'ws://172.16.101.32:8080';
// export const prefix = '';
export const PROTOCOL_COLORS = {
'rdp': 'red',
'ssh': 'blue',
'telnet': 'geekblue',
'vnc': 'purple'
}

115
web/src/common/request.js Normal file
View File

@ -0,0 +1,115 @@
import axios from 'axios'
import {prefix, server} from "./constants";
import {message} from 'antd';
import {getHeaders} from "../utils/utils";
// 测试地址
// axios.defaults.baseURL = server;
// 线上地址
axios.defaults.baseURL = server + prefix;
const handleError = (error) => {
if ("Network Error" === error.toString()) {
message.error('网络异常');
return;
}
if (error.response !== undefined && error.response.status === 403) {
window.location.href = '#/login';
return;
}
if (error.response !== undefined) {
message.error(error.response.data.message);
}
};
const handleResult = (result) => {
if (result['code'] === 403) {
window.location.href = '#/login';
}
}
const request = {
get: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.get(url, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
post: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.post(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
put: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.put(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
delete: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.delete(url, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
patch: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.patch(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
};
export default request

View File

@ -0,0 +1,21 @@
.login-form {
max-width: 300px;
min-width: 300px;
}
.login-form-forgot {
float: right;
}
.login-form-button {
width: 100%;
}
.login-card {
position: absolute;
left: 50%;
top: 50%;
margin-left: -175px;
margin-top: -189px;
}

View File

@ -0,0 +1,78 @@
import React, {Component} from 'react';
import {Button, Card, Checkbox, Form, Input} from "antd";
import './Login.css'
import request from "../common/request";
import {message} from "antd/es";
import {withRouter} from "react-router-dom";
import {
UserOutlined, LockOutlined
} from '@ant-design/icons';
class LoginForm extends Component {
state = {
inLogin: false
};
handleSubmit = async params => {
this.setState({
inLogin: true
});
try {
let result = await request.post('/login', params);
if (result.code !== 1) {
throw new Error(result.message);
}
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
localStorage.setItem('X-Auth-Token', result.data);
// this.props.history.push();
window.location.href = "/"
let r = await request.get('/info');
if (r.code === 1) {
this.props.updateUser(r.data);
} else {
message.error(r.message);
}
} catch (e) {
message.error(e.message);
} finally {
this.setState({
inLogin: false
});
}
};
render() {
return (
<Card className='login-card' title="登录">
<Form onFinish={this.handleSubmit} className="login-form">
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
<Input prefix={<UserOutlined/>} placeholder="登录账号"/>
</Form.Item>
<Form.Item name='password' rules={[{required: true, message: '请输入登录密码!'}]}>
<Input.Password prefix={<LockOutlined/>} placeholder="登录密码"/>
</Form.Item>
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
<Checkbox>记住登录</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button"
loading={this.state.inLogin}>
登录
</Button>
</Form.Item>
</Form>
</Card>
);
}
}
export default withRouter(LoginForm);

View File

@ -0,0 +1,3 @@
.container div {
margin: 0 auto;
}

View File

@ -0,0 +1,889 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {
Affix,
Alert,
Button,
Card,
Col,
Drawer,
Form,
Input,
message,
Modal,
Row,
Space,
Spin,
Tooltip,
Tree
} from 'antd'
import qs from "qs";
import request from "../../common/request";
import {prefix, server, wsServer} from "../../common/constants";
import {
CloudDownloadOutlined,
CloudUploadOutlined,
DeleteOutlined,
FolderAddOutlined,
LoadingOutlined,
ReloadOutlined,
UploadOutlined
} from '@ant-design/icons';
import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined";
import FolderOpenOutlined from "@ant-design/icons/lib/icons/FolderOpenOutlined";
import Upload from "antd/es/upload";
import {download, getToken} from "../../utils/utils";
import './Access.css'
const {TextArea} = Input;
const {DirectoryTree} = Tree;
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const antIcon = <LoadingOutlined/>;
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
class Access extends Component {
formRef = React.createRef()
state = {
sessionId: '',
client: {},
clipboardVisible: false,
clipboardText: '',
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
fileSystemVisible: false,
fileSystem: {
loading: false,
object: null,
currentDirectory: '/',
files: [],
},
uploadAction: '',
uploadHeaders: {},
keyboard: {},
protocol: '',
treeData: [],
selectNode: {},
confirmVisible: false,
confirmLoading: false,
uploadVisible: false,
uploadLoading: false,
};
async componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
let assetsId = params.get('assetsId');
let protocol = params.get('protocol');
let sessionId = await this.createSession(assetsId);
this.setState({
sessionId: sessionId,
protocol: protocol
});
this.renderDisplay(sessionId, protocol);
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('onfocus', this.onWindowFocus);
}
componentWillUnmount() {
if (this.state.client) {
this.state.client.disconnect();
}
window.removeEventListener('resize', this.onWindowResize);
document.removeEventListener("onfocus", this.onWindowFocus);
}
sendClipboard(data) {
let writer;
// Create stream with proper mimetype
const stream = this.state.client.createClipboardStream(data.type);
// Send data as a string if it is stored as a string
if (typeof data.data === 'string') {
writer = new Guacamole.StringWriter(stream);
writer.sendText(data.data);
writer.sendEnd();
} else {
// Write File/Blob asynchronously
writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
// Begin sending data
writer.sendBlob(data.data);
}
this.setState({
clipboardText: data.data
})
if (this.state.protocol === 'ssh') {
message.success('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。');
} else {
message.success('您输入的内容已复制到远程服务器上');
}
}
onTunnelStateChange = (state) => {
console.log('onTunnelStateChange', state);
};
updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/content`);
if (result.code !== 1) {
message.error(result.message);
}
}
onClientStateChange = (state) => {
switch (state) {
case STATE_IDLE:
console.log('初始化');
message.destroy();
message.loading('正在初始化中...', 0);
break;
case STATE_CONNECTING:
console.log('正在连接...');
message.destroy();
message.loading('正在努力连接中...', 0);
break;
case STATE_WAITING:
console.log('正在等待...');
message.destroy();
message.loading('正在等待服务器响应...', 0);
break;
case STATE_CONNECTED:
console.log('连接成功。');
this.onWindowResize(null);
message.destroy();
message.success('连接成功');
// 向后台发送请求,更新会话的状态
this.updateSessionStatus(this.state.sessionId).then(_ => {
})
break;
case STATE_DISCONNECTING:
console.log('连接正在关闭中...');
message.destroy();
message.loading('正在关闭连接...', 0);
break;
case STATE_DISCONNECTED:
console.log('连接关闭。');
message.destroy();
message.error('连接关闭');
break;
default:
break;
}
};
onError = (status) => {
console.log('通道异常。', status);
switch (status.code) {
case 256:
this.showMessage('未支持的访问');
break;
case 512:
this.showMessage('远程服务异常');
break;
case 513:
this.showMessage('服务器忙碌');
break;
case 514:
this.showMessage('服务器连接超时');
break;
case 515:
this.showMessage('远程服务异常');
break;
case 516:
this.showMessage('资源未找到');
break;
case 517:
this.showMessage('资源冲突');
break;
case 518:
this.showMessage('资源已关闭');
break;
case 519:
this.showMessage('远程服务未找到');
break;
case 520:
this.showMessage('远程服务不可用');
break;
case 521:
this.showMessage('会话冲突');
break;
case 522:
this.showMessage('会话连接超时');
break;
case 523:
this.showMessage('会话已关闭');
break;
case 768:
this.showMessage('网络不可达');
break;
case 769:
this.showMessage('服务器密码验证失败');
break;
case 771:
this.showMessage('客户端被禁止');
break;
case 776:
this.showMessage('客户端连接超时');
break;
case 781:
this.showMessage('客户端异常');
break;
case 783:
this.showMessage('错误的请求类型');
break;
case 797:
this.showMessage('客户端连接数量过多');
break;
default:
this.showMessage('未知错误。');
}
};
showMessage(message) {
Modal.error({
title: '提示',
content: message,
});
}
clientClipboardReceived = (stream, mimetype) => {
let reader;
// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {
reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
let data = '';
reader.ontext = function textReceived(text) {
data += text;
};
// Set clipboard contents once stream is finished
reader.onend = async () => {
message.success('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。');
this.setState({
clipboardText: data
});
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
}
};
}
// Otherwise read the clipboard data as a Blob
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
this.setState({
clipboardText: reader.getBlob()
})
}
}
};
uploadChange = (info) => {
if (info.file.status !== 'uploading') {
}
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功。`, 3);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败。`, 10);
}
}
onKeyDown = (keysym) => {
if (true === this.state.clipboardVisible || true === this.state.confirmVisible) {
return true;
}
this.state.client.sendKeyEvent(1, keysym);
if (keysym === 65288) {
return false;
}
};
onKeyUp = (keysym) => {
this.state.client.sendKeyEvent(0, keysym);
};
showFileSystem = () => {
this.setState({
fileSystemVisible: true,
});
this.loadDirData('/');
};
hideFileSystem = () => {
this.setState({
fileSystemVisible: false,
});
};
showClipboard = () => {
this.setState({
clipboardVisible: true
}, () => {
let element = document.getElementById('clipboard');
if (element) {
element.value = this.state.clipboardText;
}
});
};
hideClipboard = () => {
this.setState({
clipboardVisible: false
});
};
updateClipboardFormTextarea = () => {
let clipboardText = document.getElementById('clipboard').value;
this.setState({
clipboardText: clipboardText
});
this.sendClipboard({
'data': clipboardText,
'type': 'text/plain'
});
};
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
return result.data['id'];
}
async renderDisplay(sessionId, protocol) {
let tunnel = new Guacamole.WebSocketTunnel(wsServer + prefix + '/tunnel');
tunnel.onstatechange = this.onTunnelStateChange;
// Get new client instance
let client = new Guacamole.Client(tunnel);
// 设置虚拟机剪贴板内容
client.sendClipboard = this.sendClipboard;
// 处理从虚拟机收到的剪贴板内容
client.onclipboard = this.clientClipboardReceived;
// 处理客户端的状态变化事件
client.onstatechange = this.onClientStateChange;
client.onerror = this.onError;
// Get display div from document
const display = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
display.appendChild(element);
let width = window.innerWidth;
let height = window.innerHeight;
let token = getToken();
let params = {
'sessionId': sessionId,
'width': width,
'height': height,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
// Connect
client.connect(paramStr);
// Disconnect on close
window.onunload = function () {
client.disconnect();
};
// Mouse
const mouse = new Guacamole.Mouse(element);
mouse.onmousedown = mouse.onmouseup = function (mouseState) {
client.sendMouseState(mouseState);
};
mouse.onmousemove = function (mouseState) {
if (protocol === 'ssh' || protocol === 'telnet') {
mouseState.x = mouseState.x * 2;
mouseState.y = mouseState.y * 2;
client.sendMouseState(mouseState);
} else {
client.sendMouseState(mouseState);
}
};
// Keyboard
const keyboard = new Guacamole.Keyboard(document);
keyboard.onkeydown = this.onKeyDown;
keyboard.onkeyup = this.onKeyUp;
this.setState({
client: client,
containerWidth: width,
containerHeight: height,
keyboard: keyboard
});
}
onWindowResize = (e) => {
if (this.state.client) {
const display = this.state.client.getDisplay();
const width = window.innerWidth;
const height = window.innerHeight;
if (this.state.protocol === 'ssh' || this.state.protocol === 'telnet') {
let r = 2;
display.scale(1 / r);
this.state.client.sendSize(width * r, height * r);
} else {
this.state.client.sendSize(width, height);
}
this.setState({
containerWidth: width,
containerHeight: height,
})
this.resize(this.state.sessionId, width, height).then(_ => {
});
}
};
resize = async (sessionId, width, height) => {
let result = await request.post(`/sessions/${sessionId}/resize?width=${width}&height=${height}`);
if (result.code !== 1) {
message.error(result.message);
}
}
onWindowFocus = (e) => {
if (navigator.clipboard) {
navigator.clipboard.readText().then((text) => {
this.sendClipboard({
'data': text,
'type': 'text/plain'
});
})
}
};
onPaste = (e) => {
const cbd = e.clipboardData;
const ua = window.navigator.userAgent;
// 如果是 Safari 直接 return
if (!(e.clipboardData && e.clipboardData.items)) {
return;
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if (cbd.items && cbd.items.length === 2 && cbd.items[0].kind === "string" && cbd.items[1].kind === "file" &&
cbd.types && cbd.types.length === 2 && cbd.types[0] === "text/plain" && cbd.types[1] === "Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49) {
return;
}
for (let i = 0; i < cbd.items.length; i++) {
let item = cbd.items[i];
if (item.kind === "file") {
let blob = item.getAsFile();
if (blob.size === 0) {
return;
}
// blob 就是从剪切板获得的文件 可以进行上传或其他操作
} else if (item.kind === 'string') {
item.getAsString((str) => {
this.sendClipboard({
'data': str,
'type': 'text/plain'
});
})
}
}
};
onSelect = (keys, event) => {
this.setState({
selectNode: {
key: keys[0],
isLeaf: event.node.isLeaf
}
})
};
handleOk = async (values) => {
let params = {
'dir': this.state.selectNode.key + '/' + values['dir']
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/sessions/${this.state.sessionId}/mkdir?${paramStr}`);
if (result.code === 1) {
message.success('创建成功');
let parentPath = this.state.selectNode.key;
let items = await this.getTreeNodes(parentPath);
this.setState({
treeData: this.updateTreeData(this.state.treeData, parentPath, items),
selectNode: {}
});
} else {
message.error(result.message);
}
this.setState({
confirmLoading: false,
confirmVisible: false
})
}
handleConfirmCancel = () => {
this.setState({
confirmVisible: false
})
}
handleUploadCancel = () => {
this.setState({
uploadVisible: false
})
}
mkdir = () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
message.warning('请选择一个目录');
return;
}
this.setState({
confirmVisible: true
})
}
upload = () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
message.warning('请选择一个目录进行上传');
return;
}
this.setState({
uploadVisible: true
})
}
download = () => {
if (!this.state.selectNode.key || !this.state.selectNode.isLeaf) {
message.warning('当前只支持下载文件');
return;
}
download(`${server}${prefix}/sessions/${this.state.sessionId}/download?file=${this.state.selectNode.key}`);
}
rmdir = async () => {
if (!this.state.selectNode.key) {
message.warning('请选择一个文件或目录');
return;
}
let result;
if (this.state.selectNode.isLeaf) {
result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectNode.key}`);
} else {
result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectNode.key}`);
}
if (result.code !== 1) {
message.error(result.message);
} else {
message.success('删除成功');
let path = this.state.selectNode.key;
let parentPath = path.substring(0, path.lastIndexOf('/'));
let items = await this.getTreeNodes(parentPath);
this.setState({
treeData: this.updateTreeData(this.state.treeData, parentPath, items),
selectNode: {}
});
}
}
refresh = async () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
await this.loadDirData('/');
} else {
let key = this.state.selectNode.key;
let items = await this.getTreeNodes(key);
this.setState({
treeData: this.updateTreeData(this.state.treeData, key, items),
});
}
message.success('刷新目录成功');
}
onRightClick = ({event, node}) => {
};
loadDirData = async (key) => {
let items = await this.getTreeNodes(key);
this.setState({
treeData: items,
});
}
getTreeNodes = async (key) => {
const url = server + prefix + '/sessions/' + this.state.sessionId + '/ls?dir=' + key;
let result = await request.get(url);
if (result.code !== 1) {
return [];
}
let data = result.data;
data = data.sort(((a, b) => a.name.localeCompare(b.name)));
return data.map(item => {
return {
title: item['name'],
key: item['path'],
isLeaf: !item['isDir'] && !item['isLink'],
}
});
}
onLoadData = ({key, children}) => {
return new Promise(async (resolve) => {
if (children) {
resolve();
return;
}
let items = await this.getTreeNodes(key);
this.setState({
treeData: this.updateTreeData(this.state.treeData, key, items),
});
resolve();
});
}
updateTreeData = (list, key, children) => {
return list.map((node) => {
if (node.key === key) {
return {...node, children};
} else if (node.children) {
return {...node, children: this.updateTreeData(node.children, key, children)};
}
return node;
});
}
render() {
const title = (
<Row>
<Space>
远程文件管理
&nbsp;
&nbsp;
<Tooltip title="创建文件夹">
<Button type="primary" size="small" icon={<FolderAddOutlined/>}
onClick={this.mkdir} ghost/>
</Tooltip>
<Tooltip title="上传">
<Button type="primary" size="small" icon={<CloudUploadOutlined/>}
onClick={this.upload} ghost/>
</Tooltip>
<Tooltip title="下载">
<Button type="primary" size="small" icon={<CloudDownloadOutlined/>}
onClick={this.download} ghost/>
</Tooltip>
<Tooltip title="删除文件">
<Button type="dashed" size="small" icon={<DeleteOutlined/>} onClick={this.rmdir}
danger/>
</Tooltip>
<Tooltip title="刷新">
<Button type="primary" size="small" icon={<ReloadOutlined/>} onClick={this.refresh}
ghost/>
</Tooltip>
</Space>
</Row>
);
return (
<div>
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight
}}>
<div id="display"/>
</div>
<Modal
title="创建文件夹"
visible={this.state.confirmVisible}
onOk={() => {
this.formRef.current
.validateFields()
.then(values => {
this.formRef.current.resetFields();
this.handleOk(values);
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={this.handleConfirmCancel}
>
<Form ref={this.formRef} {...formItemLayout}>
<Form.Item label="文件夹名称" name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal>
<Modal
title="上传文件"
visible={this.state.uploadVisible}
onOk={() => {
}}
confirmLoading={this.state.uploadLoading}
onCancel={this.handleUploadCancel}
>
<Upload
action={server + prefix + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.selectNode.key}>
<Button icon={<UploadOutlined/>}>上传文件</Button>
</Upload>
</Modal>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button
icon={<CopyOutlined/>}
onClick={() => {
this.showClipboard();
}}
>
</Button>
</Affix>
<Affix style={{position: 'absolute', top: 50, right: 50}}>
<Button
icon={<FolderOpenOutlined/>}
onClick={() => {
this.showFileSystem();
}}
>
</Button>
</Affix>
<Drawer
title={title}
placement="right"
width={window.innerWidth * 0.3}
closable={true}
maskClosable={false}
onClose={this.hideFileSystem}
visible={this.state.fileSystemVisible}
>
<Row style={{marginTop: 10}}>
<Col span={24}>
<Card title={this.state.fileSystem.currentDirectory} bordered={true} size="small">
<Spin indicator={antIcon} spinning={this.state.fileSystem.loading}>
<DirectoryTree
// multiple
onSelect={this.onSelect}
loadData={this.onLoadData}
treeData={this.state.treeData}
onRightClick={this.onRightClick}
/>
</Spin>
</Card>
</Col>
</Row>
</Drawer>
<Drawer
title="剪贴板"
placement="right"
width={window.innerWidth * 0.3}
onClose={this.hideClipboard}
visible={this.state.clipboardVisible}
>
<Alert message="复制/剪切的文本将出现在这里。对下面文本内容所作的修改将会影响远程电脑上的剪贴板。" type="info" showIcon closable/>
<div style={{marginTop: 10, marginBottom: 10}}>
<TextArea id='clipboard' rows={10} onBlur={this.updateClipboardFormTextarea}/>
</div>
</Drawer>
</div>
);
}
}
export default Access;

View File

@ -0,0 +1,3 @@
.console-card .ant-card-body {
padding: 0;
}

View File

@ -0,0 +1,110 @@
import React, {Component} from 'react';
import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import {AttachAddon} from 'xterm-addon-attach';
import qs from "qs";
import {prefix, wsServer} from "../../common/constants";
import "./Console.css"
import {getToken} from "../../utils/utils";
function getGeometry(width, height) {
const cols = Math.floor(width / 9);
const rows = Math.floor(height / 17);
return [cols, rows];
}
class Console extends Component {
state = {
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
term: null,
};
componentDidMount() {
let command = this.props.command;
let assetId = this.props.assetId;
let width = this.props.width;
let height = this.props.height;
// let width = Math.floor(window.innerWidth * scale);
// let height = Math.floor(window.innerHeight * scale);
let params = {
'width': width,
'height': height,
'assetId': assetId
};
let paramStr = qs.stringify(params);
let [cols, rows] = getGeometry(width, height);
let term = new Terminal({
cols: cols,
rows: rows,
// screenKeys: true,
// fontFamily: 'menlo',
});
// let fitAddon = new FitAddon();
// term.loadAddon(fitAddon);
term.open(this.refs.terminal);
// fitAddon.fit();
term.writeln('正在努力连接服务器中...');
term.onResize(e => {
});
let token = getToken();
let webSocket = new WebSocket(wsServer + prefix + '/ssh?X-Auth-Token=' + token + '&' + paramStr);
term.loadAddon(new AttachAddon(webSocket));
this.props.appendWebsocket(webSocket);
webSocket.onopen = (e => {
term.clear();
term.focus();
if (command !== '') {
webSocket.send(command + String.fromCharCode(13));
}
});
this.setState({
term: term,
containerWidth: width,
containerHeight: height
});
// window.addEventListener('resize', this.onWindowResize);
}
onWindowResize = (e) => {
let term = this.state.term;
if (term) {
const [cols, rows] = getGeometry(this.state.containerWidth, this.state.containerHeight);
term.resize(cols, rows);
}
};
render() {
return (
<div>
<div ref='terminal' style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight,
backgroundColor: 'black'
}}/>
</div>
);
}
}
export default Console;

View File

@ -0,0 +1,237 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {message, Modal} from 'antd'
import qs from "qs";
import {prefix, wsServer} from "../../common/constants";
import {LoadingOutlined} from '@ant-design/icons';
import {getToken} from "../../utils/utils";
import './Access.css'
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const antIcon = <LoadingOutlined/>;
class Access extends Component {
formRef = React.createRef()
state = {
client: {},
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
rate: 1
};
async componentDidMount() {
const connectionId = this.props.connectionId;
let rate = this.props.rate;
let protocol = this.props.protocol;
let width = this.props.width;
let height = this.props.height;
if (protocol === 'ssh' || protocol === 'telnet') {
rate = rate * 0.5;
width = width * 2;
height = height * 2;
}
this.setState({
containerWidth: width * rate,
containerHeight: height * rate,
rate: rate,
})
this.renderDisplay(connectionId);
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('onfocus', this.onWindowFocus);
}
componentWillUnmount() {
if (this.state.client) {
this.state.client.disconnect();
}
window.removeEventListener('resize', this.onWindowResize);
document.removeEventListener("onfocus", this.onWindowFocus);
}
onTunnelStateChange = (state) => {
console.log('onTunnelStateChange', state);
};
onClientStateChange = (state) => {
switch (state) {
case STATE_IDLE:
console.log('初始化');
message.destroy();
message.loading('正在初始化中...', 0);
break;
case STATE_CONNECTING:
console.log('正在连接...');
message.destroy();
message.loading('正在努力连接中...', 0);
break;
case STATE_WAITING:
console.log('正在等待...');
message.destroy();
message.loading('正在等待服务器响应...', 0);
break;
case STATE_CONNECTED:
console.log('连接成功。');
message.destroy();
message.success('连接成功');
if (this.state.client) {
this.state.client.getDisplay().scale(this.state.rate);
}
break;
case STATE_DISCONNECTING:
console.log('连接正在关闭中...');
message.destroy();
break;
case STATE_DISCONNECTED:
console.log('连接关闭。');
message.destroy();
break;
default:
break;
}
};
onError = (status) => {
console.log('通道异常。', status);
switch (status.code) {
case 256:
this.showMessage('未支持的访问');
break;
case 512:
this.showMessage('远程服务异常');
break;
case 513:
this.showMessage('服务器忙碌');
break;
case 514:
this.showMessage('服务器连接超时');
break;
case 515:
this.showMessage('远程服务异常');
break;
case 516:
this.showMessage('资源未找到');
break;
case 517:
this.showMessage('资源冲突');
break;
case 518:
this.showMessage('资源已关闭');
break;
case 519:
this.showMessage('远程服务未找到');
break;
case 520:
this.showMessage('远程服务不可用');
break;
case 521:
this.showMessage('会话冲突');
break;
case 522:
this.showMessage('会话连接超时');
break;
case 523:
this.showMessage('会话已关闭');
break;
case 768:
this.showMessage('网络不可达');
break;
case 769:
this.showMessage('服务器密码验证失败');
break;
case 771:
this.showMessage('客户端被禁止');
break;
case 776:
this.showMessage('客户端连接超时');
break;
case 781:
this.showMessage('客户端异常');
break;
case 783:
this.showMessage('错误的请求类型');
break;
case 797:
this.showMessage('客户端连接数量过多');
break;
default:
this.showMessage('未知错误。');
}
};
showMessage(message) {
Modal.error({
title: '提示',
content: message,
});
}
async renderDisplay(connectionId, protocol) {
let tunnel = new Guacamole.WebSocketTunnel(wsServer + prefix + '/tunnel');
tunnel.onstatechange = this.onTunnelStateChange;
let client = new Guacamole.Client(tunnel);
// 处理客户端的状态变化事件
client.onstatechange = this.onClientStateChange;
client.onerror = this.onError;
const display = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
display.appendChild(element);
let token = getToken();
let params = {
'connectionId': connectionId,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
// Connect
client.connect(paramStr);
// Disconnect on close
window.onunload = function () {
client.disconnect();
};
this.setState({
client: client
})
}
render() {
return (
<div>
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight
}}>
<div id="display"/>
</div>
</div>
);
}
}
export default Access;

View File

@ -0,0 +1,512 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import AssetModal from "./AssetModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
CodeTwoTone,
CopyTwoTone,
DeleteOutlined,
DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import {PROTOCOL_COLORS} from "../../common/constants";
const confirm = Modal.confirm;
const {Search} = Input;
const {Content} = Layout;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'assets',
breadcrumbName: '资产管理',
}
];
class Asset extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: ''
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
credentials: [],
model: {},
selectedRowKeys: [],
delBtnLoading: false,
};
async componentDidMount() {
await this.loadTableData();
}
async delete(id) {
const result = await request.delete('/assets/' + id);
if (result['code'] === 1) {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/assets/paging?' + paramsStr);
if (result['code'] === 1) {
data = result['data'];
} else {
message.error(result['message']);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此资产吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
async update(id) {
let result = await request.get(`/assets/${id}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
await this.showModal('更新资产', result.data);
}
async copy(id) {
let result = await request.get(`/assets/${id}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
result.data['id'] = undefined;
await this.showModal('复制资产', result.data);
}
async showModal(title, assets = {}) {
let result = await request.get('/credentials');
let credentials = [];
if (result.code === 1) {
credentials = result.data;
}
this.setState({
modalTitle: title,
modalVisible: true,
credentials: credentials,
model: assets
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/assets/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/assets', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
access = async (id, protocol) => {
message.loading({content: '正在检测资产是否在线...', key: id});
let result = await request.post(`/assets/${id}/tcping`);
if (result.code === 1) {
if (result.data === true) {
message.success({content: '检测完成,您访问的资产在线,即将打开窗口进行访问。', key: id, duration: 3});
window.open(`#/access?assetsId=${id}&protocol=${protocol}`);
} else {
message.warn('您访问的资产未在线,请确认网络状态。', 10);
}
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/assets/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '资产名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: 'IP',
dataIndex: 'ip',
key: 'ip',
}, {
title: '端口',
dataIndex: 'port',
key: 'port',
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '状态',
dataIndex: 'active',
key: 'active',
render: text => {
if (text) {
return (<Badge status="processing" text="运行中"/>);
} else {
return (<Badge status="error" text="不可用"/>);
}
}
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created'
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<CodeTwoTone/>}
onClick={() => this.access(record.id, record.protocol)}>接入</Button>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.update(record.id)}>编辑</Button>
<Button type="link" size='small' icon={<CopyTwoTone/>}
onClick={() => this.copy(record.id)}>复制</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="资产管理"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="资产"
>
</PageHeader>
<Content key='page-content' className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>资产列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="资产名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Select onChange={this.handleSearchByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增资产', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table key='assets-table'
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<AssetModal
modalFormRef={this.modalFormRef}
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
credentials={this.state.credentials}
model={this.state.model}
/>
: null
}
</Content>
</>
);
}
}
export default Asset;

View File

@ -0,0 +1,157 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, InputNumber, Modal, Radio, Select, Tooltip} from "antd/lib/index";
const {TextArea} = Input;
const {Option} = Select;
// 子级页面
// Ant form create 表单内置方法
const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoading, credentials, model}) {
const [form] = Form.useForm();
let [accountType, setAccountType] = useState(model.accountType);
useEffect(() => {
setAccountType(model.accountType);
});
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const handleProtocolChange = e => {
let port;
switch (e.target.value) {
case 'ssh':
port = 22;
break;
case 'rdp':
port = 3389;
break;
case 'vnc':
port = 5901;
form.setFieldsValue({
accountType: 'custom',
});
break;
case 'telnet':
port = 23;
break;
default:
port = 65535;
}
form.setFieldsValue({
port: port,
});
};
return (
<Modal
title={title}
visible={visible}
maskClosable={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="资产名称" name='name' rules={[{required: true, message: "请输入资产名称"}]}>
<Input placeholder="请输入资产名称"/>
</Form.Item>
<Form.Item label="IP" name='ip' rules={[{required: true, message: '请输入资产IP'}]}>
<Input placeholder="请输入资产IP"/>
</Form.Item>
<Form.Item label="接入协议" name='protocol' rules={[{required: true, message: '请选择接入协议'}]}>
<Radio.Group onChange={handleProtocolChange}>
<Radio value="rdp">rdp</Radio>
<Radio value="ssh">ssh</Radio>
<Radio value="vnc">vnc</Radio>
<Radio value="telnet">telnet</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="端口" name='port' rules={[{required: true, message: '请输入资产端口'}]}>
<InputNumber min={1} max={65535}/>
</Form.Item>
<Form.Item label="账户类型" name='accountType' rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={(v) => {
setAccountType(v);
model.accountType = v;
}}>
<Option value="custom">自定义</Option>
<Option value="credential">授权凭证</Option>
<Option value="secret-key">密钥</Option>
</Select>
</Form.Item>
{
accountType === 'credential' ?
<Form.Item label="授权凭证" name='credentialId' rules={[{required: true, message: '请选择授权凭证'}]}>
<Select onChange={() => null}>
{credentials.map(item => {
return (
<Option key={item.id} value={item.id}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
: null
}
{
accountType === 'custom' ?
<>
<Form.Item label="授权账户" name='username' rules={[{required: true, message: '请输入授权账户'}]}
noStyle={!(accountType === 'custom')}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password' rules={[{required: true, message: '请输入授权密码'}]}
noStyle={!(accountType === 'custom')}>
<Input placeholder="输入授权密码"/>
</Form.Item>
</>
: null
}
{
accountType === 'secret-key' ?
<Form.Item label="私钥" name='passphrase' rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
: null
}
</Form>
</Modal>
)
}
export default AssetModal;

View File

@ -0,0 +1,92 @@
import React, {Component} from 'react';
import {List, Card, Input, PageHeader} from "antd";
import Console from "../access/Console";
import {itemRender} from "../../utils/utils";
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: '/dynamic-command',
breadcrumbName: '动态指令',
},
{
path: '/batch-command',
breadcrumbName: '批量执行命令',
}
];
class BatchCommand extends Component {
commandRef = React.createRef();
state = {
webSockets: [],
assets: []
}
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
let command = params.get('command');
let assets = JSON.parse(params.get('assets'));
this.setState({
command: command,
assets: assets
})
}
onPaneChange = activeKey => {
this.setState({activeKey});
};
appendWebsocket = (webSocket) => {
this.state.webSockets.push(webSocket);
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="批量执行命令"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="动态指令"
>
</PageHeader>
<div className="page-search">
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
for (let i = 0; i < this.state.webSockets.length; i++) {
this.state.webSockets[i].send(value + String.fromCharCode(13))
}
this.commandRef.current.setValue('');
}} enterButton='执行'/>
</div>
<div className="page-card">
<List
grid={{gutter: 16, column: 2}}
dataSource={this.state.assets}
renderItem={item => (
<List.Item>
<Card title={item.name}>
<Console assetId={item.id} command={this.state.command}
width={(window.innerWidth - 350) / 2}
height={400}
appendWebsocket={this.appendWebsocket}/>
</Card>
</List.Item>
)}
/>
</div>
</>
);
}
}
export default BatchCommand;

View File

@ -0,0 +1,501 @@
import React, {Component} from 'react';
import {
Button,
Checkbox,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import DynamicCommandModal from "./DynamicCommandModal";
import {
CodeTwoTone,
DeleteOutlined,
DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'command',
breadcrumbName: '动态指令',
}
];
class DynamicCommand extends Component {
inputRefOfName = React.createRef();
inputRefOfContent = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
assetsVisible: false,
assets: [],
checkedAssets: [],
command: '',
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/commands/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/commands/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByContent = content => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'content': content,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此指令吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, assets = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: assets
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleChecked = e => {
let checkedAssets = this.state.checkedAssets;
if (e.target.checked) {
checkedAssets.push(e.target.value);
} else {
for (let i = 0; i < checkedAssets.length; i++) {
if (checkedAssets[i].id === e.target.value.id) {
checkedAssets.splice(i, 1);
}
}
}
this.setState({
checkedAssets: checkedAssets
});
};
executeCommand = e => {
let checkedAssets = this.state.checkedAssets;
if (checkedAssets.length === 0) {
message.warning('请至少选择一个资产');
return;
}
let assets = checkedAssets.map(item => {
return {
id: item.id,
name: item.name
}
});
window.location.href = '#/batch-command?command=' + this.state.command + '&assets=' + JSON.stringify(assets);
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/commands/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/commands', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/commands/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '指令名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: '指令内容',
dataIndex: 'content',
key: 'content',
}, {
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
<Button type="link" size='small' icon={<CodeTwoTone/>} onClick={async () => {
this.setState({
assetsVisible: true,
command: record.content
});
let result = await request.get('/assets?protocol=ssh');
if (result.code === 1) {
this.setState({
assets: result.data
});
} else {
message.error(result.message);
}
}}>执行</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="动态指令"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="批量动态指令执行"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>指令列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="指令名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Search
ref={this.inputRefOfContent}
placeholder="指令内容"
allowClear
onSearch={this.handleSearchByContent}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfContent.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增指令', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<DynamicCommandModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</DynamicCommandModal>
: null
}
<Modal
title="选择资产"
visible={this.state.assetsVisible}
onOk={this.executeCommand}
onCancel={() => {
this.setState({
assetsVisible: false
});
}}
>
{this.state.assets.map(item => {
return (<Checkbox key={item.id} value={item}
onChange={this.handleChecked}>{item.name}</Checkbox>);
})}
</Modal>
</Content>
</>
);
}
}
export default DynamicCommand;

View File

@ -0,0 +1,59 @@
import React from 'react';
import {Form, Input, Modal} from "antd/lib/index";
const {TextArea} = Input;
// 子级页面
// Ant form create 表单内置方法
const DynamicCommandModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 18},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="指令名称" name='name' rules={[{required: true, message: '请输入指令名称'}]}>
<Input placeholder="请输入指令内容"/>
</Form.Item>
<Form.Item label="指令内容" name='content' rules={[{required: true, message: '请输入指令内容'}]}>
<TextArea autoSize={{ minRows: 5, maxRows: 10 }} placeholder="一行一个命令"/>
</Form.Item>
</Form>
</Modal>
)
};
export default DynamicCommandModal;

View File

@ -0,0 +1,400 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {Button, Col, Divider, Input, Layout, Modal, PageHeader, Row, Space, Table, Tooltip, Typography} from "antd";
import qs from "qs";
import CredentialModal from "./CredentialModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'credentials',
breadcrumbName: '授权凭证',
}
];
class Credential extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/credentials/' + id);
if (result.code === 1) {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/credentials/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此记录吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id).then(r => {});
}
});
};
showModal(title, idcard = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: idcard
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/credentials/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/credentials', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/credentials/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '凭证名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
}, {
title: '授权密码',
dataIndex: 'password',
key: 'password',
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>} onClick={() => this.showModal('更新凭证', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone />} onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="授权凭证"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="访问资产的账户、密钥等"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>授权凭证列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="凭证名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增凭证', null)}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
rowKey='id'
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<CredentialModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</CredentialModal>
: null
}
</Content>
</>
);
}
}
export default Credential;

View File

@ -0,0 +1,59 @@
import React from 'react';
import {Form, Input, Modal} from "antd/lib/index";
const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading,model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="凭证名称" name='name' rules={[{required: true, message: '请输入凭证名称'}]}>
<Input placeholder="请输入凭证名称"/>
</Form.Item>
<Form.Item label="授权账户" name='username' rules={[{required: true, message: '请输入授权账户'}]}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password' rules={[{required: true, message: '请输入授权密码',}]}>
<Input placeholder="输入授权密码"/>
</Form.Item>
</Form>
</Modal>
)
};
export default CredentialModal;

View File

@ -0,0 +1,4 @@
.text-center{
width: 100px;
text-align: center;
}

View File

@ -0,0 +1,196 @@
import React, {Component} from 'react';
import {Layout, PageHeader, Card, Row, Col, Progress, Typography, Popover, Statistic} from "antd";
import {DesktopOutlined, IdcardOutlined, LikeOutlined, LinkOutlined, UserOutlined} from '@ant-design/icons';
import {itemRender} from '../../utils/utils'
import request from "../../common/request";
import './Dashboard.css'
import {Link} from "react-router-dom";
const {Content} = Layout;
const {Title, Paragraph} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'dashboard',
breadcrumbName: '仪表盘',
}
];
class Dashboard extends Component {
state = {
status: {
load: {
load1: 0,
load5: 0,
load15: 0,
},
cpu: {
percent: 0,
logicalCount: 0,
physicalCount: 0
},
memory: {
usedPercent: 0,
available: 0,
total: 0,
used: 0
}
},
interval: null,
counter: {}
}
componentDidMount() {
this.getCounter();
this.getStatus();
this.setState({
interval: setInterval(() => this.getStatus(), 5000)
})
}
componentWillUnmount() {
if (this.state.interval != null) {
clearInterval(this.state.interval);
}
}
getStatus = async () => {
let result = await request.get('/overview/status');
if (result.code === 1) {
this.setState({
status: result.data
})
}
}
getCounter = async () => {
let result = await request.get('/overview/counter');
if (result.code === 1) {
this.setState({
counter: result.data
})
}
}
render() {
const loadContent = (
<div>
<p>最近1分钟负载{this.state.status.load['load1'].toFixed(1)}</p>
<p>最近5分钟负载{this.state.status.load['load5'].toFixed(1)}</p>
<p>最近15分钟负载{this.state.status.load['load15'].toFixed(1)}</p>
</div>
);
const cpuContent = (
<div>
<p>CPU型号{this.state.status.cpu['modelName']}</p>
<p>物理核心{this.state.status.cpu['physicalCount']}</p>
<p>逻辑核心{this.state.status.cpu['logicalCount']}</p>
</div>
);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="dashboard"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="仪表盘"
>
</PageHeader>
<div className="page-card">
<Row gutter={16}>
<Col span={6}>
<Card bordered={true}>
<Link to={'/user'}>
<Statistic title="在线用户" value={this.state.counter['user']}
prefix={<UserOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/asset'}>
<Statistic title="存活资产" value={this.state.counter['asset']}
prefix={<DesktopOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/credential'}>
<Statistic title="授权凭证" value={this.state.counter['credential']}
prefix={<IdcardOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/online-session'}>
<Statistic title="在线会话" value={this.state.counter['onlineSession']}
prefix={<LinkOutlined/>}/>
</Link>
</Card>
</Col>
</Row>
</div>
<div className="page-card">
<Card title="状态" bordered={true}>
<Row>
<Col span={4}>
<Title level={5} className="text-center">负载状态</Title>
<Popover placement="topLeft" title={"负载详情"} content={loadContent}>
<Progress type="circle" width={100}
percent={this.state.status.load['load1'].toFixed(1)}/>
</Popover>
<Paragraph className="text-center">运行流畅</Paragraph>
</Col>
<Col span={4}>
<Title level={5} className="text-center">CPU使用率</Title>
<Popover placement="topLeft" title={"CPU详情"} content={cpuContent}>
<Progress type="circle" width={100}
percent={this.state.status.cpu['percent'].toFixed(1)}/>
</Popover>
<Paragraph className="text-center">{this.state.status.cpu['logicalCount']}核心</Paragraph>
</Col>
<Col span={4}>
<Title level={5} className="text-center">内存使用率</Title>
<Progress type="circle" width={100}
percent={this.state.status.memory['usedPercent'].toFixed(1)}/>
<Paragraph className="text-center">
{Math.floor(this.state.status.memory['used'] / 1024 / 1024)}
/
{Math.floor(this.state.status.memory['total'] / 1024 / 1024)}
(MB)
</Paragraph>
</Col>
<Col span={4}>
</Col>
</Row>
</Card>
</div>
</>
);
}
}
export default Dashboard;

View File

@ -0,0 +1,499 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {differTime, formatDate, itemRender} from "../../utils/utils";
import Playback from "./Playback";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
ExclamationCircleOutlined,
PlaySquareTwoTone,
SyncOutlined,
UndoOutlined
} from "@ant-design/icons";
import {PROTOCOL_COLORS} from "../../common/constants";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'offlineSession',
breadcrumbName: '离线会话',
}
];
class OfflineSession extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
},
loading: false,
playbackVisible: false,
playbackSessionId: null,
videoPlayerVisible: false,
videoPlayerSource: null,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
assets: [],
};
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
this.handleSearchByAssetName('');
}
async loadTableData(queryParams) {
queryParams = queryParams || this.state.queryParams;
queryParams['status'] = 'disconnected';
this.setState({
queryParams: queryParams,
loading: true
});
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/sessions/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
showPlayback = (sessionId) => {
this.setState({
playbackVisible: true,
playbackSessionId: sessionId
});
};
hidePlayback = () => {
this.setState({
playbackVisible: false,
playbackSessionId: null
});
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
}
this.loadTableData(query);
}
handleSearchByAssetName = async assetName => {
const result = await request.get(`/assets/paging?pageIndex=1&pageSize=100&name=${assetName}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
assets: result.data.items
})
}
handleChangeByAssetId = (assetId, options) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'assetId': assetId,
}
this.loadTableData(query);
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/sessions/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
key: 'creatorName'
}, {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '资产名称',
dataIndex: 'assetName',
key: 'assetName'
}, {
title: '远程连接',
dataIndex: 'access',
key: 'access',
render: (text, record) => {
return `${record.username}@${record.ip}:${record.port}`;
}
}, {
title: '屏幕大小',
dataIndex: 'screen',
key: 'screen',
render: (text, record) => {
return `${record.width}x${record.height}`;
}
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '接入时间',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '接入时长',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return differTime(new Date(record['connectedTime']), new Date(record['disconnectedTime']));
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<PlaySquareTwoTone />} onClick={() => this.showPlayback(record.id)}>回放</Button>
<Button type="link" size='small' icon={<DeleteTwoTone />} onClick={() => {
confirm({
title: '您确定要删除此会话吗?',
content: '',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
del(record.id)
}
});
const del = async (id) => {
const result = await request.delete(`/sessions/${id}`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '删除成功',
});
this.loadTableData();
} else {
notification['error']({
message: '提示',
description: '删除失败 :( ' + result.message,
});
}
}
}}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
const assetOptions = this.state.assets.map(d => <Select.Option key={d.id}
value={d.id}>{d.name}</Select.Option>);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="离线会话"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="离线会话管理"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>离线会话列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.userId}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
>
{userOptions}
</Select>
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.assetId}
placeholder='资产名称'
onSearch={this.handleSearchByAssetName}
onChange={this.handleChangeByAssetId}
filterOption={false}
>
{assetOptions}
</Select>
<Select onChange={this.handleChangeByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
<Modal
title="会话回放"
visible={this.state.playbackVisible}
onCancel={this.hidePlayback}
width={window.innerWidth * 0.8}
footer={null}
destroyOnClose
>
<Playback sessionId={this.state.playbackSessionId}/>
</Modal>
</Content>
</>
);
}
}
export default OfflineSession;

View File

@ -0,0 +1,511 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {differTime, formatDate, itemRender} from "../../utils/utils";
import {message} from "antd/es";
import {PROTOCOL_COLORS} from "../../common/constants";
import {
ApiTwoTone,
DisconnectOutlined,
ExclamationCircleOutlined,
EyeTwoTone,
SyncOutlined,
UndoOutlined
} from "@ant-design/icons";
import Monitor from "../access/Monitor";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'onlineSession',
breadcrumbName: '在线会话',
}
];
class OnlineSession extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
},
loading: false,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
assets: [],
accessVisible: false,
sessionWidth: 1024,
sessionHeight: 768,
sessionProtocol: ''
};
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
this.handleSearchByAssetName('');
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
queryParams['status'] = 'connected';
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/sessions/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
}
this.loadTableData(query);
}
handleSearchByAssetName = async assetName => {
const result = await request.get(`/assets/paging?pageIndex=1&pageSize=100&name=${assetName}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
assets: result.data.items
})
}
handleChangeByAssetId = (assetId, options) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'assetId': assetId,
}
this.loadTableData(query);
}
batchDis = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.post('/sessions/' + this.state.selectedRowKeys.join(',') + '/discontent');
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
showMonitor = (record) => {
this.setState({
connectionId: record.connectionId,
sessionProtocol: record.protocol,
accessVisible: true,
sessionWidth: record.width,
sessionHeight: record.height,
sessionTitle: `${record.username}@${record.ip}:${record.port} ${record.width}x${record.height}`
})
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
key: 'creatorName'
}, {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '资产名称',
dataIndex: 'assetName',
key: 'assetName'
}, {
title: '远程连接',
dataIndex: 'access',
key: 'access',
render: (text, record) => {
return `${record.username}@${record.ip}:${record.port}`;
}
}, {
title: '屏幕大小',
dataIndex: 'screen',
key: 'screen',
render: (text, record) => {
return `${record.width}x${record.height}`;
}
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '接入时间',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '接入时长',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return differTime(new Date(record['connectedTime']), new Date());
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EyeTwoTone/>} onClick={() => {
this.showMonitor(record)
}}>监控</Button>
<Button type="link" size='small' icon={<ApiTwoTone/>} onClick={async () => {
confirm({
title: '您确定要断开此会话吗?',
content: '',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
dis(record.id)
}
});
const dis = async (id) => {
const result = await request.post(`/sessions/${id}/discontent`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '断开成功',
});
this.loadTableData();
} else {
notification['success']({
message: '提示',
description: '断开失败 :( ' + result.message,
});
}
}
}}>断开</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
const assetOptions = this.state.assets.map(d => <Select.Option key={d.id}
value={d.id}>{d.name}</Select.Option>);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="在线会话"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="查询实时在线会话"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>在线会话列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.userId}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
>
{userOptions}
</Select>
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.assetId}
placeholder='资产名称'
onSearch={this.handleSearchByAssetName}
onChange={this.handleChangeByAssetId}
filterOption={false}
>
{assetOptions}
</Select>
<Select onChange={this.handleChangeByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量断开">
<Button type="primary" danger disabled={!hasSelected}
icon={<DisconnectOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要断开选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDis()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
<Modal
className='monitor'
title={this.state.sessionTitle}
centered
visible={this.state.accessVisible}
footer={null}
width={window.innerWidth * 0.8}
height={window.innerWidth * 0.8 / this.state.sessionWidth * this.state.sessionHeight}
onCancel={() => {
this.setState({accessVisible: false})
}}
>
<Monitor connectionId={this.state.connectionId}
width={this.state.sessionWidth}
height={this.state.sessionHeight}
protocol={this.state.sessionProtocol}
rate={window.innerWidth * 0.8 / this.state.sessionWidth}>
</Monitor>
</Modal>
</Content>
</>
);
}
}
export default OnlineSession;

View File

@ -0,0 +1,173 @@
import React, {Component} from 'react';
import Guacamole from "guacamole-common-js";
class Playback extends Component {
componentDidMount() {
let sessionId = this.props.sessionId;
this.initPlayer(sessionId);
}
componentWillMount() {
}
initPlayer(sessionId) {
var RECORDING_URL = '/recording/' + sessionId + '.guac';
var player = document.getElementById('player');
var display = document.getElementById('display');
var playPause = document.getElementById('play-pause');
var position = document.getElementById('position');
var positionSlider = document.getElementById('position-slider');
var duration = document.getElementById('duration');
var tunnel = new Guacamole.StaticHTTPTunnel(RECORDING_URL);
var recording = new Guacamole.SessionRecording(tunnel);
var recordingDisplay = recording.getDisplay();
/**
* Converts the given number to a string, adding leading zeroes as necessary
* to reach a specific minimum length.
*
* @param {Numer} num
* The number to convert to a string.
*
* @param {Number} minLength
* The minimum length of the resulting string, in characters.
*
* @returns {String}
* A string representation of the given number, with leading zeroes
* added as necessary to reach the specified minimum length.
*/
var zeroPad = function zeroPad(num, minLength) {
// Convert provided number to string
var str = num.toString();
// Add leading zeroes until string is long enough
while (str.length < minLength)
str = '0' + str;
return str;
};
/**
* Converts the given millisecond timestamp into a human-readable string in
* MM:SS format.
*
* @param {Number} millis
* An arbitrary timestamp, in milliseconds.
*
* @returns {String}
* A human-readable string representation of the given timestamp, in
* MM:SS format.
*/
var formatTime = function formatTime(millis) {
// Calculate total number of whole seconds
var totalSeconds = Math.floor(millis / 1000);
// Split into seconds and minutes
var seconds = totalSeconds % 60;
var minutes = Math.floor(totalSeconds / 60);
// Format seconds and minutes as MM:SS
return zeroPad(minutes, 2) + ':' + zeroPad(seconds, 2);
};
// Add playback display to DOM
display.appendChild(recordingDisplay.getElement());
// Begin downloading the recording
recording.connect();
// If playing, the play/pause button should read "Pause"
recording.onplay = function () {
playPause.textContent = 'Pause';
};
// If paused, the play/pause button should read "Play"
recording.onpause = function () {
playPause.textContent = 'Play';
};
// Toggle play/pause when display or button are clicked
display.onclick = playPause.onclick = function () {
if (!recording.isPlaying())
recording.play();
else
recording.pause();
};
// Fit display within containing div
recordingDisplay.onresize = function displayResized(width, height) {
// Do not scale if display has no width
if (!width)
return;
// Scale display to fit width of container
recordingDisplay.scale(display.offsetWidth / width);
};
// Update slider and status when playback position changes
recording.onseek = function positionChanged(millis) {
position.textContent = formatTime(millis);
positionSlider.value = millis;
};
// Update slider and status when duration changes
recording.onprogress = function durationChanged(millis) {
duration.textContent = formatTime(millis);
positionSlider.max = millis;
};
// Seek within recording if slider is moved
positionSlider.onchange = function sliderPositionChanged() {
// Request seek
recording.seek(positionSlider.value, function seekComplete() {
// Seek has completed
player.className = '';
});
// Seek is in progress
player.className = 'seeking';
};
}
render() {
return (
<div>
<div id="player">
<div id="display">
<div className="notification-container">
<div className="seek-notification">
</div>
</div>
</div>
<div className="controls">
<button id="play-pause">Play</button>
<input id="position-slider" type="range"/>
<span id="position">00:00</span>
<span>/</span>
<span id="duration">00:00</span>
</div>
</div>
</div>
);
}
}
export default Playback;

View File

@ -0,0 +1,315 @@
import React, {Component} from 'react';
import {Layout, PageHeader, Switch, Select} from "antd";
import {itemRender} from '../../utils/utils'
import {Form, Input, Button, Checkbox} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
const {Content} = Layout;
const {Option} = Select;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'setting',
breadcrumbName: '系统设置',
}
];
const formItemLayout = {
labelCol: {span: 3},
wrapperCol: {span: 9},
};
const formTailLayout = {
labelCol: {span: 3},
wrapperCol: {span: 9, offset: 3},
};
class Setting extends Component {
state = {}
settingFormRef = React.createRef();
componentDidMount() {
this.getProperties();
}
changeProperties = async (values) => {
let result = await request.put('/properties', values);
if (result.code === 1) {
message.success('修改成功');
} else {
message.error(result.message);
}
}
getProperties = async () => {
// eslint-disable-next-line no-extend-native
String.prototype.bool = function() {
return (/^true$/i).test(this);
};
let result = await request.get('/properties');
if (result.code === 1) {
let properties = {}
for (let i = 0; i < result.data.length; i++) {
let item = result.data[i];
if (item['name'].startsWith('enable') ||
item['name'].startsWith('disable')) {
properties[item['name']] = item['value'].bool()
}else {
properties[item['name']] = item['value']
}
}
this.settingFormRef.current.setFieldsValue(properties)
} else {
message.error(result.message);
}
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="系统设置"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="系统设置"
>
</PageHeader>
<Content className="site-layout-background page-content">
<Form ref={this.settingFormRef} name="password" onFinish={this.changeProperties}>
<h3>Guacd 服务配置</h3>
<Form.Item
{...formItemLayout}
name="host"
label="监听地址"
rules={[
{
required: true,
message: '监听地址',
},
]}
>
<Input type='text' placeholder="请输入监听地址"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="port"
label="监听端口"
rules={[
{
required: true,
message: '监听端口',
min: 1,
max: 65535
},
]}
>
<Input type='number' placeholder="请输入监听端口"/>
</Form.Item>
<h3>远程桌面RDP配置</h3>
<Form.Item
{...formItemLayout}
name="enable-drive"
label="启用设备映射"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="drive-name"
label="设备名称"
rules={[
{
required: true,
message: '请输入设备名称',
},
]}
>
<Input type='text' placeholder="请输入设备名称"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="drive-path"
label="设备路径"
rules={[
{
required: true,
message: '请输入设备路径',
},
]}
>
<Input type='text' placeholder="请输入设备路径"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-wallpaper"
label="启用桌面墙纸"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-theming"
label="启用桌面主题"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-font-smoothing"
label="启用字体平滑ClearType"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-full-window-drag"
label="启用全窗口拖拽"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-desktop-composition"
label="启用桌面合成效果Aero"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-menu-animations"
label="启用菜单动画"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-bitmap-caching"
label="禁用位图缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-offscreen-caching"
label="禁用离屏缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-glyph-caching"
label="禁用字形缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<h3>SSH配置</h3>
<Form.Item
{...formItemLayout}
name="font-name"
label="字体名称"
rules={[
{
required: true,
message: '字体名称',
},
]}
>
<Input type='text' placeholder="请输入字体名称"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="font-size"
label="字体大小"
rules={[
{
required: true,
message: '字体大小',
},
]}
>
<Input type='number' placeholder="请输入字体大小"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="color-scheme"
label="颜色主题"
rules={[
{
required: true,
message: '颜色主题',
},
]}
initialValue="gray-black"
>
<Select style={{width: 120}} onChange={null}>
<Option value="gray-black">黑底灰字</Option>
<Option value="green-black">黑底绿字</Option>
<Option value="white-black">黑底白字</Option>
<Option value="black-white">白底黑字</Option>
</Select>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-sftp"
label="启用SFTP"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Content>
</>
);
}
}
export default Setting;

View File

@ -0,0 +1,144 @@
import React, {Component} from 'react';
import {Layout, PageHeader} from "antd";
import {itemRender} from '../../utils/utils'
import {Form, Input, Button, Checkbox} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'info',
breadcrumbName: '个人中心',
}
];
const formItemLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6},
};
const formTailLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6, offset: 3},
};
class Info extends Component {
state = {}
passwordFormRef = React.createRef();
onNewPasswordChange(value) {
this.setState({
'newPassword': value.target.value
})
}
onNewPassword2Change = (value) => {
this.setState({
...this.validateNewPassword(value.target.value),
'newPassword2': value.target.value
})
}
validateNewPassword = (newPassword2) => {
if (newPassword2 === this.state.newPassword) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: '两次输入的密码不一致',
};
}
changePassword = async (values) => {
let result = await request.post('/change-password', values);
if (result.code === 1) {
message.success('密码修改成功');
} else {
message.error(result.message);
}
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="个人中心"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="个人中心"
>
</PageHeader>
<Content className="site-layout-background page-content">
<h1>修改密码</h1>
<Form ref={this.passwordFormRef} name="password" onFinish={this.changePassword}>
<Form.Item
{...formItemLayout}
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码" onChange={(value) => this.onNewPasswordChange(value)}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={this.state.validateStatus}
help={this.state.errorMsg || ''}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致" onChange={(value) => this.onNewPassword2Change(value)}/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Content>
</>
);
}
}
export default Info;

View File

@ -0,0 +1,431 @@
import React, {Component} from 'react';
import {itemRender} from '../../utils/utils'
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tooltip,
Typography,
} from "antd";
import qs from "qs";
import UserModal from "./UserModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'user',
breadcrumbName: '用户',
}
];
class User extends Component {
inputRefOfNickname = React.createRef();
inputRefOfUsername = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
let result = await request.delete('/users/' + id);
if (result.code === 1) {
message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/users/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message, 10);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams).then(r => {})
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此用户吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, user = {}) {
this.setState({
model: user,
modalVisible: true,
modalTitle: title
});
};
handleCancelModal = e => {
this.setState({
modalVisible: false,
modalTitle: ''
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/users/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/users', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
handleSearchByUsername = username => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'username': username,
}
this.loadTableData(query);
};
handleSearchByNickname = nickname => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'nickname': nickname,
}
this.loadTableData(query);
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/users/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '登录账号',
dataIndex: 'username',
key: 'username',
}, {
title: '用户昵称',
dataIndex: 'nickname',
key: 'nickname',
}, {
title: '在线状态',
dataIndex: 'online',
key: 'online',
render: text => {
if (text) {
return (<Badge status="success" text="在线"/>);
} else {
return (<Badge status="default" text="离线"/>);
}
}
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created'
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.showModal('更新用户', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="用户管理"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="平台用户管理"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>用户列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfNickname}
placeholder="用户昵称"
allowClear
onSearch={this.handleSearchByNickname}
/>
<Search
ref={this.inputRefOfUsername}
placeholder="登录账号"
allowClear
onSearch={this.handleSearchByUsername}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfUsername.current.setValue('');
this.inputRefOfNickname.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增用户', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<UserModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</UserModal>
: null
}
</Content>
</>
);
}
}
export default User;

View File

@ -0,0 +1,58 @@
import React from 'react';
import {Form, Input, Modal, Radio} from "antd/lib/index";
const UserModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="登录账户" name='username' rules={[{required: true, message: '请输入登录账户'}]}>
<Input autoComplete="off" placeholder="请输入登录账户"/>
</Form.Item>
<Form.Item label="用户昵称" name='nickname' rules={[{required: true, message: '请输入用户昵称'}]}>
<Input placeholder="请输入用户昵称"/>
</Form.Item>
{
title.indexOf('新增') > -1 ?
(<Form.Item label="登录密码" name='password' rules={[{required: true, message: '请输入登录密码'}]}>
<Input type="password" autoComplete="new-password" placeholder="输入登录密码"/>
</Form.Item>) : null
}
</Form>
</Modal>
)
};
export default UserModal;

Binary file not shown.

14
web/src/index.css Normal file
View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

23
web/src/index.js Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import zhCN from 'antd/es/locale-provider/zh_CN';
import {ConfigProvider} from 'antd';
import {HashRouter as Router} from "react-router-dom";
ReactDOM.render(
<ConfigProvider locale={zhCN}>
<Router>
<App/>
</Router>
</ConfigProvider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

7
web/src/logo.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,7 @@
import request from "../common/request";
const treeNodeService = {
};
export default treeNodeService;

135
web/src/serviceWorker.js Normal file
View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve asset; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onStateChange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

145
web/src/utils/utils.js Normal file
View File

@ -0,0 +1,145 @@
import React from "react";
import {Link} from "react-router-dom";
export const sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export const getToken = function () {
return localStorage.getItem('X-Auth-Token');
}
export const getHeaders = function () {
return {'X-Auth-Token': getToken()};
}
export const itemRender = function (route, params, routes, paths) {
const last = routes.indexOf(route) === routes.length - 1;
return last ? (
<span>{route.breadcrumbName}</span>
) : (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
);
}
export const formatDate = function (time, format) {
let date = new Date(time);
let o = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
"q+": Math.floor((date.getMonth() + 3) / 3), //quarter
"S": date.getMilliseconds() //millisecond
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
};
export const isLeapYear = function (year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 100 === 0 && year % 400 === 0);
};
export const groupBy = (list, fn) => {
const groups = {};
list.forEach(x => {
let groupKey = fn(x).toString();
groups[groupKey] = groups[groupKey] || [];
groups[groupKey].push(x);
});
return groups;
};
export const cloneObj = (obj, ignoreFields) => {
let str, newObj = obj.constructor === Array ? [] : {};
if (typeof obj !== 'object') {
return;
} else if (window.JSON) {
str = JSON.stringify(obj);
newObj = JSON.parse(str);
} else {
for (const i in obj) {
newObj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i];
}
}
return newObj;
};
export function download(url) {
let aElement = document.createElement('a');
aElement.setAttribute('download', '');
aElement.setAttribute('target', '_blank');
aElement.setAttribute('href', url);
aElement.click();
}
export function differTime(start, end) {
//总秒数
let millisecond = Math.floor((end.getTime() - start.getTime()) / 1000);
//总天数
let allDay = Math.floor(millisecond / (24 * 60 * 60));
//注意同getYear的区别
let startYear = start.getFullYear();
let currentYear = end.getFullYear();
//闰年个数
let leapYear = 0;
for (let i = startYear; i < currentYear; i++) {
if (isLeapYear(i)) {
leapYear++;
}
}
//年数
const year = Math.floor((allDay - leapYear * 366) / 365 + leapYear);
//天数
let day;
if (allDay > 366) {
day = (allDay - leapYear * 366) % 365;
} else {
day = allDay;
}
//取余数(秒)
const remainder = millisecond % (24 * 60 * 60);
//小时数
const hour = Math.floor(remainder / (60 * 60));
//分钟数
const minute = Math.floor(remainder % (60 * 60) / 60);
//秒数
const second = remainder - hour * 60 * 60 - minute * 60;
let show = '';
if (year > 0) {
show += year + '年';
}
if (day > 0) {
show += day + '天';
}
if (hour > 0) {
show += hour + '小时';
}
if (minute > 0) {
show += minute + '分钟';
}
if (second > 0) {
show += second + '秒';
}
return show;
}