Initial commit
This commit is contained in:
commit
e7f2773c77
19
.dockerignore
Normal file
19
.dockerignore
Normal 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
29
Dockerfile
Normal 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
339
LICENSE
Normal 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.
|
20
go.mod
Normal file
20
go.mod
Normal 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
420
go.sum
Normal 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
83
main.go
Normal 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
4
next-terminal.yml
Normal 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
52
pkg/api/account.go
Normal 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
96
pkg/api/asset.go
Normal 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
70
pkg/api/command.go
Normal 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
72
pkg/api/credential.go
Normal 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
34
pkg/api/middleware.go
Normal 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
98
pkg/api/overview.go
Normal 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
29
pkg/api/property.go
Normal 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
145
pkg/api/routes.go
Normal 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
430
pkg/api/session.go
Normal 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
175
pkg/api/ssh.go
Normal 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
33
pkg/api/test/ps.go
Normal 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
158
pkg/api/tunnel.go
Normal 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
83
pkg/api/user.go
Normal 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
14
pkg/config/config.go
Normal 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
|
31
pkg/config/next-terminal.go
Normal file
31
pkg/config/next-terminal.go
Normal 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
37
pkg/config/store.go
Normal 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
227
pkg/guacd/guacd.go
Normal 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
199
pkg/handle/runner.go
Normal 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
86
pkg/model/asset.go
Normal 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
56
pkg/model/command.go
Normal 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
63
pkg/model/credential.go
Normal 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
78
pkg/model/property.go
Normal 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
138
pkg/model/session.go
Normal 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
79
pkg/model/user.go
Normal 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
81
pkg/utils/utils.go
Normal 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
2
web/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Project exclude paths
|
||||
/node_modules/
|
339
web/LICENSE
Normal file
339
web/LICENSE
Normal 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
4
web/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Next Terminal dashboard
|
||||
just do go dashboard
|
||||
|
||||
## 主要功能
|
17686
web/package-lock.json
generated
Normal file
17686
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
web/package.json
Normal file
43
web/package.json
Normal 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
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
31
web/public/index.html
Normal 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
1
web/public/logo.svg
Normal 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
15
web/public/manifest.json
Normal 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
95
web/src/App.css
Normal 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
241
web/src/App.js
Normal 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 ?
|
||||
|
||||
<> <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
9
web/src/App.test.js
Normal 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);
|
||||
});
|
20
web/src/common/constants.js
Normal file
20
web/src/common/constants.js
Normal 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
115
web/src/common/request.js
Normal 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
|
21
web/src/components/Login.css
Normal file
21
web/src/components/Login.css
Normal 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;
|
||||
}
|
||||
|
78
web/src/components/Login.js
Normal file
78
web/src/components/Login.js
Normal 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);
|
3
web/src/components/access/Access.css
Normal file
3
web/src/components/access/Access.css
Normal file
@ -0,0 +1,3 @@
|
||||
.container div {
|
||||
margin: 0 auto;
|
||||
}
|
889
web/src/components/access/Access.js
Normal file
889
web/src/components/access/Access.js
Normal 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>
|
||||
远程文件管理
|
||||
|
||||
|
||||
<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;
|
3
web/src/components/access/Console.css
Normal file
3
web/src/components/access/Console.css
Normal file
@ -0,0 +1,3 @@
|
||||
.console-card .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
110
web/src/components/access/Console.js
Normal file
110
web/src/components/access/Console.js
Normal 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;
|
237
web/src/components/access/Monitor.js
Normal file
237
web/src/components/access/Monitor.js
Normal 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;
|
512
web/src/components/asset/Asset.js
Normal file
512
web/src/components/asset/Asset.js
Normal 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;
|
157
web/src/components/asset/AssetModal.js
Normal file
157
web/src/components/asset/AssetModal.js
Normal 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;
|
92
web/src/components/command/BatchCommand.js
Normal file
92
web/src/components/command/BatchCommand.js
Normal 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;
|
501
web/src/components/command/DynamicCommand.js
Normal file
501
web/src/components/command/DynamicCommand.js
Normal 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;
|
59
web/src/components/command/DynamicCommandModal.js
Normal file
59
web/src/components/command/DynamicCommandModal.js
Normal 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;
|
400
web/src/components/credential/Credential.js
Normal file
400
web/src/components/credential/Credential.js
Normal 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;
|
59
web/src/components/credential/CredentialModal.js
Normal file
59
web/src/components/credential/CredentialModal.js
Normal 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;
|
4
web/src/components/dashboard/Dashboard.css
Normal file
4
web/src/components/dashboard/Dashboard.css
Normal file
@ -0,0 +1,4 @@
|
||||
.text-center{
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
196
web/src/components/dashboard/Dashboard.js
Normal file
196
web/src/components/dashboard/Dashboard.js
Normal 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;
|
499
web/src/components/session/OfflineSession.js
Normal file
499
web/src/components/session/OfflineSession.js
Normal 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;
|
511
web/src/components/session/OnlineSession.js
Normal file
511
web/src/components/session/OnlineSession.js
Normal 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;
|
173
web/src/components/session/Playback.js
Normal file
173
web/src/components/session/Playback.js
Normal 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;
|
315
web/src/components/setting/Setting.js
Normal file
315
web/src/components/setting/Setting.js
Normal 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;
|
144
web/src/components/user/Info.js
Normal file
144
web/src/components/user/Info.js
Normal 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;
|
431
web/src/components/user/User.js
Normal file
431
web/src/components/user/User.js
Normal 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;
|
58
web/src/components/user/UserModal.js
Normal file
58
web/src/components/user/UserModal.js
Normal 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;
|
BIN
web/src/fonts/Menlo/Menlo-Italic-4.ttf
Normal file
BIN
web/src/fonts/Menlo/Menlo-Italic-4.ttf
Normal file
Binary file not shown.
14
web/src/index.css
Normal file
14
web/src/index.css
Normal 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
23
web/src/index.js
Normal 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
7
web/src/logo.svg
Normal 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 |
7
web/src/service/TreeNodeService.js
Normal file
7
web/src/service/TreeNodeService.js
Normal file
@ -0,0 +1,7 @@
|
||||
import request from "../common/request";
|
||||
|
||||
const treeNodeService = {
|
||||
|
||||
|
||||
};
|
||||
export default treeNodeService;
|
135
web/src/serviceWorker.js
Normal file
135
web/src/serviceWorker.js
Normal 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
145
web/src/utils/utils.js
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user